PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.15
MxChat – AI Chatbot & Content Generation for WordPress v3.2.15
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / includes / class-mxchat-admin.php

class-mxchat-admin.php in MxChat – AI Chatbot & Content Generation for WordPress 3.2.15, at includes/class-mxchat-admin.php

10,826 lines 460.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit; // Exit if accessed directly
4 }
5
6 class MxChat_Admin {
7 private $options;
8 private $chat_count;
9 private $is_activated;
10 private $knowledge_manager;
11
12 /**
13 * Get whether the license is activated
14 * @return bool
15 */
16 public function is_activated() {
17 return $this->is_activated;
18 }
19
20 /**
21 * Get plugin options
22 * @return array
23 */
24 public function get_options() {
25 return $this->options;
26 }
27
28 public function __construct($knowledge_manager = null) {
29 $this->options = get_option('mxchat_options');
30 $this->chat_count = get_option('mxchat_chat_count', 0);
31 $this->is_activated = $this->is_license_active();
32 $this->knowledge_manager = $knowledge_manager;
33
34 // Initialize default options if they are not set
35 if (!$this->options) {
36 $this->initialize_default_options();
37 }
38
39 // Add admin menu and initialize settings
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 add_action('admin_init', array($this, 'mxchat_page_init'));
49 add_action('admin_init', array($this, 'mxchat_prompts_page_init'));
50 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 add_action('wp_ajax_mxchat_delete_chat_history', array($this, 'mxchat_delete_chat_history'));
56 add_action('admin_post_mxchat_delete_prompt', array($this, 'mxchat_handle_delete_prompt'));
57 add_action('wp_ajax_mxchat_fetch_chat_history', array($this, 'mxchat_fetch_chat_history'));
58 add_action('wp_ajax_nopriv_mxchat_fetch_chat_history', array($this, 'mxchat_fetch_chat_history'));
59 add_action('wp_ajax_mxchat_fetch_conversation', array($this, 'mxchat_fetch_conversation'));
60 add_action('wp_footer', array($this, 'mxchat_append_chatbot_to_body'));
61 add_action('admin_head-mxchat-prompts', array($this, 'mxchat_enqueue_admin_assets'));
62 add_action('admin_head-toplevel_page_mxchat-max', array($this, 'mxchat_enqueue_admin_assets'));
63 add_action('admin_notices', array($this, 'mxchat_display_admin_notice'));
64 add_action('admin_post_mxchat_delete_all_prompts', array($this, 'mxchat_handle_delete_all_prompts'));
65 add_action('admin_post_mxchat_add_intent', array($this, 'mxchat_handle_add_intent'));
66 add_action('admin_post_mxchat_delete_intent', array($this, 'mxchat_handle_delete_intent'));
67 add_action('admin_post_mxchat_edit_intent', array($this, 'mxchat_handle_edit_intent'));
68 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 add_action('admin_init', array($this, 'mxchat_transcripts_page_init'));
76 add_action('wp_ajax_dismiss_live_agent_notice', array($this, 'dismiss_live_agent_notice'));
77 add_action('wp_ajax_dismiss_theme_migration_notice', array($this, 'dismiss_theme_migration_notice'));
78 add_action('mxchat_cleanup_old_transcripts', array($this, 'cleanup_old_transcripts'));
79 // Self-heal: the cleanup event is otherwise only scheduled at activation or
80 // when the retention setting CHANGES — if it is ever lost (deactivate cycle,
81 // cron-option wipe, DB restore) nothing re-registers it and retention goes
82 // silently inert while the UI still says it is on (plan-bc08a6).
83 add_action('admin_init', array($this, 'ensure_transcript_cleanup_scheduled'));
84
85 add_action('admin_init', array($this, 'register_pinecone_settings'));
86 add_action('admin_init', array($this, 'register_openai_vectorstore_settings'));
87
88 add_action('admin_notices', array($this, 'display_admin_notices'));
89
90 add_action('wp_ajax_mxchat_test_streaming_actual', [$this, 'mxchat_handle_test_streaming_actual']);
91 add_action('wp_ajax_mxchat_test_streaming', [$this, 'mxchat_handle_test_streaming']); // Keep existing as fallback
92 add_action('wp_ajax_mxchat_test_vectorstore_connection', array($this, 'mxchat_test_vectorstore_connection'));
93
94 add_action('wp_ajax_mxchat_save_selected_bot', array($this, 'mxchat_save_selected_bot'));
95 add_action('wp_ajax_mxchat_fetch_openrouter_models', array($this, 'fetch_openrouter_models'));
96 add_action('wp_ajax_mxchat_get_rag_context', array($this, 'mxchat_get_rag_context'));
97
98 // Actions page AJAX handlers
99 add_action('wp_ajax_mxchat_fetch_actions_list', array($this, 'mxchat_fetch_actions_list'));
100 add_action('wp_ajax_mxchat_toggle_action_status', array($this, 'mxchat_toggle_action_status'));
101 add_action('wp_ajax_mxchat_bulk_delete_actions', array($this, 'mxchat_bulk_delete_actions'));
102 add_action('wp_ajax_mxchat_add_intent_ajax', array($this, 'mxchat_add_intent_ajax'));
103 add_action('wp_ajax_mxchat_edit_intent_ajax', array($this, 'mxchat_edit_intent_ajax'));
104 add_action('wp_ajax_mxchat_delete_intent_ajax', array($this, 'mxchat_delete_intent_ajax'));
105 add_action('wp_ajax_mxchat_add_phrase', array($this, 'mxchat_add_phrase_ajax'));
106 add_action('wp_ajax_mxchat_delete_phrase', array($this, 'mxchat_delete_phrase_ajax'));
107 add_action('wp_ajax_mxchat_get_phrases', array($this, 'mxchat_get_phrases_ajax'));
108 add_action('wp_ajax_mxchat_delete_legacy_phrases', array($this, 'mxchat_delete_legacy_phrases_ajax'));
109
110 // Slack test connection
111 add_action('wp_ajax_mxchat_test_slack_connection', array($this, 'mxchat_test_slack_connection'));
112
113 // Translation handlers
114 add_action('wp_ajax_mxchat_translate_messages', array($this, 'mxchat_translate_messages'));
115 add_action('wp_ajax_mxchat_get_transcript_translation', array($this, 'mxchat_get_transcript_translation'));
116
117 // 3.2.3: Embedding model switch protection
118 add_action('wp_ajax_mxchat_check_embedding_switch', array($this, 'mxchat_check_embedding_switch_ajax'));
119 add_action('wp_ajax_mxchat_dismiss_embedding_mismatch', array($this, 'mxchat_dismiss_embedding_mismatch_ajax'));
120 add_action('wp_ajax_mxchat_dismiss_telegram_secret_notice', array($this, 'mxchat_dismiss_telegram_secret_notice_ajax'));
121 add_action('admin_notices', array($this, 'mxchat_embedding_mismatch_notice'));
122 add_action('admin_notices', array($this, 'mxchat_telegram_secret_notice'));
123 }
124
125 /**
126 * Nudge admins to configure a Telegram webhook secret (plan-0c17b5).
127 *
128 * Shown only when a Telegram bot token is configured (integration active)
129 * AND no webhook secret is set. Without a secret the webhook falls back to a
130 * Telegram source-IP check — safer than the old fail-open, but a real secret
131 * is the recommended protection. Dismissible; does not block anything.
132 */
133 public function mxchat_telegram_secret_notice() {
134 if (!current_user_can('manage_options')) {
135 return;
136 }
137 $options = get_option('mxchat_options', array());
138 $has_token = !empty($options['telegram_bot_token']);
139 $has_secret = !empty($options['telegram_webhook_secret']);
140 if (!$has_token || $has_secret) {
141 return;
142 }
143 if (get_option('mxchat_dismissed_telegram_secret_notice', '') === '1') {
144 return;
145 }
146 $settings_url = admin_url('admin.php?page=mxchat-max');
147 ?>
148 <div class="notice notice-warning is-dismissible mxchat-telegram-secret-notice">
149 <p><strong><?php esc_html_e('MxChat: Telegram webhook is running without a secret token', 'mxchat'); ?></strong></p>
150 <p>
151 <?php esc_html_e('Your Telegram integration is active but no webhook secret is configured. Requests are currently verified only by origin IP. For full protection, set a webhook secret token in the Telegram settings and re-register your webhook.', 'mxchat'); ?>
152 </p>
153 <p>
154 <a href="<?php echo esc_url($settings_url); ?>" class="button button-primary"><?php esc_html_e('Open Telegram settings', 'mxchat'); ?></a>
155 </p>
156 </div>
157 <script>
158 (function(){
159 var n = document.querySelector('.mxchat-telegram-secret-notice');
160 if (!n) return;
161 n.addEventListener('click', function(e){
162 if (e.target && e.target.classList.contains('notice-dismiss')) {
163 var d = new FormData();
164 d.append('action', 'mxchat_dismiss_telegram_secret_notice');
165 d.append('nonce', '<?php echo esc_js(wp_create_nonce('mxchat_dismiss_telegram_secret')); ?>');
166 fetch('<?php echo esc_url(admin_url('admin-ajax.php')); ?>', { method: 'POST', body: d, credentials: 'same-origin' });
167 }
168 });
169 })();
170 </script>
171 <?php
172 }
173
174 /**
175 * AJAX: persist dismissal of the Telegram-secret admin notice (plan-0c17b5).
176 */
177 public function mxchat_dismiss_telegram_secret_notice_ajax() {
178 if (!current_user_can('manage_options')
179 || !check_ajax_referer('mxchat_dismiss_telegram_secret', 'nonce', false)) {
180 wp_send_json_error();
181 }
182 update_option('mxchat_dismissed_telegram_secret_notice', '1', 'no');
183 wp_send_json_success();
184 }
185
186 /**
187 * 3.2.3: Preflight check before allowing the embedding model dropdown to
188 * switch. Pure option comparison — no counts, no DB queries beyond the
189 * cached options. The dialog is shown whenever the user has previously
190 * embedded with a different model than the one they're switching to.
191 */
192 public function mxchat_check_embedding_switch_ajax() {
193 check_ajax_referer('mxchat_admin_nonce', 'security');
194 if (!current_user_can('manage_options')) {
195 wp_send_json_error(__('Unauthorized', 'mxchat'));
196 }
197
198 $new_model = isset($_POST['new_model']) ? sanitize_text_field(wp_unslash($_POST['new_model'])) : '';
199 $active_model = MxChat_Utils::get_active_embedding_model();
200
201 $is_mismatch = !empty($active_model) && !empty($new_model) && $active_model !== $new_model;
202
203 $active_dims = MxChat_Utils::embedding_model_dimensions($active_model);
204 $new_dims = MxChat_Utils::embedding_model_dimensions($new_model);
205
206 wp_send_json_success(array(
207 'is_mismatch' => $is_mismatch,
208 'active_model' => $active_model,
209 'active_label' => MxChat_Utils::embedding_model_label($active_model),
210 'new_model' => $new_model,
211 'new_label' => MxChat_Utils::embedding_model_label($new_model),
212 'dims_differ' => ($active_dims > 0 && $new_dims > 0 && $active_dims !== $new_dims),
213 'active_dims' => $active_dims,
214 'new_dims' => $new_dims,
215 ));
216 }
217
218 /**
219 * 3.2.3: Dismiss the persistent mismatch banner. Tied to the active+selected
220 * pair so the banner reappears on the next switch event.
221 */
222 public function mxchat_dismiss_embedding_mismatch_ajax() {
223 check_ajax_referer('mxchat_admin_nonce', 'security');
224 if (!current_user_can('manage_options')) {
225 wp_send_json_error(__('Unauthorized', 'mxchat'));
226 }
227
228 $options = get_option('mxchat_options', array());
229 $selected = $options['embedding_model'] ?? '';
230 $active = MxChat_Utils::get_active_embedding_model();
231 update_option('mxchat_dismissed_embedding_mismatch', $active . '|' . $selected, false);
232 wp_send_json_success();
233 }
234
235 /**
236 * 3.2.3: Persistent admin banner shown whenever the active embedding model
237 * (last used to actually embed something) differs from the currently
238 * selected model. Pure option comparison — no DB queries on every page
239 * load. The banner auto-clears once both match again, i.e. after a delete
240 * + re-embed cycle.
241 */
242 public function mxchat_embedding_mismatch_notice() {
243 if (!current_user_can('manage_options')) {
244 return;
245 }
246
247 $options = get_option('mxchat_options', array());
248 $selected = $options['embedding_model'] ?? '';
249 $active = MxChat_Utils::get_active_embedding_model();
250
251 if (empty($active) || empty($selected) || $active === $selected) {
252 return;
253 }
254
255 $dismissed = get_option('mxchat_dismissed_embedding_mismatch', '');
256 if ($dismissed === $active . '|' . $selected) {
257 return;
258 }
259
260 $active_label = MxChat_Utils::embedding_model_label($active);
261 $selected_label = MxChat_Utils::embedding_model_label($selected);
262 $active_dims = MxChat_Utils::embedding_model_dimensions($active);
263 $selected_dims = MxChat_Utils::embedding_model_dimensions($selected);
264 $dims_differ = ($active_dims > 0 && $selected_dims > 0 && $active_dims !== $selected_dims);
265
266 $kb_url = admin_url('admin.php?page=mxchat-prompts');
267 $actions_url = admin_url('admin.php?page=mxchat-actions');
268
269 ?>
270 <div class="notice notice-error is-dismissible mxchat-embedding-mismatch-notice"
271 data-active="<?php echo esc_attr($active); ?>"
272 data-selected="<?php echo esc_attr($selected); ?>">
273 <p><strong><?php esc_html_e('MxChat: Embedding model mismatch detected', 'mxchat'); ?></strong></p>
274 <p>
275 <?php
276 printf(
277 /* translators: 1: previously-used model name, 2: currently-selected model name */
278 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'),
279 '<code>' . esc_html($active_label) . '</code>',
280 '<code>' . esc_html($selected_label) . '</code>'
281 );
282 ?>
283 </p>
284 <?php if ($dims_differ) : ?>
285 <p>
286 <strong><?php esc_html_e('Dimension mismatch:', 'mxchat'); ?></strong>
287 <?php
288 printf(
289 /* translators: 1: old dim count, 2: new dim count */
290 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'),
291 (int) $active_dims,
292 (int) $selected_dims
293 );
294 ?>
295 </p>
296 <?php endif; ?>
297 <p>
298 <?php esc_html_e('To fix this:', 'mxchat'); ?>
299 <a href="<?php echo esc_url($kb_url); ?>"><?php esc_html_e('Delete all knowledge base entries', 'mxchat'); ?></a> ·
300 <a href="<?php echo esc_url($actions_url); ?>"><?php esc_html_e('Delete all actions', 'mxchat'); ?></a> ·
301 <?php esc_html_e('then re-import / re-add them with the new model selected.', 'mxchat'); ?>
302 </p>
303 </div>
304 <script>
305 (function($){
306 $(document).on('click', '.mxchat-embedding-mismatch-notice .notice-dismiss', function(){
307 $.post(ajaxurl, {
308 action: 'mxchat_dismiss_embedding_mismatch',
309 security: '<?php echo esc_js(wp_create_nonce('mxchat_admin_nonce')); ?>'
310 });
311 });
312 })(jQuery);
313 </script>
314 <?php
315 }
316
317 private function is_license_active() {
318 $license_status = get_option('mxchat_license_status', 'inactive');
319 return ($license_status === 'active');
320 }
321
322 private function initialize_default_options() {
323 $default_options = array(
324 'api_key' => '',
325 'xai_api_key' => '',
326 'claude_api_key' => '',
327 'deepseek_api_key' => '',
328 'voyage_api_key' => '',
329 'gemini_api_key' => '',
330 'enable_streaming_toggle' => 'off',
331 'enable_web_search' => 'off',
332 'embedding_model' => 'text-embedding-ada-002',
333 'system_prompt_instructions' => 'You are an AI Chatbot assistant for this website. Your main goal is to assist visitors with questions and provide helpful information. Here are your key guidelines:
334
335 # Response Style - CRITICALLY IMPORTANT
336 - MAXIMUM LENGTH: 1-3 short sentences per response
337 - Ultra-concise: Get straight to the answer with no filler
338 - No introductions like "Sure!" or "I\'d be happy to help"
339 - No phrases like "based on my knowledge" or "according to information"
340 - No explanatory text before giving the answer
341 - No summaries or repetition
342 - Hyperlink all URLs
343 - Respond in user\'s language
344 - Minor chit chat or conversation is okay, but try to keep it focused on [insert topic]
345
346 # Knowledge Base Requirements - PREVENT HALLUCINATIONS
347 - ONLY answer questions using information explicitly provided in OFFICIAL KNOWLEDGE DATABASE CONTENT sections marked with ===== delimiters
348 - If required information is NOT in the knowledge database: "I don\'t have enough information in my knowledge base to answer that question accurately."
349 - NEVER invent or hallucinate URLs, links, product specs, procedures, dates, statistics, names, contacts, or company information
350 - When knowledge base information is unclear or contradictory, acknowledge the limitation rather than guessing
351 - Better to admit insufficient information than provide inaccurate answers',
352 'model' => esc_html__('gpt-5.1-chat-latest', 'mxchat'),
353 'rate_limit_logged_out' => esc_html__('100', 'mxchat'),
354 'role_rate_limits' => array(),
355 'rate_limit_message' => esc_html__('Rate limit exceeded. Please try again later.', 'mxchat'),
356 'enable_email_block' => '',
357 'email_blocker_header_content' => __("<h2>Welcome to Our Chat!</h2>\n<p>Let's get started. Enter your email to begin chatting with us.</p>", 'mxchat'),
358 'email_blocker_button_text' => esc_html__('Start Chat', 'mxchat'),
359 'enable_name_field' => 'off', // NEW
360 'name_field_placeholder' => esc_html__('Enter your name', 'mxchat'), // NEW
361 'top_bar_title' => esc_html__('MxChat', 'mxchat'),
362 'intro_message' => __('Hello! How can I assist you today?', 'mxchat'),
363 'ai_agent_text' => esc_html__('AI Agent', 'mxchat'),
364 'input_copy' => esc_html__('How can I assist?', 'mxchat'),
365 'append_to_body' => esc_html__('off', 'mxchat'),
366 'post_type_visibility_mode' => 'all', // 'all', 'include', 'exclude'
367 'post_type_visibility_list' => array(), // Array of post type slugs
368 'contextual_awareness_toggle' => 'off',
369 'citation_links_toggle' => 'on',
370 'satisfaction_rating_enabled' => 'off',
371 'satisfaction_rating_idle_seconds' => 60,
372 'satisfaction_rating_question' => '',
373 'satisfaction_rating_thanks' => '',
374 'satisfaction_rating_placeholder' => '',
375 'satisfaction_rating_saved' => '',
376 'close_button_color' => esc_html__('#fff', 'mxchat'),
377 'chatbot_bg_color' => esc_html__('#fff', 'mxchat'),
378 'user_message_bg_color' => esc_html__('#fff', 'mxchat'),
379 'user_message_font_color' => esc_html__('#212121', 'mxchat'),
380 'bot_message_bg_color' => esc_html__('#212121', 'mxchat'),
381 'bot_message_font_color' => esc_html__('#fff', 'mxchat'),
382 'top_bar_bg_color' => esc_html__('#212121', 'mxchat'),
383 'send_button_font_color' => esc_html__('#212121', 'mxchat'),
384 'chat_input_font_color' => esc_html__('#212121', 'mxchat'),
385 'chatbot_background_color' => esc_html__('#212121', 'mxchat'),
386 'icon_color' => esc_html__('#fff', 'mxchat'),
387 'enable_woocommerce_integration' => esc_html__('0', 'mxchat'),
388 'link_target_toggle' => esc_html__('off', 'mxchat'),
389 'pre_chat_message' => esc_html__('Hey there! Ask me anything!', 'mxchat'),
390
391 // New fields for Loops Integration
392 'loops_api_key' => '',
393 'loops_mailing_list' => '',
394 'triggered_phrase_response' => __('Would you like to join our mailing list? Please provide your email below.', 'mxchat'),
395 'email_capture_response' => __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat'),
396 'popular_question_1' => '',
397 'popular_question_2' => '',
398 'popular_question_3' => '',
399 'pdf_intent_trigger_text' => __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat'),
400 'pdf_intent_success_text' => __("I've processed the PDF. What questions do you have about it?", 'mxchat'),
401 'pdf_intent_error_text' => __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat'),
402 'pdf_max_pages' => 69,
403 'show_pdf_upload_button' => 'on',
404 'show_word_upload_button' => 'on',
405
406 // Live Agent Integration (Slack)
407 'live_agent_webhook_url' => '',
408 'live_agent_secret_key' => '',
409 'live_agent_bot_token' => '',
410 'live_agent_message_bg_color' => esc_html__('#ffffff', 'mxchat'),
411 'live_agent_message_font_color' => esc_html__('#333333', 'mxchat'),
412
413 // Telegram Integration
414 'telegram_status' => 'off',
415 'telegram_bot_token' => '',
416 'telegram_group_id' => '',
417 'telegram_webhook_secret' => '',
418 'telegram_notification_message' => __("I've notified a support agent. Please allow a moment for them to respond. If you'd like to continue with AI, just type \"Switch to AI\" at any time.", 'mxchat'),
419 'telegram_away_message' => __("I just checked, and it looks like our support team isn't personally available at the moment. If you'd like, you can leave your email address, and they'll get back to you as soon as possible. In the meantime, you can keep chatting with me — just let me know how I can help!", 'mxchat'),
420
421 'chat_toolbar_toggle' => esc_html__('off', 'mxchat'),
422 'mode_indicator_bg_color' => esc_html__('#767676', 'mxchat'),
423 'mode_indicator_font_color' => esc_html__('#ffffff', 'mxchat'),
424 'toolbar_icon_color' => esc_html__('#212121', 'mxchat'),
425
426 // Optimization settings
427 'script_loading_strategy' => 'default',
428
429 // Debug settings
430 'debug_mode' => 'off',
431 );
432
433
434 // Merge existing options with defaults
435 $existing_options = get_option('mxchat_options', array());
436 $merged_options = wp_parse_args($existing_options, $default_options);
437
438 // Update the options if they have changed
439 if ($existing_options !== $merged_options) {
440 update_option('mxchat_options', $merged_options);
441 }
442
443 // Add default limits for each role
444 $roles = wp_roles()->get_names();
445 foreach ($roles as $role_id => $role_name) {
446 $default_options['role_rate_limits'][$role_id] = esc_html__('100', 'mxchat');
447 }
448
449 return $default_options;
450
451 // Update the $this->options property
452 $this->options = $merged_options;
453 }
454
455 public function mxchat_add_plugin_page() {
456 // Onboarding lifecycle helpers (admin_init redirect + ajax handlers live in the file).
457 require_once plugin_dir_path(__FILE__) . 'admin-onboarding-page.php';
458
459 // Main menu page — `mxchat-max` remains the parent slug for every MxChat submenu
460 // (Settings, Knowledge, Transcripts, …). Hitting `?page=mxchat-max` directly now
461 // dispatches to the Onboarding page (or Settings if the user has dismissed onboarding).
462 add_menu_page(
463 esc_html__('MxChat', 'mxchat'),
464 esc_html__('MxChat', 'mxchat'),
465 'manage_options',
466 'mxchat-max',
467 array($this, 'mxchat_create_dashboard_page'),
468 'dashicons-testimonial',
469 6
470 );
471
472 // Onboarding submenu — first child under MxChat (plan-d14e89).
473 // First registration uses menu_slug === parent slug 'mxchat-max' →
474 // WP-canonical override of the auto-duplicate "MxChat" entry. Result: the
475 // first child shows as "Onboarding" instead of a redundant pair.
476 add_submenu_page(
477 'mxchat-max',
478 esc_html__('MxChat Onboarding', 'mxchat'),
479 esc_html__('Onboarding', 'mxchat'),
480 'manage_options',
481 'mxchat-max',
482 array($this, 'mxchat_create_dashboard_page')
483 );
484 // Hidden route for `?page=mxchat-onboarding` (Settings "Show again" link
485 // + legacy redirects still target this slug). Parent === null keeps it
486 // out of the menu while remaining accessible by URL.
487 add_submenu_page(
488 null,
489 esc_html__('MxChat Onboarding', 'mxchat'),
490 esc_html__('Onboarding', 'mxchat'),
491 'manage_options',
492 'mxchat-onboarding',
493 array($this, 'mxchat_create_dashboard_page')
494 );
495
496 // Settings submenu — same callback as before, just at a new slug.
497 add_submenu_page(
498 'mxchat-max',
499 esc_html__('MxChat Settings', 'mxchat'),
500 esc_html__('Settings', 'mxchat'),
501 'manage_options',
502 'mxchat-settings',
503 array($this, 'mxchat_create_admin_page')
504 );
505
506 // Submenu page for Knowledge
507 add_submenu_page(
508 'mxchat-max',
509 esc_html__('Prompts', 'mxchat'),
510 esc_html__('Knowledge', 'mxchat'),
511 'manage_options',
512 'mxchat-prompts',
513 array($this, 'mxchat_create_prompts_page')
514 );
515
516 add_submenu_page(
517 'mxchat-max',
518 esc_html__('Chat Transcripts', 'mxchat'),
519 esc_html__('Transcripts', 'mxchat'),
520 'manage_options',
521 'mxchat-transcripts',
522 array($this, 'mxchat_create_transcripts_page')
523 );
524
525 add_submenu_page(
526 'mxchat-max',
527 esc_html__('MxChat Actions', 'mxchat'),
528 esc_html__('Actions', 'mxchat'),
529 'manage_options',
530 'mxchat-actions',
531 array($this, 'mxchat_actions_page_html')
532 );
533
534 // Content Generator page
535 add_submenu_page(
536 'mxchat-max',
537 esc_html__('Content', 'mxchat'),
538 esc_html__('Content', 'mxchat'),
539 'manage_options',
540 'mxchat-content',
541 array($this, 'mxchat_create_content_page')
542 );
543
544 }
545
546 /**
547 * Register the Pro & Extensions submenu on a later admin_menu priority so it
548 * always renders as the bottom-most item in the MxChat sidebar — below
549 * configuration pages like API Access (priority 20).
550 */
551 public function mxchat_add_pro_extensions_page() {
552 add_submenu_page(
553 'mxchat-max',
554 esc_html__('Pro & Extensions', 'mxchat'),
555 esc_html__('Pro & Extensions', 'mxchat'),
556 'manage_options',
557 'mxchat-activation',
558 array($this, 'mxchat_create_activation_page')
559 );
560 }
561
562 public function mxchat_create_addons_page() {
563 require_once plugin_dir_path(__FILE__) . 'class-mxchat-addons.php';
564 $addons_page = new MxChat_Addons();
565 $addons_page->render_page();
566 }
567
568 /**
569 * Render the Content Generator admin page
570 */
571 public function mxchat_create_content_page() {
572 require_once plugin_dir_path(__FILE__) . 'admin-content-page.php';
573 mxchat_render_content_page($this);
574 }
575
576 /**
577 * Test actual streaming functionality in the WordPress environment
578 */
579 public function mxchat_handle_test_streaming_actual() {
580 check_ajax_referer('mxchat_test_streaming_nonce', 'nonce');
581
582 // Check if headers have already been sent
583 if (headers_sent()) {
584 wp_send_json_error(['message' => 'Headers already sent - streaming not possible']);
585 return;
586 }
587
588 // Check for required functions
589 if (!function_exists('curl_init')) {
590 wp_send_json_error(['message' => 'cURL not available - streaming requires cURL']);
591 return;
592 }
593
594 // Get user's selected model and API key
595 $options = get_option('mxchat_options', []);
596 $selected_model = $options['model'] ?? 'gpt-5.1-chat-latest';
597
598 // Get the provider from the model
599 $model_parts = explode('-', $selected_model);
600 $provider = strtolower($model_parts[0]);
601
602 // Get the appropriate API key
603 $api_key = '';
604 switch ($provider) {
605 case 'gpt':
606 case 'o1':
607 $api_key = $options['api_key'] ?? '';
608 break;
609 case 'claude':
610 $api_key = $options['claude_api_key'] ?? '';
611 break;
612 case 'grok':
613 $api_key = $options['xai_api_key'] ?? '';
614 break;
615 case 'deepseek':
616 $api_key = $options['deepseek_api_key'] ?? '';
617 break;
618 case 'gemini':
619 $api_key = $options['gemini_api_key'] ?? '';
620 break;
621 default:
622 // Default to OpenAI for unknown models
623 $api_key = $options['api_key'] ?? '';
624 $provider = 'gpt';
625 break;
626 }
627
628 if (empty($api_key)) {
629 wp_send_json_error(['message' => "API key not configured for {$provider} provider"]);
630 return;
631 }
632
633 // Test streaming with the selected model and provider
634 try {
635 $this->perform_streaming_test($provider, $selected_model, $api_key);
636 } catch (Exception $e) {
637 wp_send_json_error(['message' => 'Streaming test exception: ' . $e->getMessage()]);
638 }
639 }
640
641 /**
642 * Perform the actual streaming test
643 */
644 private function perform_streaming_test($provider, $model, $api_key) {
645 // Set streaming headers
646 header('Content-Type: text/event-stream');
647 header('Cache-Control: no-cache');
648 header('X-Accel-Buffering: no'); // Disable nginx buffering
649
650 // Prepare test message
651 $test_message = "Please respond with exactly: 'Streaming test successful!' - send this as a short response for testing.";
652
653 // Configure API request based on provider
654 $url = '';
655 $headers = [];
656 $body = [];
657
658 switch ($provider) {
659 case 'gpt':
660 case 'o1':
661 $url = 'https://api.openai.com/v1/chat/completions';
662 $headers = [
663 'Content-Type: application/json',
664 'Authorization: Bearer ' . $api_key
665 ];
666 $body = [
667 'model' => $model,
668 'messages' => [['role' => 'user', 'content' => $test_message]],
669 'max_tokens' => 50,
670 'temperature' => 0.3,
671 'stream' => true
672 ];
673 break;
674
675 case 'claude':
676 $url = 'https://api.anthropic.com/v1/messages';
677 $headers = [
678 'Content-Type: application/json',
679 'x-api-key: ' . $api_key,
680 'anthropic-version: 2023-06-01'
681 ];
682 $body = [
683 'model' => $model,
684 'messages' => [['role' => 'user', 'content' => $test_message]],
685 'max_tokens' => 50,
686 'temperature' => 0.3,
687 'stream' => true
688 ];
689 // Claude flagships from Opus 4.7 onward reject the temperature
690 // param outright (400). Reuse the catalog's decision — this path
691 // previously sent temperature unconditionally, so the streaming
692 // test was broken for Opus 4.7/4.8, Fable 5 and Sonnet 5.
693 if (class_exists('MxChat_Model_Catalog')
694 && method_exists('MxChat_Model_Catalog', 'supports_temperature')
695 && !MxChat_Model_Catalog::supports_temperature($model)) {
696 unset($body['temperature']);
697 }
698 break;
699
700 case 'grok':
701 $url = 'https://api.x.ai/v1/chat/completions';
702 $headers = [
703 'Content-Type: application/json',
704 'Authorization: Bearer ' . $api_key
705 ];
706 $body = [
707 'model' => $model,
708 'messages' => [['role' => 'user', 'content' => $test_message]],
709 'max_tokens' => 50,
710 'temperature' => 0.3,
711 'stream' => true
712 ];
713 break;
714
715 case 'deepseek':
716 $url = 'https://api.deepseek.com/v1/chat/completions';
717 $headers = [
718 'Content-Type: application/json',
719 'Authorization: Bearer ' . $api_key
720 ];
721 $body = [
722 'model' => $model,
723 'messages' => [['role' => 'user', 'content' => $test_message]],
724 'max_tokens' => 50,
725 'temperature' => 0.3,
726 'stream' => true,
727 // DeepSeek V4 defaults to thinking mode ON — reasoning would
728 // consume the 50-token test budget and return no visible text.
729 'thinking' => ['type' => 'disabled']
730 ];
731 break;
732
733 default:
734 echo "data: " . json_encode(['error' => 'Unsupported provider for streaming test: ' . $provider]) . "\n\n";
735 flush();
736 return;
737 }
738
739 // Initialize cURL
740 $ch = curl_init();
741 curl_setopt($ch, CURLOPT_URL, $url);
742 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
743 curl_setopt($ch, CURLOPT_POST, true);
744 curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
745 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
746 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
747 curl_setopt($ch, CURLOPT_TIMEOUT, 30);
748 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use ($provider) {
749 return $this->process_streaming_test_data($data, $provider);
750 });
751
752 // Send initial test message
753 echo "data: " . json_encode(['content' => '[Starting streaming test...]']) . "\n\n";
754 flush();
755
756 $result = curl_exec($ch);
757 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
758 $curl_error = curl_error($ch);
759 curl_close($ch);
760
761 if ($curl_error) {
762 echo "data: " . json_encode(['error' => 'cURL Error: ' . $curl_error]) . "\n\n";
763 flush();
764 return;
765 }
766
767 if ($http_code !== 200) {
768 echo "data: " . json_encode(['error' => 'API returned HTTP ' . $http_code]) . "\n\n";
769 flush();
770 return;
771 }
772
773 // Send completion signal
774 echo "data: [DONE]\n\n";
775 flush();
776 }
777
778 /**
779 * Process streaming data for the test
780 */
781 private function process_streaming_test_data($data, $provider) {
782 static $chunk_count = 0;
783
784 $lines = explode("\n", $data);
785
786 foreach ($lines as $line) {
787 if (trim($line) === '') {
788 continue;
789 }
790
791 // Handle different provider formats
792 if ($provider === 'claude') {
793 // Claude uses event: and data: format
794 if (strpos($line, 'data: ') === 0) {
795 $json_str = substr($line, 6);
796 $json = json_decode($json_str, true);
797
798 if (isset($json['type']) && $json['type'] === 'content_block_delta') {
799 if (isset($json['delta']['text'])) {
800 $chunk_count++;
801 echo "data: " . json_encode([
802 'content' => $json['delta']['text'],
803 'test_chunk' => $chunk_count
804 ]) . "\n\n";
805 flush();
806 }
807 }
808 }
809 } else {
810 // OpenAI, X.AI, DeepSeek format
811 if (strpos($line, 'data: ') === 0) {
812 $json_str = substr($line, 6);
813
814 if ($json_str === '[DONE]') {
815 // Don't echo [DONE] here, let the main function handle it
816 continue;
817 }
818
819 $json = json_decode($json_str, true);
820 if (isset($json['choices'][0]['delta']['content'])) {
821 $chunk_count++;
822 echo "data: " . json_encode([
823 'content' => $json['choices'][0]['delta']['content'],
824 'test_chunk' => $chunk_count
825 ]) . "\n\n";
826 flush();
827 }
828 }
829 }
830 }
831
832 return strlen($data);
833 }
834
835 /**
836 * Updated version of your existing test method (keep this as a fallback)
837 */
838 public function mxchat_handle_test_streaming() {
839 check_ajax_referer('mxchat_test_streaming_nonce', 'nonce');
840
841 // Use the actual streaming test instead
842 $this->mxchat_handle_test_streaming_actual();
843 }
844
845
846 public function register_pinecone_settings() {
847 register_setting(
848 'mxchat_pinecone_addon_options',
849 'mxchat_pinecone_addon_options',
850 array(
851 'type' => 'array',
852 'sanitize_callback' => array($this, 'sanitize_pinecone_settings'),
853 'default' => array(
854 'mxchat_use_pinecone' => '0',
855 'mxchat_pinecone_api_key' => '',
856 'mxchat_pinecone_host' => '',
857 'mxchat_pinecone_index' => '',
858 'mxchat_pinecone_environment' => ''
859 )
860 )
861 );
862 }
863
864 public function sanitize_pinecone_settings($input) {
865 $sanitized = array();
866
867 $sanitized['mxchat_use_pinecone'] = isset($input['mxchat_use_pinecone']) ? '1' : '0';
868 $sanitized['mxchat_pinecone_api_key'] = sanitize_text_field($input['mxchat_pinecone_api_key'] ?? '');
869 $sanitized['mxchat_pinecone_host'] = sanitize_text_field($input['mxchat_pinecone_host'] ?? '');
870 $sanitized['mxchat_pinecone_index'] = sanitize_text_field($input['mxchat_pinecone_index'] ?? '');
871 $sanitized['mxchat_pinecone_environment'] = sanitize_text_field($input['mxchat_pinecone_environment'] ?? '');
872
873 // Remove https:// from host if present
874 $sanitized['mxchat_pinecone_host'] = str_replace(['https://', 'http://'], '', $sanitized['mxchat_pinecone_host']);
875
876 return $sanitized;
877 }
878
879 public function register_openai_vectorstore_settings() {
880 register_setting(
881 'mxchat_openai_vectorstore_options',
882 'mxchat_openai_vectorstore_options',
883 array(
884 'type' => 'array',
885 'sanitize_callback' => array($this, 'sanitize_openai_vectorstore_settings'),
886 'default' => array(
887 'mxchat_use_openai_vectorstore' => '0',
888 'mxchat_vectorstore_ids' => '',
889 'mxchat_vectorstore_max_results' => 5
890 )
891 )
892 );
893 }
894
895 public function sanitize_openai_vectorstore_settings($input) {
896 $sanitized = array();
897
898 $sanitized['mxchat_use_openai_vectorstore'] = isset($input['mxchat_use_openai_vectorstore']) ? '1' : '0';
899 $sanitized['mxchat_vectorstore_ids'] = sanitize_text_field($input['mxchat_vectorstore_ids'] ?? '');
900 $sanitized['mxchat_vectorstore_max_results'] = absint($input['mxchat_vectorstore_max_results'] ?? 5);
901
902 // Ensure max results is within reasonable range
903 if ($sanitized['mxchat_vectorstore_max_results'] < 1) {
904 $sanitized['mxchat_vectorstore_max_results'] = 1;
905 }
906 if ($sanitized['mxchat_vectorstore_max_results'] > 20) {
907 $sanitized['mxchat_vectorstore_max_results'] = 20;
908 }
909
910 return $sanitized;
911 }
912
913 /**
914 * AJAX handler to test OpenAI Vector Store connection
915 */
916 public function mxchat_test_vectorstore_connection() {
917 check_ajax_referer('mxchat_admin_nonce', 'nonce');
918
919 if (!current_user_can('manage_options')) {
920 wp_send_json_error(array('message' => __('Permission denied.', 'mxchat')));
921 return;
922 }
923
924 $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
925 $vectorstore_ids = $vectorstore_options['mxchat_vectorstore_ids'] ?? '';
926 $mxchat_options = get_option('mxchat_options', array());
927 $api_key = $mxchat_options['api_key'] ?? '';
928
929 if (empty($api_key)) {
930 wp_send_json_error(array('message' => __('OpenAI API key is not configured.', 'mxchat')));
931 return;
932 }
933
934 if (empty($vectorstore_ids)) {
935 wp_send_json_error(array('message' => __('No Vector Store ID configured.', 'mxchat')));
936 return;
937 }
938
939 // Get the first Vector Store ID for testing
940 $ids_array = array_map('trim', explode(',', $vectorstore_ids));
941 $test_id = $ids_array[0];
942
943 // Test by retrieving the Vector Store info
944 $response = wp_remote_get(
945 'https://api.openai.com/v1/vector_stores/' . $test_id,
946 array(
947 'headers' => array(
948 'Authorization' => 'Bearer ' . $api_key,
949 'Content-Type' => 'application/json',
950 'OpenAI-Beta' => 'assistants=v2'
951 ),
952 'timeout' => 30
953 )
954 );
955
956 if (is_wp_error($response)) {
957 wp_send_json_error(array('message' => __('Connection failed: ', 'mxchat') . $response->get_error_message()));
958 return;
959 }
960
961 $status_code = wp_remote_retrieve_response_code($response);
962 $body = json_decode(wp_remote_retrieve_body($response), true);
963
964 if ($status_code === 200 && isset($body['id'])) {
965 $file_count = $body['file_counts']['completed'] ?? 0;
966 $name = $body['name'] ?? $test_id;
967 wp_send_json_success(array(
968 'message' => sprintf(
969 __('Connected successfully! Vector Store: %s (%d files)', 'mxchat'),
970 esc_html($name),
971 $file_count
972 )
973 ));
974 } elseif ($status_code === 404) {
975 wp_send_json_error(array('message' => __('Vector Store not found. Please check the ID.', 'mxchat')));
976 } elseif ($status_code === 401) {
977 wp_send_json_error(array('message' => __('Invalid API key.', 'mxchat')));
978 } else {
979 $error_message = $body['error']['message'] ?? __('Unknown error occurred.', 'mxchat');
980 wp_send_json_error(array('message' => $error_message));
981 }
982 }
983
984 /**
985 * AJAX handler to test Slack connection and validate scopes
986 */
987 public function mxchat_test_slack_connection() {
988 check_ajax_referer('mxchat_admin_nonce', 'nonce');
989
990 if (!current_user_can('manage_options')) {
991 wp_send_json_error(array('message' => __('Permission denied.', 'mxchat')));
992 return;
993 }
994
995 $bot_token = $this->options['live_agent_bot_token'] ?? '';
996
997 if (empty($bot_token)) {
998 wp_send_json_error(array('message' => __('Slack Bot Token is not configured. Please enter your token and save settings first.', 'mxchat')));
999 return;
1000 }
1001
1002 // Test 1: Validate bot token with auth.test
1003 $auth_response = wp_remote_post('https://slack.com/api/auth.test', array(
1004 'headers' => array(
1005 'Authorization' => 'Bearer ' . $bot_token,
1006 'Content-Type' => 'application/json'
1007 ),
1008 'timeout' => 15
1009 ));
1010
1011 if (is_wp_error($auth_response)) {
1012 wp_send_json_error(array('message' => __('Connection failed: ', 'mxchat') . $auth_response->get_error_message()));
1013 return;
1014 }
1015
1016 $auth_body = json_decode(wp_remote_retrieve_body($auth_response), true);
1017
1018 if (!isset($auth_body['ok']) || !$auth_body['ok']) {
1019 $error = $auth_body['error'] ?? 'unknown_error';
1020 $error_messages = array(
1021 'invalid_auth' => __('Invalid bot token. Please check your token starts with xoxb-', 'mxchat'),
1022 'not_authed' => __('No authentication token provided.', 'mxchat'),
1023 'account_inactive' => __('The Slack workspace has been deactivated.', 'mxchat'),
1024 'token_revoked' => __('The bot token has been revoked. Please generate a new one.', 'mxchat'),
1025 );
1026 $message = $error_messages[$error] ?? sprintf(__('Authentication failed: %s', 'mxchat'), $error);
1027 wp_send_json_error(array('message' => $message));
1028 return;
1029 }
1030
1031 $team_name = $auth_body['team'] ?? 'Unknown Workspace';
1032 $bot_name = $auth_body['user'] ?? 'Unknown Bot';
1033
1034 // Test 2: Check if we can list channels (tests channels:read scope)
1035 $channels_response = wp_remote_post('https://slack.com/api/conversations.list', array(
1036 'headers' => array(
1037 'Authorization' => 'Bearer ' . $bot_token,
1038 'Content-Type' => 'application/json'
1039 ),
1040 'body' => json_encode(array('limit' => 1)),
1041 'timeout' => 15
1042 ));
1043
1044 $channels_body = json_decode(wp_remote_retrieve_body($channels_response), true);
1045 $can_read_channels = isset($channels_body['ok']) && $channels_body['ok'];
1046
1047 // Test 3: Check if we can create channels (tests channels:manage scope)
1048 // We'll just check the error message without actually creating
1049 $create_response = wp_remote_post('https://slack.com/api/conversations.create', array(
1050 'headers' => array(
1051 'Authorization' => 'Bearer ' . $bot_token,
1052 'Content-Type' => 'application/json'
1053 ),
1054 'body' => json_encode(array('name' => 'mxchat-test-' . time(), 'is_private' => false)),
1055 'timeout' => 15
1056 ));
1057
1058 $create_body = json_decode(wp_remote_retrieve_body($create_response), true);
1059
1060 // If channel was created, delete it immediately
1061 if (isset($create_body['ok']) && $create_body['ok'] && isset($create_body['channel']['id'])) {
1062 wp_remote_post('https://slack.com/api/conversations.archive', array(
1063 'headers' => array(
1064 'Authorization' => 'Bearer ' . $bot_token,
1065 'Content-Type' => 'application/json'
1066 ),
1067 'body' => json_encode(array('channel' => $create_body['channel']['id'])),
1068 'timeout' => 15
1069 ));
1070 $can_create_channels = true;
1071 } else {
1072 $create_error = $create_body['error'] ?? '';
1073 // name_taken means we have permission but channel exists
1074 $can_create_channels = ($create_error === 'name_taken' || (isset($create_body['ok']) && $create_body['ok']));
1075
1076 // Check for missing scope errors
1077 if ($create_error === 'missing_scope') {
1078 $can_create_channels = false;
1079 }
1080 }
1081
1082 // Build result message
1083 $results = array();
1084 $results[] = sprintf(__('Workspace: %s', 'mxchat'), esc_html($team_name));
1085 $results[] = sprintf(__('Bot: %s', 'mxchat'), esc_html($bot_name));
1086 $results[] = '';
1087 $results[] = ($can_read_channels ? '✓' : '✗') . ' ' . __('channels:read - List channels', 'mxchat');
1088 $results[] = ($can_create_channels ? '✓' : '✗') . ' ' . __('channels:manage - Create channels', 'mxchat');
1089
1090 $missing_scopes = array();
1091 if (!$can_read_channels) $missing_scopes[] = 'channels:read';
1092 if (!$can_create_channels) $missing_scopes[] = 'channels:manage';
1093
1094 if (!empty($missing_scopes)) {
1095 wp_send_json_error(array(
1096 'message' => implode("\n", $results),
1097 'missing_scopes' => $missing_scopes,
1098 'partial' => true
1099 ));
1100 } else {
1101 wp_send_json_success(array(
1102 'message' => implode("\n", $results)
1103 ));
1104 }
1105 }
1106
1107 public function mxchat_display_admin_notice() {
1108 // Success notice
1109 if ($message = get_transient('mxchat_admin_notice_success')) {
1110 ?>
1111 <div class="notice notice-success is-dismissible">
1112 <p><?php echo esc_html($message); ?></p>
1113 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php echo esc_html__('Dismiss this notice.', 'mxchat'); ?></span></button>
1114 </div>
1115 <?php
1116 delete_transient('mxchat_admin_notice_success'); // Clear the transient after displaying
1117 }
1118
1119 // Error notice
1120 if ($message = get_transient('mxchat_admin_notice_error')) {
1121 ?>
1122 <div class="notice notice-error is-dismissible">
1123 <p><?php echo esc_html($message); ?></p>
1124 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php echo esc_html__('Dismiss this notice.', 'mxchat'); ?></span></button>
1125 </div>
1126 <?php
1127 delete_transient('mxchat_admin_notice_error'); // Clear the transient after displaying
1128 }
1129 }
1130
1131 public function show_live_agent_disabled_banner() {
1132 $show_disabled_notice = get_option('mxchat_show_live_agent_disabled_notice', false);
1133
1134 if ($show_disabled_notice) {
1135 ?>
1136 <div class="mxchat-live-agent-disabled-notice" id="mxchat-disabled-notice">
1137 <div class="mxchat-pro-notification">
1138 <button type="button" class="mxchat-dismiss-btn" onclick="dismissLiveAgentNotice()" aria-label="Dismiss notification">
1139 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1140 <line x1="18" y1="6" x2="6" y2="18"></line>
1141 <line x1="6" y1="6" x2="18" y2="18"></line>
1142 </svg>
1143 </button>
1144 <div class="mxchat-live-agent-content">
1145 <h3>🔧 Live Agent Integration Updated!</h3>
1146 <p>We've temporarily disabled your Live Agent integration due to recent enhancements that have made it much better! You can easily turn it back on by going to <strong>Toolbar & Components → Live Agent Settings</strong> and reviewing the new configuration options.</p>
1147 </div>
1148 </div>
1149 </div>
1150 <?php
1151 }
1152 }
1153 /**
1154 * Render the Onboarding page. Delegates to the procedural renderer in
1155 * includes/admin-onboarding-page.php. Wired to both `?page=mxchat-max`
1156 * (legacy top-level URL) and `?page=mxchat-onboarding` (the canonical
1157 * Onboarding submenu).
1158 *
1159 * When the user has dismissed onboarding and lands on `mxchat-max` (the
1160 * legacy URL, since the Onboarding submenu has been removed), redirect to
1161 * Settings instead — the page is "graduated" and we shouldn't dump them
1162 * back onto it. They can still navigate here directly via the unhide link.
1163 */
1164 public function mxchat_create_dashboard_page() {
1165 require_once plugin_dir_path(__FILE__) . 'admin-onboarding-page.php';
1166
1167 $current = isset($_GET['page']) ? sanitize_key($_GET['page']) : '';
1168 if ($current === 'mxchat-max' && function_exists('mxchat_onboarding_is_dismissed') && mxchat_onboarding_is_dismissed()) {
1169 wp_safe_redirect(admin_url('admin.php?page=mxchat-settings'));
1170 exit;
1171 }
1172
1173 if (function_exists('mxchat_render_onboarding_page')) {
1174 mxchat_render_onboarding_page();
1175 return;
1176 }
1177 // Defensive: if the include failed to load, fall back to the old Settings page
1178 // so the top-level menu never lands on an empty screen.
1179 $this->mxchat_create_admin_page();
1180 }
1181
1182 /**
1183 * Hide the Onboarding submenu when the user has dismissed it (either
1184 * manually or via auto-graduation). The page itself remains routable so
1185 * the Settings "Show MxChat Onboarding again" link can navigate back to it.
1186 */
1187 public function mxchat_apply_onboarding_visibility() {
1188 if (!function_exists('mxchat_onboarding_is_dismissed')) {
1189 return;
1190 }
1191 if (mxchat_onboarding_is_dismissed()) {
1192 // The first MxChat child is the same-slug-as-parent registration
1193 // (slug 'mxchat-max', labelled "Onboarding") added in plan-d14e89.
1194 // Remove it so the menu opens straight to Settings after dismiss.
1195 remove_submenu_page('mxchat-max', 'mxchat-max');
1196 }
1197 }
1198
1199 public function mxchat_create_admin_page() {
1200 $this->add_live_agent_nonce();
1201 $this->add_theme_migration_nonce();
1202
1203 // Include and render the new sidebar-based settings page
1204 require_once plugin_dir_path(__FILE__) . 'admin-settings-page.php';
1205 mxchat_render_settings_page($this);
1206 }
1207
1208 public function dismiss_live_agent_notice() {
1209 // Add debugging
1210 //error_log('dismiss_live_agent_notice called');
1211 //error_log('POST data: ' . print_r($_POST, true));
1212
1213 // Verify nonce
1214 if (!wp_verify_nonce($_POST['nonce'], 'dismiss_live_agent_notice')) {
1215 //error_log('Nonce verification failed');
1216 wp_die('Security check failed');
1217 }
1218
1219 // Remove the notice flag
1220 $deleted = delete_option('mxchat_show_live_agent_disabled_notice');
1221 //error_log('Option deleted: ' . ($deleted ? 'yes' : 'no'));
1222
1223 wp_send_json_success();
1224 }
1225 public function add_live_agent_nonce() {
1226 if (get_option('mxchat_show_live_agent_disabled_notice', false)) {
1227 // Make sure your admin script is enqueued and localize the data
1228 wp_localize_script('mxchat-admin-js', 'mxchatLiveAgent', array(
1229 'nonce' => wp_create_nonce('dismiss_live_agent_notice'),
1230 'ajaxurl' => admin_url('admin-ajax.php')
1231 ));
1232 }
1233 }
1234
1235 /**
1236 * Show theme migration notice for Pro users with AI-generated themes
1237 * Only shown once - dismissible and stored in options
1238 */
1239 public function show_theme_migration_banner() {
1240 // Only show if Pro is activated
1241 if (!$this->is_activated) {
1242 return;
1243 }
1244
1245 // Check if notice should be shown
1246 $show_notice = get_option('mxchat_show_theme_migration_notice', false);
1247
1248 if ($show_notice) {
1249 ?>
1250 <div class="mxchat-theme-migration-notice" id="mxchat-theme-migration-notice">
1251 <div class="mxchat-pro-notification">
1252 <button type="button" class="mxchat-dismiss-btn" onclick="dismissThemeMigrationNotice()" aria-label="Dismiss notification">
1253 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1254 <line x1="18" y1="6" x2="6" y2="18"></line>
1255 <line x1="6" y1="6" x2="18" y2="18"></line>
1256 </svg>
1257 </button>
1258 <div class="mxchat-theme-migration-content">
1259 <h3>🎨 AI Theme Migration Required</h3>
1260 <p>If you're using an AI-generated chatbot theme, you'll need to migrate it to match the new CSS structure. Go to <strong>Theme Settings</strong>, select your theme from the sidebar, and click the <strong>Migrate</strong> button.</p>
1261 </div>
1262 </div>
1263 </div>
1264 <?php
1265 }
1266 }
1267
1268 /**
1269 * Dismiss theme migration notice via AJAX
1270 */
1271 public function dismiss_theme_migration_notice() {
1272 // Verify nonce
1273 if (!wp_verify_nonce($_POST['nonce'], 'dismiss_theme_migration_notice')) {
1274 wp_die('Security check failed');
1275 }
1276
1277 // Remove the notice flag
1278 delete_option('mxchat_show_theme_migration_notice');
1279
1280 wp_send_json_success();
1281 }
1282
1283 /**
1284 * Add nonce for theme migration notice dismiss
1285 */
1286 public function add_theme_migration_nonce() {
1287 if (get_option('mxchat_show_theme_migration_notice', false) && $this->is_activated) {
1288 wp_localize_script('mxchat-admin-js', 'mxchatThemeMigration', array(
1289 'nonce' => wp_create_nonce('dismiss_theme_migration_notice'),
1290 'ajaxurl' => admin_url('admin-ajax.php')
1291 ));
1292 }
1293 }
1294
1295 public function mxchat_create_transcripts_page() {
1296 global $wpdb;
1297 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1298
1299 // Get basic stats
1300 $total_chats = $wpdb->get_var("SELECT COUNT(DISTINCT session_id) FROM $table_name") ?: 0;
1301 $total_messages = $wpdb->get_var("SELECT COUNT(*) FROM $table_name") ?: 0;
1302
1303 // Count unique users with detailed breakdown
1304 $total_users = $wpdb->get_var("
1305 SELECT COUNT(DISTINCT
1306 CASE
1307 WHEN user_email != '' AND user_email IS NOT NULL THEN user_email
1308 WHEN user_id != 0 THEN CONCAT('user_', user_id)
1309 WHEN user_identifier NOT LIKE 'Tech-Savvy User'
1310 AND user_identifier NOT LIKE 'Detail-Oriented User'
1311 AND user_identifier NOT LIKE 'Language Learner'
1312 AND user_identifier NOT LIKE 'Casual Browser'
1313 AND user_identifier NOT LIKE 'Policy Enforcer'
1314 AND user_identifier NOT LIKE 'Researcher'
1315 AND user_identifier NOT LIKE 'Loyalty Member'
1316 AND user_identifier NOT LIKE 'Gift Buyer'
1317 AND user_identifier NOT LIKE 'Parent or Caregiver'
1318 THEN user_identifier
1319 ELSE session_id
1320 END
1321 )
1322 FROM $table_name
1323 WHERE role != 'assistant'
1324 ");
1325
1326 // Get user type breakdown
1327 $registered_users = $wpdb->get_var("
1328 SELECT COUNT(DISTINCT user_email)
1329 FROM $table_name
1330 WHERE user_email != '' AND user_email IS NOT NULL
1331 ");
1332
1333 $guest_users = $wpdb->get_var("
1334 SELECT COUNT(DISTINCT user_identifier)
1335 FROM $table_name
1336 WHERE (user_email = '' OR user_email IS NULL)
1337 AND role != 'assistant'
1338 AND user_identifier NOT LIKE 'Tech-Savvy User'
1339 AND user_identifier NOT LIKE 'Detail-Oriented User'
1340 AND user_identifier NOT LIKE 'Language Learner'
1341 AND user_identifier NOT LIKE 'Casual Browser'
1342 AND user_identifier NOT LIKE 'Policy Enforcer'
1343 AND user_identifier NOT LIKE 'Researcher'
1344 AND user_identifier NOT LIKE 'Loyalty Member'
1345 AND user_identifier NOT LIKE 'Gift Buyer'
1346 AND user_identifier NOT LIKE 'Parent or Caregiver'
1347 ");
1348
1349 // Get agent test messages count
1350 $agent_tests = $wpdb->get_var("
1351 SELECT COUNT(DISTINCT session_id)
1352 FROM $table_name
1353 WHERE user_identifier IN (
1354 'Tech-Savvy User',
1355 'Detail-Oriented User',
1356 'Language Learner',
1357 'Casual Browser',
1358 'Policy Enforcer',
1359 'Researcher',
1360 'Loyalty Member',
1361 'Gift Buyer',
1362 'Parent or Caregiver'
1363 )
1364 ");
1365 // Get activity metrics
1366 $today_chats = $wpdb->get_var("
1367 SELECT COUNT(DISTINCT session_id)
1368 FROM $table_name
1369 WHERE DATE(timestamp) = CURDATE()
1370 ");
1371
1372 $week_chats = $wpdb->get_var("
1373 SELECT COUNT(DISTINCT session_id)
1374 FROM $table_name
1375 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)
1376 ");
1377
1378 $month_chats = $wpdb->get_var("
1379 SELECT COUNT(DISTINCT session_id)
1380 FROM $table_name
1381 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 30 DAY)
1382 ");
1383
1384 // Get daily chat data for last 7 days
1385 $daily_stats = $wpdb->get_results("
1386 SELECT
1387 DATE(timestamp) as date,
1388 COUNT(DISTINCT session_id) as chats,
1389 COUNT(*) as messages
1390 FROM $table_name
1391 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)
1392 GROUP BY DATE(timestamp)
1393 ORDER BY date ASC
1394 ");
1395
1396 // Get average messages per chat
1397 $avg_messages = $wpdb->get_var("
1398 SELECT AVG(message_count)
1399 FROM (
1400 SELECT session_id, COUNT(*) as message_count
1401 FROM $table_name
1402 GROUP BY session_id
1403 ) as chat_counts
1404 ");
1405 $avg_messages = $avg_messages ? round($avg_messages, 1) : 0;
1406
1407 // Get busiest hour
1408 $busiest_hour = $wpdb->get_row("
1409 SELECT HOUR(timestamp) as hour, COUNT(DISTINCT session_id) as chat_count
1410 FROM $table_name
1411 GROUP BY HOUR(timestamp)
1412 ORDER BY chat_count DESC
1413 LIMIT 1
1414 ");
1415
1416 // Prepare chart data
1417 $chart_labels = array();
1418 $chart_chats = array();
1419 $chart_messages = array();
1420
1421 // Fill last 7 days with data
1422 for ($i = 6; $i >= 0; $i--) {
1423 $date = date('Y-m-d', strtotime("-$i days"));
1424 $day_name = date('D', strtotime("-$i days"));
1425 $chart_labels[] = $day_name;
1426
1427 $found = false;
1428 foreach ($daily_stats as $stat) {
1429 if ($stat->date === $date) {
1430 $chart_chats[] = (int)$stat->chats;
1431 $chart_messages[] = (int)$stat->messages;
1432 $found = true;
1433 break;
1434 }
1435 }
1436 if (!$found) {
1437 $chart_chats[] = 0;
1438 $chart_messages[] = 0;
1439 }
1440 }
1441
1442 // Satisfaction rating rollup — last 30 days, grouped by bot (plan-a5b006).
1443 $satisfaction_stats = $this->get_satisfaction_rating_stats(30);
1444
1445 // Prepare page data for the template
1446 $page_data = array(
1447 'total_chats' => $total_chats,
1448 'total_messages' => $total_messages,
1449 'total_users' => $total_users,
1450 'registered_users' => $registered_users,
1451 'guest_users' => $guest_users,
1452 'agent_tests' => $agent_tests,
1453 'today_chats' => $today_chats,
1454 'week_chats' => $week_chats,
1455 'month_chats' => $month_chats,
1456 'avg_messages' => $avg_messages,
1457 'busiest_hour' => $busiest_hour,
1458 'chart_labels' => $chart_labels,
1459 'chart_chats' => $chart_chats,
1460 'chart_messages' => $chart_messages,
1461 'satisfaction_stats' => $satisfaction_stats,
1462 );
1463
1464 // Include and render the new template
1465 require_once plugin_dir_path(__FILE__) . 'admin-transcripts-page.php';
1466 mxchat_render_transcripts_page($this, $page_data);
1467 }
1468
1469 /**
1470 * Per-bot satisfaction rating rollup over the last $days days. Used by the
1471 * Satisfaction card on the Transcripts dashboard (plan-a5b006).
1472 *
1473 * @param int $days Window in days.
1474 * @return array Each entry: ['bot_id', 'total', 'positive', 'negative', 'positive_pct', 'negative_pct'].
1475 */
1476 public function get_satisfaction_rating_stats($days = 30) {
1477 global $wpdb;
1478 $table = $wpdb->prefix . 'mxchat_session_ratings';
1479 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table)) !== $table) {
1480 return array();
1481 }
1482 $days = max(1, (int) $days);
1483 $rows = $wpdb->get_results($wpdb->prepare(
1484 "SELECT bot_id,
1485 COUNT(*) AS total,
1486 SUM(CASE WHEN rating_value = 1 THEN 1 ELSE 0 END) AS positive,
1487 SUM(CASE WHEN rating_value = -1 THEN 1 ELSE 0 END) AS negative
1488 FROM {$table}
1489 WHERE created_at >= DATE_SUB(NOW(), INTERVAL %d DAY)
1490 GROUP BY bot_id
1491 ORDER BY total DESC",
1492 $days
1493 ));
1494 $out = array();
1495 foreach ((array) $rows as $row) {
1496 $total = (int) $row->total;
1497 $positive = (int) $row->positive;
1498 $negative = (int) $row->negative;
1499 $out[] = array(
1500 'bot_id' => $row->bot_id ?: 'default',
1501 'total' => $total,
1502 'positive' => $positive,
1503 'negative' => $negative,
1504 'positive_pct' => $total > 0 ? (int) round(($positive / $total) * 100) : 0,
1505 'negative_pct' => $total > 0 ? (int) round(($negative / $total) * 100) : 0,
1506 );
1507 }
1508 return $out;
1509 }
1510
1511 /**
1512 * Get chart data for transcripts page
1513 * Used by both page render and script localization
1514 *
1515 * @return array Chart data with labels, chats, and messages arrays
1516 */
1517 private function get_transcripts_chart_data() {
1518 global $wpdb;
1519 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1520
1521 // Get daily chat data for last 7 days - same query as mxchat_transcripts_page()
1522 $daily_stats = $wpdb->get_results("
1523 SELECT
1524 DATE(timestamp) as date,
1525 COUNT(DISTINCT session_id) as chats,
1526 COUNT(*) as messages
1527 FROM $table_name
1528 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)
1529 GROUP BY DATE(timestamp)
1530 ORDER BY date ASC
1531 ");
1532
1533 // Prepare chart data
1534 $chart_labels = array();
1535 $chart_chats = array();
1536 $chart_messages = array();
1537
1538 // Fill last 7 days with data - same logic as mxchat_transcripts_page()
1539 for ($i = 6; $i >= 0; $i--) {
1540 $date = date('Y-m-d', strtotime("-$i days"));
1541 $day_name = date('D', strtotime("-$i days"));
1542 $chart_labels[] = $day_name;
1543
1544 $found = false;
1545 if ($daily_stats) {
1546 foreach ($daily_stats as $stat) {
1547 if ($stat->date === $date) {
1548 $chart_chats[] = (int)$stat->chats;
1549 $chart_messages[] = (int)$stat->messages;
1550 $found = true;
1551 break;
1552 }
1553 }
1554 }
1555 if (!$found) {
1556 $chart_chats[] = 0;
1557 $chart_messages[] = 0;
1558 }
1559 }
1560
1561 return array(
1562 'labels' => $chart_labels,
1563 'chats' => $chart_chats,
1564 'messages' => $chart_messages
1565 );
1566 }
1567
1568 public function mxchat_transcripts_notification_section_callback() {
1569 echo '<p>' . esc_html__('Configure email notifications for new chat transcripts. You will receive an email notification when a new chat session begins.', 'mxchat') . '</p>';
1570 }
1571 public function mxchat_enable_notifications_callback() {
1572 $options = get_option('mxchat_transcripts_options', array());
1573 $enabled = isset($options['mxchat_enable_notifications']) ? $options['mxchat_enable_notifications'] : 0;
1574 ?>
1575 <label for="mxchat_enable_notifications">
1576 <input type="checkbox" id="mxchat_enable_notifications"
1577 name="mxchat_transcripts_options[mxchat_enable_notifications]"
1578 value="1" <?php checked(1, $enabled); ?>>
1579 <?php esc_html_e('Send email notification when a new chat session starts', 'mxchat'); ?>
1580 </label>
1581 <p class="description">
1582 <?php esc_html_e('Enable this option to receive email notifications for new chat sessions.', 'mxchat'); ?>
1583 </p>
1584 <?php
1585 }
1586 public function mxchat_notification_email_callback() {
1587 $options = get_option('mxchat_transcripts_options', array());
1588 $email = isset($options['mxchat_notification_email']) ? $options['mxchat_notification_email'] : get_option('admin_email');
1589 ?>
1590 <input type="email" id="mxchat_notification_email"
1591 name="mxchat_transcripts_options[mxchat_notification_email]"
1592 value="<?php echo esc_attr($email); ?>"
1593 class="regular-text">
1594 <p class="description">
1595 <?php esc_html_e('Enter the email address where notifications should be sent. Defaults to the admin email address.', 'mxchat'); ?>
1596 </p>
1597 <?php
1598 }
1599
1600 public function mxchat_auto_delete_transcripts_callback() {
1601 $options = get_option('mxchat_transcripts_options', array());
1602 $interval = isset($options['mxchat_auto_delete_transcripts']) ? $options['mxchat_auto_delete_transcripts'] : 'never';
1603 ?>
1604 <select id="mxchat_auto_delete_transcripts"
1605 name="mxchat_transcripts_options[mxchat_auto_delete_transcripts]">
1606 <option value="never" <?php selected($interval, 'never'); ?>>
1607 <?php esc_html_e('Never (Keep All Transcripts)', 'mxchat'); ?>
1608 </option>
1609 <option value="1week" <?php selected($interval, '1week'); ?>>
1610 <?php esc_html_e('After 1 Week', 'mxchat'); ?>
1611 </option>
1612 <option value="2weeks" <?php selected($interval, '2weeks'); ?>>
1613 <?php esc_html_e('After 2 Weeks', 'mxchat'); ?>
1614 </option>
1615 <option value="1month" <?php selected($interval, '1month'); ?>>
1616 <?php esc_html_e('After 1 Month', 'mxchat'); ?>
1617 </option>
1618 </select>
1619 <p class="description">
1620 <?php esc_html_e('Automatically delete old chat transcripts after the selected time period. This helps manage database size and privacy.', 'mxchat'); ?>
1621 </p>
1622 <?php
1623 }
1624
1625 /**
1626 * Custom retention-days input. When > 0 it overrides the bucket dropdown above
1627 * and deletes transcripts older than the given number of days. Set to 0 to fall
1628 * back to the dropdown (or "Never" if the dropdown is also Never).
1629 *
1630 * Devs can override the final day count via the `mxchat_transcript_retention_days`
1631 * filter — runs in `cleanup_old_transcripts()` after this option is read.
1632 *
1633 * (plan-mxchat-20260509-9b80b1)
1634 */
1635 public function mxchat_retention_days_callback() {
1636 $options = get_option('mxchat_transcripts_options', array());
1637 $days = isset($options['mxchat_retention_days']) ? (int) $options['mxchat_retention_days'] : 0;
1638 ?>
1639 <input type="number"
1640 id="mxchat_retention_days"
1641 name="mxchat_transcripts_options[mxchat_retention_days]"
1642 value="<?php echo esc_attr($days); ?>"
1643 min="0"
1644 max="3650"
1645 step="1"
1646 style="width: 90px;" />
1647 <p class="description">
1648 <?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'); ?>
1649 <br>
1650 <code>apply_filters( 'mxchat_transcript_retention_days', $days )</code>
1651 <?php esc_html_e('lets developers override the final day count programmatically.', 'mxchat'); ?>
1652 </p>
1653 <?php
1654 }
1655
1656 public function mxchat_auto_email_transcript_callback() {
1657 $options = get_option('mxchat_transcripts_options', array());
1658 $enabled = isset($options['mxchat_auto_email_transcript_enabled']) ? $options['mxchat_auto_email_transcript_enabled'] : 0;
1659 $delay = isset($options['mxchat_auto_email_transcript_delay']) ? $options['mxchat_auto_email_transcript_delay'] : '30';
1660 $require_contact = isset($options['mxchat_auto_email_transcript_require_contact']) ? $options['mxchat_auto_email_transcript_require_contact'] : 0;
1661 ?>
1662 <label>
1663 <input type="checkbox"
1664 id="mxchat_auto_email_transcript_enabled"
1665 name="mxchat_transcripts_options[mxchat_auto_email_transcript_enabled]"
1666 value="1"
1667 <?php checked($enabled, 1); ?>>
1668 <?php esc_html_e('Enable Auto-Email of Full Transcript', 'mxchat'); ?>
1669 </label>
1670 <br><br>
1671 <label for="mxchat_auto_email_transcript_delay">
1672 <?php esc_html_e('Send transcript after:', 'mxchat'); ?>
1673 </label>
1674 <select id="mxchat_auto_email_transcript_delay"
1675 name="mxchat_transcripts_options[mxchat_auto_email_transcript_delay]">
1676 <option value="15" <?php selected($delay, '15'); ?>>
1677 <?php esc_html_e('15 minutes', 'mxchat'); ?>
1678 </option>
1679 <option value="30" <?php selected($delay, '30'); ?>>
1680 <?php esc_html_e('30 minutes', 'mxchat'); ?>
1681 </option>
1682 <option value="60" <?php selected($delay, '60'); ?>>
1683 <?php esc_html_e('1 hour', 'mxchat'); ?>
1684 </option>
1685 </select>
1686 <br><br>
1687 <label>
1688 <input type="checkbox"
1689 id="mxchat_auto_email_transcript_require_contact"
1690 name="mxchat_transcripts_options[mxchat_auto_email_transcript_require_contact]"
1691 value="1"
1692 <?php checked($require_contact, 1); ?>>
1693 <?php esc_html_e('Only send if visitor provided contact info', 'mxchat'); ?>
1694 </label>
1695 <p class="description">
1696 <?php esc_html_e('Automatically email the full transcript as a .txt file after the specified time has passed since the last user message. The scheduled email will be cancelled if a new message is received.', 'mxchat'); ?>
1697 <br>
1698 <?php esc_html_e('When "Only send if visitor provided contact info" is enabled, transcripts will only be emailed if the visitor shared an email address or phone number (including WhatsApp) in the chat.', 'mxchat'); ?>
1699 </p>
1700 <?php
1701 }
1702
1703
1704
1705 public function sanitize_transcripts_options($input) {
1706 $sanitized = array();
1707
1708 $sanitized['mxchat_enable_notifications'] = isset($input['mxchat_enable_notifications']) ? 1 : 0;
1709
1710 if (isset($input['mxchat_notification_email'])) {
1711 $sanitized['mxchat_notification_email'] = sanitize_email($input['mxchat_notification_email']);
1712 if (!is_email($sanitized['mxchat_notification_email'])) {
1713 add_settings_error(
1714 'mxchat_transcripts_options',
1715 'invalid_email',
1716 __('Please enter a valid email address for notifications.', 'mxchat'),
1717 'error'
1718 );
1719 $sanitized['mxchat_notification_email'] = get_option('admin_email');
1720 }
1721 }
1722
1723 // Sanitize auto-delete setting
1724 $valid_intervals = array('never', '1week', '2weeks', '1month');
1725 if (isset($input['mxchat_auto_delete_transcripts'])) {
1726 $sanitized['mxchat_auto_delete_transcripts'] = in_array($input['mxchat_auto_delete_transcripts'], $valid_intervals)
1727 ? $input['mxchat_auto_delete_transcripts']
1728 : 'never';
1729 } else {
1730 $sanitized['mxchat_auto_delete_transcripts'] = 'never';
1731 }
1732
1733 // Sanitize custom retention-days override (plan-9b80b1).
1734 if (isset($input['mxchat_retention_days'])) {
1735 $days = (int) $input['mxchat_retention_days'];
1736 $sanitized['mxchat_retention_days'] = max(0, min(3650, $days));
1737 } else {
1738 $sanitized['mxchat_retention_days'] = 0;
1739 }
1740
1741 // Get old values to check if auto-delete or retention-days changed.
1742 $old_options = get_option('mxchat_transcripts_options');
1743 $old_interval = isset($old_options['mxchat_auto_delete_transcripts']) ? $old_options['mxchat_auto_delete_transcripts'] : 'never';
1744 $old_retention = isset($old_options['mxchat_retention_days']) ? (int) $old_options['mxchat_retention_days'] : 0;
1745
1746 // If either setting changed, reschedule the cron job. The schedule_transcript_cleanup
1747 // helper now treats "any active retention" (dropdown != never OR custom days > 0) as a
1748 // reason to keep the daily cron registered.
1749 $interval_changed = ($old_interval !== $sanitized['mxchat_auto_delete_transcripts']);
1750 $retention_changed = ($old_retention !== $sanitized['mxchat_retention_days']);
1751 if ($interval_changed || $retention_changed) {
1752 $any_active = ($sanitized['mxchat_auto_delete_transcripts'] !== 'never') || ($sanitized['mxchat_retention_days'] > 0);
1753 $this->schedule_transcript_cleanup($any_active ? 'active' : 'never');
1754 }
1755
1756 // Sanitize auto-email transcript settings
1757 $sanitized['mxchat_auto_email_transcript_enabled'] = isset($input['mxchat_auto_email_transcript_enabled']) ? 1 : 0;
1758
1759 $valid_delays = array('15', '30', '60');
1760 if (isset($input['mxchat_auto_email_transcript_delay'])) {
1761 $sanitized['mxchat_auto_email_transcript_delay'] = in_array($input['mxchat_auto_email_transcript_delay'], $valid_delays)
1762 ? $input['mxchat_auto_email_transcript_delay']
1763 : '30';
1764 } else {
1765 $sanitized['mxchat_auto_email_transcript_delay'] = '30';
1766 }
1767
1768 // Sanitize require contact info setting
1769 $sanitized['mxchat_auto_email_transcript_require_contact'] = isset($input['mxchat_auto_email_transcript_require_contact']) ? 1 : 0;
1770
1771 return $sanitized;
1772 }
1773
1774 /**
1775 * Schedule or unschedule the transcript cleanup cron job
1776 */
1777 public function schedule_transcript_cleanup($interval) {
1778 // Clear any existing scheduled event
1779 $timestamp = wp_next_scheduled('mxchat_cleanup_old_transcripts');
1780 if ($timestamp) {
1781 wp_unschedule_event($timestamp, 'mxchat_cleanup_old_transcripts');
1782 }
1783
1784 // Schedule new event whenever retention is active. "active" is the canonical
1785 // value passed by sanitize_transcripts_options when either the dropdown != never
1786 // OR the custom retention-days > 0; "never" turns the cron off. Any other value
1787 // (the legacy "1week" / "2weeks" / "1month" strings) is also treated as active.
1788 if ($interval !== 'never') {
1789 // Schedule to run daily at 3 AM
1790 $next_run = strtotime('tomorrow 3:00 AM');
1791 wp_schedule_event($next_run, 'daily', 'mxchat_cleanup_old_transcripts');
1792 }
1793 }
1794
1795 /**
1796 * Self-heal guard: keep the cleanup cron in sync with the retention setting.
1797 *
1798 * Runs on admin_init. Cost when nothing is wrong: one get_option() (cached) and
1799 * one wp_next_scheduled() (reads the cached cron option) — no writes. It only
1800 * schedules when retention is active but the event is missing, and only
1801 * unschedules when retention is off but the event survived. Both directions
1802 * reuse schedule_transcript_cleanup() so there is exactly one scheduling path.
1803 * Legacy dropdown values (1week/2weeks/1month) count as active, matching the
1804 * helper's own semantics. Deliberately not gated on DISABLE_WP_CRON: scheduling
1805 * writes the cron option regardless of how cron is executed, so a server-side
1806 * cron runner still picks the event up.
1807 */
1808 public function ensure_transcript_cleanup_scheduled() {
1809 $options = get_option('mxchat_transcripts_options', array());
1810 $interval = isset($options['mxchat_auto_delete_transcripts']) ? $options['mxchat_auto_delete_transcripts'] : 'never';
1811 $days = isset($options['mxchat_retention_days']) ? (int) $options['mxchat_retention_days'] : 0;
1812 $active = ($interval !== 'never') || ($days > 0);
1813 $scheduled = (bool) wp_next_scheduled('mxchat_cleanup_old_transcripts');
1814
1815 if ($active && !$scheduled) {
1816 $this->schedule_transcript_cleanup('active');
1817 } elseif (!$active && $scheduled) {
1818 $this->schedule_transcript_cleanup('never');
1819 }
1820 }
1821
1822 /**
1823 * Delete old transcripts based on the configured interval
1824 */
1825 public function cleanup_old_transcripts() {
1826 $options = get_option('mxchat_transcripts_options', array());
1827 $interval = isset($options['mxchat_auto_delete_transcripts']) ? $options['mxchat_auto_delete_transcripts'] : 'never';
1828 $custom_days = isset($options['mxchat_retention_days']) ? (int) $options['mxchat_retention_days'] : 0;
1829
1830 // Custom retention-days (plan-9b80b1) takes precedence over the bucket dropdown.
1831 $days = 0;
1832 if ($custom_days > 0) {
1833 $days = $custom_days;
1834 } else {
1835 switch ($interval) {
1836 case '1week': $days = 7; break;
1837 case '2weeks': $days = 14; break;
1838 case '1month': $days = 30; break;
1839 case 'never':
1840 default:
1841 $days = 0;
1842 }
1843 }
1844
1845 // Devs can override the final day count programmatically.
1846 $days = (int) apply_filters('mxchat_transcript_retention_days', $days);
1847
1848 if ($days <= 0) {
1849 return; // Retention disabled — bail.
1850 }
1851
1852 global $wpdb;
1853 $transcripts_table = $wpdb->prefix . 'mxchat_chat_transcripts';
1854 $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
1855 $url_clicks_table = $wpdb->prefix . 'mxchat_url_clicks';
1856
1857 // Defensive: if the main table doesn't exist (fresh-ish install), bail.
1858 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $transcripts_table)) !== $transcripts_table) {
1859 return;
1860 }
1861
1862 $cutoff_date = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
1863
1864 // Cap at 5000 session_ids per run so a large unattended site doesn't OOM —
1865 // the cron will pick up where it left off on the next tick (within hours).
1866 $batch_cap = (int) apply_filters('mxchat_transcript_retention_batch_cap', 5000);
1867
1868 $sessions_to_delete = $wpdb->get_col(
1869 $wpdb->prepare(
1870 "SELECT DISTINCT session_id FROM {$transcripts_table} WHERE timestamp IS NOT NULL AND timestamp < %s LIMIT %d",
1871 $cutoff_date,
1872 $batch_cap
1873 )
1874 );
1875
1876 if (empty($sessions_to_delete)) {
1877 return;
1878 }
1879
1880 $placeholders = implode(',', array_fill(0, count($sessions_to_delete), '%s'));
1881
1882 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1883 // $placeholders is a server-built list of literal "%s" tokens.
1884 $deleted_transcripts = (int) $wpdb->query(
1885 $wpdb->prepare(
1886 "DELETE FROM {$transcripts_table} WHERE session_id IN ($placeholders)",
1887 $sessions_to_delete
1888 )
1889 );
1890
1891 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $translations_table)) === $translations_table) {
1892 $wpdb->query(
1893 $wpdb->prepare(
1894 "DELETE FROM {$translations_table} WHERE session_id IN ($placeholders)",
1895 $sessions_to_delete
1896 )
1897 );
1898 }
1899
1900 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $url_clicks_table)) === $url_clicks_table) {
1901 $wpdb->query(
1902 $wpdb->prepare(
1903 "DELETE FROM {$url_clicks_table} WHERE session_id IN ($placeholders)",
1904 $sessions_to_delete
1905 )
1906 );
1907 }
1908 // phpcs:enable
1909
1910 update_option('mxchat_retention_last_swept_at', time(), false);
1911 update_option('mxchat_retention_rows_last_deleted', $deleted_transcripts, false);
1912 }
1913
1914 public function export_chat_transcripts() {
1915 if (!current_user_can('manage_options')) {
1916 wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'mxchat'));
1917 }
1918
1919 check_ajax_referer('mxchat_export_transcripts', 'security');
1920
1921 global $wpdb;
1922 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1923
1924 // Get all transcripts ordered by session and timestamp
1925 $results = $wpdb->get_results(
1926 "SELECT session_id, user_email, user_identifier, role, message, timestamp
1927 FROM {$table_name}
1928 ORDER BY session_id, timestamp ASC"
1929 );
1930
1931 if (empty($results)) {
1932 wp_send_json_error(array('message' => 'No transcripts found.'));
1933 wp_die();
1934 }
1935
1936 // Set headers for CSV download
1937 header('Content-Type: text/csv');
1938 header('Content-Disposition: attachment; filename="chat-transcripts-' . date('Y-m-d') . '.csv"');
1939 header('Pragma: no-cache');
1940 header('Expires: 0');
1941
1942 // Create output stream
1943 $output = fopen('php://output', 'w');
1944
1945 // Add UTF-8 BOM for proper Excel encoding
1946 fputs($output, "\xEF\xBB\xBF");
1947
1948 // Add CSV headers
1949 fputcsv($output, array(
1950 'Session ID',
1951 'Email',
1952 'User Identifier',
1953 'Role',
1954 'Message',
1955 'Timestamp'
1956 ));
1957
1958 // Add data rows
1959 foreach ($results as $row) {
1960 fputcsv($output, array(
1961 $row->session_id,
1962 $row->user_email,
1963 $row->user_identifier,
1964 $row->role,
1965 $row->message,
1966 $row->timestamp
1967 ));
1968 }
1969
1970 fclose($output);
1971 wp_die();
1972 }
1973
1974 // ============================================================================
1975 // Leads tab (inside Transcripts)
1976 //
1977 // Leads are derived from existing data — no dedicated table. Primary source:
1978 // wp_mxchat_chat_transcripts rows where user_email is populated. Secondary
1979 // source: wp_options entries `mxchat_email_{session_id}` / `mxchat_name_{sid}`
1980 // for "orphan" leads who submitted the pre-chat form but never chatted.
1981 // ============================================================================
1982
1983 /**
1984 * Fetch leads: dedup-by-email rows, stats strip, and top pages in one call.
1985 */
1986 public function mxchat_fetch_leads() {
1987 if (!current_user_can('manage_options')) {
1988 wp_send_json_error(['message' => 'Insufficient permissions']);
1989 wp_die();
1990 }
1991
1992 global $wpdb;
1993 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
1994
1995 $page = isset($_POST['page']) ? max(1, absint($_POST['page'])) : 1;
1996 $per_page = isset($_POST['per_page']) ? min(100, max(10, absint($_POST['per_page']))) : 25;
1997 $offset = ($page - 1) * $per_page;
1998 $search = isset($_POST['search']) ? sanitize_text_field(wp_unslash($_POST['search'])) : '';
1999 $date_range = isset($_POST['date_range']) ? sanitize_key($_POST['date_range']) : 'all';
2000 $status = isset($_POST['status']) ? sanitize_key($_POST['status']) : 'all';
2001 $page_filter = isset($_POST['page_url']) ? esc_url_raw(wp_unslash($_POST['page_url'])) : '';
2002 $sort = isset($_POST['sort']) ? sanitize_key($_POST['sort']) : 'last_seen';
2003 $sort_dir = (isset($_POST['sort_dir']) && $_POST['sort_dir'] === 'asc') ? 'ASC' : 'DESC';
2004
2005 $date_cutoff = self::mxchat_leads_date_cutoff($date_range);
2006 $has_page_url_column = !empty($wpdb->get_results("SHOW COLUMNS FROM $table LIKE 'originating_page_url'"));
2007
2008 // Base WHERE for transcripts leads.
2009 $where_clauses = ["user_email IS NOT NULL", "user_email != ''"];
2010 $where_params = [];
2011
2012 if ($date_cutoff) {
2013 $where_clauses[] = 'timestamp >= %s';
2014 $where_params[] = $date_cutoff;
2015 }
2016 if ($page_filter && $has_page_url_column) {
2017 $where_clauses[] = 'originating_page_url = %s';
2018 $where_params[] = $page_filter;
2019 }
2020 if ($search !== '') {
2021 $like = '%' . $wpdb->esc_like($search) . '%';
2022 $where_clauses[] = '(user_email LIKE %s OR user_name LIKE %s)';
2023 $where_params[] = $like;
2024 $where_params[] = $like;
2025 }
2026 $where_sql = 'WHERE ' . implode(' AND ', $where_clauses);
2027
2028 // Aggregate query grouped by email.
2029 $select_sql = $has_page_url_column
2030 ? "SELECT user_email, MAX(timestamp) AS last_seen, MIN(timestamp) AS first_seen,
2031 COUNT(DISTINCT session_id) AS conversation_count"
2032 : "SELECT user_email, MAX(timestamp) AS last_seen, MIN(timestamp) AS first_seen,
2033 COUNT(DISTINCT session_id) AS conversation_count";
2034
2035 $order_column = in_array($sort, ['last_seen', 'conversation_count', 'first_seen'], true) ? $sort : 'last_seen';
2036 $group_order_limit = " GROUP BY user_email ORDER BY {$order_column} {$sort_dir} LIMIT %d OFFSET %d";
2037
2038 $transcripts_sql = $wpdb->prepare(
2039 "{$select_sql} FROM {$table} {$where_sql}{$group_order_limit}",
2040 array_merge($where_params, [$per_page, $offset])
2041 );
2042 $transcript_rows = $wpdb->get_results($transcripts_sql);
2043
2044 // Count of unique transcript-based leads under the same filters.
2045 $count_sql = $wpdb->prepare(
2046 "SELECT COUNT(DISTINCT user_email) FROM {$table} {$where_sql}",
2047 $where_params
2048 );
2049 $transcripts_lead_count = (int) $wpdb->get_var($count_sql);
2050
2051 // Hydrate each row: name, latest_session_id, top page.
2052 $leads = [];
2053 foreach ($transcript_rows as $row) {
2054 $detail = $has_page_url_column
2055 ? $wpdb->get_row($wpdb->prepare(
2056 "SELECT session_id, user_name, originating_page_url, originating_page_title
2057 FROM {$table} WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
2058 $row->user_email
2059 ))
2060 : $wpdb->get_row($wpdb->prepare(
2061 "SELECT session_id, user_name FROM {$table}
2062 WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
2063 $row->user_email
2064 ));
2065
2066 $leads[] = [
2067 'email' => $row->user_email,
2068 'name' => isset($detail->user_name) ? (string) $detail->user_name : '',
2069 'conversation_count' => (int) $row->conversation_count,
2070 'last_seen' => $row->last_seen,
2071 'last_seen_display' => self::mxchat_leads_format_relative($row->last_seen),
2072 'first_seen' => $row->first_seen,
2073 'latest_session_id' => isset($detail->session_id) ? $detail->session_id : '',
2074 'top_page_url' => isset($detail->originating_page_url) ? $detail->originating_page_url : '',
2075 'top_page_title' => isset($detail->originating_page_title) ? $detail->originating_page_title : '',
2076 'is_orphan' => false,
2077 'status' => 'active',
2078 ];
2079 }
2080
2081 // Non-transcript lead sources. Built once here, then filtered/merged based on the
2082 // status filter below. Deduplication priority when the same email appears in multiple
2083 // sources: transcripts > chat_deleted > orphan.
2084 $transcripts_emails_seen = array_flip(array_map(
2085 function ($r) { return strtolower($r['email']); },
2086 $leads
2087 ));
2088
2089 // Chat-deleted leads: had a conversation that an admin removed. Preserved via
2090 // mxchat_lead_del_* options, with timestamps so they respect date filters.
2091 $chat_deleted_leads_all = [];
2092 if ($status === 'all' || $status === 'chat_deleted') {
2093 $chat_deleted_leads_all = self::mxchat_collect_chat_deleted_leads($search, $date_cutoff);
2094 // Dedup: drop any chat_deleted row whose email is already in the transcripts set.
2095 $chat_deleted_leads_all = array_values(array_filter(
2096 $chat_deleted_leads_all,
2097 function ($row) use ($transcripts_emails_seen) {
2098 return !isset($transcripts_emails_seen[strtolower($row['email'])]);
2099 }
2100 ));
2101 foreach ($chat_deleted_leads_all as $row) {
2102 $transcripts_emails_seen[strtolower($row['email'])] = true;
2103 }
2104 }
2105
2106 // Orphans: pre-chat form captures with no conversation. No timestamp, so skipped
2107 // when a date filter is active.
2108 $orphan_leads_all = [];
2109 if (($status === 'all' || $status === 'orphan') && !$date_cutoff && !$page_filter) {
2110 $orphan_leads_all = self::mxchat_collect_orphan_leads($search);
2111 $orphan_leads_all = array_values(array_filter(
2112 $orphan_leads_all,
2113 function ($row) use ($transcripts_emails_seen) {
2114 return !isset($transcripts_emails_seen[strtolower($row['email'])]);
2115 }
2116 ));
2117 }
2118
2119 // Apply status filter to the transcripts-derived list.
2120 if ($status === 'orphan' || $status === 'chat_deleted') {
2121 $leads = [];
2122 $transcripts_lead_count = 0;
2123 }
2124
2125 // Stitch the current page from the three buckets in priority order.
2126 $total_count = $transcripts_lead_count + count($chat_deleted_leads_all) + count($orphan_leads_all);
2127 $remaining_slots = $per_page - count($leads);
2128
2129 if ($remaining_slots > 0 && !empty($chat_deleted_leads_all)) {
2130 $start = max(0, ($page - 1) * $per_page - $transcripts_lead_count);
2131 if ($start < count($chat_deleted_leads_all)) {
2132 $leads = array_merge($leads, array_slice($chat_deleted_leads_all, $start, $remaining_slots));
2133 $remaining_slots = $per_page - count($leads);
2134 }
2135 }
2136
2137 if ($remaining_slots > 0 && !empty($orphan_leads_all)) {
2138 $before = $transcripts_lead_count + count($chat_deleted_leads_all);
2139 $start = max(0, ($page - 1) * $per_page - $before);
2140 if ($start < count($orphan_leads_all)) {
2141 $leads = array_merge($leads, array_slice($orphan_leads_all, $start, $remaining_slots));
2142 }
2143 }
2144
2145 $total_pages = $per_page > 0 ? (int) ceil($total_count / $per_page) : 1;
2146
2147 // Stats strip: always computed over full dataset, unaffected by filters.
2148 $stats = self::mxchat_leads_stats($table, $has_page_url_column);
2149
2150 // Top pages: top 5 by distinct emails captured.
2151 $top_pages = [];
2152 if ($has_page_url_column) {
2153 $top_pages_rows = $wpdb->get_results(
2154 "SELECT originating_page_url AS url,
2155 MAX(originating_page_title) AS title,
2156 COUNT(DISTINCT user_email) AS lead_count
2157 FROM {$table}
2158 WHERE user_email IS NOT NULL AND user_email != ''
2159 AND originating_page_url IS NOT NULL AND originating_page_url != ''
2160 GROUP BY originating_page_url
2161 ORDER BY lead_count DESC, url ASC
2162 LIMIT 5"
2163 );
2164 foreach ($top_pages_rows as $p) {
2165 $top_pages[] = [
2166 'url' => $p->url,
2167 'title' => $p->title ?: $p->url,
2168 'lead_count' => (int) $p->lead_count,
2169 ];
2170 }
2171 }
2172
2173 wp_send_json([
2174 'success' => true,
2175 'leads' => $leads,
2176 'page' => $page,
2177 'per_page' => $per_page,
2178 'total_count' => $total_count,
2179 'total_pages' => $total_pages,
2180 'showing_start' => $total_count === 0 ? 0 : ($offset + 1),
2181 'showing_end' => min($offset + $per_page, $total_count),
2182 'stats' => $stats,
2183 'top_pages' => $top_pages,
2184 ]);
2185 wp_die();
2186 }
2187
2188 /**
2189 * Delete one or more leads by email. Removes every transcripts row for that
2190 * email and cleans up related wp_options (mxchat_email_{sid}, mxchat_name_{sid},
2191 * mxchat_history_{sid}) and any orphan option entries matching the email.
2192 */
2193 public function mxchat_delete_leads() {
2194 if (!current_user_can('manage_options')) {
2195 wp_send_json_error(['message' => 'Insufficient permissions']);
2196 wp_die();
2197 }
2198 check_ajax_referer('mxchat_delete_leads', 'security');
2199
2200 $emails_raw = isset($_POST['emails']) ? (array) wp_unslash($_POST['emails']) : [];
2201 $emails = [];
2202 foreach ($emails_raw as $e) {
2203 $clean = sanitize_email((string) $e);
2204 if ($clean) {
2205 $emails[] = $clean;
2206 }
2207 }
2208 if (empty($emails)) {
2209 wp_send_json_error(['message' => 'No emails provided']);
2210 wp_die();
2211 }
2212
2213 $summary = self::mxchat_wipe_leads_by_email($emails);
2214
2215 wp_send_json([
2216 'success' => true,
2217 'deleted_leads' => count($emails),
2218 'deleted_sessions' => $summary['deleted_sessions'],
2219 'deleted_rows' => $summary['deleted_rows'],
2220 ]);
2221 wp_die();
2222 }
2223
2224 /**
2225 * Fully wipe one or more leads by email: every transcripts row, every related wp_options
2226 * entry (history, pre-chat capture, chat_deleted preservation, agent name, translations).
2227 *
2228 * Shared between the Leads-tab Delete button and the transcript-delete opt-in checkbox.
2229 * Input emails must already be sanitized with sanitize_email().
2230 */
2231 private static function mxchat_wipe_leads_by_email(array $emails) {
2232 global $wpdb;
2233 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2234 $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
2235 $has_translations = $wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table;
2236
2237 $deleted_sessions = 0;
2238 $deleted_rows = 0;
2239 $emails_lc = array_map('strtolower', $emails);
2240
2241 foreach ($emails as $email) {
2242 $session_ids = $wpdb->get_col($wpdb->prepare(
2243 "SELECT DISTINCT session_id FROM {$table} WHERE user_email = %s",
2244 $email
2245 ));
2246
2247 $rows_removed = $wpdb->delete($table, ['user_email' => $email], ['%s']);
2248 if ($rows_removed !== false) {
2249 $deleted_rows += (int) $rows_removed;
2250 }
2251
2252 foreach ($session_ids as $sid) {
2253 $deleted_sessions++;
2254 wp_cache_delete('chat_session_' . $sid, 'mxchat_chat_sessions');
2255 delete_option('mxchat_history_' . $sid);
2256 delete_option('mxchat_email_' . $sid);
2257 delete_option('mxchat_name_' . $sid);
2258 delete_option('mxchat_agent_name_' . $sid);
2259 delete_option('mxchat_lead_del_email_' . $sid);
2260 delete_option('mxchat_lead_del_name_' . $sid);
2261 delete_option('mxchat_lead_del_ts_' . $sid);
2262 if ($has_translations) {
2263 $wpdb->delete($translations_table, ['session_id' => $sid], ['%s']);
2264 }
2265 }
2266 }
2267
2268 // Clean up any lingering option entries (orphan pre-chat captures + chat_deleted
2269 // preservations) whose stored value matches one of the emails being wiped.
2270 $lingering = $wpdb->get_results(
2271 "SELECT option_name, option_value FROM {$wpdb->options}
2272 WHERE option_name LIKE 'mxchat_email_%' OR option_name LIKE 'mxchat_lead_del_email_%'"
2273 );
2274 foreach ($lingering as $opt) {
2275 if (!in_array(strtolower(trim($opt->option_value)), $emails_lc, true)) {
2276 continue;
2277 }
2278 if (strpos($opt->option_name, 'mxchat_lead_del_email_') === 0) {
2279 $sid = substr($opt->option_name, strlen('mxchat_lead_del_email_'));
2280 delete_option('mxchat_lead_del_email_' . $sid);
2281 delete_option('mxchat_lead_del_name_' . $sid);
2282 delete_option('mxchat_lead_del_ts_' . $sid);
2283 } else {
2284 $sid = substr($opt->option_name, strlen('mxchat_email_'));
2285 delete_option('mxchat_email_' . $sid);
2286 delete_option('mxchat_name_' . $sid);
2287 }
2288 }
2289
2290 wp_cache_delete('all_chat_sessions', 'mxchat_chat_sessions');
2291
2292 return [
2293 'deleted_sessions' => $deleted_sessions,
2294 'deleted_rows' => $deleted_rows,
2295 ];
2296 }
2297
2298 /**
2299 * Stream a leads CSV. scope=all exports every lead under current filters is not
2300 * supported to keep semantics simple; caller either exports all leads or a
2301 * specific set of selected emails.
2302 */
2303 public function mxchat_export_leads() {
2304 if (!current_user_can('manage_options')) {
2305 wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'mxchat'));
2306 }
2307 check_ajax_referer('mxchat_export_leads', 'security');
2308
2309 $scope = isset($_POST['scope']) ? sanitize_key($_POST['scope']) : 'all';
2310 $fields_mode = isset($_POST['fields']) ? sanitize_key($_POST['fields']) : 'email_and_name';
2311 $emails_in = isset($_POST['emails']) ? (array) wp_unslash($_POST['emails']) : [];
2312
2313 $emails_in_clean = [];
2314 foreach ($emails_in as $e) {
2315 $clean = sanitize_email((string) $e);
2316 if ($clean) {
2317 $emails_in_clean[] = $clean;
2318 }
2319 }
2320
2321 global $wpdb;
2322 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2323 $has_page_url_column = !empty($wpdb->get_results("SHOW COLUMNS FROM $table LIKE 'originating_page_url'"));
2324
2325 // Collect leads from transcripts.
2326 $transcripts_sql = "SELECT user_email AS email,
2327 MAX(timestamp) AS last_seen,
2328 COUNT(DISTINCT session_id) AS conversation_count
2329 FROM {$table}
2330 WHERE user_email IS NOT NULL AND user_email != ''";
2331 $params = [];
2332 if ($scope === 'selected' && !empty($emails_in_clean)) {
2333 $placeholders = implode(',', array_fill(0, count($emails_in_clean), '%s'));
2334 $transcripts_sql .= " AND user_email IN ({$placeholders})";
2335 $params = $emails_in_clean;
2336 }
2337 $transcripts_sql .= " GROUP BY user_email ORDER BY last_seen DESC";
2338
2339 $rows = !empty($params)
2340 ? $wpdb->get_results($wpdb->prepare($transcripts_sql, $params))
2341 : $wpdb->get_results($transcripts_sql);
2342
2343 // Hydrate each row with name + top page.
2344 $export_rows = [];
2345 foreach ($rows as $row) {
2346 $detail = $has_page_url_column
2347 ? $wpdb->get_row($wpdb->prepare(
2348 "SELECT user_name, originating_page_url FROM {$table}
2349 WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
2350 $row->email
2351 ))
2352 : $wpdb->get_row($wpdb->prepare(
2353 "SELECT user_name FROM {$table}
2354 WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
2355 $row->email
2356 ));
2357 $export_rows[] = [
2358 'email' => $row->email,
2359 'name' => isset($detail->user_name) ? (string) $detail->user_name : '',
2360 'conversation_count' => (int) $row->conversation_count,
2361 'last_seen' => $row->last_seen,
2362 'top_page_url' => isset($detail->originating_page_url) ? $detail->originating_page_url : '',
2363 ];
2364 }
2365
2366 // Include orphan + chat_deleted leads when exporting all.
2367 if ($scope === 'all') {
2368 $transcripts_emails_lc = array_flip(array_map(
2369 function ($r) { return strtolower($r['email']); },
2370 $export_rows
2371 ));
2372 foreach (self::mxchat_collect_chat_deleted_leads('') as $cd) {
2373 if (isset($transcripts_emails_lc[strtolower($cd['email'])])) continue;
2374 $transcripts_emails_lc[strtolower($cd['email'])] = true;
2375 $export_rows[] = [
2376 'email' => $cd['email'],
2377 'name' => $cd['name'],
2378 'conversation_count' => 0,
2379 'last_seen' => $cd['last_seen'],
2380 'top_page_url' => '',
2381 ];
2382 }
2383 foreach (self::mxchat_collect_orphan_leads('') as $orphan) {
2384 if (isset($transcripts_emails_lc[strtolower($orphan['email'])])) continue;
2385 $transcripts_emails_lc[strtolower($orphan['email'])] = true;
2386 $export_rows[] = [
2387 'email' => $orphan['email'],
2388 'name' => $orphan['name'],
2389 'conversation_count' => 0,
2390 'last_seen' => '',
2391 'top_page_url' => '',
2392 ];
2393 }
2394 }
2395
2396 if (empty($export_rows)) {
2397 wp_send_json_error(['message' => 'No leads to export.']);
2398 wp_die();
2399 }
2400
2401 $filename = 'mxchat-leads-' . date('Y-m-d') . '.csv';
2402 header('Content-Type: text/csv');
2403 header('Content-Disposition: attachment; filename="' . $filename . '"');
2404 header('Pragma: no-cache');
2405 header('Expires: 0');
2406
2407 $output = fopen('php://output', 'w');
2408 fputs($output, "\xEF\xBB\xBF"); // UTF-8 BOM for Excel
2409
2410 if ($fields_mode === 'email_only') {
2411 fputcsv($output, ['Email']);
2412 foreach ($export_rows as $r) {
2413 fputcsv($output, [$r['email']]);
2414 }
2415 } else {
2416 fputcsv($output, ['Email', 'Name', 'Conversations', 'Last seen', 'Top page']);
2417 foreach ($export_rows as $r) {
2418 fputcsv($output, [
2419 $r['email'],
2420 $r['name'],
2421 $r['conversation_count'],
2422 $r['last_seen'],
2423 $r['top_page_url'],
2424 ]);
2425 }
2426 }
2427
2428 fclose($output);
2429 wp_die();
2430 }
2431
2432 /**
2433 * Stats strip payload (independent of filters).
2434 */
2435 private static function mxchat_leads_stats($table, $has_page_url_column) {
2436 global $wpdb;
2437
2438 $total_transcripts_emails = (int) $wpdb->get_var(
2439 "SELECT COUNT(DISTINCT user_email) FROM {$table}
2440 WHERE user_email IS NOT NULL AND user_email != ''"
2441 );
2442
2443 $new_this_week = (int) $wpdb->get_var($wpdb->prepare(
2444 "SELECT COUNT(*) FROM (
2445 SELECT user_email FROM {$table}
2446 WHERE user_email IS NOT NULL AND user_email != ''
2447 GROUP BY user_email
2448 HAVING MIN(timestamp) >= %s
2449 ) AS new_leads",
2450 gmdate('Y-m-d H:i:s', strtotime('-7 days'))
2451 ));
2452
2453 $total_convos = (int) $wpdb->get_var(
2454 "SELECT COUNT(DISTINCT session_id) FROM {$table}
2455 WHERE user_email IS NOT NULL AND user_email != ''"
2456 );
2457
2458 $orphan_count = count(self::mxchat_collect_orphan_leads(''));
2459 $chat_deleted_count = self::mxchat_count_chat_deleted_leads();
2460
2461 // Total leads = unique emails across all three sources (dedup priority: transcripts > chat_deleted > orphan
2462 // is already enforced at collection time in mxchat_fetch_leads; stats re-apply it here).
2463 $total_leads = $total_transcripts_emails + $chat_deleted_count + $orphan_count;
2464
2465 $avg = $total_transcripts_emails > 0
2466 ? round($total_convos / $total_transcripts_emails, 1)
2467 : 0;
2468
2469 // Orphan % reflects *true* orphans only (pre-chat dropoffs). Chat-deleted leads are
2470 // excluded so the metric stays meaningful — admins shouldn't see their cleanups
2471 // inflate this number.
2472 $orphan_pct = $total_leads > 0
2473 ? (int) round(($orphan_count / $total_leads) * 100)
2474 : 0;
2475
2476 return [
2477 'total_leads' => $total_leads,
2478 'new_this_week' => $new_this_week,
2479 'avg_convos' => $avg,
2480 'orphan_pct' => $orphan_pct,
2481 'orphan_count' => $orphan_count,
2482 'chat_deleted_count' => $chat_deleted_count,
2483 ];
2484 }
2485
2486 /**
2487 * Collect leads who had a conversation that an admin later deleted (preserved via
2488 * mxchat_lead_del_* options). Returns rows tagged status='chat_deleted' with the
2489 * original last-seen timestamp so they still sort and filter sensibly.
2490 *
2491 * @param string $search Optional email/name substring filter.
2492 * @param string $date_cutoff Optional 'Y-m-d H:i:s' cutoff — only rows with last_ts >= cutoff.
2493 * @return array
2494 */
2495 private static function mxchat_collect_chat_deleted_leads($search = '', $date_cutoff = '') {
2496 global $wpdb;
2497 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2498
2499 $rows = $wpdb->get_results(
2500 "SELECT option_name, option_value FROM {$wpdb->options}
2501 WHERE option_name LIKE 'mxchat_lead_del_email_%'"
2502 );
2503 if (empty($rows)) {
2504 return [];
2505 }
2506
2507 // Emails that currently have transcripts rows should not appear as chat_deleted —
2508 // they've come back and chatted, so they're active leads again.
2509 $emails_in_transcripts = array_map(
2510 'strtolower',
2511 (array) $wpdb->get_col(
2512 "SELECT DISTINCT user_email FROM {$table}
2513 WHERE user_email IS NOT NULL AND user_email != ''"
2514 )
2515 );
2516 $emails_in_transcripts = array_flip($emails_in_transcripts);
2517
2518 $needle = strtolower(trim((string) $search));
2519 $by_email = [];
2520
2521 foreach ($rows as $opt) {
2522 $email = sanitize_email(trim((string) $opt->option_value));
2523 if (!$email) {
2524 continue;
2525 }
2526 if (isset($emails_in_transcripts[strtolower($email)])) {
2527 continue;
2528 }
2529 $sid = substr($opt->option_name, strlen('mxchat_lead_del_email_'));
2530 if (!$sid) {
2531 continue;
2532 }
2533 $name = (string) get_option('mxchat_lead_del_name_' . $sid, '');
2534 $ts = (string) get_option('mxchat_lead_del_ts_' . $sid, '');
2535
2536 if ($date_cutoff !== '' && ($ts === '' || $ts < $date_cutoff)) {
2537 continue;
2538 }
2539 if ($needle !== '') {
2540 $hay = strtolower($email . ' ' . $name);
2541 if (strpos($hay, $needle) === false) {
2542 continue;
2543 }
2544 }
2545
2546 $key = strtolower($email);
2547 if (!isset($by_email[$key]) || (isset($by_email[$key]['last_seen']) && $ts > $by_email[$key]['last_seen'])) {
2548 $by_email[$key] = [
2549 'email' => $email,
2550 'name' => $name,
2551 'conversation_count' => 0,
2552 'last_seen' => $ts,
2553 'last_seen_display' => $ts ? self::mxchat_leads_format_relative($ts) : __('Chat deleted', 'mxchat'),
2554 'first_seen' => $ts,
2555 'latest_session_id' => '',
2556 'top_page_url' => '',
2557 'top_page_title' => '',
2558 'is_orphan' => false,
2559 'status' => 'chat_deleted',
2560 ];
2561 }
2562 }
2563
2564 // Newest chat_deleted first.
2565 usort($by_email, function ($a, $b) {
2566 return strcmp((string) $b['last_seen'], (string) $a['last_seen']);
2567 });
2568 return array_values($by_email);
2569 }
2570
2571 /**
2572 * Count unique emails preserved as "chat deleted" (for the stats strip).
2573 */
2574 private static function mxchat_count_chat_deleted_leads() {
2575 global $wpdb;
2576 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2577
2578 $emails = $wpdb->get_col(
2579 "SELECT DISTINCT option_value FROM {$wpdb->options}
2580 WHERE option_name LIKE 'mxchat_lead_del_email_%'"
2581 );
2582 if (empty($emails)) {
2583 return 0;
2584 }
2585
2586 $transcripts_emails = array_map(
2587 'strtolower',
2588 (array) $wpdb->get_col(
2589 "SELECT DISTINCT user_email FROM {$table}
2590 WHERE user_email IS NOT NULL AND user_email != ''"
2591 )
2592 );
2593 $transcripts_emails = array_flip($transcripts_emails);
2594
2595 $count = 0;
2596 $seen = [];
2597 foreach ($emails as $raw) {
2598 $email = strtolower(trim((string) $raw));
2599 if (!$email || isset($seen[$email]) || isset($transcripts_emails[$email])) {
2600 continue;
2601 }
2602 $seen[$email] = true;
2603 $count++;
2604 }
2605 return $count;
2606 }
2607
2608 /**
2609 * Find leads who submitted the pre-chat form but never produced a transcripts row.
2610 * Returned rows have no conversation_count, no timestamp.
2611 *
2612 * @param string $search Optional email/name substring filter.
2613 * @return array
2614 */
2615 private static function mxchat_collect_orphan_leads($search = '') {
2616 global $wpdb;
2617 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2618
2619 $option_rows = $wpdb->get_results(
2620 "SELECT option_name, option_value FROM {$wpdb->options}
2621 WHERE option_name LIKE 'mxchat_email_%'"
2622 );
2623 if (empty($option_rows)) {
2624 return [];
2625 }
2626
2627 // Collect all session_ids that have real transcripts rows so we can exclude them.
2628 $session_ids_with_rows = $wpdb->get_col(
2629 "SELECT DISTINCT session_id FROM {$table}
2630 WHERE user_email IS NOT NULL AND user_email != ''"
2631 );
2632 $session_ids_with_rows = array_flip($session_ids_with_rows);
2633
2634 // Seen emails in transcripts (so orphans only include truly never-chatted leads).
2635 $emails_in_transcripts = array_map(
2636 'strtolower',
2637 (array) $wpdb->get_col(
2638 "SELECT DISTINCT user_email FROM {$table}
2639 WHERE user_email IS NOT NULL AND user_email != ''"
2640 )
2641 );
2642 $emails_in_transcripts = array_flip($emails_in_transcripts);
2643
2644 $orphans_by_email = [];
2645 $needle = strtolower(trim((string) $search));
2646
2647 foreach ($option_rows as $opt) {
2648 $email = sanitize_email(trim((string) $opt->option_value));
2649 if (!$email) {
2650 continue;
2651 }
2652 $sid = substr($opt->option_name, strlen('mxchat_email_'));
2653 if (!$sid) {
2654 continue;
2655 }
2656 // Exclude leads who have any transcripts rows (they appear in the main list).
2657 if (isset($emails_in_transcripts[strtolower($email)])) {
2658 continue;
2659 }
2660 if (isset($session_ids_with_rows[$sid])) {
2661 continue;
2662 }
2663
2664 $name_option = get_option('mxchat_name_' . $sid, '');
2665 $name = is_string($name_option) ? trim($name_option) : '';
2666
2667 if ($needle !== '') {
2668 $hay = strtolower($email . ' ' . $name);
2669 if (strpos($hay, $needle) === false) {
2670 continue;
2671 }
2672 }
2673
2674 $key = strtolower($email);
2675 if (!isset($orphans_by_email[$key])) {
2676 $orphans_by_email[$key] = [
2677 'email' => $email,
2678 'name' => $name,
2679 'conversation_count' => 0,
2680 'last_seen' => '',
2681 'last_seen_display' => __('No conversation yet', 'mxchat'),
2682 'first_seen' => '',
2683 'latest_session_id' => '',
2684 'top_page_url' => '',
2685 'top_page_title' => '',
2686 'is_orphan' => true,
2687 'status' => 'orphan',
2688 ];
2689 }
2690 }
2691
2692 return array_values($orphans_by_email);
2693 }
2694
2695 /**
2696 * Map a date_range key to a SQL-comparable cutoff string, or '' for all-time.
2697 */
2698 private static function mxchat_leads_date_cutoff($date_range) {
2699 switch ($date_range) {
2700 case 'today': return gmdate('Y-m-d H:i:s', strtotime('-24 hours'));
2701 case '7d': return gmdate('Y-m-d H:i:s', strtotime('-7 days'));
2702 case '30d': return gmdate('Y-m-d H:i:s', strtotime('-30 days'));
2703 case '90d': return gmdate('Y-m-d H:i:s', strtotime('-90 days'));
2704 case 'all':
2705 default: return '';
2706 }
2707 }
2708
2709 /**
2710 * Turn a UTC timestamp into a short relative display like "2h ago" or "Apr 12".
2711 */
2712 private static function mxchat_leads_format_relative($timestamp) {
2713 if (!$timestamp) {
2714 return '';
2715 }
2716 $ts = strtotime($timestamp . ' UTC');
2717 if (!$ts) {
2718 return '';
2719 }
2720 $diff = time() - $ts;
2721 if ($diff < 60) return __('just now', 'mxchat');
2722 if ($diff < 3600) return floor($diff / 60) . __('m ago', 'mxchat');
2723 if ($diff < 86400) return floor($diff / 3600) . __('h ago', 'mxchat');
2724 if ($diff < 604800) return floor($diff / 86400) . __('d ago', 'mxchat');
2725 return wp_date('M j', $ts);
2726 }
2727
2728 /**
2729 * Handle translation of chat messages via AJAX
2730 */
2731 public function mxchat_translate_messages() {
2732 if (!current_user_can('manage_options')) {
2733 wp_send_json_error(['error' => 'Insufficient permissions']);
2734 wp_die();
2735 }
2736
2737 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
2738 $target_lang = isset($_POST['target_lang']) ? sanitize_text_field($_POST['target_lang']) : 'en';
2739 $messages_json = isset($_POST['messages']) ? wp_unslash($_POST['messages']) : '[]';
2740 $messages = json_decode($messages_json, true);
2741
2742 if (empty($session_id)) {
2743 wp_send_json_error(['error' => 'No session ID provided']);
2744 wp_die();
2745 }
2746
2747 if (empty($messages) || !is_array($messages)) {
2748 wp_send_json_error(['error' => 'No messages to translate']);
2749 wp_die();
2750 }
2751
2752 // Language names for prompting
2753 $languages = [
2754 'en' => 'English',
2755 'es' => 'Spanish',
2756 'fr' => 'French',
2757 'de' => 'German',
2758 'it' => 'Italian',
2759 'pt' => 'Portuguese',
2760 'nl' => 'Dutch',
2761 'ru' => 'Russian',
2762 'zh' => 'Chinese',
2763 'ja' => 'Japanese',
2764 'ko' => 'Korean',
2765 'ar' => 'Arabic',
2766 'hi' => 'Hindi',
2767 'tr' => 'Turkish',
2768 'pl' => 'Polish',
2769 'vi' => 'Vietnamese',
2770 'th' => 'Thai',
2771 'id' => 'Indonesian',
2772 'sv' => 'Swedish',
2773 'da' => 'Danish'
2774 ];
2775
2776 $target_lang_name = isset($languages[$target_lang]) ? $languages[$target_lang] : 'English';
2777
2778 // Build combined text for translation (numbered for parsing)
2779 $numbered_messages = [];
2780 foreach ($messages as $i => $msg) {
2781 $content = isset($msg['content']) ? trim($msg['content']) : '';
2782 if (!empty($content)) {
2783 $numbered_messages[] = "[MSG" . $i . "]" . $content . "[/MSG" . $i . "]";
2784 }
2785 }
2786
2787 if (empty($numbered_messages)) {
2788 wp_send_json_error(['error' => 'No valid messages to translate']);
2789 wp_die();
2790 }
2791
2792 $combined_text = implode("\n\n", $numbered_messages);
2793
2794 // Prepare the translation prompt
2795 $system_prompt = "You are a translator. Translate the following messages to {$target_lang_name}. Keep the [MSG#] and [/MSG#] tags exactly as they are - only translate the content between them. Maintain the original formatting, line breaks, and any HTML tags. Return ONLY the translated messages with the tags, no explanations.";
2796
2797 // Get user's selected model and determine provider
2798 $options = get_option('mxchat_options', []);
2799 $selected_model = $options['model'] ?? 'gpt-5.1-chat-latest';
2800
2801 // Check if using OpenRouter
2802 if ($selected_model === 'openrouter') {
2803 $provider = 'openrouter';
2804 $selected_model = $options['openrouter_selected_model'] ?? '';
2805 $api_key = $options['openrouter_api_key'] ?? '';
2806
2807 if (empty($selected_model)) {
2808 wp_send_json_error(['error' => 'No OpenRouter model selected']);
2809 wp_die();
2810 }
2811 } else {
2812 // Determine provider from model name
2813 $model_parts = explode('-', $selected_model);
2814 $provider = strtolower($model_parts[0]);
2815
2816 // Get the appropriate API key based on provider
2817 $api_key = '';
2818 switch ($provider) {
2819 case 'gpt':
2820 case 'o1':
2821 $api_key = $options['api_key'] ?? '';
2822 break;
2823 case 'claude':
2824 $api_key = $options['claude_api_key'] ?? '';
2825 break;
2826 case 'grok':
2827 $api_key = $options['xai_api_key'] ?? '';
2828 break;
2829 case 'deepseek':
2830 $api_key = $options['deepseek_api_key'] ?? '';
2831 break;
2832 case 'gemini':
2833 $api_key = $options['gemini_api_key'] ?? '';
2834 break;
2835 default:
2836 // Default to OpenAI
2837 $api_key = $options['api_key'] ?? '';
2838 $provider = 'gpt';
2839 break;
2840 }
2841 }
2842
2843 if (empty($api_key)) {
2844 wp_send_json_error(['error' => 'No API key configured for ' . $provider]);
2845 wp_die();
2846 }
2847
2848 // Make API request based on provider
2849 $response = $this->translate_with_provider($provider, $selected_model, $api_key, $system_prompt, $combined_text);
2850
2851 if (is_wp_error($response)) {
2852 wp_send_json_error(['error' => $response->get_error_message()]);
2853 wp_die();
2854 }
2855
2856 // Parse the response to extract translated messages
2857 $translations = [];
2858 foreach ($messages as $msg) {
2859 $index = $msg['index'];
2860 $pattern = '/\[MSG' . $index . '\](.*?)\[\/MSG' . $index . '\]/s';
2861 if (preg_match($pattern, $response, $matches)) {
2862 $translations[] = [
2863 'index' => $index,
2864 'translated' => trim($matches[1])
2865 ];
2866 }
2867 }
2868
2869 // Save translations to database
2870 if (!empty($translations)) {
2871 $this->save_transcript_translation($session_id, $target_lang, $translations);
2872 }
2873
2874 wp_send_json(['success' => true, 'translations' => $translations, 'language' => $target_lang]);
2875 wp_die();
2876 }
2877
2878 /**
2879 * Save transcript translation to database
2880 */
2881 private function save_transcript_translation($session_id, $language_code, $translations) {
2882 global $wpdb;
2883 $table_name = $wpdb->prefix . 'mxchat_transcript_translations';
2884
2885 // Check if table exists, create if not
2886 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") !== $table_name) {
2887 mxchat_create_translations_table();
2888 }
2889
2890 $now = current_time('mysql');
2891 $translations_json = wp_json_encode($translations);
2892
2893 // Use REPLACE to insert or update
2894 $wpdb->query($wpdb->prepare(
2895 "REPLACE INTO $table_name (session_id, language_code, translations, created_at, updated_at)
2896 VALUES (%s, %s, %s, %s, %s)",
2897 $session_id,
2898 $language_code,
2899 $translations_json,
2900 $now,
2901 $now
2902 ));
2903 }
2904
2905 /**
2906 * Get saved translation for a session
2907 */
2908 public function mxchat_get_transcript_translation() {
2909 if (!current_user_can('manage_options')) {
2910 wp_send_json_error(['error' => 'Insufficient permissions']);
2911 wp_die();
2912 }
2913
2914 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
2915
2916 if (empty($session_id)) {
2917 wp_send_json_error(['error' => 'No session ID provided']);
2918 wp_die();
2919 }
2920
2921 global $wpdb;
2922 $table_name = $wpdb->prefix . 'mxchat_transcript_translations';
2923
2924 // Check if table exists
2925 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") !== $table_name) {
2926 wp_send_json(['success' => true, 'has_translation' => false]);
2927 wp_die();
2928 }
2929
2930 // Get the most recent translation for this session
2931 $result = $wpdb->get_row($wpdb->prepare(
2932 "SELECT language_code, translations FROM $table_name WHERE session_id = %s ORDER BY updated_at DESC LIMIT 1",
2933 $session_id
2934 ));
2935
2936 if ($result) {
2937 $translations = json_decode($result->translations, true);
2938 wp_send_json([
2939 'success' => true,
2940 'has_translation' => true,
2941 'language' => $result->language_code,
2942 'translations' => $translations
2943 ]);
2944 } else {
2945 wp_send_json(['success' => true, 'has_translation' => false]);
2946 }
2947 wp_die();
2948 }
2949
2950 /**
2951 * Translate text using the user's selected provider and model
2952 */
2953 private function translate_with_provider($provider, $model, $api_key, $system_prompt, $text) {
2954 switch ($provider) {
2955 case 'claude':
2956 return $this->translate_with_claude($api_key, $model, $system_prompt, $text);
2957 case 'grok':
2958 return $this->translate_with_xai($api_key, $model, $system_prompt, $text);
2959 case 'deepseek':
2960 return $this->translate_with_deepseek($api_key, $model, $system_prompt, $text);
2961 case 'gemini':
2962 return $this->translate_with_gemini($api_key, $model, $system_prompt, $text);
2963 case 'openrouter':
2964 return $this->translate_with_openrouter($api_key, $model, $system_prompt, $text);
2965 case 'gpt':
2966 case 'o1':
2967 default:
2968 return $this->translate_with_openai($api_key, $model, $system_prompt, $text);
2969 }
2970 }
2971
2972 /**
2973 * Translate text using OpenAI API
2974 */
2975 private function translate_with_openai($api_key, $model, $system_prompt, $text) {
2976 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', [
2977 'timeout' => 60,
2978 'headers' => [
2979 'Authorization' => 'Bearer ' . $api_key,
2980 'Content-Type' => 'application/json'
2981 ],
2982 'body' => wp_json_encode([
2983 'model' => $model,
2984 'messages' => [
2985 ['role' => 'system', 'content' => $system_prompt],
2986 ['role' => 'user', 'content' => $text]
2987 ],
2988 'temperature' => 0.3
2989 ])
2990 ]);
2991
2992 if (is_wp_error($response)) {
2993 return $response;
2994 }
2995
2996 $body = json_decode(wp_remote_retrieve_body($response), true);
2997
2998 if (isset($body['error'])) {
2999 return new WP_Error('api_error', $body['error']['message']);
3000 }
3001
3002 if (isset($body['choices'][0]['message']['content'])) {
3003 return $body['choices'][0]['message']['content'];
3004 }
3005
3006 return new WP_Error('api_error', 'Invalid API response');
3007 }
3008
3009 /**
3010 * Translate text using Claude API
3011 */
3012 private function translate_with_claude($api_key, $model, $system_prompt, $text) {
3013 // Anthropic retired claude-opus-4-20250514 / claude-sonnet-4-20250514 on 2026-06-15.
3014 // Read-time rescue: remap a saved dead ID to the current equivalent before the API call.
3015 if ($model === 'claude-opus-4-20250514') { $model = 'claude-opus-4-8'; }
3016 elseif ($model === 'claude-sonnet-4-20250514') { $model = 'claude-sonnet-4-6'; }
3017 $response = wp_remote_post('https://api.anthropic.com/v1/messages', [
3018 'timeout' => 60,
3019 'headers' => [
3020 'x-api-key' => $api_key,
3021 'anthropic-version' => '2023-06-01',
3022 'Content-Type' => 'application/json'
3023 ],
3024 'body' => wp_json_encode([
3025 'model' => $model,
3026 'max_tokens' => 4096,
3027 'system' => $system_prompt,
3028 'messages' => [
3029 ['role' => 'user', 'content' => $text]
3030 ]
3031 ])
3032 ]);
3033
3034 if (is_wp_error($response)) {
3035 return $response;
3036 }
3037
3038 $body = json_decode(wp_remote_retrieve_body($response), true);
3039
3040 if (isset($body['error'])) {
3041 return new WP_Error('api_error', $body['error']['message']);
3042 }
3043
3044 if (isset($body['content'][0]['text'])) {
3045 return $body['content'][0]['text'];
3046 }
3047
3048 return new WP_Error('api_error', 'Invalid API response');
3049 }
3050
3051 /**
3052 * Translate text using xAI (Grok) API
3053 */
3054 private function translate_with_xai($api_key, $model, $system_prompt, $text) {
3055 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', [
3056 'timeout' => 60,
3057 'headers' => [
3058 'Authorization' => 'Bearer ' . $api_key,
3059 'Content-Type' => 'application/json'
3060 ],
3061 'body' => wp_json_encode([
3062 'model' => $model,
3063 'messages' => [
3064 ['role' => 'system', 'content' => $system_prompt],
3065 ['role' => 'user', 'content' => $text]
3066 ],
3067 'temperature' => 0.3
3068 ])
3069 ]);
3070
3071 if (is_wp_error($response)) {
3072 return $response;
3073 }
3074
3075 $body = json_decode(wp_remote_retrieve_body($response), true);
3076
3077 if (isset($body['error'])) {
3078 return new WP_Error('api_error', $body['error']['message']);
3079 }
3080
3081 if (isset($body['choices'][0]['message']['content'])) {
3082 return $body['choices'][0]['message']['content'];
3083 }
3084
3085 return new WP_Error('api_error', 'Invalid API response');
3086 }
3087
3088 /**
3089 * Translate text using DeepSeek API
3090 */
3091 private function translate_with_deepseek($api_key, $model, $system_prompt, $text) {
3092 $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', [
3093 'timeout' => 60,
3094 'headers' => [
3095 'Authorization' => 'Bearer ' . $api_key,
3096 'Content-Type' => 'application/json'
3097 ],
3098 'body' => wp_json_encode([
3099 'model' => $model,
3100 'messages' => [
3101 ['role' => 'system', 'content' => $system_prompt],
3102 ['role' => 'user', 'content' => $text]
3103 ],
3104 'temperature' => 0.3,
3105 // DeepSeek V4 defaults to thinking mode ON (temperature ignored,
3106 // slow responses); translation wants non-thinking.
3107 'thinking' => ['type' => 'disabled']
3108 ])
3109 ]);
3110
3111 if (is_wp_error($response)) {
3112 return $response;
3113 }
3114
3115 $body = json_decode(wp_remote_retrieve_body($response), true);
3116
3117 if (isset($body['error'])) {
3118 return new WP_Error('api_error', $body['error']['message']);
3119 }
3120
3121 if (isset($body['choices'][0]['message']['content'])) {
3122 return $body['choices'][0]['message']['content'];
3123 }
3124
3125 return new WP_Error('api_error', 'Invalid API response');
3126 }
3127
3128 /**
3129 * Translate text using Google Gemini API
3130 */
3131 private function translate_with_gemini($api_key, $model, $system_prompt, $text) {
3132 if ($model === 'gemini-3-pro-preview') {
3133 $model = 'gemini-3.1-pro-preview';
3134 }
3135 $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':generateContent?key=' . $api_key;
3136
3137 $response = wp_remote_post($url, [
3138 'timeout' => 60,
3139 'headers' => [
3140 'Content-Type' => 'application/json'
3141 ],
3142 'body' => wp_json_encode([
3143 'contents' => [
3144 [
3145 'parts' => [
3146 ['text' => $system_prompt . "\n\n" . $text]
3147 ]
3148 ]
3149 ],
3150 'generationConfig' => [
3151 'temperature' => 0.3
3152 ]
3153 ])
3154 ]);
3155
3156 if (is_wp_error($response)) {
3157 return $response;
3158 }
3159
3160 $body = json_decode(wp_remote_retrieve_body($response), true);
3161
3162 if (isset($body['error'])) {
3163 return new WP_Error('api_error', $body['error']['message']);
3164 }
3165
3166 if (isset($body['candidates'][0]['content']['parts'][0]['text'])) {
3167 return $body['candidates'][0]['content']['parts'][0]['text'];
3168 }
3169
3170 return new WP_Error('api_error', 'Invalid API response');
3171 }
3172
3173 /**
3174 * Translate text using OpenRouter API
3175 */
3176 private function translate_with_openrouter($api_key, $model, $system_prompt, $text) {
3177 $response = wp_remote_post('https://openrouter.ai/api/v1/chat/completions', [
3178 'timeout' => 60,
3179 'headers' => [
3180 'Authorization' => 'Bearer ' . $api_key,
3181 'Content-Type' => 'application/json',
3182 'HTTP-Referer' => home_url(),
3183 'X-Title' => 'MxChat Translation'
3184 ],
3185 'body' => wp_json_encode([
3186 'model' => $model,
3187 'messages' => [
3188 ['role' => 'system', 'content' => $system_prompt],
3189 ['role' => 'user', 'content' => $text]
3190 ],
3191 'temperature' => 0.3
3192 ])
3193 ]);
3194
3195 if (is_wp_error($response)) {
3196 return $response;
3197 }
3198
3199 $body = json_decode(wp_remote_retrieve_body($response), true);
3200
3201 if (isset($body['error'])) {
3202 return new WP_Error('api_error', $body['error']['message']);
3203 }
3204
3205 if (isset($body['choices'][0]['message']['content'])) {
3206 return $body['choices'][0]['message']['content'];
3207 }
3208
3209 return new WP_Error('api_error', 'Invalid API response');
3210 }
3211
3212 public function mxchat_fetch_chat_history() {
3213 global $wpdb;
3214 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
3215 $url_clicks_table = $wpdb->prefix . 'mxchat_url_clicks';
3216
3217 if (!current_user_can('manage_options')) {
3218 wp_send_json_error(['message' => 'Insufficient permissions']);
3219 wp_die();
3220 }
3221
3222 $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
3223 $per_page = isset($_POST['per_page']) ? absint($_POST['per_page']) : 50;
3224 $offset = ($page - 1) * $per_page;
3225 $search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
3226 $sort_raw = isset($_POST['sort_order']) ? sanitize_key($_POST['sort_order']) : 'desc';
3227 $allowed_sorts = array('asc', 'desc', 'rating_positive', 'rating_negative');
3228 if (!in_array($sort_raw, $allowed_sorts, true)) { $sort_raw = 'desc'; }
3229 $sort_order = ($sort_raw === 'asc') ? 'ASC' : 'DESC';
3230 $ratings_table = $wpdb->prefix . 'mxchat_session_ratings';
3231 $ratings_join = '';
3232 $rating_table_exists = ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $ratings_table)) === $ratings_table);
3233 if ($rating_table_exists && ($sort_raw === 'rating_positive' || $sort_raw === 'rating_negative')) {
3234 // We sort by rating then recency. Wrap rating_value so NULL sorts last.
3235 $rating_dir = ($sort_raw === 'rating_positive') ? 'DESC' : 'ASC';
3236 $ratings_join = " LEFT JOIN {$ratings_table} r ON r.session_id = t.session_id ";
3237 }
3238
3239 // Build search condition
3240 $search_condition = '';
3241 $search_params = [];
3242 if (!empty($search)) {
3243 $search_condition = "WHERE (
3244 session_id LIKE %s
3245 OR user_email LIKE %s
3246 OR user_name LIKE %s
3247 OR user_identifier LIKE %s
3248 OR message LIKE %s
3249 )";
3250 $search_params = array_fill(0, 5, '%' . $wpdb->esc_like($search) . '%');
3251 }
3252
3253 // Get total count
3254 $count_query = !empty($search)
3255 ? $wpdb->prepare("SELECT COUNT(DISTINCT session_id) FROM {$table_name} {$search_condition}", $search_params)
3256 : "SELECT COUNT(DISTINCT session_id) FROM {$table_name}";
3257 $total_sessions = (int) $wpdb->get_var($count_query);
3258
3259 // Get session IDs for current page. Default sort is recency; rating sorts join the ratings table
3260 // and order by rating value (NULLs last) with recency as tiebreaker.
3261 if ($ratings_join) {
3262 $order_by = "ORDER BY (r.rating_value IS NULL), r.rating_value {$rating_dir}, MAX(t.timestamp) DESC";
3263 } else {
3264 $order_by = "ORDER BY MAX(t.timestamp) {$sort_order}";
3265 }
3266 $session_query = !empty($search)
3267 ? $wpdb->prepare(
3268 "SELECT DISTINCT t.session_id FROM {$table_name} t {$ratings_join} {$search_condition}
3269 GROUP BY t.session_id {$order_by} LIMIT %d OFFSET %d",
3270 array_merge($search_params, [$per_page, $offset])
3271 )
3272 : $wpdb->prepare(
3273 "SELECT DISTINCT t.session_id FROM {$table_name} t {$ratings_join}
3274 GROUP BY t.session_id {$order_by} LIMIT %d OFFSET %d",
3275 $per_page, $offset
3276 );
3277 $session_ids = $wpdb->get_col($session_query);
3278
3279 $total_pages = ceil($total_sessions / $per_page);
3280
3281 if (empty($session_ids)) {
3282 wp_send_json([
3283 'success' => true,
3284 'sessions' => [],
3285 'page' => $page,
3286 'total_pages' => 0,
3287 'total_sessions' => 0,
3288 'showing_start' => 0,
3289 'showing_end' => 0
3290 ]);
3291 wp_die();
3292 }
3293
3294 // Check for optional columns/tables
3295 $url_table_exists = $wpdb->get_var("SHOW TABLES LIKE '$url_clicks_table'") === $url_clicks_table;
3296 $originating_columns_exist = !empty($wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'"));
3297
3298 // Batch-fetch session ratings for this page (plan-a5b006).
3299 $ratings_map = array();
3300 if ($rating_table_exists && !empty($session_ids)) {
3301 $placeholders = implode(',', array_fill(0, count($session_ids), '%s'));
3302 $rating_rows = $wpdb->get_results($wpdb->prepare(
3303 "SELECT session_id, rating_value, rating_feedback FROM {$ratings_table} WHERE session_id IN ($placeholders)",
3304 $session_ids
3305 ));
3306 foreach ($rating_rows as $row) {
3307 $ratings_map[$row->session_id] = array(
3308 'value' => (int) $row->rating_value,
3309 'feedback' => (string) $row->rating_feedback,
3310 );
3311 }
3312 }
3313
3314 // Build session list data
3315 $sessions = [];
3316 foreach ($session_ids as $session_id) {
3317 // Get session metadata
3318 $session_data = $originating_columns_exist
3319 ? $wpdb->get_row($wpdb->prepare(
3320 "SELECT user_email, user_name, user_identifier, originating_page_url, originating_page_title, timestamp
3321 FROM {$table_name} WHERE session_id = %s ORDER BY timestamp ASC LIMIT 1",
3322 $session_id
3323 ))
3324 : $wpdb->get_row($wpdb->prepare(
3325 "SELECT user_email, user_name, user_identifier, timestamp FROM {$table_name}
3326 WHERE session_id = %s LIMIT 1",
3327 $session_id
3328 ));
3329
3330 // Get message count and latest timestamp
3331 $message_stats = $wpdb->get_row($wpdb->prepare(
3332 "SELECT COUNT(*) as count, MAX(timestamp) as latest FROM {$table_name} WHERE session_id = %s",
3333 $session_id
3334 ));
3335
3336 // Get first user message as preview
3337 $first_user_msg = $wpdb->get_var($wpdb->prepare(
3338 "SELECT message FROM {$table_name} WHERE session_id = %s AND role = 'user' ORDER BY timestamp ASC LIMIT 1",
3339 $session_id
3340 ));
3341 $preview = $first_user_msg ? wp_trim_words(wp_strip_all_tags(stripslashes($first_user_msg)), 12, '...') : 'No messages';
3342
3343 // Build display name
3344 $user_email = !empty($session_data->user_email) ? $session_data->user_email : '';
3345 $user_name = !empty($session_data->user_name) ? $session_data->user_name : '';
3346 $user_identifier = !empty($session_data->user_identifier) ? $session_data->user_identifier : 'Guest';
3347
3348 $display_name = $user_name ?: ($user_email ? explode('@', $user_email)[0] : $user_identifier);
3349 $display_sub = $user_email ?: ('ID: ' . $user_identifier);
3350
3351 // Format time - show relative for recent, date for older
3352 $timestamp = strtotime($message_stats->latest . ' UTC');
3353 $now = time();
3354 $diff = $now - $timestamp;
3355 if ($diff < 3600) {
3356 $time_display = floor($diff / 60) . 'm ago';
3357 } elseif ($diff < 86400) {
3358 $time_display = floor($diff / 3600) . 'h ago';
3359 } elseif ($diff < 604800) {
3360 $time_display = floor($diff / 86400) . 'd ago';
3361 } else {
3362 $time_display = wp_date('M j', $timestamp);
3363 }
3364
3365 // Get initials for avatar
3366 $initials = strtoupper(substr($display_name, 0, 2));
3367 if (strlen($display_name) > 2 && strpos($display_name, ' ') !== false) {
3368 $parts = explode(' ', $display_name);
3369 $initials = strtoupper(substr($parts[0], 0, 1) . substr(end($parts), 0, 1));
3370 }
3371
3372 $rating_entry = isset($ratings_map[$session_id]) ? $ratings_map[$session_id] : null;
3373 $rating_value = $rating_entry ? $rating_entry['value'] : null;
3374 $rating_feedback = $rating_entry ? $rating_entry['feedback'] : '';
3375
3376 $sessions[] = [
3377 'session_id' => $session_id,
3378 'display_name' => $display_name,
3379 'display_sub' => $display_sub,
3380 'initials' => $initials,
3381 'preview' => $preview,
3382 'message_count' => (int) $message_stats->count,
3383 'time_display' => $time_display,
3384 'timestamp' => $message_stats->latest,
3385 'rating_value' => $rating_value,
3386 'rating_feedback' => $rating_feedback,
3387 ];
3388 }
3389
3390 wp_send_json([
3391 'success' => true,
3392 'sessions' => $sessions,
3393 'page' => $page,
3394 'total_pages' => $total_pages,
3395 'total_sessions' => $total_sessions,
3396 'showing_start' => $offset + 1,
3397 'showing_end' => min($offset + $per_page, $total_sessions)
3398 ]);
3399 wp_die();
3400 }
3401
3402 /**
3403 * Fetch single conversation details for split-panel view
3404 */
3405 public function mxchat_fetch_conversation() {
3406 global $wpdb;
3407 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
3408 $url_clicks_table = $wpdb->prefix . 'mxchat_url_clicks';
3409
3410 if (!current_user_can('manage_options')) {
3411 wp_send_json_error(['message' => 'Insufficient permissions']);
3412 wp_die();
3413 }
3414
3415 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
3416 if (empty($session_id)) {
3417 wp_send_json_error(['message' => 'No session ID provided']);
3418 wp_die();
3419 }
3420
3421 // Check for optional columns/tables
3422 $url_table_exists = $wpdb->get_var("SHOW TABLES LIKE '$url_clicks_table'") === $url_clicks_table;
3423 $originating_columns_exist = !empty($wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'"));
3424
3425 // Get session metadata
3426 $session_data = $originating_columns_exist
3427 ? $wpdb->get_row($wpdb->prepare(
3428 "SELECT user_email, user_name, user_identifier, originating_page_url, originating_page_title, timestamp
3429 FROM {$table_name} WHERE session_id = %s ORDER BY timestamp ASC LIMIT 1",
3430 $session_id
3431 ))
3432 : $wpdb->get_row($wpdb->prepare(
3433 "SELECT user_email, user_name, user_identifier, timestamp FROM {$table_name}
3434 WHERE session_id = %s LIMIT 1",
3435 $session_id
3436 ));
3437
3438 if (!$session_data) {
3439 wp_send_json_error(['message' => 'Session not found']);
3440 wp_die();
3441 }
3442
3443 // Get clicked URLs
3444 $clicked_urls = [];
3445 if ($url_table_exists) {
3446 $url_clicks = $wpdb->get_results($wpdb->prepare(
3447 "SELECT DISTINCT clicked_url FROM {$url_clicks_table}
3448 WHERE session_id = %s ORDER BY click_timestamp ASC",
3449 $session_id
3450 ));
3451 foreach ($url_clicks as $click) {
3452 $clicked_urls[] = $click->clicked_url;
3453 }
3454 }
3455
3456 // Get all messages
3457 $messages = $wpdb->get_results($wpdb->prepare(
3458 "SELECT * FROM {$table_name} WHERE session_id = %s ORDER BY timestamp ASC",
3459 $session_id
3460 ));
3461
3462 // Build user info
3463 $user_email = !empty($session_data->user_email) ? $session_data->user_email : '';
3464 $user_name = !empty($session_data->user_name) ? $session_data->user_name : '';
3465 $user_identifier = !empty($session_data->user_identifier) ? $session_data->user_identifier : 'Guest';
3466
3467 $display_name = $user_name ?: ($user_email ? explode('@', $user_email)[0] : $user_identifier);
3468 $display_sub = $user_email ?: ('ID: ' . $user_identifier);
3469
3470 // Get initials
3471 $initials = strtoupper(substr($display_name, 0, 2));
3472 if (strlen($display_name) > 2 && strpos($display_name, ' ') !== false) {
3473 $parts = explode(' ', $display_name);
3474 $initials = strtoupper(substr($parts[0], 0, 1) . substr(end($parts), 0, 1));
3475 }
3476
3477 // Page info
3478 $page_url = $originating_columns_exist && !empty($session_data->originating_page_url) ? $session_data->originating_page_url : '';
3479 $page_title = '';
3480 if ($page_url) {
3481 $page_title = !empty($session_data->originating_page_title) ? $session_data->originating_page_title : parse_url($page_url, PHP_URL_PATH);
3482 }
3483
3484 // Format messages for output
3485 $formatted_messages = [];
3486 foreach ($messages as $msg) {
3487 $is_user = ($msg->role === 'user');
3488 // Live-agent replies (Slack/Telegram handoff) persist with role 'agent'.
3489 // Kept OUT of $is_bot so the RAG Sources link below stays bot-only.
3490 $is_agent = ($msg->role === 'agent');
3491 $is_bot = ($msg->role === 'bot' || $msg->role === 'assistant');
3492
3493 $content = wp_kses(
3494 stripslashes($msg->message),
3495 [
3496 'b' => [], 'strong' => [], 'i' => [], 'em' => [], 'u' => [],
3497 'br' => [], 'p' => [], 'ul' => [], 'ol' => [], 'li' => [],
3498 'a' => ['href' => [], 'title' => [], 'target' => [], 'class' => []],
3499 'div' => ['class' => [], 'id' => [], 'data-nonce' => []],
3500 'img' => ['src' => [], 'alt' => [], 'class' => []],
3501 'h3' => ['class' => []],
3502 'h4' => ['class' => []],
3503 'button' => ['type' => [], 'class' => [], 'data-product-id' => [], 'data-nonce' => [], 'data-product-type' => [], 'data-original-text' => [], 'data-mxchat-action' => []],
3504 'select' => ['class' => [], 'data-attribute' => []],
3505 'option' => ['value' => []],
3506 'span' => ['class' => []],
3507 'del' => [], 'ins' => [],
3508 ]
3509 );
3510 $formatted_content = $this->format_transcript_message($content);
3511
3512 $has_rag = $is_bot && !empty($msg->rag_context);
3513
3514 $formatted_messages[] = [
3515 'id' => $msg->id,
3516 'role' => $msg->role,
3517 'is_user' => $is_user,
3518 'is_agent' => $is_agent,
3519 'is_bot' => $is_bot,
3520 'content' => $formatted_content,
3521 'timestamp' => wp_date('g:i A', strtotime($msg->timestamp . ' UTC')),
3522 'full_timestamp' => wp_date('F j, Y g:i A', strtotime($msg->timestamp . ' UTC')),
3523 'has_rag' => $has_rag
3524 ];
3525 }
3526
3527 // First message timestamp for "started" display
3528 $started = !empty($messages) ? wp_date('M j, Y g:i A', strtotime($messages[0]->timestamp . ' UTC')) : '-';
3529
3530 // Pull rating_feedback for this session (if any) so the details drawer can show it.
3531 $rating_feedback = '';
3532 $ratings_table_det = $wpdb->prefix . 'mxchat_session_ratings';
3533 if ($wpdb->get_var("SHOW TABLES LIKE '$ratings_table_det'") === $ratings_table_det) {
3534 $rating_feedback = (string) $wpdb->get_var($wpdb->prepare(
3535 "SELECT rating_feedback FROM {$ratings_table_det} WHERE session_id = %s LIMIT 1",
3536 $session_id
3537 ));
3538 }
3539
3540 wp_send_json([
3541 'success' => true,
3542 'session_id' => $session_id,
3543 'user' => [
3544 'name' => $display_name,
3545 'sub' => $display_sub,
3546 'initials' => $initials,
3547 'email' => $user_email,
3548 'identifier' => $user_identifier
3549 ],
3550 'page' => [
3551 'url' => $page_url,
3552 'title' => $page_title
3553 ],
3554 'clicked_urls' => $clicked_urls,
3555 'messages' => $formatted_messages,
3556 'message_count' => count($messages),
3557 'started' => $started,
3558 'rating_feedback' => $rating_feedback
3559 ]);
3560 wp_die();
3561 }
3562
3563
3564 /**
3565 * ALTERNATIVE: Simpler helper method using string replacement
3566 */
3567 private function highlight_clicked_links($message_content, $clicked_urls) {
3568 if (empty($clicked_urls)) {
3569 return wp_kses(
3570 $message_content,
3571 [
3572 'b' => [], 'strong' => [], 'i' => [], 'em' => [], 'u' => [],
3573 'br' => [], 'p' => [], 'ul' => [], 'ol' => [], 'li' => [],
3574 'a' => ['href' => [], 'title' => [], 'target' => [], 'class' => []]
3575 ]
3576 );
3577 }
3578
3579 // First apply standard sanitization
3580 $message_content = wp_kses(
3581 $message_content,
3582 [
3583 'b' => [], 'strong' => [], 'i' => [], 'em' => [], 'u' => [],
3584 'br' => [], 'p' => [], 'ul' => [], 'ol' => [], 'li' => [],
3585 'a' => ['href' => [], 'title' => [], 'target' => [], 'class' => []]
3586 ]
3587 );
3588
3589 // Process each clicked URL
3590 foreach ($clicked_urls as $clicked_url) {
3591 // Try multiple patterns to catch different link formats
3592 $patterns = [
3593 // Standard link format
3594 '/<a([^>]*href=["\']' . preg_quote($clicked_url, '/') . '["\'][^>]*)>/i',
3595 // Link with trailing slash
3596 '/<a([^>]*href=["\']' . preg_quote(rtrim($clicked_url, '/'), '/') . '\/?["\'][^>]*)>/i',
3597 // Encoded entities version
3598 '/<a([^>]*href=["\']' . preg_quote(htmlentities($clicked_url), '/') . '["\'][^>]*)>/i',
3599 ];
3600
3601 foreach ($patterns as $pattern) {
3602 if (preg_match($pattern, $message_content)) {
3603 $message_content = preg_replace_callback(
3604 $pattern,
3605 function($matches) {
3606 $full_match = $matches[0];
3607 $attributes = $matches[1];
3608
3609 // Check if it already has the class
3610 if (strpos($full_match, 'mxchat-clicked-link') !== false) {
3611 return $full_match;
3612 }
3613
3614 // Check if class attribute exists
3615 if (preg_match('/class=["\']([^"\']*)["\']/', $attributes, $class_matches)) {
3616 // Add to existing class
3617 $new_attributes = preg_replace(
3618 '/class=["\']([^"\']*)["\']/',
3619 'class="$1 mxchat-clicked-link"',
3620 $attributes
3621 );
3622 } else {
3623 // Add new class attribute
3624 $new_attributes = $attributes . ' class="mxchat-clicked-link"';
3625 }
3626
3627 // Add title if not present
3628 if (strpos($new_attributes, 'title=') === false) {
3629 $new_attributes .= ' title="User clicked this link"';
3630 }
3631
3632 return '<a' . $new_attributes . '>';
3633 },
3634 $message_content
3635 );
3636
3637 // If we found and replaced, break out of the patterns loop
3638 break;
3639 }
3640 }
3641 }
3642
3643 return $message_content;
3644 }
3645
3646 public function mxchat_create_prompts_page() {
3647 //error_log('=== DEBUG: mxchat_create_prompts_page started ===');
3648
3649 global $wpdb;
3650 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3651
3652 $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
3653 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
3654
3655 // Display success message if all prompts were deleted
3656 if (isset($_GET['all_deleted']) && $_GET['all_deleted'] === 'true') {
3657 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__('All knowledge has been deleted successfully.', 'mxchat') . '</p><button type="button" class="notice-dismiss"><span class="screen-reader-text">' . esc_html__('Dismiss this notice.', 'mxchat') . '</span></button></div>';
3658 }
3659
3660 // Set up pagination, search query, and content type filter
3661 $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field($_GET['_wpnonce']) : '';
3662 $search_query = (!empty($nonce) && wp_verify_nonce($nonce, 'mxchat_prompts_search_nonce') && isset($_GET['search'])) ? sanitize_text_field($_GET['search']) : '';
3663 $content_type_filter = isset($_GET['content_type']) ? sanitize_key($_GET['content_type']) : ''; // ADDED 2.5.6
3664 $current_page = isset($_GET['paged']) ? absint($_GET['paged']) : 1;
3665 $per_page = 25;
3666
3667 //error_log('DEBUG: Search query: ' . $search_query);
3668 //error_log('DEBUG: Content type filter: ' . $content_type_filter);
3669 //error_log('DEBUG: Current page: ' . $current_page);
3670 //error_log('DEBUG: Per page: ' . $per_page);
3671
3672 // ================================
3673 // MULTI-BOT CONFIGURATION
3674 // ================================
3675
3676 // Check for multi-bot and set up bot selection
3677 if (class_exists('MxChat_Multi_Bot_Manager')) {
3678 $multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
3679 $available_bots = $multi_bot_manager->get_available_bots();
3680
3681 // Get saved bot selection (user-specific first, then site-wide default)
3682 $user_id = get_current_user_id();
3683 $saved_bot_id = get_user_meta($user_id, 'mxchat_selected_knowledge_bot', true);
3684 if (empty($saved_bot_id)) {
3685 $saved_bot_id = get_option('mxchat_current_knowledge_bot', 'default');
3686 }
3687
3688 // Allow URL override but default to saved selection
3689 $current_bot_id = isset($_GET['bot_id']) ? sanitize_key($_GET['bot_id']) : $saved_bot_id;
3690 $multibot_active = true;
3691 } else {
3692 $current_bot_id = 'default';
3693 $multibot_active = false;
3694 }
3695
3696 // ================================
3697 // DATA SOURCE CONFIGURATION
3698 // ================================
3699
3700 // Get bot-specific Pinecone settings
3701 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($current_bot_id);
3702 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3703 $pinecone_api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3704
3705 // Get OpenAI Vector Store settings
3706 $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
3707 $use_vectorstore = ($vectorstore_options['mxchat_use_openai_vectorstore'] ?? '0') === '1';
3708
3709 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Use Pinecone: ' . ($use_pinecone ? 'YES' : 'NO'));
3710 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Has API Key: ' . (!empty($pinecone_api_key) ? 'YES' : 'NO'));
3711 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Host: ' . ($pinecone_options['mxchat_pinecone_host'] ?? 'NOT SET'));
3712 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Namespace: ' . ($pinecone_options['mxchat_pinecone_namespace'] ?? 'NOT SET'));
3713
3714 if ($use_pinecone && !empty($pinecone_api_key)) {
3715 //error_log('DEBUG: Using PINECONE data source with bot-specific config');
3716 // PINECONE DATA SOURCE
3717 $data_source = 'pinecone';
3718
3719 // IMPORTANT: Pass the bot-specific $pinecone_options, not default options!
3720 // UPDATED 2.5.6: Added content_type_filter parameter
3721 $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, $search_query, $current_page, $per_page, $current_bot_id, $content_type_filter);
3722
3723 // TEMPORARY DEBUG - Add this right after the fetch call
3724 //error_log('=== DEBUG: Bot switching issue ===');
3725 //error_log('Current bot ID: ' . $current_bot_id);
3726 //error_log('Use Pinecone: ' . ($use_pinecone ? 'YES' : 'NO'));
3727 //error_log('Records returned: ' . count($records['data'] ?? []));
3728 //error_log('Total records: ' . ($records['total'] ?? 0));
3729
3730 // Check first few records to see their bot_id
3731 if (!empty($records['data'])) {
3732 foreach (array_slice($records['data'], 0, 3) as $i => $record) {
3733 $record_bot_id = $record->bot_id ?? 'NOT_SET';
3734 //error_log('Record ' . ($i+1) . ' bot_id: ' . $record_bot_id . ', content preview: ' . substr($record->article_content ?? '', 0, 30) . '...');
3735 }
3736 }
3737 //error_log('=== END DEBUG ===');
3738
3739 $total_records = $records['total'] ?? 0;
3740 $prompts = $records['data'] ?? array();
3741 $total_in_database = $records['total_in_database'] ?? 0;
3742 $showing_recent_only = $records['showing_recent_only'] ?? false;
3743
3744 $total_pages = ceil($total_records / $per_page);
3745
3746 } else {
3747 //error_log('DEBUG: Using WORDPRESS DB data source');
3748 // WORDPRESS DB DATA SOURCE (your existing logic)
3749 $data_source = 'wordpress';
3750
3751 // Initialize these variables for WordPress DB
3752 $total_in_database = 0;
3753 $showing_recent_only = false;
3754
3755 $offset = ($current_page - 1) * $per_page;
3756
3757 // UPDATED 2.5.6: Build WHERE clause for search and content type filtering
3758 $where_clauses = array();
3759 $where_values = array();
3760
3761 if ($search_query) {
3762 $where_clauses[] = "article_content LIKE %s";
3763 $where_values[] = '%' . $wpdb->esc_like($search_query) . '%';
3764 }
3765
3766 if ($content_type_filter) {
3767 $where_clauses[] = "content_type = %s";
3768 $where_values[] = $content_type_filter;
3769 }
3770
3771 $sql_where = "";
3772 if (!empty($where_clauses)) {
3773 $sql_where = "WHERE " . implode(" AND ", $where_clauses);
3774 }
3775
3776 // UPDATED 2.6.3: Count unique entries (by source_url) instead of individual rows
3777 // This ensures pagination shows X entries per page, not X chunks
3778 // Entries with empty source_url are counted individually
3779 if (!empty($where_values)) {
3780 // Count unique source_urls + count of rows with empty source_url
3781 $count_query = $wpdb->prepare(
3782 "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$sql_where} AND source_url != '') +
3783 (SELECT COUNT(*) FROM {$table_name} {$sql_where} AND (source_url = '' OR source_url IS NULL))",
3784 array_merge($where_values, $where_values)
3785 );
3786 } else {
3787 $count_query = "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '') +
3788 (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL)";
3789 }
3790 $total_records = $wpdb->get_var($count_query);
3791 $total_pages = ceil($total_records / $per_page);
3792
3793 // UPDATED 2.6.3: Get unique source_urls for pagination, then fetch all their rows
3794 // Step 1: Get the source_urls for this page (distinct URLs ordered by latest timestamp)
3795 if (!empty($where_values)) {
3796 $urls_query = $wpdb->prepare(
3797 "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name} {$sql_where}
3798 GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
3799 array_merge($where_values, array($per_page, $offset))
3800 );
3801 } else {
3802 $urls_query = $wpdb->prepare(
3803 "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
3804 GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
3805 $per_page, $offset
3806 );
3807 }
3808 $page_urls = $wpdb->get_results($urls_query);
3809
3810 // Step 2: Fetch all rows for these source_urls
3811 $prompts = array();
3812 if (!empty($page_urls)) {
3813 // Build URL order map to preserve newest-first ordering from step 1
3814 $url_order_map = array();
3815 $url_list = array();
3816 $has_empty_url = false;
3817 $order_index = 0;
3818 foreach ($page_urls as $url_row) {
3819 if (empty($url_row->source_url)) {
3820 $has_empty_url = true;
3821 $url_order_map['__empty__'] = $order_index++;
3822 } else {
3823 $url_list[] = $url_row->source_url;
3824 $url_order_map[$url_row->source_url] = $order_index++;
3825 }
3826 }
3827
3828 // Build query to fetch all rows for these URLs
3829 $url_conditions = array();
3830 $url_values = array();
3831
3832 if (!empty($url_list)) {
3833 $placeholders = implode(',', array_fill(0, count($url_list), '%s'));
3834 $url_conditions[] = "source_url IN ($placeholders)";
3835 $url_values = array_merge($url_values, $url_list);
3836 }
3837
3838 if ($has_empty_url) {
3839 $url_conditions[] = "(source_url = '' OR source_url IS NULL)";
3840 }
3841
3842 if (!empty($url_conditions)) {
3843 $url_where = "WHERE (" . implode(" OR ", $url_conditions) . ")";
3844
3845 // Add original filters back
3846 if (!empty($where_clauses)) {
3847 $url_where .= " AND " . implode(" AND ", $where_clauses);
3848 $url_values = array_merge($url_values, $where_values);
3849 }
3850
3851 if (!empty($url_values)) {
3852 $prompts_query = $wpdb->prepare(
3853 "SELECT * FROM {$table_name} {$url_where} ORDER BY timestamp DESC",
3854 $url_values
3855 );
3856 } else {
3857 $prompts_query = "SELECT * FROM {$table_name} {$url_where} ORDER BY timestamp DESC";
3858 }
3859
3860 $prompts = $wpdb->get_results($prompts_query);
3861
3862 // Sort prompts by the original URL order (newest first), then by timestamp within each URL group
3863 usort($prompts, function($a, $b) use ($url_order_map) {
3864 $url_a = empty($a->source_url) ? '__empty__' : $a->source_url;
3865 $url_b = empty($b->source_url) ? '__empty__' : $b->source_url;
3866 $order_a = $url_order_map[$url_a] ?? PHP_INT_MAX;
3867 $order_b = $url_order_map[$url_b] ?? PHP_INT_MAX;
3868
3869 // First sort by URL order (newest URLs first)
3870 if ($order_a !== $order_b) {
3871 return $order_a - $order_b;
3872 }
3873
3874 // Within same URL, sort by timestamp DESC (newest chunks first)
3875 return strtotime($b->timestamp) - strtotime($a->timestamp);
3876 });
3877 }
3878 }
3879 }
3880
3881 // ================================
3882 // PAGINATION GENERATION
3883 // ================================
3884
3885 // Generate pagination links
3886 if ($total_pages > 1) {
3887 // Build a clean base URL with only necessary parameters
3888 $pagination_args = array('page' => 'mxchat-prompts');
3889
3890 // Preserve bot_id parameter if multi-bot is active
3891 if ($multibot_active && !empty($current_bot_id) && $current_bot_id !== 'default') {
3892 $pagination_args['bot_id'] = $current_bot_id;
3893 }
3894
3895 // Preserve search query and nonce if present
3896 if ($search_query) {
3897 $pagination_args['search'] = $search_query;
3898 if (!empty($nonce)) {
3899 $pagination_args['_wpnonce'] = $nonce;
3900 }
3901 }
3902
3903 // Preserve content type filter if present
3904 if ($content_type_filter) {
3905 $pagination_args['content_type'] = $content_type_filter;
3906 }
3907
3908 // Build clean base URL
3909 $base_url = add_query_arg($pagination_args, admin_url('admin.php'));
3910
3911 $page_links = paginate_links(array(
3912 'base' => $base_url . '%_%',
3913 'format' => '&paged=%#%',
3914 'prev_text' => __('&laquo; Previous', 'mxchat'),
3915 'next_text' => __('Next &raquo;', 'mxchat'),
3916 'total' => $total_pages,
3917 'current' => $current_page,
3918 'type' => 'plain',
3919 ));
3920 } else {
3921 $page_links = '';
3922 }
3923
3924 // ================================
3925 // PROCESSING STATUS RETRIEVAL
3926 // ================================
3927
3928 // Retrieve processing statuses using queue-based method
3929 $processing_statuses = $knowledge_manager->mxchat_get_processing_statuses();
3930 $pdf_status = $processing_statuses['pdf_status'];
3931 $sitemap_status = $processing_statuses['sitemap_status'];
3932 $is_processing = $processing_statuses['is_processing'];
3933
3934 //error_log('=== DEBUG: mxchat_create_prompts_page data preparation completed ===');
3935
3936 // ================================
3937 // RENDER PAGE WITH NEW SIDEBAR LAYOUT
3938 // ================================
3939
3940 // Include the new knowledge page template
3941 require_once plugin_dir_path(__FILE__) . 'admin-knowledge-page.php';
3942
3943 // Package all page data for the render function
3944 $page_data = array(
3945 'prompts' => $prompts,
3946 'total_records' => $total_records,
3947 'total_pages' => $total_pages,
3948 'current_page' => $current_page,
3949 'per_page' => $per_page,
3950 'page_links' => $page_links,
3951 'search_query' => $search_query,
3952 'content_type_filter' => $content_type_filter,
3953 'data_source' => $data_source,
3954 'use_pinecone' => $use_pinecone,
3955 'use_vectorstore' => $use_vectorstore,
3956 'multibot_active' => $multibot_active,
3957 'current_bot_id' => $current_bot_id,
3958 'pdf_status' => $pdf_status,
3959 'sitemap_status' => $sitemap_status,
3960 'is_processing' => $is_processing,
3961 'total_in_database' => $total_in_database ?? 0,
3962 'showing_recent_only' => $showing_recent_only ?? false,
3963 );
3964
3965 // Render the new sidebar-based page
3966 mxchat_render_knowledge_page($this, $knowledge_manager, $page_data);
3967 }
3968
3969 /**
3970 * Get bot-specific Pinecone configuration
3971 * Used in the knowledge retrieval functions
3972 */
3973 private function get_bot_pinecone_config($bot_id = 'default') {
3974 //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
3975
3976 // If default bot or multi-bot add-on not active, use default Pinecone config
3977 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
3978 //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
3979 $addon_options = get_option('mxchat_pinecone_addon_options', array());
3980 $config = array(
3981 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
3982 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
3983 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
3984 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
3985 );
3986 //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
3987 return $config;
3988 }
3989
3990 //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
3991
3992 // Hook for multi-bot add-on to provide bot-specific Pinecone config
3993 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
3994
3995 if (!empty($bot_pinecone_config)) {
3996 //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
3997 //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
3998 //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
3999 //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
4000 } else {
4001 //error_log("MXCHAT DEBUG: Filter returned empty config!");
4002 }
4003
4004 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
4005 }
4006
4007 public function mxchat_delete_chat_history() {
4008 if (!current_user_can('manage_options')) {
4009 echo wp_json_encode(['error' => esc_html__('You do not have sufficient permissions.', 'mxchat')]);
4010 wp_die();
4011 }
4012 check_ajax_referer('mxchat_delete_chat_history', 'security');
4013
4014 if (!isset($_POST['delete_session_ids']) || !is_array($_POST['delete_session_ids'])) {
4015 echo wp_json_encode(['error' => esc_html__('No chat sessions selected for deletion.', 'mxchat')]);
4016 wp_die();
4017 }
4018
4019 global $wpdb;
4020 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
4021 $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
4022 $has_translations = $wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table;
4023
4024 // When true, any lead attached to these sessions is fully wiped (all their sessions,
4025 // across the whole table). Default false: the chat rows go away but the lead is
4026 // preserved as a separate "chat deleted" lead in the Leads tab.
4027 $also_delete_lead = !empty($_POST['also_delete_lead']) && $_POST['also_delete_lead'] !== 'false';
4028
4029 $deleted_count = 0;
4030 $preserved_as_deleted_leads = 0;
4031 $emails_to_fully_wipe = [];
4032
4033 foreach ((array) $_POST['delete_session_ids'] as $session_id) {
4034 $session_id_sanitized = sanitize_text_field($session_id);
4035 if ($session_id_sanitized === '') {
4036 continue;
4037 }
4038
4039 // Capture the lead info attached to this session *before* we delete the rows.
4040 $lead_row = $wpdb->get_row($wpdb->prepare(
4041 "SELECT user_email, user_name, MAX(timestamp) AS last_ts
4042 FROM {$table_name}
4043 WHERE session_id = %s AND user_email IS NOT NULL AND user_email != ''
4044 GROUP BY user_email, user_name
4045 ORDER BY last_ts DESC LIMIT 1",
4046 $session_id_sanitized
4047 ));
4048
4049 wp_cache_delete('chat_session_' . $session_id_sanitized, 'mxchat_chat_sessions');
4050 $wpdb->delete($table_name, ['session_id' => $session_id_sanitized]);
4051
4052 if ($has_translations) {
4053 $wpdb->delete($translations_table, ['session_id' => $session_id_sanitized]);
4054 }
4055
4056 delete_option('mxchat_history_' . $session_id_sanitized);
4057 delete_option('mxchat_agent_name_' . $session_id_sanitized);
4058
4059 if ($lead_row && !empty($lead_row->user_email)) {
4060 if ($also_delete_lead) {
4061 // Full-wipe requested — queue the email so that all their sessions and
4062 // related options get swept below. Also clear this session's pre-chat
4063 // capture options (they're no longer meaningful).
4064 $emails_to_fully_wipe[strtolower($lead_row->user_email)] = $lead_row->user_email;
4065 delete_option('mxchat_email_' . $session_id_sanitized);
4066 delete_option('mxchat_name_' . $session_id_sanitized);
4067 } else {
4068 // Preserve the lead in a "Chat deleted" state via distinct option keys so
4069 // they stay out of the orphan bucket (orphan = pre-chat form dropoff).
4070 update_option('mxchat_lead_del_email_' . $session_id_sanitized, $lead_row->user_email, false);
4071 if (!empty($lead_row->user_name)) {
4072 update_option('mxchat_lead_del_name_' . $session_id_sanitized, $lead_row->user_name, false);
4073 }
4074 if (!empty($lead_row->last_ts)) {
4075 update_option('mxchat_lead_del_ts_' . $session_id_sanitized, $lead_row->last_ts, false);
4076 }
4077 // Clean up pre-chat capture options for this session — chat_deleted supersedes.
4078 delete_option('mxchat_email_' . $session_id_sanitized);
4079 delete_option('mxchat_name_' . $session_id_sanitized);
4080 $preserved_as_deleted_leads++;
4081 }
4082 } else {
4083 // No lead attached — nothing to preserve. Clean up any orphan options anyway.
4084 delete_option('mxchat_email_' . $session_id_sanitized);
4085 delete_option('mxchat_name_' . $session_id_sanitized);
4086 }
4087
4088 $deleted_count++;
4089 }
4090
4091 // Opt-in full-lead wipe: sweep every remaining row + every option key (including
4092 // chat_deleted preservation) for each affected email. Reuses the same internal
4093 // helper as the Leads-tab Delete button for consistency.
4094 if (!empty($emails_to_fully_wipe)) {
4095 self::mxchat_wipe_leads_by_email(array_values($emails_to_fully_wipe));
4096 }
4097
4098 wp_cache_delete('all_chat_sessions', 'mxchat_chat_sessions');
4099
4100 echo wp_json_encode([
4101 'success' => sprintf(
4102 esc_html__('%d chat session(s) have been deleted.', 'mxchat'),
4103 $deleted_count
4104 ),
4105 'preserved_as_deleted_leads' => $preserved_as_deleted_leads,
4106 'leads_fully_wiped' => count($emails_to_fully_wipe),
4107 ]);
4108 wp_die();
4109 }
4110
4111 /**
4112 * Format transcript message content with markdown processing
4113 * Converts markdown links, bold, italic, code blocks, and plain URLs to HTML
4114 */
4115 private function format_transcript_message($text) {
4116 if (empty($text)) {
4117 return '';
4118 }
4119
4120 // Normalize line endings and clean up excessive whitespace
4121 $text = str_replace("\r\n", "\n", $text);
4122 $text = str_replace("\r", "\n", $text);
4123
4124 // Clean up existing <br> tags that may have been saved (legacy data)
4125 // Convert <br>, <br/>, <br /> back to newlines for consistent processing
4126 $text = preg_replace('/<br\s*\/?>\s*/i', "\n", $text);
4127
4128 // Collapse 3+ consecutive newlines to just 2 (paragraph break)
4129 $text = preg_replace('/\n{3,}/', "\n\n", $text);
4130
4131 // Process markdown headers (# Header)
4132 $text = preg_replace_callback('/^(#{1,6})\s+(.+)$/m', function($matches) {
4133 $level = strlen($matches[1]);
4134 $content = esc_html(trim($matches[2]));
4135 return "<h{$level}>{$content}</h{$level}>";
4136 }, $text);
4137
4138 // Process code blocks with triple backticks
4139 $text = preg_replace_callback('/```(\w+)?\n?([\s\S]*?)```/', function($matches) {
4140 $language = !empty($matches[1]) ? ' class="language-' . esc_attr($matches[1]) . '"' : '';
4141 $code = esc_html($matches[2]);
4142 return "<pre><code{$language}>{$code}</code></pre>";
4143 }, $text);
4144
4145 // Process inline code with single backticks
4146 $text = preg_replace('/`([^`]+)`/', '<code>$1</code>', $text);
4147
4148 // Process bold text **text** or __text__
4149 $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
4150 $text = preg_replace('/__(.+?)__/', '<strong>$1</strong>', $text);
4151
4152 // Process italic text *text* or _text_ (but not if part of URL)
4153 $text = preg_replace('/(?<![*_\w])\*([^*]+)\*(?![*\w])/', '<em>$1</em>', $text);
4154 $text = preg_replace('/(?<![*_\w])_([^_]+)_(?![*\w])/', '<em>$1</em>', $text);
4155
4156 // Process markdown links [text](url)
4157 $text = preg_replace_callback('/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/', function($matches) {
4158 $link_text = esc_html($matches[1]);
4159 $url = esc_url($matches[2]);
4160 return "<a href=\"{$url}\" target=\"_blank\" rel=\"noopener\">{$link_text}</a>";
4161 }, $text);
4162
4163 // Process citation-style brackets [URL]
4164 $text = preg_replace_callback('/\[(https?:\/\/[^\]]+)\]/', function($matches) {
4165 $url = esc_url($matches[1]);
4166 return "<a href=\"{$url}\" target=\"_blank\" rel=\"noopener\">{$url}</a>";
4167 }, $text);
4168
4169 // Process standalone URLs (not already in links or img src)
4170 $text = preg_replace_callback(
4171 '/(?<!href="|src="|">)(https?:\/\/[^\s<>"]+)(?![^<]*<\/a>)/',
4172 function($matches) {
4173 $url = esc_url($matches[1]);
4174 // Truncate display URL if too long
4175 $display = strlen($matches[1]) > 50 ? substr($matches[1], 0, 47) . '...' : $matches[1];
4176 return "<a href=\"{$url}\" target=\"_blank\" rel=\"noopener\">{$display}</a>";
4177 },
4178 $text
4179 );
4180
4181 // Process mailto links
4182 $text = preg_replace_callback('/\[([^\]]+)\]\((mailto:[^)]+)\)/', function($matches) {
4183 $link_text = esc_html($matches[1]);
4184 $mailto = esc_url($matches[2]);
4185 return "<a href=\"{$mailto}\">{$link_text}</a>";
4186 }, $text);
4187
4188 // Convert paragraphs: split by double newlines, wrap in <p> tags
4189 // This creates proper paragraph structure instead of excessive <br> tags
4190 $paragraphs = preg_split('/\n\n+/', $text);
4191
4192 // Filter out empty paragraphs but preserve content like "0"
4193 $paragraphs = array_values(array_filter(array_map('trim', $paragraphs), function($p) {
4194 return $p !== '';
4195 }));
4196
4197 if (empty($paragraphs)) {
4198 // No content after filtering
4199 return '';
4200 } elseif (count($paragraphs) > 1) {
4201 // Multiple paragraphs - wrap each in <p> tags, convert single newlines to <br>
4202 $formatted_paragraphs = array_map(function($p) {
4203 return nl2br($p);
4204 }, $paragraphs);
4205 $text = '<p>' . implode('</p><p>', $formatted_paragraphs) . '</p>';
4206 } else {
4207 // Single paragraph - just convert newlines to <br>
4208 $text = nl2br($paragraphs[0]);
4209 }
4210
4211 return $text;
4212 }
4213
4214 /**
4215 * AJAX handler to fetch RAG context for a specific message
4216 * Used by the transcript viewer to show retrieved documents
4217 */
4218 public function mxchat_get_rag_context() {
4219 // Check permissions
4220 if (!current_user_can('manage_options')) {
4221 wp_send_json_error(['message' => esc_html__('You do not have sufficient permissions.', 'mxchat')]);
4222 wp_die();
4223 }
4224
4225 // Validate message ID
4226 if (!isset($_POST['message_id']) || empty($_POST['message_id'])) {
4227 wp_send_json_error(['message' => esc_html__('Message ID is required.', 'mxchat')]);
4228 wp_die();
4229 }
4230
4231 $message_id = absint($_POST['message_id']);
4232
4233 global $wpdb;
4234 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
4235
4236 // Fetch the RAG context for this message
4237 $result = $wpdb->get_row($wpdb->prepare(
4238 "SELECT rag_context FROM {$table_name} WHERE id = %d",
4239 $message_id
4240 ));
4241
4242 if (!$result || empty($result->rag_context)) {
4243 wp_send_json_error(['message' => esc_html__('No RAG context found for this message.', 'mxchat')]);
4244 wp_die();
4245 }
4246
4247 // Decode the JSON data
4248 $rag_context = json_decode($result->rag_context, true);
4249
4250 if (json_last_error() !== JSON_ERROR_NONE) {
4251 wp_send_json_error(['message' => esc_html__('Invalid RAG context data.', 'mxchat')]);
4252 wp_die();
4253 }
4254
4255 wp_send_json_success($rag_context);
4256 wp_die();
4257 }
4258
4259 public function display_admin_notices() {
4260 // Check if we're on a MXChat admin page
4261 $screen = get_current_screen();
4262 if (!$screen || strpos($screen->base, 'mxchat') === false) {
4263 return;
4264 }
4265
4266 $dismiss_button = '<button type="button" class="notice-dismiss"><span class="screen-reader-text">' . esc_html__('Dismiss this notice.', 'mxchat') . '</span></button>';
4267
4268 // Check for error notices
4269 $error_notice = get_transient('mxchat_admin_notice_error');
4270 if ($error_notice) {
4271 echo '<div class="notice notice-error is-dismissible"><p>' . wp_kses_post($error_notice) . '</p>' . $dismiss_button . '</div>';
4272 delete_transient('mxchat_admin_notice_error');
4273 }
4274
4275 // Check for success notices
4276 $success_notice = get_transient('mxchat_admin_notice_success');
4277 if ($success_notice) {
4278 echo '<div class="notice notice-success is-dismissible"><p>' . wp_kses_post($success_notice) . '</p>' . $dismiss_button . '</div>';
4279 delete_transient('mxchat_admin_notice_success');
4280 }
4281
4282 // Check for info notices
4283 $info_notice = get_transient('mxchat_admin_notice_info');
4284 if ($info_notice) {
4285 echo '<div class="notice notice-info is-dismissible"><p>' . wp_kses_post($info_notice) . '</p>' . $dismiss_button . '</div>';
4286 delete_transient('mxchat_admin_notice_info');
4287 }
4288 }
4289
4290
4291 public function mxchat_create_activation_page() {
4292 // Include the new Pro & Extensions page template
4293 require_once plugin_dir_path(__FILE__) . 'admin-pro-page.php';
4294
4295 // Get addons configuration from the MxChat_Addons class
4296 require_once plugin_dir_path(__FILE__) . 'class-mxchat-addons.php';
4297 $addons_instance = new MxChat_Addons();
4298 $addons_config = $addons_instance->get_addons_config();
4299
4300 // Render the consolidated Pro & Extensions page
4301 mxchat_render_pro_page($this, $addons_config);
4302 }
4303
4304 /**
4305 * Check if current activation is linked to a domain
4306 * This checks YOUR website's database, not the user's local database
4307 */
4308 public function is_current_activation_linked($domain) {
4309 $license_key = get_option('mxchat_activation_key');
4310 $email = get_option('mxchat_pro_email');
4311
4312 if (empty($license_key) || empty($email)) {
4313 return false;
4314 }
4315
4316 // Check with YOUR website's API
4317 $response = wp_remote_post('https://mxchat.ai/mxchat-api/check-domain', array(
4318 'body' => array(
4319 'license_key' => $license_key,
4320 'email' => $email,
4321 'domain' => $domain
4322 ),
4323 'timeout' => 10,
4324 'sslverify' => false
4325 ));
4326
4327 if (is_wp_error($response)) {
4328 return false;
4329 }
4330
4331 $body = json_decode(wp_remote_retrieve_body($response), true);
4332 return isset($body['success']) && $body['success'] && isset($body['data']['linked']) && $body['data']['linked'];
4333 }
4334
4335
4336 public function mxchat_actions_page_html() {
4337 if (!current_user_can('manage_options')) {
4338 return;
4339 }
4340
4341 global $wpdb;
4342 $table_name = $wpdb->prefix . 'mxchat_intents';
4343
4344 // Get stats for dashboard
4345 $total_actions = $wpdb->get_var("SELECT COUNT(*) FROM $table_name");
4346 $enabled_actions = $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE enabled = 1");
4347 $disabled_actions = $total_actions - $enabled_actions;
4348
4349 // Get unique action types count
4350 $action_types_count = $wpdb->get_var("SELECT COUNT(DISTINCT callback_function) FROM $table_name");
4351
4352 // Get action type distribution
4353 $type_distribution_raw = $wpdb->get_results("SELECT callback_function, COUNT(*) as count FROM $table_name GROUP BY callback_function ORDER BY count DESC LIMIT 10");
4354 $available_callbacks = $this->mxchat_get_available_callbacks();
4355 $callback_groups = $this->mxchat_get_available_callbacks(true, true);
4356
4357 $action_type_distribution = array();
4358 foreach ($type_distribution_raw as $row) {
4359 $label = isset($available_callbacks[$row->callback_function]['label'])
4360 ? $available_callbacks[$row->callback_function]['label']
4361 : $row->callback_function;
4362 $action_type_distribution[$label] = $row->count;
4363 }
4364
4365 // Native function-calling (AI Tools) data — plan-mxchat-20260617-a41dee.
4366 // The AI Tools checklist reads from MxChat_Tool_Registry, the SAME single
4367 // source the chat-time function-calling loop reads, so the two never drift.
4368 if (!class_exists('MxChat_Tool_Registry')) {
4369 require_once plugin_dir_path(__FILE__) . 'class-mxchat-tool-registry.php';
4370 }
4371 if (!class_exists('MxChat_Model_Catalog')) {
4372 require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
4373 }
4374 $fc_options = get_option('mxchat_options', array());
4375 $fc_current_model = isset($fc_options['model']) ? $fc_options['model'] : 'gpt-5.1-chat-latest';
4376 $fc_model_capable = class_exists('MxChat_Model_Catalog')
4377 ? MxChat_Model_Catalog::supports_tools($fc_current_model) : true;
4378 $active_tab = (isset($_GET['tab']) && $_GET['tab'] === 'ai-tools') ? 'ai-tools' : 'dashboard';
4379
4380 // Enrich each AI Tool with the dashicon its callback already uses on the
4381 // Trigger Phrases "Choose what it does" grid, so the AI Tools cards/modal
4382 // share the same iconography (plan 8bbf98 part 4). View-layer only — the
4383 // registry's model-facing data is untouched. Default admin-generic.
4384 $fc_tools = MxChat_Tool_Registry::available_tools();
4385 foreach ($fc_tools as &$fc_tool_ref) {
4386 $fc_cb_ref = $fc_tool_ref['callback'];
4387 $fc_tool_ref['icon'] = isset($available_callbacks[$fc_cb_ref]['icon'])
4388 ? $available_callbacks[$fc_cb_ref]['icon']
4389 : 'admin-generic';
4390 }
4391 unset($fc_tool_ref);
4392
4393 // Count of ACTIVE tools — drives the AI Tools sidebar nav badge, mirroring
4394 // $total_actions for Trigger Phrases. A tool in the list = active (plan
4395 // d450a7), so the badge shows the same number the list pane shows (plan 5f7409).
4396 $total_tools = count(array_filter($fc_tools, function ($t) {
4397 return !empty($t['enabled']);
4398 }));
4399
4400 // Brave-key dependency check (plan 183856): Web Search + Image Search run on
4401 // the Brave Search API. If either is enabled as a tool but no brave_api_key is
4402 // configured, surface a graceful "key not set" notice so the admin isn't met
4403 // with a silently no-firing tool.
4404 $fc_brave_missing = false;
4405 $brave_key = isset($fc_options['brave_api_key']) ? trim($fc_options['brave_api_key']) : '';
4406 if ($brave_key === '') {
4407 foreach ($fc_tools as $fc_t) {
4408 if (!empty($fc_t['enabled']) && isset($fc_t['requires_key']) && $fc_t['requires_key'] === 'brave_api_key') {
4409 $fc_brave_missing = true;
4410 break;
4411 }
4412 }
4413 }
4414
4415 // Prepare page data
4416 $page_data = array(
4417 'total_actions' => $total_actions,
4418 'enabled_actions' => $enabled_actions,
4419 'disabled_actions' => $disabled_actions,
4420 'action_types_count' => $action_types_count,
4421 'action_type_distribution' => $action_type_distribution,
4422 'available_callbacks' => $available_callbacks,
4423 'callback_groups' => $callback_groups,
4424 // AI Tools section
4425 'active_tab' => $active_tab,
4426 'fc_enabled' => MxChat_Tool_Registry::is_enabled(),
4427 'total_tools' => $total_tools,
4428 'fc_tools' => $fc_tools,
4429 'fc_current_model' => $fc_current_model,
4430 'fc_model_capable' => $fc_model_capable,
4431 'fc_brave_missing' => $fc_brave_missing,
4432 'fc_saved' => isset($_GET['mxchat_fc_saved']),
4433 );
4434
4435 // Include and render the new template
4436 require_once plugin_dir_path(__FILE__) . 'admin-actions-page.php';
4437 mxchat_render_actions_page($this, $page_data);
4438 }
4439
4440 /**
4441 * LEGACY HTML - Preserved below for reference, to be removed in future update
4442 */
4443 function mxchat_actions_page_legacy_html() {
4444 // This function is deprecated and no longer used
4445 // The new template is in includes/admin-actions-page.php
4446 ?>
4447 <div class="wrap mxchat-wrapper">
4448 <!-- Hero Section -->
4449 <div class="mxchat-hero">
4450 <h1 class="mxchat-main-title">
4451 <span class="mxchat-gradient-text">Actions</span> Manager
4452 </h1>
4453 <p class="mxchat-hero-subtitle">
4454 <?php esc_html_e('Create and manage custom actions to enhance your chatbot\'s capabilities.', 'mxchat'); ?>
4455 </p>
4456 </div>
4457
4458 <!-- Actions Header with Search and Filter -->
4459 <div class="mxchat-actions-header">
4460 <div class="mxchat-actions-filters">
4461 <form method="get" class="mxchat-search-form">
4462 <input type="hidden" name="page" value="mxchat-actions">
4463 <div class="mxchat-search-group">
4464 <span class="dashicons dashicons-search"></span>
4465 <input type="text" name="s" class="mxchat-search-input"
4466 placeholder="<?php esc_attr_e('Search Actions', 'mxchat'); ?>"
4467 value="<?php echo esc_attr($search_term); ?>">
4468 </div>
4469 <select name="callback_filter" class="mxchat-action-filter">
4470 <option value=""><?php esc_html_e('All Action Types', 'mxchat'); ?></option>
4471 <?php foreach ($available_callbacks as $function => $callback_data) :
4472 $label = $callback_data['label']; ?>
4473 <option value="<?php echo esc_attr($function); ?>"
4474 <?php selected($callback_filter, $function); ?>>
4475 <?php echo esc_html($label); ?>
4476 </option>
4477 <?php endforeach; ?>
4478 </select>
4479 <button type="submit" class="mxchat-button-secondary">
4480 <?php esc_html_e('Filter', 'mxchat'); ?>
4481 </button>
4482 </form>
4483 </div>
4484 <div class="mxchat-actions-controls">
4485 <button type="button" id="mxchat-add-action-btn" class="mxchat-button-primary">
4486 <span class="dashicons dashicons-plus-alt"></span>
4487 <?php esc_html_e('Add New Action', 'mxchat'); ?>
4488 </button>
4489 </div>
4490 </div>
4491
4492 <!-- Actions Grid Layout - All actions in a single grid -->
4493 <div class="mxchat-actions-grid">
4494 <div class="mxchat-cards-container">
4495 <?php if (!empty($actions)) : ?>
4496 <?php foreach ($actions as $action) :
4497 $callback_function = $action->callback_function;
4498 $callback_label = isset($available_callbacks[$callback_function]['label'])
4499 ? $available_callbacks[$callback_function]['label']
4500 : $callback_function;
4501 $threshold_value = isset($action->similarity_threshold)
4502 ? round($action->similarity_threshold * 100)
4503 : 85;
4504
4505 // Check if this is a form action
4506 $is_form_action = strpos($action->intent_label, 'Form ') === 0;
4507
4508 // Get action status (enabled/disabled) - default to true if column doesn't exist
4509 $is_enabled = isset($action->enabled) ? (bool)$action->enabled : true;
4510
4511 // Get enabled bots for display
4512 $enabled_bots = [];
4513 if (isset($action->enabled_bots) && !empty($action->enabled_bots)) {
4514 $enabled_bots = json_decode($action->enabled_bots, true);
4515 if (!is_array($enabled_bots)) {
4516 $enabled_bots = ['default'];
4517 }
4518 } else {
4519 $enabled_bots = ['default']; // Backward compatibility
4520 }
4521 ?>
4522 <div class="mxchat-action-card <?php echo $is_form_action ? 'mxchat-form-action' : ''; ?>">
4523 <div class="mxchat-card-header">
4524 <div class="mxchat-card-title"><?php echo esc_html($action->intent_label); ?></div>
4525 <div class="mxchat-card-toggle">
4526 <label class="mxchat-switch">
4527 <input type="checkbox" class="mxchat-action-toggle"
4528 data-action-id="<?php echo esc_attr($action->id); ?>"
4529 <?php checked($is_enabled); ?>>
4530 <span class="mxchat-slider round"></span>
4531 </label>
4532 </div>
4533 </div>
4534
4535 <div class="mxchat-card-body">
4536 <div class="mxchat-card-description">
4537 <strong><?php esc_html_e('Type:', 'mxchat'); ?></strong>
4538 <?php echo esc_html($callback_label); ?>
4539 </div>
4540
4541 <div class="mxchat-card-phrases">
4542 <strong><?php esc_html_e('Trigger phrases:', 'mxchat'); ?></strong>
4543 <div class="mxchat-phrases-preview">
4544 <?php
4545 // Check if the helper function exists, otherwise use a simple substring
4546 if (method_exists($this, 'get_trimmed_phrases')) {
4547 echo esc_html($this->get_trimmed_phrases($action->phrases));
4548 } else {
4549 echo esc_html(strlen($action->phrases) > 100 ?
4550 substr($action->phrases, 0, 97) . '...' :
4551 $action->phrases);
4552 }
4553 ?>
4554 </div>
4555 </div>
4556
4557 <div class="mxchat-threshold-control">
4558 <div class="mxchat-threshold-label">
4559 <?php esc_html_e('Similarity Threshold:', 'mxchat'); ?>
4560 <span class="mxchat-threshold-value"><?php echo esc_html($threshold_value); ?>%</span>
4561 </div>
4562 </div>
4563
4564 <!-- Bot Availability Display (Read-only) -->
4565 <div class="mxchat-card-bots">
4566 <strong><?php esc_html_e('Assigned bots:', 'mxchat'); ?></strong>
4567 <div class="mxchat-bot-badges">
4568 <?php
4569 foreach ($enabled_bots as $bot_id) {
4570 if ($bot_id === 'default') {
4571 echo '<span class="mxchat-bot-badge">' . esc_html__('Default Bot', 'mxchat') . '</span>';
4572 } else {
4573 // Try to get bot name from multi-bot manager
4574 if (class_exists('MxChat_Multi_Bot_Core_Manager')) {
4575 $multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
4576 $available_bots = $multi_bot_manager->get_available_bots();
4577 $bot_name = isset($available_bots[$bot_id]) ? $available_bots[$bot_id] : $bot_id;
4578 echo '<span class="mxchat-bot-badge">' . esc_html($bot_name) . '</span>';
4579 } else {
4580 echo '<span class="mxchat-bot-badge">' . esc_html($bot_id) . '</span>';
4581 }
4582 }
4583 }
4584 ?>
4585 </div>
4586 </div>
4587 </div>
4588
4589 <div class="mxchat-card-footer">
4590 <?php
4591 // Check if it's a form action
4592 $is_form_action = preg_match('/Form (\d+)/', $action->intent_label, $form_matches);
4593
4594 // Check if it's a recommendation flow action
4595 $is_flow_action = preg_match('/Recommendation Flow (\d+)/', $action->intent_label, $flow_matches);
4596
4597 if ($is_form_action) {
4598 $form_id = isset($form_matches[1]) ? $form_matches[1] : '';
4599 ?>
4600 <a href="<?php echo esc_url(admin_url('admin.php?page=mxchat-forms&action=edit&form_id=' . $form_id)); ?>"
4601 class="mxchat-button-primary">
4602 <span class="dashicons dashicons-feedback"></span>
4603 <?php esc_html_e('Edit Form', 'mxchat'); ?>
4604 </a>
4605 <?php } elseif ($is_flow_action) {
4606 ?>
4607 <a href="<?php echo esc_url(admin_url('admin.php?page=mxchat-smart-recommender')); ?>"
4608 class="mxchat-button-primary">
4609 <span class="dashicons dashicons-list-view"></span>
4610 <?php esc_html_e('Manage Flows', 'mxchat'); ?>
4611 </a>
4612 <?php } else { ?>
4613 <button type="button"
4614 class="mxchat-button-secondary mxchat-edit-button"
4615 data-action-id="<?php echo esc_attr($action->id); ?>"
4616 data-phrases="<?php echo esc_attr($action->phrases); ?>"
4617 data-label="<?php echo esc_attr($action->intent_label); ?>"
4618 data-threshold="<?php echo esc_attr(round($action->similarity_threshold * 100)); ?>"
4619 data-callback-function="<?php echo esc_attr($action->callback_function); ?>"
4620 data-enabled-bots="<?php echo esc_attr(json_encode($enabled_bots)); ?>">
4621 <span class="dashicons dashicons-edit"></span>
4622 <?php esc_html_e('Edit', 'mxchat'); ?>
4623 </button>
4624 <form method="post"
4625 action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
4626 class="mxchat-delete-form"
4627 onsubmit="return confirm('<?php esc_attr_e('Are you sure you want to delete this action?', 'mxchat'); ?>');">
4628 <?php wp_nonce_field('mxchat_delete_intent_nonce'); ?>
4629 <input type="hidden" name="action" value="mxchat_delete_intent">
4630 <input type="hidden" name="intent_id" value="<?php echo esc_attr($action->id); ?>">
4631 <button type="submit" class="mxchat-button-text mxchat-delete-button">
4632 <span class="dashicons dashicons-trash"></span>
4633 <?php esc_html_e('Delete', 'mxchat'); ?>
4634 </button>
4635 </form>
4636 <?php } ?>
4637 </div>
4638 </div>
4639 <?php endforeach; ?>
4640 <?php else : ?>
4641 <!-- If no actions found -->
4642 <div class="mxchat-no-actions">
4643 <div class="mxchat-empty-state">
4644 <span class="dashicons dashicons-format-chat"></span>
4645 <h2><?php esc_html_e('No actions found', 'mxchat'); ?></h2>
4646 <p><?php esc_html_e('Get started by creating your first action to enhance your chatbot.', 'mxchat'); ?></p>
4647 <button type="button" id="mxchat-create-first-action" class="mxchat-button-primary">
4648 <?php esc_html_e('Create Your First Action', 'mxchat'); ?>
4649 </button>
4650 </div>
4651 </div>
4652 <?php endif; ?>
4653 </div>
4654 </div>
4655
4656 <?php if ($total_pages > 1) : ?>
4657 <div class="mxchat-pagination">
4658 <?php
4659 echo paginate_links(array(
4660 'base' => add_query_arg('paged', '%#%'),
4661 'format' => '',
4662 'prev_text' => __('&laquo; Previous', 'mxchat'),
4663 'next_text' => __('Next &raquo;', 'mxchat'),
4664 'total' => $total_pages,
4665 'current' => $page
4666 ));
4667 ?>
4668 </div>
4669 <?php endif; ?>
4670
4671 <!-- Add/Edit Action Modal with Step-Based Approach -->
4672 <!-- Complete Modal HTML with Defined Groups Variable -->
4673 <div id="mxchat-action-modal" class="mxchat-modal" style="display: none;">
4674 <div class="mxchat-modal-content">
4675 <span class="mxchat-modal-close">&times;</span>
4676
4677 <form id="mxchat-action-form" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
4678 <!-- Dynamic nonce field -->
4679 <div id="action-nonce-container">
4680 <?php wp_nonce_field('mxchat_add_intent_nonce', 'add_intent_nonce'); ?>
4681 </div>
4682 <input type="hidden" name="action" id="form_action_type" value="mxchat_add_intent">
4683 <input type="hidden" name="intent_id" id="edit_action_id" value="">
4684 <input type="hidden" name="callback_function" id="callback_function" value="">
4685
4686 <!-- Step 1: Action Type Selection -->
4687 <div id="mxchat-action-step-1" class="mxchat-action-step active">
4688 <div class="mxchat-step-indicator">
4689 <div class="mxchat-step-number">1</div>
4690 <div class="mxchat-step-title"><?php esc_html_e('Select Action Type', 'mxchat'); ?></div>
4691 </div>
4692
4693 <div id="mxchat-action-type-selector" class="mxchat-action-type-selector">
4694 <div class="mxchat-action-type-search">
4695 <span class="dashicons dashicons-search"></span>
4696 <input type="text" id="action-type-search" placeholder="<?php esc_attr_e('Search action types...', 'mxchat'); ?>" class="mxchat-action-type-search-input">
4697 </div>
4698
4699 <?php
4700 // Get the callbacks - IMPORTANT: Define the $groups variable here
4701 $groups = $this->mxchat_get_available_callbacks(true, true);
4702 ?>
4703
4704 <div class="mxchat-action-type-categories">
4705 <button type="button" class="mxchat-category-button active" data-category="all"><?php esc_html_e('All', 'mxchat'); ?></button>
4706 <?php
4707 // Get unique categories from the defined groups
4708 foreach ($groups as $group_label => $group_callbacks) :
4709 $category_slug = sanitize_title($group_label);
4710 ?>
4711 <button type="button" class="mxchat-category-button" data-category="<?php echo esc_attr($category_slug); ?>"><?php echo esc_html($group_label); ?></button>
4712 <?php endforeach; ?>
4713 </div>
4714
4715 <div class="mxchat-action-types-grid">
4716 <?php
4717 // Generate action cards from available callbacks
4718 foreach ($groups as $group_label => $group_callbacks) :
4719 $category_slug = sanitize_title($group_label);
4720
4721 foreach ($group_callbacks as $function => $data) :
4722 $label = $data['label'];
4723 $icon = isset($data['icon']) ? $data['icon'] : 'admin-generic';
4724 $description = isset($data['description']) ? $data['description'] : '';
4725 $is_addon = isset($data['addon']) && $data['addon'] !== false;
4726 $addon_name = isset($data['addon_name']) ? $data['addon_name'] : '';
4727 $is_installed = isset($data['installed']) ? $data['installed'] : true;
4728 $is_promo = !empty($data['addon_promo']) && !$is_installed;
4729
4730 // Determine card status and styling
4731 $card_class = 'mxchat-action-type-card';
4732 $icon_class = 'mxchat-action-type-icon';
4733 $status_badge = '';
4734
4735 if ($is_promo) {
4736 // Promotional card — not selectable, just informational
4737 $card_class .= ' not-installed mxchat-promo-card';
4738 $status_badge = '<span class="mxchat-addon-badge">' . esc_html__('Add-on Required', 'mxchat') . '</span>';
4739 } elseif ($is_addon && !$is_installed) {
4740 // Add-on not installed
4741 $card_class .= ' not-installed';
4742 $status_badge .= '<span class="mxchat-addon-badge">' . esc_html__('Add-on Required', 'mxchat') . '</span>';
4743 }
4744
4745 // Default description if none provided
4746 if (empty($description)) {
4747 $description = sprintf(
4748 esc_html__('Use the %s action in your chatbot', 'mxchat'),
4749 $label
4750 );
4751 }
4752 ?>
4753 <div class="<?php echo esc_attr($card_class); ?>"
4754 data-category="<?php echo esc_attr($category_slug); ?>"
4755 <?php if (!$is_promo) : ?>
4756 data-value="<?php echo esc_attr($function); ?>"
4757 data-label="<?php echo esc_attr($label); ?>"
4758 <?php endif; ?>
4759 data-pro="false"
4760 data-addon="<?php echo esc_attr($is_addon ? $data['addon'] : ''); ?>"
4761 data-installed="<?php echo $is_installed ? 'true' : 'false'; ?>"
4762 <?php if ($is_promo) : ?>data-promo="true"<?php endif; ?>>
4763 <div class="<?php echo esc_attr($icon_class); ?>">
4764 <span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span>
4765 </div>
4766 <div class="mxchat-action-type-info">
4767 <h4><?php echo esc_html($label); ?></h4>
4768 <p><?php echo esc_html($description); ?></p>
4769 <?php if (!empty($status_badge)) : ?>
4770 <?php echo $status_badge; ?>
4771 <?php endif; ?>
4772
4773 <?php if ($is_promo || ($is_addon && !$is_installed)) : ?>
4774 <div class="mxchat-addon-info">
4775 <?php echo esc_html(sprintf(
4776 __('Requires %s', 'mxchat'),
4777 $addon_name
4778 )); ?>
4779 — <a href="https://mxchat.ai/" target="_blank"><?php esc_html_e('Get Add-on', 'mxchat'); ?></a>
4780 </div>
4781 <?php endif; ?>
4782 </div>
4783 </div>
4784 <?php endforeach;
4785 endforeach; ?>
4786 </div>
4787 </div>
4788
4789 <div class="mxchat-modal-actions">
4790 <button type="button" class="mxchat-button-secondary mxchat-modal-cancel">
4791 <?php esc_html_e('Cancel', 'mxchat'); ?>
4792 </button>
4793 </div>
4794 </div>
4795
4796 <!-- Step 2: Action Configuration -->
4797 <div id="mxchat-action-step-2" class="mxchat-action-step">
4798 <div class="mxchat-step-indicator">
4799 <div class="mxchat-step-number">2</div>
4800 <div class="mxchat-step-title"><?php esc_html_e('Configure Action', 'mxchat'); ?></div>
4801 </div>
4802
4803 <div class="mxchat-selected-action">
4804 <button type="button" class="mxchat-back-button" id="mxchat-back-to-step-1">
4805 <span class="dashicons dashicons-arrow-left-alt"></span>
4806 <?php esc_html_e('Back to Action Types', 'mxchat'); ?>
4807 </button>
4808 <div class="mxchat-selected-action-info">
4809 <div id="selected-action-icon" class="mxchat-action-type-icon">
4810 <span class="dashicons dashicons-admin-generic"></span>
4811 </div>
4812 <div class="mxchat-selected-action-details">
4813 <h3 id="selected-action-title"><?php esc_html_e('Selected Action', 'mxchat'); ?></h3>
4814 <p id="selected-action-description"><?php esc_html_e('Configure this action for your chatbot', 'mxchat'); ?></p>
4815 </div>
4816 </div>
4817 </div>
4818
4819 <div class="mxchat-form-group">
4820 <label for="intent_label">
4821 <?php esc_html_e('Action Label (For your reference only)', 'mxchat'); ?>
4822 </label>
4823 <input name="intent_label" type="text" id="intent_label" required
4824 class="mxchat-intent-input"
4825 placeholder="<?php esc_attr_e('Example: Newsletter Signup', 'mxchat'); ?>">
4826 </div>
4827
4828 <div class="mxchat-form-group">
4829 <label for="phrases">
4830 <?php esc_html_e('Trigger Phrases (comma-separated)', 'mxchat'); ?>
4831 </label>
4832 <textarea name="phrases" id="action_phrases" rows="5" required
4833 class="mxchat-intent-textarea"
4834 placeholder="<?php esc_attr_e('Example: sign me up, subscribe me, I want to join, add me to the newsletter', 'mxchat'); ?>"></textarea>
4835 </div>
4836
4837 <div class="mxchat-form-group">
4838 <label for="similarity_threshold">
4839 <?php esc_html_e('Similarity Threshold', 'mxchat'); ?>
4840 <span class="mxchat-threshold-value-display">85%</span>
4841 </label>
4842 <div class="mxchat-slider-group modal-slider">
4843 <input type="range"
4844 name="similarity_threshold"
4845 id="similarity_threshold"
4846 min="10"
4847 max="95"
4848 value="85"
4849 class="mxchat-intent-slider"
4850 oninput="document.querySelector('.mxchat-threshold-value-display').textContent = this.value + '%'">
4851 </div>
4852 <div class="mxchat-threshold-hint">
4853 <?php esc_html_e('Lower values (10-30) make the action trigger more easily. Higher values (70-95) require more exact matches.', 'mxchat'); ?>
4854 </div>
4855 </div>
4856
4857 <!-- Bot Selection Section -->
4858 <div class="mxchat-form-group">
4859 <label for="enabled_bots">
4860 <?php esc_html_e('Which bots should we enable this action for?', 'mxchat'); ?>
4861 </label>
4862 <div class="mxchat-bot-selector">
4863 <div class="mxchat-bot-option">
4864 <label class="mxchat-checkbox-label">
4865 <input type="checkbox"
4866 name="enabled_bots[]"
4867 value="default"
4868 id="bot_default"
4869 checked="checked">
4870 <span class="mxchat-checkmark"></span>
4871 <?php esc_html_e('Default Bot', 'mxchat'); ?>
4872 </label>
4873 </div>
4874
4875 <?php if (class_exists('MxChat_Multi_Bot_Core_Manager')) :
4876 $multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
4877 $available_bots = $multi_bot_manager->get_available_bots();
4878
4879 foreach ($available_bots as $bot_id => $bot_name) :
4880 if ($bot_id === 'default') continue; // Skip default, already shown above
4881 ?>
4882 <div class="mxchat-bot-option">
4883 <label class="mxchat-checkbox-label">
4884 <input type="checkbox"
4885 name="enabled_bots[]"
4886 value="<?php echo esc_attr($bot_id); ?>"
4887 id="bot_<?php echo esc_attr($bot_id); ?>">
4888 <span class="mxchat-checkmark"></span>
4889 <?php echo esc_html($bot_name); ?>
4890 </label>
4891 </div>
4892 <?php
4893 endforeach;
4894 endif; ?>
4895 </div>
4896 </div>
4897
4898 <div class="mxchat-modal-actions">
4899 <button type="button" class="mxchat-button-secondary mxchat-modal-cancel">
4900 <?php esc_html_e('Cancel', 'mxchat'); ?>
4901 </button>
4902 <button type="submit" class="mxchat-button-primary" id="mxchat-save-action-btn">
4903 <?php esc_html_e('Save Action', 'mxchat'); ?>
4904 </button>
4905 </div>
4906 </div>
4907 </form>
4908 </div>
4909 </div>
4910
4911 <div id="mxchat-action-loading" class="mxchat-action-loading" style="display: none;">
4912 <div class="mxchat-action-loading-spinner"></div>
4913 <div class="mxchat-action-loading-text">
4914 <?php esc_html_e('Saving action, please wait...', 'mxchat'); ?>
4915 </div>
4916 </div>
4917 </div><!-- .mxchat-wrapper -->
4918 <?php
4919 }
4920 private function get_trimmed_phrases($phrases, $max_length = 100) {
4921 if (strlen($phrases) <= $max_length) {
4922 return $phrases;
4923 }
4924
4925 $trimmed = substr($phrases, 0, $max_length);
4926 $last_comma = strrpos($trimmed, ',');
4927
4928 if ($last_comma !== false) {
4929 $trimmed = substr($trimmed, 0, $last_comma);
4930 }
4931
4932 return $trimmed . '...';
4933 }
4934 public function mxchat_add_enabled_column_to_intents() {
4935 global $wpdb;
4936 $table_name = $wpdb->prefix . 'mxchat_intents';
4937
4938 // Check if the column already exists
4939 $columns = $wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'enabled'");
4940
4941 if (empty($columns)) {
4942 // Add the column with default value of 1 (enabled)
4943 $wpdb->query("ALTER TABLE $table_name ADD COLUMN enabled TINYINT(1) NOT NULL DEFAULT 1");
4944 }
4945 }
4946
4947 public function mxchat_handle_delete_intent() {
4948 if ( ! current_user_can( 'manage_options' ) ) {
4949 wp_die( esc_html__('Unauthorized user', 'mxchat') );
4950 }
4951
4952 check_admin_referer('mxchat_delete_intent_nonce');
4953
4954 if (isset($_POST['intent_id'])) {
4955 global $wpdb;
4956 $table_name = $wpdb->prefix . 'mxchat_intents';
4957 $intent_id = intval($_POST['intent_id']);
4958
4959 $wpdb->delete($table_name, ['id' => $intent_id], ['%d']);
4960 }
4961
4962 wp_safe_redirect(admin_url('admin.php?page=mxchat-actions'));
4963 exit;
4964 }
4965 public function mxchat_handle_edit_intent() {
4966 // Security checks (nonce and permissions)
4967 if (!current_user_can('manage_options')) {
4968 wp_die(esc_html__('Unauthorized user', 'mxchat'));
4969 }
4970 check_admin_referer('mxchat_edit_intent');
4971
4972 // Get POST data
4973 $intent_id = isset($_POST['intent_id']) ? absint($_POST['intent_id']) : 0;
4974 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
4975 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
4976 $threshold_percentage = isset($_POST['similarity_threshold']) ? intval($_POST['similarity_threshold']) : 85;
4977 $similarity_threshold = min(95, max(10, $threshold_percentage)) / 100; // Convert to 0.10–0.95
4978
4979 // Handle enabled_bots
4980 $enabled_bots = isset($_POST['enabled_bots']) ? $_POST['enabled_bots'] : array('default');
4981 $enabled_bots = array_map('sanitize_text_field', $enabled_bots);
4982
4983 // Ensure default is always included for backward compatibility
4984 if (!in_array('default', $enabled_bots)) {
4985 $enabled_bots[] = 'default';
4986 }
4987
4988 $enabled_bots_json = json_encode($enabled_bots);
4989
4990 // Validate inputs
4991 if (!$intent_id || empty($intent_label) || empty($phrases_input)) {
4992 $this->handle_embedding_error(__('Invalid input. Please ensure all fields are filled out.', 'mxchat'));
4993 return;
4994 }
4995
4996 $phrases_array = array_map('sanitize_text_field', array_filter(array_map('trim', explode(',', $phrases_input))));
4997 if (empty($phrases_array)) {
4998 $this->handle_embedding_error(__('Please enter at least one valid phrase.', 'mxchat'));
4999 return;
5000 }
5001
5002 // Generate embeddings with improved error handling
5003 $vectors = [];
5004 $failed_phrases = [];
5005
5006 foreach ($phrases_array as $phrase) {
5007 $embedding_vector = $this->mxchat_generate_embedding($phrase, $this->options['api_key']);
5008 if (is_array($embedding_vector)) {
5009 $vectors[] = $embedding_vector;
5010 } else {
5011 $failed_phrases[] = $phrase;
5012 }
5013 }
5014
5015 if (!empty($failed_phrases)) {
5016 $this->handle_embedding_error(
5017 sprintf(
5018 __('Error generating embeddings for phrases: %s. Check your embedding API.', 'mxchat'),
5019 implode(', ', $failed_phrases)
5020 )
5021 );
5022 return;
5023 }
5024
5025 if (empty($vectors)) {
5026 $this->handle_embedding_error(__('No valid embeddings generated. Please check your phrases.', 'mxchat'));
5027 return;
5028 }
5029
5030 $combined_vector = $this->mxchat_average_vectors($vectors);
5031 $serialized_vector = maybe_serialize($combined_vector);
5032
5033 // Update the database
5034 global $wpdb;
5035 $table_name = $wpdb->prefix . 'mxchat_intents';
5036
5037 $result = $wpdb->update(
5038 $table_name,
5039 array(
5040 'intent_label' => $intent_label,
5041 'phrases' => implode(', ', $phrases_array),
5042 'embedding_vector' => $serialized_vector,
5043 'similarity_threshold' => $similarity_threshold,
5044 'enabled_bots' => $enabled_bots_json, // Include enabled_bots in update
5045 ),
5046 array('id' => $intent_id),
5047 array('%s', '%s', '%s', '%f', '%s'), // Format: string, string, string, float, string
5048 array('%d') // Where format: integer
5049 );
5050
5051 if (false === $result) {
5052 $this->handle_embedding_error(__('Failed to update action in database.', 'mxchat'));
5053 return;
5054 }
5055
5056 // Set success message and redirect
5057 set_transient('mxchat_admin_notice_success', __('Intent updated successfully!', 'mxchat'), 60);
5058
5059 $redirect_url = add_query_arg(
5060 array(
5061 'page' => 'mxchat-actions'
5062 ),
5063 admin_url('admin.php')
5064 );
5065 wp_safe_redirect($redirect_url);
5066 exit;
5067 }
5068 public function mxchat_handle_add_intent() {
5069 if (!current_user_can('manage_options')) {
5070 wp_die(esc_html__('Unauthorized user', 'mxchat'));
5071 }
5072 check_admin_referer('mxchat_add_intent_nonce');
5073 global $wpdb;
5074 $table_name = $wpdb->prefix . 'mxchat_intents';
5075
5076 // Sanitize and get form data
5077 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
5078 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
5079 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
5080
5081 // Get similarity threshold from form (convert percentage to decimal)
5082 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
5083
5084 // Handle enabled_bots
5085 $enabled_bots = isset($_POST['enabled_bots']) ? $_POST['enabled_bots'] : array('default');
5086 $enabled_bots = array_map('sanitize_text_field', $enabled_bots);
5087
5088 // Ensure default is always included for backward compatibility with existing actions
5089 if (!in_array('default', $enabled_bots)) {
5090 $enabled_bots[] = 'default';
5091 }
5092
5093 $enabled_bots_json = json_encode($enabled_bots);
5094
5095 // Validate required fields
5096 if (empty($intent_label) || empty($callback_function) || empty($phrases_input)) {
5097 $this->handle_embedding_error(__('Invalid input. Please ensure all fields are filled out.', 'mxchat'));
5098 return;
5099 }
5100
5101 // Validate callback function
5102 $available_callbacks = $this->mxchat_get_available_callbacks();
5103 if (!array_key_exists($callback_function, $available_callbacks)) {
5104 $this->handle_embedding_error(__('Invalid callback function selected.', 'mxchat'));
5105 return;
5106 }
5107
5108 // Check if this is an add-on promotional placeholder (not a real action)
5109 if (!empty($available_callbacks[$callback_function]['addon_promo'])) {
5110 $addon_name = isset($available_callbacks[$callback_function]['addon_name']) ? $available_callbacks[$callback_function]['addon_name'] : __('an add-on', 'mxchat');
5111 $this->handle_embedding_error(sprintf(
5112 __('This action requires the %s to be installed and activated.', 'mxchat'),
5113 $addon_name
5114 ));
5115 return;
5116 }
5117
5118 // Process phrases
5119 $phrases_array = array_map('sanitize_text_field', array_filter(array_map('trim', explode(',', $phrases_input))));
5120 if (empty($phrases_array)) {
5121 $this->handle_embedding_error(__('Please enter at least one valid phrase.', 'mxchat'));
5122 return;
5123 }
5124
5125 // Generate embeddings with improved error handling
5126 $vectors = [];
5127 $failed_phrases = [];
5128 foreach ($phrases_array as $phrase) {
5129 $embedding_vector = $this->mxchat_generate_embedding($phrase, $this->options['api_key']);
5130 if (is_array($embedding_vector)) {
5131 $vectors[] = $embedding_vector;
5132 } else {
5133 $failed_phrases[] = $phrase;
5134 }
5135 }
5136
5137 // Check for embedding failures
5138 if (!empty($failed_phrases)) {
5139 $this->handle_embedding_error(
5140 sprintf(
5141 __('Error generating embeddings for phrases: %s. Check your embedding API.', 'mxchat'),
5142 implode(', ', $failed_phrases)
5143 )
5144 );
5145 return;
5146 }
5147
5148 if (empty($vectors)) {
5149 $this->handle_embedding_error(__('No valid embeddings generated. Please check your phrases.', 'mxchat'));
5150 return;
5151 }
5152
5153 // Create combined vector and insert into database
5154 $combined_vector = $this->mxchat_average_vectors($vectors);
5155 $serialized_vector = maybe_serialize($combined_vector);
5156
5157 $result = $wpdb->insert($table_name, [
5158 'intent_label' => $intent_label,
5159 'phrases' => implode(', ', $phrases_array),
5160 'embedding_vector' => $serialized_vector,
5161 'callback_function' => $callback_function,
5162 'similarity_threshold' => $similarity_threshold,
5163 'enabled_bots' => $enabled_bots_json, // NEW field
5164 ]);
5165
5166 if ($result === false) {
5167 $this->handle_embedding_error(__('Database error: ', 'mxchat') . $wpdb->last_error);
5168 return;
5169 }
5170
5171 // Set success message and redirect
5172 set_transient('mxchat_admin_notice_success', __('New intent added successfully!', 'mxchat'), 60);
5173 wp_safe_redirect(admin_url('admin.php?page=mxchat-actions'));
5174 exit;
5175 }
5176
5177
5178
5179
5180 private function handle_embedding_error($message, $redirect = true) {
5181 // Store the error message in the existing transient
5182 set_transient('mxchat_admin_notice_error', $message, 60);
5183
5184 if ($redirect) {
5185 // Redirect back to the actions page
5186 $redirect_url = add_query_arg(
5187 array(
5188 'page' => 'mxchat-actions'
5189 ),
5190 admin_url('admin.php')
5191 );
5192 wp_safe_redirect($redirect_url);
5193 exit;
5194 }
5195 }
5196
5197 /**
5198 * AJAX handler to fetch actions list for the new split-panel UI
5199 */
5200 public function mxchat_fetch_actions_list() {
5201 check_ajax_referer('mxchat_actions_nonce', 'security');
5202
5203 if (!current_user_can('manage_options')) {
5204 wp_send_json_error(__('Unauthorized', 'mxchat'));
5205 }
5206
5207 global $wpdb;
5208 $table_name = $wpdb->prefix . 'mxchat_intents';
5209
5210 $page = isset($_POST['page']) ? max(1, intval($_POST['page'])) : 1;
5211 $per_page = isset($_POST['per_page']) ? min(100, max(1, intval($_POST['per_page']))) : 50;
5212 $offset = ($page - 1) * $per_page;
5213 $search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
5214 $callback_filter = isset($_POST['callback_filter']) ? sanitize_text_field($_POST['callback_filter']) : '';
5215 $sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] === 'asc' ? 'ASC' : 'DESC';
5216
5217 // Build WHERE clause
5218 $where = '1=1';
5219 $params = array();
5220
5221 if ($search) {
5222 $search_like = '%' . $wpdb->esc_like($search) . '%';
5223 $where .= ' AND (intent_label LIKE %s OR phrases LIKE %s)';
5224 $params[] = $search_like;
5225 $params[] = $search_like;
5226 }
5227
5228 if ($callback_filter) {
5229 $where .= ' AND callback_function = %s';
5230 $params[] = $callback_filter;
5231 }
5232
5233 // Get total count
5234 $count_query = "SELECT COUNT(*) FROM $table_name WHERE $where";
5235 if (!empty($params)) {
5236 $count_query = $wpdb->prepare($count_query, $params);
5237 }
5238 $total_actions = $wpdb->get_var($count_query);
5239
5240 // Get actions
5241 $query = "SELECT * FROM $table_name WHERE $where ORDER BY id $sort_order LIMIT %d OFFSET %d";
5242 $all_params = array_merge($params, array($per_page, $offset));
5243 $actions = $wpdb->get_results($wpdb->prepare($query, $all_params));
5244
5245 // Get available callbacks for labels/icons
5246 $available_callbacks = $this->mxchat_get_available_callbacks();
5247
5248 // Prefetch individual phrase counts for all fetched actions
5249 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5250 $phrase_counts = array();
5251 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5252 $action_ids = wp_list_pluck($actions, 'id');
5253 if (!empty($action_ids)) {
5254 $id_placeholders = implode(',', array_fill(0, count($action_ids), '%d'));
5255 $count_results = $wpdb->get_results($wpdb->prepare(
5256 "SELECT intent_id, COUNT(*) as cnt FROM $phrases_table WHERE intent_id IN ($id_placeholders) GROUP BY intent_id",
5257 $action_ids
5258 ));
5259 foreach ($count_results as $row) {
5260 $phrase_counts[$row->intent_id] = intval($row->cnt);
5261 }
5262 }
5263 }
5264
5265 // Format actions for response
5266 $formatted_actions = array();
5267 foreach ($actions as $action) {
5268 $callback_data = isset($available_callbacks[$action->callback_function])
5269 ? $available_callbacks[$action->callback_function]
5270 : array('label' => $action->callback_function, 'icon' => 'admin-generic');
5271
5272 $enabled_bots = json_decode($action->enabled_bots, true);
5273 if (!is_array($enabled_bots)) {
5274 $enabled_bots = array('default');
5275 }
5276
5277 $formatted_actions[] = array(
5278 'id' => intval($action->id),
5279 'label' => $action->intent_label,
5280 'phrases' => $action->phrases,
5281 'callback_function' => $action->callback_function,
5282 'callback_label' => $callback_data['label'],
5283 'icon' => isset($callback_data['icon']) ? $callback_data['icon'] : 'admin-generic',
5284 'threshold' => round($action->similarity_threshold * 100),
5285 'enabled' => (bool) $action->enabled,
5286 'enabled_bots' => $enabled_bots,
5287 'has_legacy_vector' => !empty($action->embedding_vector),
5288 'individual_phrase_count' => isset($phrase_counts[$action->id]) ? $phrase_counts[$action->id] : 0,
5289 );
5290 }
5291
5292 $total_pages = ceil($total_actions / $per_page);
5293 $showing_start = $total_actions > 0 ? $offset + 1 : 0;
5294 $showing_end = min($offset + $per_page, $total_actions);
5295
5296 wp_send_json_success(array(
5297 'actions' => $formatted_actions,
5298 'page' => $page,
5299 'per_page' => $per_page,
5300 'total_actions' => intval($total_actions),
5301 'total_pages' => $total_pages,
5302 'showing_start' => $showing_start,
5303 'showing_end' => $showing_end,
5304 ));
5305 }
5306
5307 /**
5308 * AJAX handler to toggle action enabled status
5309 */
5310 public function mxchat_toggle_action_status() {
5311 check_ajax_referer('mxchat_actions_nonce', 'security');
5312
5313 if (!current_user_can('manage_options')) {
5314 wp_send_json_error(__('Unauthorized', 'mxchat'));
5315 }
5316
5317 $action_id = isset($_POST['action_id']) ? intval($_POST['action_id']) : 0;
5318 $enabled = isset($_POST['enabled']) ? intval($_POST['enabled']) : 0;
5319
5320 if (!$action_id) {
5321 wp_send_json_error(__('Invalid action ID', 'mxchat'));
5322 }
5323
5324 global $wpdb;
5325 $table_name = $wpdb->prefix . 'mxchat_intents';
5326
5327 $result = $wpdb->update(
5328 $table_name,
5329 array('enabled' => $enabled ? 1 : 0),
5330 array('id' => $action_id),
5331 array('%d'),
5332 array('%d')
5333 );
5334
5335 if ($result === false) {
5336 wp_send_json_error(__('Failed to update action status', 'mxchat'));
5337 }
5338
5339 wp_send_json_success(array('enabled' => (bool) $enabled));
5340 }
5341
5342 /**
5343 * AJAX handler to bulk delete actions
5344 */
5345 public function mxchat_bulk_delete_actions() {
5346 check_ajax_referer('mxchat_delete_intent_nonce', 'security');
5347
5348 if (!current_user_can('manage_options')) {
5349 wp_send_json_error(__('Unauthorized', 'mxchat'));
5350 }
5351
5352 $action_ids = isset($_POST['action_ids']) ? array_map('intval', (array) $_POST['action_ids']) : array();
5353
5354 if (empty($action_ids)) {
5355 wp_send_json_error(__('No actions selected', 'mxchat'));
5356 }
5357
5358 global $wpdb;
5359 $table_name = $wpdb->prefix . 'mxchat_intents';
5360
5361 $placeholders = implode(',', array_fill(0, count($action_ids), '%d'));
5362 $query = $wpdb->prepare("DELETE FROM $table_name WHERE id IN ($placeholders)", $action_ids);
5363 $result = $wpdb->query($query);
5364
5365 if ($result === false) {
5366 wp_send_json_error(__('Failed to delete actions', 'mxchat'));
5367 }
5368
5369 // Also delete individual phrases for these intents
5370 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5371 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5372 $wpdb->query($wpdb->prepare("DELETE FROM $phrases_table WHERE intent_id IN ($placeholders)", $action_ids));
5373 }
5374
5375 wp_send_json_success(array('deleted' => $result));
5376 }
5377
5378 /**
5379 * AJAX handler to add a new intent/action
5380 */
5381 public function mxchat_add_intent_ajax() {
5382 check_ajax_referer('mxchat_add_intent_nonce', 'security');
5383
5384 if (!current_user_can('manage_options')) {
5385 wp_send_json_error(__('Unauthorized', 'mxchat'));
5386 }
5387
5388 global $wpdb;
5389 $table_name = $wpdb->prefix . 'mxchat_intents';
5390
5391 // Sanitize input
5392 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
5393 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
5394 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
5395 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
5396 $enabled_bots = isset($_POST['enabled_bots']) ? array_map('sanitize_text_field', (array) $_POST['enabled_bots']) : array('default');
5397
5398 // Validate
5399 // Check if using new individual phrases mode or legacy mode
5400 $individual_phrases = isset($_POST['individual_phrases']) ? array_filter(array_map('sanitize_text_field', (array) $_POST['individual_phrases'])) : array();
5401 $use_individual = !empty($individual_phrases);
5402
5403 // Validate required fields (phrases not required when using individual mode)
5404 if (empty($intent_label) || empty($callback_function)) {
5405 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
5406 }
5407 if (!$use_individual && empty($phrases_input)) {
5408 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
5409 }
5410
5411 // Ensure default bot is included
5412 if (!in_array('default', $enabled_bots)) {
5413 $enabled_bots[] = 'default';
5414 }
5415 $enabled_bots_json = json_encode($enabled_bots);
5416
5417 if ($use_individual) {
5418 // New mode: individual phrases each get their own vector
5419 // Insert the intent row with empty legacy fields
5420 $result = $wpdb->insert(
5421 $table_name,
5422 array(
5423 'intent_label' => $intent_label,
5424 'phrases' => '',
5425 'embedding_vector' => '',
5426 'similarity_threshold' => $similarity_threshold,
5427 'callback_function' => $callback_function,
5428 'enabled' => 1,
5429 'enabled_bots' => $enabled_bots_json,
5430 )
5431 );
5432
5433 if ($result === false) {
5434 wp_send_json_error(__('Failed to add action to database.', 'mxchat'));
5435 }
5436
5437 $intent_id = $wpdb->insert_id;
5438
5439 // Insert each phrase individually with its own embedding
5440 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5441 $failed_phrases = array();
5442 foreach ($individual_phrases as $phrase) {
5443 $phrase = trim($phrase);
5444 if (empty($phrase)) continue;
5445
5446 $embedding_vector = $this->mxchat_generate_embedding($phrase);
5447 if (is_wp_error($embedding_vector)) {
5448 $failed_phrases[] = $phrase;
5449 continue;
5450 }
5451
5452 $wpdb->insert(
5453 $phrases_table,
5454 array(
5455 'intent_id' => $intent_id,
5456 'phrase' => $phrase,
5457 'embedding_vector' => maybe_serialize($embedding_vector),
5458 )
5459 );
5460 }
5461
5462 $response = array('id' => $intent_id);
5463 if (!empty($failed_phrases)) {
5464 $response['failed_phrases'] = $failed_phrases;
5465 }
5466 wp_send_json_success($response);
5467
5468 } else {
5469 // Legacy mode: combine all phrases into one embedding (backwards compatible)
5470 $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
5471 if (empty($phrases_array)) {
5472 wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
5473 }
5474
5475 // Generate embedding (combine phrases into single string for embedding)
5476 $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
5477 if (is_wp_error($embedding_vector)) {
5478 // Fallback: store without embedding
5479 $serialized_vector = null;
5480 } else {
5481 $serialized_vector = maybe_serialize($embedding_vector);
5482 }
5483
5484 // Insert
5485 $result = $wpdb->insert(
5486 $table_name,
5487 array(
5488 'intent_label' => $intent_label,
5489 'phrases' => implode(', ', $phrases_array),
5490 'embedding_vector' => $serialized_vector,
5491 'similarity_threshold' => $similarity_threshold,
5492 'callback_function' => $callback_function,
5493 'enabled' => 1,
5494 'enabled_bots' => $enabled_bots_json,
5495 )
5496 );
5497
5498 if ($result === false) {
5499 wp_send_json_error(__('Failed to add action to database.', 'mxchat'));
5500 }
5501
5502 wp_send_json_success(array('id' => $wpdb->insert_id));
5503 }
5504 }
5505
5506 /**
5507 * AJAX handler to edit an existing intent/action
5508 */
5509 public function mxchat_edit_intent_ajax() {
5510 check_ajax_referer('mxchat_edit_intent', 'security');
5511
5512 if (!current_user_can('manage_options')) {
5513 wp_send_json_error(__('Unauthorized', 'mxchat'));
5514 }
5515
5516 global $wpdb;
5517 $table_name = $wpdb->prefix . 'mxchat_intents';
5518
5519 // Sanitize input
5520 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5521 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
5522 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
5523 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
5524 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
5525 $enabled_bots = isset($_POST['enabled_bots']) ? array_map('sanitize_text_field', (array) $_POST['enabled_bots']) : array('default');
5526
5527 // Validate
5528 if (!$intent_id || empty($intent_label)) {
5529 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
5530 }
5531
5532 // Check if phrases are managed individually (empty phrases_input means individual mode)
5533 $uses_individual_phrases = empty($phrases_input);
5534
5535 if ($uses_individual_phrases) {
5536 // Individual phrase mode: only update non-phrase fields, skip embedding regeneration
5537 $update_data = array(
5538 'intent_label' => $intent_label,
5539 'similarity_threshold' => $similarity_threshold,
5540 'callback_function' => $callback_function,
5541 'enabled_bots' => json_encode($enabled_bots),
5542 );
5543 } else {
5544 // Legacy mode: process phrases and regenerate embedding (backwards compatible for add-ons)
5545 $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
5546 if (empty($phrases_array)) {
5547 wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
5548 }
5549
5550 // Generate new embedding (combine phrases into single string for embedding)
5551 $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
5552 if (is_wp_error($embedding_vector)) {
5553 // Keep existing embedding
5554 $update_data = array(
5555 'intent_label' => $intent_label,
5556 'phrases' => implode(', ', $phrases_array),
5557 'similarity_threshold' => $similarity_threshold,
5558 'callback_function' => $callback_function,
5559 'enabled_bots' => json_encode($enabled_bots),
5560 );
5561 } else {
5562 $serialized_vector = maybe_serialize($embedding_vector);
5563 $update_data = array(
5564 'intent_label' => $intent_label,
5565 'phrases' => implode(', ', $phrases_array),
5566 'embedding_vector' => $serialized_vector,
5567 'similarity_threshold' => $similarity_threshold,
5568 'callback_function' => $callback_function,
5569 'enabled_bots' => json_encode($enabled_bots),
5570 );
5571 }
5572 }
5573
5574 // Ensure default bot is included
5575 if (!in_array('default', $enabled_bots)) {
5576 $enabled_bots[] = 'default';
5577 }
5578 $update_data['enabled_bots'] = json_encode($enabled_bots);
5579
5580 $result = $wpdb->update(
5581 $table_name,
5582 $update_data,
5583 array('id' => $intent_id),
5584 null,
5585 array('%d')
5586 );
5587
5588 if ($result === false) {
5589 wp_send_json_error(__('Failed to update action.', 'mxchat'));
5590 }
5591
5592 wp_send_json_success(array('updated' => true));
5593 }
5594
5595 /**
5596 * AJAX handler to delete a single intent/action
5597 */
5598 public function mxchat_delete_intent_ajax() {
5599 check_ajax_referer('mxchat_delete_intent_nonce', 'security');
5600
5601 if (!current_user_can('manage_options')) {
5602 wp_send_json_error(__('Unauthorized', 'mxchat'));
5603 }
5604
5605 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5606
5607 if (!$intent_id) {
5608 wp_send_json_error(__('Invalid action ID', 'mxchat'));
5609 }
5610
5611 global $wpdb;
5612 $table_name = $wpdb->prefix . 'mxchat_intents';
5613
5614 $result = $wpdb->delete($table_name, array('id' => $intent_id), array('%d'));
5615
5616 if ($result === false) {
5617 wp_send_json_error(__('Failed to delete action', 'mxchat'));
5618 }
5619
5620 // Also delete individual phrases for this intent
5621 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5622 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5623 $wpdb->delete($phrases_table, array('intent_id' => $intent_id), array('%d'));
5624 }
5625
5626 wp_send_json_success(array('deleted' => true));
5627 }
5628
5629 /**
5630 * AJAX handler to add a single phrase with its own embedding to an intent
5631 */
5632 public function mxchat_add_phrase_ajax() {
5633 check_ajax_referer('mxchat_add_phrase_nonce', 'security');
5634
5635 if (!current_user_can('manage_options')) {
5636 wp_send_json_error(__('Unauthorized', 'mxchat'));
5637 }
5638
5639 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5640 $phrase = isset($_POST['phrase']) ? sanitize_text_field($_POST['phrase']) : '';
5641
5642 if (!$intent_id || empty($phrase)) {
5643 wp_send_json_error(__('Please provide an intent ID and phrase.', 'mxchat'));
5644 }
5645
5646 // Verify the intent exists
5647 global $wpdb;
5648 $intents_table = $wpdb->prefix . 'mxchat_intents';
5649 $intent = $wpdb->get_row($wpdb->prepare("SELECT id FROM $intents_table WHERE id = %d", $intent_id));
5650 if (!$intent) {
5651 wp_send_json_error(__('Action not found.', 'mxchat'));
5652 }
5653
5654 // Generate embedding for this single phrase
5655 $embedding_vector = $this->mxchat_generate_embedding($phrase);
5656 if (is_wp_error($embedding_vector)) {
5657 wp_send_json_error(__('Failed to generate embedding: ', 'mxchat') . $embedding_vector->get_error_message());
5658 }
5659
5660 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5661 $result = $wpdb->insert(
5662 $phrases_table,
5663 array(
5664 'intent_id' => $intent_id,
5665 'phrase' => $phrase,
5666 'embedding_vector' => maybe_serialize($embedding_vector),
5667 )
5668 );
5669
5670 if ($result === false) {
5671 wp_send_json_error(__('Failed to add phrase.', 'mxchat'));
5672 }
5673
5674 wp_send_json_success(array('id' => $wpdb->insert_id, 'phrase' => $phrase));
5675 }
5676
5677 /**
5678 * AJAX handler to delete a single phrase from wp_mxchat_intent_phrases
5679 */
5680 public function mxchat_delete_phrase_ajax() {
5681 check_ajax_referer('mxchat_delete_phrase_nonce', 'security');
5682
5683 if (!current_user_can('manage_options')) {
5684 wp_send_json_error(__('Unauthorized', 'mxchat'));
5685 }
5686
5687 $phrase_id = isset($_POST['phrase_id']) ? intval($_POST['phrase_id']) : 0;
5688 if (!$phrase_id) {
5689 wp_send_json_error(__('Invalid phrase ID.', 'mxchat'));
5690 }
5691
5692 global $wpdb;
5693 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5694 $result = $wpdb->delete($phrases_table, array('id' => $phrase_id), array('%d'));
5695
5696 if ($result === false) {
5697 wp_send_json_error(__('Failed to delete phrase.', 'mxchat'));
5698 }
5699
5700 wp_send_json_success(array('deleted' => true));
5701 }
5702
5703 /**
5704 * AJAX handler to fetch individual phrases for an intent
5705 */
5706 public function mxchat_get_phrases_ajax() {
5707 check_ajax_referer('mxchat_get_phrases_nonce', 'security');
5708
5709 if (!current_user_can('manage_options')) {
5710 wp_send_json_error(__('Unauthorized', 'mxchat'));
5711 }
5712
5713 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5714 if (!$intent_id) {
5715 wp_send_json_error(__('Invalid intent ID.', 'mxchat'));
5716 }
5717
5718 global $wpdb;
5719 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5720
5721 $phrases = array();
5722 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5723 $phrases = $wpdb->get_results($wpdb->prepare(
5724 "SELECT id, phrase, created_at FROM $phrases_table WHERE intent_id = %d ORDER BY created_at ASC",
5725 $intent_id
5726 ));
5727 }
5728
5729 wp_send_json_success(array('phrases' => $phrases));
5730 }
5731
5732 /**
5733 * AJAX handler to clear legacy phrases and embedding from the main intents table
5734 */
5735 public function mxchat_delete_legacy_phrases_ajax() {
5736 check_ajax_referer('mxchat_delete_legacy_nonce', 'security');
5737
5738 if (!current_user_can('manage_options')) {
5739 wp_send_json_error(__('Unauthorized', 'mxchat'));
5740 }
5741
5742 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5743 if (!$intent_id) {
5744 wp_send_json_error(__('Invalid intent ID.', 'mxchat'));
5745 }
5746
5747 global $wpdb;
5748 $table_name = $wpdb->prefix . 'mxchat_intents';
5749
5750 $result = $wpdb->update(
5751 $table_name,
5752 array('phrases' => '', 'embedding_vector' => ''),
5753 array('id' => $intent_id),
5754 array('%s', '%s'),
5755 array('%d')
5756 );
5757
5758 if ($result === false) {
5759 wp_send_json_error(__('Failed to clear legacy phrases.', 'mxchat'));
5760 }
5761
5762 wp_send_json_success(array('cleared' => true));
5763 }
5764
5765 /**
5766 * Enhanced get_available_callbacks function with form action exclusion
5767 *
5768 * @param bool $grouped Whether to return callbacks grouped by category
5769 * @param bool $include_all Whether to include all potential actions (even if add-on not installed)
5770 * @return array Callbacks data with icons, descriptions and availability status
5771 */
5772 private function mxchat_get_available_callbacks($grouped = false, $include_all = true) {
5773 // Load WordPress plugin functions if needed
5774 if (!function_exists('get_plugins')) {
5775 require_once ABSPATH . 'wp-admin/includes/plugin.php';
5776 }
5777
5778 // Get active plugins
5779 $active_plugins = get_option('active_plugins', array());
5780
5781 // Functions to exclude from the action selector only if Pro is activated
5782 // If user doesn't have Pro, show these so they can see what they're missing
5783 $excluded_when_pro_active_functions = array(
5784 'mxchat_handle_form_collection' // Forms add-on action
5785 );
5786
5787 // Always excluded functions (regardless of Pro status)
5788 $always_excluded_functions = array();
5789
5790 // Combine exclusion lists based on Pro activation status
5791 $excluded_functions = $always_excluded_functions;
5792 if ($this->is_activated) {
5793 // Only exclude add-on managed functions if Pro is active
5794 $excluded_functions = array_merge($excluded_functions, $excluded_when_pro_active_functions);
5795 }
5796
5797 // Define add-on plugin files and their corresponding action functions
5798 $addon_plugins = array(
5799 'mxchat-woo/mxchat-woo.php' => array(
5800 'functions' => array(
5801 'mxchat_handle_product_recommendations',
5802 'mxchat_handle_order_history',
5803 'mxchat_show_product_card',
5804 'mxchat_add_to_cart',
5805 'mxchat_checkout_redirect',
5806 'mxchat_handle_featured_products'
5807 ),
5808 'name' => __('WooCommerce Add-on', 'mxchat'),
5809 'pro_required' => true
5810 ),
5811 'mxchat-perplexity/mxchat-perplexity.php' => array(
5812 'functions' => array('mxchat_perplexity_research'),
5813 'name' => __('Perplexity Add-on', 'mxchat'),
5814 'pro_required' => true
5815 ),
5816 'mxchat-forms/mxchat-forms.php' => array(
5817 'functions' => array('mxchat_handle_form_collection'),
5818 'name' => __('Forms Add-on', 'mxchat'),
5819 'pro_required' => true
5820 ),
5821 // Add other add-ons and their functions here
5822 );
5823
5824 // Get the functions that are provided by active add-ons
5825 $addon_provided_functions = array();
5826 $addon_function_mapping = array(); // Maps functions to their add-on info
5827
5828 // Check which add-ons are active
5829 foreach ($addon_plugins as $plugin_file => $addon_info) {
5830 $is_active = in_array($plugin_file, $active_plugins);
5831
5832 // For each function in this addon
5833 foreach ($addon_info['functions'] as $function) {
5834 // Consider a function installed only if:
5835 // 1. The add-on is active AND
5836 // 2. Either it doesn't require Pro OR Pro is activated
5837 $is_installed = $is_active && (!$addon_info['pro_required'] || $this->is_activated);
5838
5839 // If the add-on is installed, mark this function as provided by an add-on
5840 if ($is_installed) {
5841 $addon_provided_functions[] = $function;
5842 }
5843
5844 // Store addon info for this function regardless of installation status
5845 $addon_function_mapping[$function] = array(
5846 'addon' => basename(dirname($plugin_file)),
5847 'addon_name' => $addon_info['name'],
5848 'pro_required' => $addon_info['pro_required'],
5849 'is_active' => $is_active,
5850 'is_installed' => $is_installed
5851 );
5852 }
5853 }
5854
5855 // Core callbacks - always available in the base plugin
5856 $core_callbacks = array(
5857 'mxchat_handle_email_capture' => array(
5858 'label' => __('Loops Email Capture', 'mxchat'),
5859 'pro_only' => false,
5860 'group' => __('Customer Engagement', 'mxchat'),
5861 'icon' => 'email-alt',
5862 'description' => __('Collect visitor emails for your mailing list in Loops', 'mxchat'),
5863 'addon' => false, // Not from an add-on
5864 'installed' => true // Always installed with base plugin
5865 ),
5866 'mxchat_handle_search_request' => array(
5867 'label' => __('Brave Web Search', 'mxchat'),
5868 'pro_only' => false,
5869 'group' => __('Search Features', 'mxchat'),
5870 'icon' => 'search',
5871 'description' => __('Let users search the web directly from the chat (requires a Brave Search API key)', 'mxchat'),
5872 'addon' => false,
5873 'installed' => true
5874 ),
5875 'mxchat_handle_image_search_request' => array(
5876 'label' => __('Brave Image Search', 'mxchat'),
5877 'pro_only' => false,
5878 'group' => __('Search Features', 'mxchat'),
5879 'icon' => 'format-image',
5880 'description' => __('Search and display images in the chat conversation (requires a Brave Search API key)', 'mxchat'),
5881 'addon' => false,
5882 'installed' => true
5883 ),
5884 // Pro core features - check is_activated property
5885 'mxchat_generate_image' => array(
5886 'label' => __('Generate Image (OpenAI)', 'mxchat'),
5887 'pro_only' => false,
5888 'group' => __('Other Features', 'mxchat'),
5889 'icon' => 'art',
5890 'description' => __('Create images with GPT Image from OpenAI (requires OpenAI API key)', 'mxchat'),
5891 'addon' => false,
5892 'installed' => true
5893 ),
5894 'mxchat_generate_gemini_image' => array(
5895 'label' => __('Generate Image (Gemini)', 'mxchat'),
5896 'pro_only' => false,
5897 'group' => __('Other Features', 'mxchat'),
5898 'icon' => 'art',
5899 'description' => __('Create images with Imagen from Google (requires Gemini API key)', 'mxchat'),
5900 'addon' => false,
5901 'installed' => true
5902 ),
5903 'mxchat_handle_pdf_discussion' => array(
5904 'label' => __('Chat with PDF', 'mxchat'),
5905 'pro_only' => false,
5906 'group' => __('Other Features', 'mxchat'),
5907 'icon' => 'media-document',
5908 'description' => __('Answer questions about uploaded PDF documents', 'mxchat'),
5909 'addon' => false,
5910 'installed' => true
5911 ),
5912 'mxchat_live_agent_handover' => array(
5913 'label' => __('Slack Live Agent', 'mxchat'),
5914 'pro_only' => false,
5915 'group' => __('Customer Engagement', 'mxchat'),
5916 'icon' => 'admin-users',
5917 'description' => __('Transfer conversation to a human support agent on Slack', 'mxchat'),
5918 'addon' => false,
5919 'installed' => true
5920 ),
5921 'mxchat_telegram_live_agent_handover' => array(
5922 'label' => __('Telegram Live Agent', 'mxchat'),
5923 'pro_only' => false,
5924 'group' => __('Customer Engagement', 'mxchat'),
5925 'icon' => 'format-chat',
5926 'description' => __('Transfer conversation to a human support agent on Telegram', 'mxchat'),
5927 'addon' => false,
5928 'installed' => true
5929 ),
5930 'mxchat_handle_switch_to_chatbot_intent' => array(
5931 'label' => __('Back to Chatbot', 'mxchat'),
5932 'pro_only' => false,
5933 'group' => __('Customer Engagement', 'mxchat'),
5934 'icon' => 'backup',
5935 'description' => __('Return from live agent mode to AI chatbot', 'mxchat'),
5936 'addon' => false,
5937 'installed' => true
5938 ),
5939 );
5940
5941 // Add-on callbacks with placeholders - only include if the add-on is NOT active
5942 // These are promotional/informational only — not selectable as real actions
5943 $addon_callbacks = array(
5944 // WooCommerce Add-on
5945 'mxchat_handle_product_recommendations' => array(
5946 'label' => __('Product Recommendations', 'mxchat'),
5947 'pro_only' => false,
5948 'addon_promo' => true,
5949 'group' => __('WooCommerce Features', 'mxchat'),
5950 'icon' => 'cart',
5951 'description' => __('Suggest products based on customer preferences', 'mxchat'),
5952 ),
5953 'mxchat_handle_order_history' => array(
5954 'label' => __('Order History', 'mxchat'),
5955 'pro_only' => false,
5956 'addon_promo' => true,
5957 'group' => __('WooCommerce Features', 'mxchat'),
5958 'icon' => 'clipboard',
5959 'description' => __('Allow customers to check their order status', 'mxchat'),
5960 ),
5961 'mxchat_show_product_card' => array(
5962 'label' => __('Show Product Card', 'mxchat'),
5963 'pro_only' => false,
5964 'addon_promo' => true,
5965 'group' => __('WooCommerce Features', 'mxchat'),
5966 'icon' => 'products',
5967 'description' => __('Display product information in the chat', 'mxchat'),
5968 ),
5969 'mxchat_add_to_cart' => array(
5970 'label' => __('Add to Cart', 'mxchat'),
5971 'pro_only' => false,
5972 'addon_promo' => true,
5973 'group' => __('WooCommerce Features', 'mxchat'),
5974 'icon' => 'plus-alt',
5975 'description' => __('Add products to cart directly from chat', 'mxchat'),
5976 ),
5977 'mxchat_checkout_redirect' => array(
5978 'label' => __('Proceed to Checkout', 'mxchat'),
5979 'pro_only' => false,
5980 'addon_promo' => true,
5981 'group' => __('WooCommerce Features', 'mxchat'),
5982 'icon' => 'arrow-right-alt',
5983 'description' => __('Redirect customer to checkout page', 'mxchat'),
5984 ),
5985 'mxchat_handle_featured_products' => array(
5986 'label' => __('Featured Products Showcase', 'mxchat'),
5987 'pro_only' => false,
5988 'addon_promo' => true,
5989 'group' => __('WooCommerce Features', 'mxchat'),
5990 'icon' => 'star-filled',
5991 'description' => __('Display a curated selection of products with an AI-generated message', 'mxchat'),
5992 ),
5993
5994 // Perplexity Add-on
5995 'mxchat_perplexity_research' => array(
5996 'label' => __('Perplexity Research', 'mxchat'),
5997 'pro_only' => false,
5998 'addon_promo' => true,
5999 'group' => __('Search Features', 'mxchat'),
6000 'icon' => 'book-alt',
6001 'description' => __('Allows the chatbot to search the web for accurate, up-to-date answers', 'mxchat'),
6002 ),
6003
6004 // Forms Add-on
6005 'mxchat_handle_form_collection' => array(
6006 'label' => __('Form Collection', 'mxchat'),
6007 'pro_only' => false,
6008 'addon_promo' => true,
6009 'group' => __('Form Features', 'mxchat'),
6010 'icon' => 'feedback',
6011 'description' => __('Collect user information through custom forms in chat', 'mxchat'),
6012 ),
6013 );
6014
6015 // Enhance add-on callbacks with installation status and addon info
6016 foreach ($addon_callbacks as $function => $data) {
6017 if (isset($addon_function_mapping[$function])) {
6018 $addon_info = $addon_function_mapping[$function];
6019
6020 $addon_callbacks[$function]['addon'] = $addon_info['addon'];
6021 $addon_callbacks[$function]['addon_name'] = $addon_info['addon_name'];
6022 $addon_callbacks[$function]['installed'] = $addon_info['is_installed'];
6023
6024 // Set pro_only based on add-on configuration
6025 $addon_callbacks[$function]['pro_only'] = $addon_info['pro_required'];
6026 } else {
6027 $addon_callbacks[$function]['addon'] = 'unknown';
6028 $addon_callbacks[$function]['addon_name'] = __('Unknown Add-on', 'mxchat');
6029 $addon_callbacks[$function]['installed'] = false;
6030 }
6031 }
6032
6033 // Initialize callbacks with core features
6034 $callbacks = $core_callbacks;
6035
6036 // Get callbacks from active add-ons
6037 $active_addon_callbacks = apply_filters('mxchat_available_callbacks', array());
6038
6039 // Add placeholder callbacks only for add-ons that aren't active
6040 if ($include_all) {
6041 foreach ($addon_callbacks as $function => $data) {
6042 // Skip placeholders for functions provided by active add-ons
6043 if (in_array($function, $addon_provided_functions)) {
6044 continue;
6045 }
6046
6047 // Skip excluded functions
6048 if (in_array($function, $excluded_functions)) {
6049 continue;
6050 }
6051
6052 // Add the placeholder
6053 $callbacks[$function] = $data;
6054 }
6055 }
6056
6057 // Add callbacks from active add-ons (will override placeholders)
6058 foreach ($active_addon_callbacks as $function => $data) {
6059 // Skip excluded functions
6060 if (in_array($function, $excluded_functions)) {
6061 continue;
6062 }
6063
6064 // Always include callbacks from add-ons
6065 $callbacks[$function] = $data;
6066
6067 // Ensure they have the proper add-on info
6068 if (isset($addon_function_mapping[$function])) {
6069 $addon_info = $addon_function_mapping[$function];
6070 $callbacks[$function]['addon'] = $addon_info['addon'];
6071 $callbacks[$function]['addon_name'] = $addon_info['addon_name'];
6072 $callbacks[$function]['installed'] = $addon_info['is_installed'];
6073 $callbacks[$function]['pro_only'] = $addon_info['pro_required'];
6074 }
6075 }
6076
6077 // Just before returning callbacks, sort them to prioritize free features
6078 if (!$grouped) {
6079 // Create temporary arrays for sorting
6080 $free_callbacks = array();
6081 $pro_callbacks = array();
6082
6083 // Split callbacks into free and pro
6084 foreach ($callbacks as $key => $data) {
6085 if (isset($data['pro_only']) && $data['pro_only']) {
6086 $pro_callbacks[$key] = $data;
6087 } else {
6088 $free_callbacks[$key] = $data;
6089 }
6090 }
6091
6092 // Merge with free callbacks first
6093 $callbacks = array_merge($free_callbacks, $pro_callbacks);
6094 }
6095
6096 // Return grouped structure if requested
6097 if ($grouped) {
6098 $grouped_callbacks = array();
6099 foreach ($callbacks as $key => $data) {
6100 $group_label = isset($data['group']) ? $data['group'] : __('Other Features', 'mxchat');
6101
6102 // Ensure we carry forward all the new fields in grouped mode
6103 $callback_data = array(
6104 'label' => $data['label'],
6105 'pro_only' => isset($data['pro_only']) ? $data['pro_only'] : false,
6106 'icon' => isset($data['icon']) ? $data['icon'] : 'admin-generic',
6107 'description' => isset($data['description']) ? $data['description'] : __('Custom action for your chatbot', 'mxchat'),
6108 'addon' => isset($data['addon']) ? $data['addon'] : false,
6109 'addon_name' => isset($data['addon_name']) ? $data['addon_name'] : '',
6110 'installed' => isset($data['installed']) ? $data['installed'] : true
6111 );
6112
6113 $grouped_callbacks[$group_label][$key] = $callback_data;
6114 }
6115
6116 // Sort within each group to prioritize free features
6117 foreach ($grouped_callbacks as $group => $items) {
6118 $free_items = array();
6119 $pro_items = array();
6120
6121 foreach ($items as $key => $data) {
6122 if (isset($data['pro_only']) && $data['pro_only']) {
6123 $pro_items[$key] = $data;
6124 } else {
6125 $free_items[$key] = $data;
6126 }
6127 }
6128
6129 $grouped_callbacks[$group] = array_merge($free_items, $pro_items);
6130 }
6131
6132 return $grouped_callbacks;
6133 }
6134
6135 return $callbacks;
6136 }
6137
6138 public function mxchat_page_init() {
6139 register_setting(
6140 'mxchat_option_group',
6141 'mxchat_options',
6142 array($this, 'mxchat_sanitize')
6143 );
6144
6145 register_setting(
6146 'mxchat_option_group',
6147 'mxchat_similarity_threshold',
6148 array(
6149 'type' => 'number',
6150 'sanitize_callback' => function($value) {
6151 $value = absint($value);
6152 return min(max($value, 20), 95);
6153 },
6154 'default' => 80,
6155 )
6156 );
6157
6158 // Chatbot Settings Section
6159 add_settings_section(
6160 'mxchat_chatbot_section',
6161 esc_html__('Chatbot Settings', 'mxchat'),
6162 null,
6163 'mxchat-chatbot'
6164 );
6165
6166 // API Keys Settings Section
6167 add_settings_section(
6168 'mxchat_api_keys_section',
6169 esc_html__('API Keys', 'mxchat'),
6170 array($this, 'mxchat_api_keys_section_callback'),
6171 'mxchat-api-keys'
6172 );
6173
6174 // OpenAI API Key
6175 add_settings_field(
6176 'api_key',
6177 esc_html__('OpenAI API Key', 'mxchat'),
6178 array($this, 'api_key_callback'),
6179 'mxchat-api-keys',
6180 'mxchat_api_keys_section'
6181 );
6182
6183 // X.AI API Key
6184 add_settings_field(
6185 'xai_api_key',
6186 esc_html__('X.AI API Key', 'mxchat'),
6187 array($this, 'xai_api_key_callback'),
6188 'mxchat-api-keys',
6189 'mxchat_api_keys_section'
6190 );
6191
6192 // Claude API Key
6193 add_settings_field(
6194 'claude_api_key',
6195 esc_html__('Claude API Key', 'mxchat'),
6196 array($this, 'claude_api_key_callback'),
6197 'mxchat-api-keys',
6198 'mxchat_api_keys_section'
6199 );
6200
6201 // DeepSeek API Key
6202 add_settings_field(
6203 'deepseek_api_key',
6204 esc_html__('DeepSeek API Key', 'mxchat'),
6205 array($this, 'deepseek_api_key_callback'),
6206 'mxchat-api-keys',
6207 'mxchat_api_keys_section'
6208 );
6209
6210 // Google Gemini API Key
6211 add_settings_field(
6212 'gemini_api_key',
6213 esc_html__('Google Gemini API Key', 'mxchat'),
6214 array($this, 'gemini_api_key_callback'),
6215 'mxchat-api-keys',
6216 'mxchat_api_keys_section'
6217 );
6218
6219 // Voyage AI API Key
6220 add_settings_field(
6221 'voyage_api_key',
6222 esc_html__('Voyage AI API Key', 'mxchat'),
6223 array($this, 'voyage_api_key_callback'),
6224 'mxchat-api-keys',
6225 'mxchat_api_keys_section'
6226 );
6227
6228 // OpenRouter API Key
6229 add_settings_field(
6230 'openrouter_api_key',
6231 esc_html__('OpenRouter API Key', 'mxchat'),
6232 array($this, 'openrouter_api_key_callback'),
6233 'mxchat-api-keys',
6234 'mxchat_api_keys_section'
6235 );
6236
6237 // Custom (OpenAI-compatible) Provider — for Ollama, LM Studio, vLLM, llama.cpp, Azure OpenAI, etc.
6238 add_settings_field(
6239 'custom_provider',
6240 esc_html__('Custom Provider (OpenAI-compatible)', 'mxchat'),
6241 array($this, 'custom_provider_callback'),
6242 'mxchat-api-keys',
6243 'mxchat_api_keys_section'
6244 );
6245
6246 // Loops API Key
6247 add_settings_field(
6248 'loops_api_key',
6249 esc_html__('Loops API Key', 'mxchat'),
6250 array($this, 'mxchat_loops_api_key_callback'),
6251 'mxchat-api-keys',
6252 'mxchat_api_keys_section'
6253 );
6254
6255 // Brave Search API Key
6256 add_settings_field(
6257 'brave_api_key',
6258 __('Brave API Key', 'mxchat'),
6259 array($this, 'mxchat_brave_api_key_callback'),
6260 'mxchat-api-keys',
6261 'mxchat_api_keys_section'
6262 );
6263
6264 // Similarity Threshold Slider
6265 add_settings_field(
6266 'similarity_threshold', // Field ID
6267 esc_html__('Similarity Threshold', 'mxchat'), // Field title
6268 array($this, 'mxchat_similarity_threshold_callback'), // Callback function
6269 'mxchat-chatbot', // Page
6270 'mxchat_chatbot_section' // Section
6271 );
6272
6273 // RAG Sources Limit Slider
6274 add_settings_field(
6275 'rag_sources_limit', // Field ID
6276 esc_html__('RAG Sources Limit', 'mxchat'), // Field title
6277 array($this, 'mxchat_rag_sources_limit_callback'), // Callback function
6278 'mxchat-chatbot', // Page
6279 'mxchat_chatbot_section' // Section
6280 );
6281
6282 // RAG Chunks Limit Slider
6283 add_settings_field(
6284 'rag_chunks_limit', // Field ID
6285 esc_html__('RAG Chunks Limit', 'mxchat'), // Field title
6286 array($this, 'mxchat_rag_chunks_limit_callback'), // Callback function
6287 'mxchat-chatbot', // Page
6288 'mxchat_chatbot_section' // Section
6289 );
6290
6291 add_settings_field(
6292 'append_to_body',
6293 esc_html__('Auto-Display Chatbot', 'mxchat'),
6294 array($this, 'mxchat_append_to_body_callback'),
6295 'mxchat-chatbot',
6296 'mxchat_chatbot_section'
6297 );
6298
6299 add_settings_field(
6300 'contextual_awareness_toggle',
6301 esc_html__('Contextual Awareness', 'mxchat'),
6302 array($this, 'mxchat_contextual_awareness_callback'),
6303 'mxchat-chatbot',
6304 'mxchat_chatbot_section'
6305 );
6306
6307 add_settings_field(
6308 'citation_links_toggle',
6309 esc_html__('Citation Links', 'mxchat'),
6310 array($this, 'mxchat_citation_links_toggle_callback'),
6311 'mxchat-chatbot',
6312 'mxchat_chatbot_section'
6313 );
6314
6315 // Satisfaction rating toggle (plan-a5b006).
6316 add_settings_field(
6317 'satisfaction_rating_enabled',
6318 esc_html__('Satisfaction Rating Prompt', 'mxchat'),
6319 array($this, 'mxchat_satisfaction_rating_toggle_callback'),
6320 'mxchat-chatbot',
6321 'mxchat_chatbot_section'
6322 );
6323
6324 // Satisfaction rating customization (plan-141a12, plan-29caac):
6325 // the 5 customization fields are now inline-rendered inside
6326 // mxchat_satisfaction_rating_toggle_callback's sub-options wrapper.
6327
6328 add_settings_field(
6329 'enable_streaming_toggle',
6330 esc_html__('Enable Streaming', 'mxchat'),
6331 array($this, 'enable_streaming_toggle_callback'),
6332 'mxchat-chatbot',
6333 'mxchat_chatbot_section', // Same section as your working toggle
6334 array(
6335 'class' => 'mxchat-setting-row streaming-setting',
6336 'style' => 'display: none;' // Hidden by default, shown when OpenAI/Claude selected
6337 )
6338 );
6339
6340
6341 add_settings_field(
6342 'model',
6343 esc_html__('Chat Model', 'mxchat'),
6344 array($this, 'mxchat_model_callback'),
6345 'mxchat-chatbot',
6346 'mxchat_chatbot_section'
6347 );
6348
6349 add_settings_field(
6350 'embedding_model',
6351 esc_html__('Embedding Model', 'mxchat'),
6352 array($this, 'embedding_model_callback'),
6353 'mxchat-chatbot',
6354 'mxchat_chatbot_section'
6355 );
6356
6357 add_settings_field(
6358 'system_prompt_instructions',
6359 esc_html__('AI Instructions (Behavior)', 'mxchat'),
6360 array($this, 'system_prompt_instructions_callback'),
6361 'mxchat-chatbot',
6362 'mxchat_chatbot_section'
6363 );
6364
6365
6366 add_settings_field(
6367 'top_bar_title',
6368 esc_html__('Top Bar Title', 'mxchat'),
6369 array($this, 'mxchat_top_bar_title_callback'),
6370 'mxchat-chatbot',
6371 'mxchat_chatbot_section'
6372 );
6373
6374 add_settings_field(
6375 'ai_agent_text',
6376 esc_html__('AI Agent Text', 'mxchat'),
6377 array($this, 'mxchat_ai_agent_text_callback'),
6378 'mxchat-chatbot',
6379 'mxchat_chatbot_section'
6380 );
6381
6382 add_settings_field(
6383 'enable_email_block',
6384 esc_html__('Require Email To Chat', 'mxchat'),
6385 array($this, 'enable_email_block_callback'),
6386 'mxchat-chatbot',
6387 'mxchat_chatbot_section'
6388 );
6389
6390 add_settings_field(
6391 'email_blocker_header_content',
6392 esc_html__('Require Email Chat Content', 'mxchat'),
6393 array($this, 'email_blocker_header_content_callback'),
6394 'mxchat-chatbot',
6395 'mxchat_chatbot_section'
6396 );
6397
6398 add_settings_field(
6399 'email_blocker_button_text',
6400 esc_html__('Require Email Chat Button Text', 'mxchat'),
6401 [$this, 'email_blocker_button_text_callback'],
6402 'mxchat-chatbot',
6403 'mxchat_chatbot_section'
6404 );
6405
6406 add_settings_field(
6407 'enable_name_field',
6408 esc_html__('Require Name Field', 'mxchat'),
6409 array($this, 'enable_name_field_callback'),
6410 'mxchat-chatbot',
6411 'mxchat_chatbot_section'
6412 );
6413
6414 add_settings_field(
6415 'name_field_placeholder',
6416 esc_html__('Name Field Placeholder', 'mxchat'),
6417 array($this, 'name_field_placeholder_callback'),
6418 'mxchat-chatbot',
6419 'mxchat_chatbot_section'
6420 );
6421
6422 add_settings_field(
6423 'intro_message',
6424 esc_html__('Introductory Message', 'mxchat'),
6425 array($this, 'mxchat_intro_message_callback'),
6426 'mxchat-chatbot',
6427 'mxchat_chatbot_section'
6428 );
6429
6430 add_settings_field(
6431 'input_copy',
6432 esc_html__('Input Copy', 'mxchat'),
6433 array($this, 'mxchat_input_copy_callback'),
6434 'mxchat-chatbot',
6435 'mxchat_chatbot_section'
6436 );
6437
6438 add_settings_field(
6439 'pre_chat_message',
6440 esc_html__('Chat Teaser Pop-up', 'mxchat'),
6441 array($this, 'mxchat_pre_chat_message_callback'),
6442 'mxchat-chatbot',
6443 'mxchat_chatbot_section'
6444 );
6445
6446 add_settings_field(
6447 'privacy_toggle',
6448 esc_html__('Toggle Privacy Notice', 'mxchat'),
6449 array($this, 'mxchat_privacy_toggle_callback'),
6450 'mxchat-chatbot',
6451 'mxchat_chatbot_section'
6452 );
6453
6454 add_settings_field(
6455 'complianz_toggle',
6456 esc_html__('Enable Complianz', 'mxchat'),
6457 array($this, 'mxchat_complianz_toggle_callback'),
6458 'mxchat-chatbot',
6459 'mxchat_chatbot_section'
6460 );
6461
6462 add_settings_field(
6463 'link_target_toggle',
6464 esc_html__('Open Links in a New Tab', 'mxchat'),
6465 array($this, 'mxchat_link_target_toggle_callback'),
6466 'mxchat-chatbot',
6467 'mxchat_chatbot_section'
6468 );
6469
6470 add_settings_field(
6471 'chat_persistence_toggle',
6472 esc_html__('Enable Chat Persistence', 'mxchat'),
6473 array($this, 'mxchat_chat_persistence_toggle_callback'),
6474 'mxchat-chatbot',
6475 'mxchat_chatbot_section'
6476 );
6477
6478 add_settings_field(
6479 'print_button_enabled',
6480 esc_html__('Show Download Transcript Button', 'mxchat'),
6481 array($this, 'mxchat_print_button_toggle_callback'),
6482 'mxchat-chatbot',
6483 'mxchat_chatbot_section'
6484 );
6485
6486 add_settings_field(
6487 'reset_chat_enabled',
6488 esc_html__('Show Start-New-Chat Button', 'mxchat'),
6489 array($this, 'mxchat_reset_chat_toggle_callback'),
6490 'mxchat-chatbot',
6491 'mxchat_chatbot_section'
6492 );
6493
6494 add_settings_field(
6495 'reset_chat_label',
6496 esc_html__('Start-New-Chat Button Label', 'mxchat'),
6497 array($this, 'mxchat_reset_chat_label_callback'),
6498 'mxchat-chatbot',
6499 'mxchat_chatbot_section'
6500 );
6501
6502 add_settings_field(
6503 'popular_question_1',
6504 esc_html__('Quick Question 1', 'mxchat'),
6505 array($this, 'mxchat_popular_question_1_callback'),
6506 'mxchat-chatbot',
6507 'mxchat_chatbot_section'
6508 );
6509
6510 add_settings_field(
6511 'popular_question_2',
6512 esc_html__('Quick Question 2', 'mxchat'),
6513 array($this, 'mxchat_popular_question_2_callback'),
6514 'mxchat-chatbot',
6515 'mxchat_chatbot_section'
6516 );
6517
6518 add_settings_field(
6519 'popular_question_3',
6520 esc_html__('Quick Question 3', 'mxchat'),
6521 array($this, 'mxchat_popular_question_3_callback'),
6522 'mxchat-chatbot',
6523 'mxchat_chatbot_section'
6524 );
6525
6526 add_settings_field(
6527 'additional_popular_questions',
6528 esc_html__('Additional Quick Questions', 'mxchat'),
6529 array($this, 'mxchat_additional_popular_questions_callback'),
6530 'mxchat-chatbot',
6531 'mxchat_chatbot_section'
6532 );
6533
6534
6535 add_settings_field(
6536 'rate_limits',
6537 __('Rate Limits Settings', 'mxchat'),
6538 array($this, 'mxchat_rate_limits_callback'),
6539 'mxchat-chatbot',
6540 'mxchat_chatbot_section'
6541 );
6542
6543 // Loops Settings Section
6544 add_settings_section(
6545 'mxchat_loops_section',
6546 esc_html__('Loops Settings', 'mxchat'),
6547 null,
6548 'mxchat-embed'
6549 );
6550
6551 // Loops Settings Fields (API Key moved to API Keys tab)
6552 add_settings_field(
6553 'loops_mailing_list',
6554 esc_html__('Loops Mailing List', 'mxchat'),
6555 array($this, 'mxchat_loops_mailing_list_callback'),
6556 'mxchat-embed',
6557 'mxchat_loops_section'
6558 );
6559
6560 add_settings_field(
6561 'triggered_phrase_response',
6562 esc_html__('Triggered Phrase Response', 'mxchat'),
6563 array($this, 'mxchat_triggered_phrase_response_callback'),
6564 'mxchat-embed',
6565 'mxchat_loops_section'
6566 );
6567
6568 add_settings_field(
6569 'email_capture_response',
6570 esc_html__('Email Capture Response', 'mxchat'),
6571 array($this, 'mxchat_email_capture_response_callback'),
6572 'mxchat-embed',
6573 'mxchat_loops_section'
6574 );
6575
6576 // Brave Search Settings Fields
6577 add_settings_section(
6578 'mxchat_brave_section',
6579 __('Brave Search Settings', 'mxchat'),
6580 array($this, 'mxchat_brave_section_callback'),
6581 'mxchat-embed'
6582 );
6583
6584 // Brave API Key moved to API Keys tab
6585 add_settings_field(
6586 'brave_image_count',
6587 __('Number of Images to Return', 'mxchat'),
6588 array($this, 'mxchat_brave_image_count_callback'),
6589 'mxchat-embed',
6590 'mxchat_brave_section'
6591 );
6592
6593 add_settings_field(
6594 'brave_safe_search',
6595 __('Safe Search', 'mxchat'),
6596 array($this, 'mxchat_brave_safe_search_callback'),
6597 'mxchat-embed',
6598 'mxchat_brave_section'
6599 );
6600
6601 add_settings_field(
6602 'brave_news_count',
6603 __('Number of News Articles', 'mxchat'),
6604 array($this, 'mxchat_brave_news_count_callback'),
6605 'mxchat-embed',
6606 'mxchat_brave_section'
6607 );
6608
6609 add_settings_field(
6610 'brave_country',
6611 __('Country', 'mxchat'),
6612 array($this, 'mxchat_brave_country_callback'),
6613 'mxchat-embed',
6614 'mxchat_brave_section'
6615 );
6616
6617 add_settings_field(
6618 'brave_language',
6619 __('Language', 'mxchat'),
6620 array($this, 'mxchat_brave_language_callback'),
6621 'mxchat-embed',
6622 'mxchat_brave_section'
6623 );
6624
6625 // Chat with PDF Intent Settings Fields
6626 add_settings_section(
6627 'mxchat_pdf_intent_section',
6628 __('Toolbar Settings & Intents', 'mxchat'),
6629 array($this, 'mxchat_pdf_intent_section_callback'),
6630 'mxchat-embed'
6631 );
6632
6633 add_settings_field(
6634 'chat_toolbar_toggle',
6635 __('Show Chat Toolbar', 'mxchat'),
6636 array($this, 'mxchat_chat_toolbar_toggle_callback'),
6637 'mxchat-embed',
6638 'mxchat_pdf_intent_section'
6639 );
6640
6641 // PDF Upload Button Toggle
6642 add_settings_field(
6643 'show_pdf_upload_button',
6644 __('Show PDF Upload Button', 'mxchat'),
6645 array($this, 'mxchat_show_pdf_upload_button_callback'),
6646 'mxchat-embed',
6647 'mxchat_pdf_intent_section'
6648 );
6649
6650 // Word Upload Button Toggle
6651 add_settings_field(
6652 'show_word_upload_button',
6653 __('Show Word Upload Button', 'mxchat'),
6654 array($this, 'mxchat_show_word_upload_button_callback'),
6655 'mxchat-embed',
6656 'mxchat_pdf_intent_section'
6657 );
6658
6659 add_settings_field(
6660 'pdf_intent_trigger_text',
6661 __('Intent Trigger Text', 'mxchat'),
6662 array($this, 'mxchat_pdf_intent_trigger_text_callback'),
6663 'mxchat-embed',
6664 'mxchat_pdf_intent_section'
6665 );
6666
6667 add_settings_field(
6668 'pdf_intent_success_text',
6669 __('Success Text', 'mxchat'),
6670 array($this, 'mxchat_pdf_intent_success_text_callback'),
6671 'mxchat-embed',
6672 'mxchat_pdf_intent_section'
6673 );
6674
6675 add_settings_field(
6676 'pdf_intent_error_text',
6677 __('Error Text', 'mxchat'),
6678 array($this, 'mxchat_pdf_intent_error_text_callback'),
6679 'mxchat-embed',
6680 'mxchat_pdf_intent_section'
6681 );
6682
6683 // Add PDF Maximum Pages Field
6684 add_settings_field(
6685 'pdf_max_pages',
6686 __('Maximum Document Pages', 'mxchat'),
6687 array($this, 'mxchat_pdf_max_pages_callback'),
6688 'mxchat-embed',
6689 'mxchat_pdf_intent_section'
6690 );
6691
6692 // Live Agent Settings Fields
6693 add_settings_section(
6694 'mxchat_live_agent_section',
6695 __('Live Agent Settings', 'mxchat'),
6696 array($this, 'mxchat_live_agent_section_callback'),
6697 'mxchat-embed'
6698 );
6699
6700 // Live Agent Status Fields (add at top of live agent settings)
6701 add_settings_field(
6702 'live_agent_status',
6703 __('Live Agent Status', 'mxchat'),
6704 array($this, 'mxchat_live_agent_status_callback'),
6705 'mxchat-embed',
6706 'mxchat_live_agent_section'
6707 );
6708
6709 // Slack availability schedule (plans 8ccaa2 + 99d7a4). Each handoff channel
6710 // owns an independent schedule rendered under its own Integrations tab; this
6711 // one governs Slack only. Same callback as Telegram's, parameterized.
6712 add_settings_field(
6713 'live_agent_schedule_slack',
6714 __('Availability Schedule', 'mxchat'),
6715 array($this, 'mxchat_live_agent_schedule_callback'),
6716 'mxchat-embed',
6717 'mxchat_live_agent_section',
6718 array('channel' => 'slack')
6719 );
6720
6721 add_settings_field(
6722 'live_agent_notification_message',
6723 __('Notification Message', 'mxchat'),
6724 array($this, 'mxchat_live_agent_notification_message_callback'),
6725 'mxchat-embed',
6726 'mxchat_live_agent_section'
6727 );
6728
6729 add_settings_field(
6730 'live_agent_away_message',
6731 __('Away Message', 'mxchat'),
6732 array($this, 'mxchat_live_agent_away_message_callback'),
6733 'mxchat-embed',
6734 'mxchat_live_agent_section'
6735 );
6736
6737 add_settings_field(
6738 'live_agent_user_ids',
6739 __('Slack Agent User IDs', 'mxchat'),
6740 array($this, 'mxchat_live_agent_user_ids_callback'),
6741 'mxchat-embed',
6742 'mxchat_live_agent_section'
6743 );
6744
6745 // Shared handoff channel (plan 9f7756): route every handoff into one
6746 // pre-existing channel as threads instead of creating chat-* channels.
6747 add_settings_field(
6748 'live_agent_shared_channel',
6749 __('Shared Handoff Channel', 'mxchat'),
6750 array($this, 'mxchat_live_agent_shared_channel_callback'),
6751 'mxchat-embed',
6752 'mxchat_live_agent_section'
6753 );
6754
6755 // Auto-archive per-conversation chat- channels on !endchat (plan 7458a7).
6756 // Default OFF; never touches the shared handoff channel.
6757 add_settings_field(
6758 'live_agent_archive_on_end_toggle',
6759 __('Archive Channel When Chat Ends', 'mxchat'),
6760 array($this, 'mxchat_live_agent_archive_on_end_callback'),
6761 'mxchat-embed',
6762 'mxchat_live_agent_section'
6763 );
6764
6765 add_settings_field(
6766 'live_agent_webhook_url',
6767 __('Slack Webhook URL', 'mxchat'),
6768 array($this, 'mxchat_live_agent_webhook_url_callback'),
6769 'mxchat-embed',
6770 'mxchat_live_agent_section'
6771 );
6772
6773 add_settings_field(
6774 'live_agent_secret_key',
6775 __('Slack Secret Key', 'mxchat'),
6776 array($this, 'mxchat_live_agent_secret_key_callback'),
6777 'mxchat-embed',
6778 'mxchat_live_agent_section'
6779 );
6780
6781 // Live Agent Integration Fields
6782 add_settings_field(
6783 'live_agent_bot_token',
6784 __('Slack Bot OAuth Token', 'mxchat'),
6785 array($this, 'mxchat_live_agent_bot_token_callback'),
6786 'mxchat-embed',
6787 'mxchat_live_agent_section'
6788 );
6789
6790 // Telegram Integration Section
6791 add_settings_section(
6792 'mxchat_telegram_section',
6793 __('Telegram Settings', 'mxchat'),
6794 array($this, 'mxchat_telegram_section_callback'),
6795 'mxchat-embed'
6796 );
6797
6798 add_settings_field(
6799 'telegram_status',
6800 __('Live Agent Status', 'mxchat'),
6801 array($this, 'mxchat_telegram_status_callback'),
6802 'mxchat-embed',
6803 'mxchat_telegram_section'
6804 );
6805
6806 // Telegram availability schedule (plan 99d7a4) — independent of Slack's,
6807 // rendered right under the Telegram status toggle it extends.
6808 add_settings_field(
6809 'live_agent_schedule_telegram',
6810 __('Availability Schedule', 'mxchat'),
6811 array($this, 'mxchat_live_agent_schedule_callback'),
6812 'mxchat-embed',
6813 'mxchat_telegram_section',
6814 array('channel' => 'telegram')
6815 );
6816
6817 add_settings_field(
6818 'telegram_notification_message',
6819 __('Notification Message', 'mxchat'),
6820 array($this, 'mxchat_telegram_notification_message_callback'),
6821 'mxchat-embed',
6822 'mxchat_telegram_section'
6823 );
6824
6825 add_settings_field(
6826 'telegram_away_message',
6827 __('Away Message', 'mxchat'),
6828 array($this, 'mxchat_telegram_away_message_callback'),
6829 'mxchat-embed',
6830 'mxchat_telegram_section'
6831 );
6832
6833 add_settings_field(
6834 'telegram_bot_token',
6835 __('Telegram Bot Token', 'mxchat'),
6836 array($this, 'mxchat_telegram_bot_token_callback'),
6837 'mxchat-embed',
6838 'mxchat_telegram_section'
6839 );
6840
6841 add_settings_field(
6842 'telegram_group_id',
6843 __('Telegram Group ID', 'mxchat'),
6844 array($this, 'mxchat_telegram_group_id_callback'),
6845 'mxchat-embed',
6846 'mxchat_telegram_section'
6847 );
6848
6849 add_settings_field(
6850 'telegram_webhook_secret',
6851 __('Webhook Secret Token', 'mxchat'),
6852 array($this, 'mxchat_telegram_webhook_secret_callback'),
6853 'mxchat-embed',
6854 'mxchat_telegram_section'
6855 );
6856
6857 // General Settings Section
6858 add_settings_section(
6859 'mxchat_general_section',
6860 esc_html__('YouTube Tutorials', 'mxchat'),
6861 null,
6862 'mxchat-general'
6863 );
6864 }
6865
6866 public function mxchat_prompts_page_init() {
6867 register_setting(
6868 'mxchat_prompts_options',
6869 'mxchat_prompts_options',
6870 array(
6871 'type' => 'array',
6872 'description' => __('MXChat Knowledge Base Settings', 'mxchat'),
6873 'default' => array(
6874 'mxchat_auto_sync_posts' => 0,
6875 'mxchat_auto_sync_pages' => 0,
6876 'mxchat_use_pinecone' => 0,
6877 'mxchat_pinecone_api_key' => '',
6878 'mxchat_pinecone_environment' => '',
6879 'mxchat_pinecone_index' => '',
6880 'mxchat_pinecone_host' => '',
6881 ),
6882 'sanitize_callback' => array($this, 'sanitize_prompts_options'),
6883 )
6884 );
6885
6886 add_action('admin_notices', array($this, 'sync_settings_notice'));
6887 }
6888
6889 public function mxchat_transcripts_page_init() {
6890 register_setting(
6891 'mxchat_transcripts_options',
6892 'mxchat_transcripts_options',
6893 array(
6894 'type' => 'array',
6895 'description' => __('MXChat Transcripts Notification Settings', 'mxchat'),
6896 'default' => array(
6897 'mxchat_enable_notifications' => 0,
6898 'mxchat_notification_email' => get_option('admin_email'),
6899 'mxchat_auto_delete_transcripts' => 'never',
6900 ),
6901 'sanitize_callback' => array($this, 'sanitize_transcripts_options'),
6902 )
6903 );
6904
6905 add_settings_section(
6906 'mxchat_transcripts_notification_section',
6907 esc_html__('Chat Notification Settings', 'mxchat'),
6908 array($this, 'mxchat_transcripts_notification_section_callback'),
6909 'mxchat-transcripts'
6910 );
6911
6912 add_settings_field(
6913 'mxchat_enable_notifications',
6914 esc_html__('Enable Chat Notifications', 'mxchat'),
6915 array($this, 'mxchat_enable_notifications_callback'),
6916 'mxchat-transcripts',
6917 'mxchat_transcripts_notification_section'
6918 );
6919
6920 add_settings_field(
6921 'mxchat_notification_email',
6922 esc_html__('Notification Email Address', 'mxchat'),
6923 array($this, 'mxchat_notification_email_callback'),
6924 'mxchat-transcripts',
6925 'mxchat_transcripts_notification_section'
6926 );
6927
6928 add_settings_field(
6929 'mxchat_auto_delete_transcripts',
6930 esc_html__('Auto-Delete Old Transcripts', 'mxchat'),
6931 array($this, 'mxchat_auto_delete_transcripts_callback'),
6932 'mxchat-transcripts',
6933 'mxchat_transcripts_notification_section'
6934 );
6935
6936 add_settings_field(
6937 'mxchat_retention_days',
6938 esc_html__('Custom Retention (Days)', 'mxchat'),
6939 array($this, 'mxchat_retention_days_callback'),
6940 'mxchat-transcripts',
6941 'mxchat_transcripts_notification_section'
6942 );
6943
6944 add_settings_field(
6945 'mxchat_auto_email_transcript',
6946 esc_html__('Auto-Email Full Transcript', 'mxchat'),
6947 array($this, 'mxchat_auto_email_transcript_callback'),
6948 'mxchat-transcripts',
6949 'mxchat_transcripts_notification_section'
6950 );
6951 }
6952
6953
6954 /**
6955 * Sanitize all prompts options
6956 *
6957 * @param array $input The unsanitized options array
6958 * @return array The sanitized options array
6959 */
6960 public function sanitize_prompts_options($input) {
6961 // Log the incoming input.
6962 //error_log('Sanitizing inputs: ' . print_r($input, true));
6963
6964 $sanitized = array();
6965
6966 // Boolean options
6967 $sanitized['mxchat_auto_sync_posts'] = isset($input['mxchat_auto_sync_posts']) ? 1 : 0;
6968 $sanitized['mxchat_auto_sync_pages'] = isset($input['mxchat_auto_sync_pages']) ? 1 : 0;
6969 $sanitized['mxchat_use_pinecone'] = !empty($input['mxchat_use_pinecone']) ? 1 : 0;
6970
6971 // API Key: if less than 32 characters, flag as invalid.
6972 $api_key = sanitize_text_field($input['mxchat_pinecone_api_key'] ?? '');
6973 if (!empty($api_key) && strlen($api_key) < 32) {
6974 add_settings_error(
6975 'mxchat_prompts_options',
6976 'invalid_api_key',
6977 __('The Pinecone API key appears to be invalid. Please check your API key.', 'mxchat')
6978 );
6979 $existing_options = get_option('mxchat_prompts_options', array());
6980 $sanitized['mxchat_pinecone_api_key'] = $existing_options['mxchat_pinecone_api_key'] ?? '';
6981 } else {
6982 $sanitized['mxchat_pinecone_api_key'] = $api_key;
6983 }
6984
6985 // Environment and Index Name
6986 $sanitized['mxchat_pinecone_environment'] = sanitize_text_field($input['mxchat_pinecone_environment'] ?? '');
6987 $sanitized['mxchat_pinecone_index'] = sanitize_text_field($input['mxchat_pinecone_index'] ?? '');
6988
6989 // Host: Remove protocol and validate format.
6990 $host = sanitize_text_field($input['mxchat_pinecone_host'] ?? '');
6991 $host = preg_replace('#^https?://#', '', $host);
6992 //error_log('Host after removing protocol: ' . $host);
6993 if (!empty($host)) {
6994 if (!preg_match('/^[\w-]+\.svc\.[\w-]+\.pinecone\.io$/', $host)) {
6995 add_settings_error(
6996 'mxchat_prompts_options',
6997 'invalid_host',
6998 __('The Pinecone host appears to be invalid. It should look like "mxchat-vectors-zrmsquq.svc.aped-4627-b74a.pinecone.io"', 'mxchat')
6999 );
7000 $existing_options = get_option('mxchat_prompts_options', array());
7001 $sanitized['mxchat_pinecone_host'] = $existing_options['mxchat_pinecone_host'] ?? '';
7002 } else {
7003 $sanitized['mxchat_pinecone_host'] = $host;
7004 }
7005 } else {
7006 $sanitized['mxchat_pinecone_host'] = '';
7007 }
7008
7009 //error_log('Final sanitized array: ' . print_r($sanitized, true));
7010
7011 return $sanitized;
7012 }
7013
7014 public function sync_settings_notice() {
7015 // Only show notice on our plugin page
7016 if (!isset($_GET['page']) || $_GET['page'] !== 'mxchat-prompts') {
7017 return;
7018 }
7019
7020 // Check if settings were updated
7021 if (isset($_GET['settings-updated'])) {
7022
7023 ?>
7024 <div class="notice notice-success is-dismissible">
7025 <p><?php esc_html_e('Sync settings updated successfully.', 'mxchat'); ?></p>
7026 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php esc_html_e('Dismiss this notice.', 'mxchat'); ?></span></button>
7027 </div>
7028 <?php
7029
7030 }
7031 }
7032 // Add this sanitization function to your class
7033 public function sanitize_sync_setting($input) {
7034 return (bool)$input ? __('1', 'mxchat') : __('', 'mxchat');
7035 }
7036
7037 public function mxchat_rate_limits_callback() {
7038 $all_options = get_option('mxchat_options', []);
7039
7040 // Define available rate limits
7041 $rate_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited');
7042
7043 // Define available timeframes
7044 $timeframes = array(
7045 'hourly' => __('Per Hour', 'mxchat'),
7046 'daily' => __('Per Day', 'mxchat'),
7047 'weekly' => __('Per Week', 'mxchat'),
7048 'monthly' => __('Per Month', 'mxchat')
7049 );
7050
7051 // Get all roles plus a "logged_out" pseudo-role
7052 $roles = wp_roles()->get_names();
7053 $roles['logged_out'] = __('Logged Out Users', 'mxchat');
7054
7055 // Start the wrapper
7056 echo '<div class="pro-feature-wrapper active">';
7057 echo '<div class="mxchat-rate-limits-container">';
7058
7059 echo '<p class="description" style="margin-bottom: 20px;">' .
7060 esc_html__('Set message limits for each user role and customize the experience when users reach those limits. You can use {limit}, {timeframe}, {count}, and {remaining} as placeholders.', 'mxchat') .
7061 '</p>';
7062
7063 // Add markdown link documentation
7064 echo '<div class="notice notice-info inline" style="margin-bottom: 20px; padding: 10px;">';
7065 echo '<p><strong>' . esc_html__('Markdown Links Supported:', 'mxchat') . '</strong></p>';
7066 echo '<p>' . esc_html__('You can include clickable links in your custom messages using markdown syntax:', 'mxchat') . '</p>';
7067 echo '<ul style="margin-left: 20px;">';
7068 echo '<li><code>[Link text](https://example.com)</code> - Creates a clickable link</li>';
7069 echo '<li><code>[Visit our pricing](https://example.com/pricing)</code> - Link with custom text</li>';
7070 echo '<li><code>Plain URLs like https://example.com will also become clickable</code></li>';
7071 echo '</ul>';
7072 echo '</div>';
7073
7074 // Output the controls for each role
7075 foreach ($roles as $role_id => $role_name) {
7076 // Get saved options or defaults
7077 $default_limit = ($role_id === 'logged_out') ? '10' : '100';
7078 $default_timeframe = 'daily';
7079 $default_message = __('Rate limit exceeded. Please try again later.', 'mxchat');
7080
7081 $selected_limit = isset($all_options['rate_limits'][$role_id]['limit'])
7082 ? $all_options['rate_limits'][$role_id]['limit']
7083 : $default_limit;
7084
7085 $selected_timeframe = isset($all_options['rate_limits'][$role_id]['timeframe'])
7086 ? $all_options['rate_limits'][$role_id]['timeframe']
7087 : $default_timeframe;
7088
7089 $custom_message = isset($all_options['rate_limits'][$role_id]['message'])
7090 ? $all_options['rate_limits'][$role_id]['message']
7091 : $default_message;
7092
7093 // Output the row
7094 echo '<div class="mxchat-rate-limit-row mxchat-autosave-section">';
7095
7096 // Role label
7097 echo '<div class="mxchat-rate-limit-role">' . esc_html($role_name) . '</div>';
7098
7099 // Controls section
7100 echo '<div class="mxchat-rate-limit-controls-wrapper">';
7101
7102 // Rate limit and timeframe controls
7103 echo '<div class="mxchat-rate-limit-controls">';
7104
7105 // Limit dropdown
7106 echo '<div>';
7107 echo '<label for="rate_limits_' . esc_attr($role_id) . '_limit">' . esc_html__('Limit:', 'mxchat') . '</label>';
7108 echo '<select
7109 id="rate_limits_' . esc_attr($role_id) . '_limit"
7110 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][limit]"
7111 class="mxchat-autosave-field">';
7112 foreach ($rate_limits as $limit) {
7113 echo '<option value="' . esc_attr($limit) . '" ' . selected($selected_limit, $limit, false) . '>' . esc_html($limit) . '</option>';
7114 }
7115 echo '</select>';
7116 echo '</div>';
7117
7118 // Timeframe dropdown
7119 echo '<div>';
7120 echo '<label for="rate_limits_' . esc_attr($role_id) . '_timeframe">' . esc_html__('Timeframe:', 'mxchat') . '</label>';
7121 echo '<select
7122 id="rate_limits_' . esc_attr($role_id) . '_timeframe"
7123 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][timeframe]"
7124 class="mxchat-autosave-field">';
7125 foreach ($timeframes as $value => $label) {
7126 echo '<option value="' . esc_attr($value) . '" ' . selected($selected_timeframe, $value, false) . '>' . esc_html($label) . '</option>';
7127 }
7128 echo '</select>';
7129 echo '</div>';
7130
7131 echo '</div>'; // End controls
7132
7133 // Custom message textarea
7134 echo '<div class="mxchat-rate-limit-message">';
7135 echo '<label for="rate_limits_' . esc_attr($role_id) . '_message">' . esc_html__('Custom Message:', 'mxchat') . '</label>';
7136 echo '<textarea
7137 id="rate_limits_' . esc_attr($role_id) . '_message"
7138 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][message]"
7139 class="mxchat-autosave-field"
7140 placeholder="' . esc_attr__('Enter custom message when rate limit is exceeded', 'mxchat') . '">' .
7141 esc_textarea($custom_message) .
7142 '</textarea>';
7143 echo '<p class="description">' .
7144 esc_html__('Example: Rate limit reached! [Visit our pricing page](https://example.com/pricing) to upgrade.', 'mxchat') .
7145 '</p>';
7146 echo '</div>'; // End message
7147
7148 echo '</div>'; // End controls wrapper
7149
7150 echo '</div>'; // End row
7151 }
7152
7153 echo '</div>'; // End container
7154
7155 echo '</div>'; // End pro-feature-wrapper
7156 }
7157
7158 private function mxchat_add_option_field($id, $title, $callback = '') {
7159 add_settings_field(
7160 $id,
7161 __($title, 'mxchat'),
7162 $callback ? array($this, $callback) : array($this, $id . '_callback'),
7163 'mxchat-max',
7164 'mxchat_setting_section_id',
7165 $id === 'model' ? ['label_for' => 'model'] : []
7166 );
7167 }
7168
7169 // API Keys Section Callback
7170 public function mxchat_api_keys_section_callback() {
7171 echo '<p>' . esc_html__('Manage all your API keys in one place. Add the API keys for the services you want to use with your chatbot.', 'mxchat') . '</p>';
7172 }
7173
7174 // OpenAI API Key
7175 public function api_key_callback() {
7176 $apiKey = isset($this->options['api_key']) ? esc_attr($this->options['api_key']) : '';
7177 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7178
7179 echo '<div class="api-key-wrapper">';
7180 echo '<input type="text" id="api_key" name="api_key" value="' . $apiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7181 echo '<button type="button" id="toggleApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7182 echo '<p class="description">' . esc_html__('Required for OpenAI GPT models and OpenAI embeddings. Get your API key from OpenAI Platform.', 'mxchat') . '</p>';
7183 echo $this->mxchat_provider_key_test_button('openai', 'api_key');
7184 echo '</div>';
7185 }
7186
7187 // X.AI API Key
7188 public function xai_api_key_callback() {
7189 $xaiApiKey = isset($this->options['xai_api_key']) ? esc_attr($this->options['xai_api_key']) : '';
7190 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7191
7192 echo '<div class="api-key-wrapper">';
7193 echo '<input type="text" id="xai_api_key" name="xai_api_key" value="' . $xaiApiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7194 echo '<button type="button" id="toggleXaiApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7195 echo '<p class="description">' . esc_html__('Required for X.AI Grok models. Get your API key from X.AI Console.', 'mxchat') . '</p>';
7196 echo $this->mxchat_provider_key_test_button('xai', 'xai_api_key');
7197 echo '</div>';
7198 }
7199 // Claude API Key
7200 public function claude_api_key_callback() {
7201 $claudeApiKey = isset($this->options['claude_api_key']) ? esc_attr($this->options['claude_api_key']) : '';
7202 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7203
7204 echo '<div class="api-key-wrapper">';
7205 echo '<input type="text" id="claude_api_key" name="claude_api_key" value="' . $claudeApiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7206 echo '<button type="button" id="toggleClaudeApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7207 echo '<p class="description">' . esc_html__('Required for Anthropic Claude models. Get your API key from Anthropic Console.', 'mxchat') . '</p>';
7208 echo $this->mxchat_provider_key_test_button('claude', 'claude_api_key');
7209 echo '</div>';
7210 }
7211
7212 // DeepSeek API Key
7213 public function deepseek_api_key_callback() {
7214 $apiKey = isset($this->options['deepseek_api_key']) ? esc_attr($this->options['deepseek_api_key']) : '';
7215 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7216
7217 echo '<div class="api-key-wrapper">';
7218 echo '<input type="text" id="deepseek_api_key" name="deepseek_api_key" value="' . $apiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7219 echo '<button type="button" id="toggleDeepSeekApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7220 echo '<p class="description">' . esc_html__('Required for DeepSeek models. Get your API key from DeepSeek Platform.', 'mxchat') . '</p>';
7221 echo $this->mxchat_provider_key_test_button('deepseek', 'deepseek_api_key');
7222 echo '</div>';
7223 }
7224
7225 // Gemini API Key
7226 public function gemini_api_key_callback() {
7227 $geminiApiKey = isset($this->options['gemini_api_key']) ? esc_attr($this->options['gemini_api_key']) : '';
7228 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7229
7230 echo '<div class="api-key-wrapper">';
7231 echo '<input type="text" id="gemini_api_key" name="gemini_api_key" value="' . $geminiApiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7232 echo '<button type="button" id="toggleGeminiApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7233 echo '<p class="description">' . esc_html__('Required for Google Gemini models and embeddings. Get your API key from Google AI Studio.', 'mxchat') . '</p>';
7234 echo $this->mxchat_provider_key_test_button('gemini', 'gemini_api_key');
7235 echo '</div>';
7236 }
7237
7238
7239 // OpenRouter API Key
7240 public function openrouter_api_key_callback() {
7241 $openrouterApiKey = isset($this->options['openrouter_api_key']) ? esc_attr($this->options['openrouter_api_key']) : '';
7242 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7243
7244 echo '<div class="api-key-wrapper">';
7245 echo '<input type="text" id="openrouter_api_key" name="openrouter_api_key" value="' . $openrouterApiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7246 echo '<button type="button" id="toggleOpenRouterApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7247 echo '<p class="description">' . esc_html__('Required for OpenRouter models. Get your API key from OpenRouter.ai', 'mxchat') . '</p>';
7248 echo $this->mxchat_provider_key_test_button('openrouter', 'openrouter_api_key');
7249 echo '</div>';
7250 }
7251
7252 /**
7253 * Renders a "Test key" button + result target next to a built-in provider key
7254 * field, and emits the shared delegated click handler ONCE (static guard). The
7255 * button carries data-target = the key field id so the owner can test the value
7256 * they just typed (test-before-save); the AJAX handler falls back to the saved
7257 * key when the field is empty. Styled with the WP `button` class to match the
7258 * adjacent Custom-provider "Test Connection" button on this same page.
7259 * plan-mxchat-20260623-c41f74.
7260 */
7261 public function mxchat_provider_key_test_button($provider, $target_id) {
7262 static $script_emitted = false;
7263 $nonce = wp_create_nonce('mxchat_test_provider_key');
7264 $html = '<div class="mxchat-key-test" style="margin-top:8px;">'
7265 . '<button type="button" class="button mxchat-test-provider-key" data-provider="' . esc_attr($provider) . '" data-target="' . esc_attr($target_id) . '" data-nonce="' . esc_attr($nonce) . '">' . esc_html__('Test key', 'mxchat') . '</button>'
7266 . '<span class="mxchat-test-provider-key-result" style="margin-left:10px;font-size:13px;vertical-align:middle;"></span>'
7267 . '</div>';
7268 if (!$script_emitted) {
7269 $script_emitted = true;
7270 $html .= $this->mxchat_provider_key_test_script();
7271 }
7272 return $html;
7273 }
7274
7275 /**
7276 * One-time delegated click handler shared by every .mxchat-test-provider-key
7277 * button. Posts the typed key value + provider to mxchat_test_provider_key and
7278 * renders the success/error message inline. Mirrors the Custom-provider test.
7279 */
7280 private function mxchat_provider_key_test_script() {
7281 $t_testing = esc_js(__('Testing...', 'mxchat'));
7282 $t_valid = esc_js(__('Key is valid.', 'mxchat'));
7283 $t_failed = esc_js(__('Failed', 'mxchat'));
7284 $t_req = esc_js(__('Request failed', 'mxchat'));
7285 return '<script>(function(){'
7286 . 'if (window.__mxchatProviderKeyTestWired) { return; }'
7287 . 'window.__mxchatProviderKeyTestWired = true;'
7288 . 'document.addEventListener("click", function(e){'
7289 . 'var btn = e.target && e.target.closest ? e.target.closest(".mxchat-test-provider-key") : null;'
7290 . 'if (!btn) { return; }'
7291 . 'e.preventDefault();'
7292 . 'var field = btn.getAttribute("data-target") ? document.getElementById(btn.getAttribute("data-target")) : null;'
7293 . 'var out = btn.parentNode ? btn.parentNode.querySelector(".mxchat-test-provider-key-result") : null;'
7294 . 'if (out) { out.textContent = "' . $t_testing . '"; out.style.color = "#646970"; }'
7295 . 'btn.disabled = true;'
7296 . 'var fd = new FormData();'
7297 . 'fd.append("action", "mxchat_test_provider_key");'
7298 . 'fd.append("_wpnonce", btn.getAttribute("data-nonce"));'
7299 . 'fd.append("provider", btn.getAttribute("data-provider") || "");'
7300 . 'fd.append("key", field ? field.value : "");'
7301 . 'fetch(ajaxurl, { method:"POST", credentials:"same-origin", body: fd })'
7302 . '.then(function(r){ return r.json(); })'
7303 . '.then(function(j){'
7304 . 'if (out) {'
7305 . 'if (j && j.success) { out.textContent = "✓ " + ((j.data && j.data.message) ? j.data.message : "' . $t_valid . '"); out.style.color = "#00a32a"; }'
7306 . 'else { out.textContent = "⚠ " + ((j && j.data && j.data.message) ? j.data.message : "' . $t_failed . '"); out.style.color = "#d63638"; }'
7307 . '}'
7308 . 'btn.disabled = false;'
7309 . '})'
7310 . '.catch(function(){ if (out) { out.textContent = "⚠ ' . $t_req . '"; out.style.color = "#d63638"; } btn.disabled = false; });'
7311 . '});'
7312 . '})();</script>';
7313 }
7314
7315 // Custom (OpenAI-compatible) Provider — Ollama, LM Studio, vLLM, llama.cpp, Azure OpenAI, etc.
7316 public function custom_provider_callback() {
7317 $base_url = isset($this->options['custom_provider_base_url']) ? esc_attr($this->options['custom_provider_base_url']) : '';
7318 $api_key = isset($this->options['custom_provider_api_key']) ? esc_attr($this->options['custom_provider_api_key']) : '';
7319 $model_name = isset($this->options['custom_provider_model']) ? esc_attr($this->options['custom_provider_model']) : '';
7320 $auth_scheme = isset($this->options['custom_provider_auth_scheme']) ? esc_attr($this->options['custom_provider_auth_scheme']) : 'bearer';
7321 $api_version = isset($this->options['custom_provider_api_version']) ? esc_attr($this->options['custom_provider_api_version']) : '';
7322 $use_embed = !empty($this->options['custom_provider_for_embeddings']) && $this->options['custom_provider_for_embeddings'] === 'on';
7323 $use_images = !empty($this->options['custom_provider_for_images']) && $this->options['custom_provider_for_images'] === 'on';
7324 $embed_model = isset($this->options['custom_provider_embedding_model']) ? esc_attr($this->options['custom_provider_embedding_model']) : '';
7325 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7326 $test_nonce = wp_create_nonce('mxchat_test_custom_provider');
7327
7328 echo '<style>
7329 .mxchat-cp { max-width: 680px; }
7330 .mxchat-cp .mxchat-cp-intro { margin: 0 0 16px; color: #50575e; font-size: 13px; line-height: 1.5; }
7331 .mxchat-cp .mxchat-cp-row { display: block; margin: 0 0 18px; }
7332 .mxchat-cp .mxchat-cp-row > label { display: block; font-weight: 600; margin: 0 0 6px; color: #1d2327; font-size: 13px; }
7333 .mxchat-cp .mxchat-cp-row > input[type="text"],
7334 .mxchat-cp .mxchat-cp-row > input[type="password"],
7335 .mxchat-cp .mxchat-cp-row > select { display: block; width: 100%; max-width: 480px; margin: 0; }
7336 .mxchat-cp .mxchat-cp-row > .description { display: block; margin: 6px 0 0; color: #646970; font-size: 12px; line-height: 1.5; max-width: 480px; }
7337 .mxchat-cp .mxchat-cp-test { margin-top: 4px; padding-top: 14px; border-top: 1px solid #e5e7eb; }
7338 .mxchat-cp .mxchat-cp-test .mxchat-cp-test-status { display: inline-block; margin-left: 10px; vertical-align: middle; font-size: 13px; }
7339 .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; }
7340 .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; }
7341 .mxchat-cp .mxchat-cp-azure ol { margin: 0; padding: 0 0 0 18px; color: #50575e; font-size: 12px; line-height: 1.7; }
7342 .mxchat-cp .mxchat-cp-azure code { background: #eceefb; padding: 1px 5px; border-radius: 3px; font-size: 11px; }
7343 </style>';
7344
7345 echo '<div class="api-key-wrapper mxchat-cp">';
7346 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>';
7347
7348 // Azure OpenAI quick start — consolidates the 4-field Azure recipe in one scannable callout
7349 // so admins can configure Azure without piecing it together from each field's hint.
7350 echo '<div class="mxchat-cp-azure">';
7351 echo '<span class="mxchat-cp-azure-title">' . esc_html__('Azure OpenAI quick start', 'mxchat') . '</span>';
7352 echo '<ol>';
7353 echo '<li>' . wp_kses(__('<strong>Base URL</strong> → <code>https://&lt;resource&gt;.openai.azure.com/openai/deployments/&lt;deployment&gt;</code>', 'mxchat'), array('strong' => array(), 'code' => array())) . '</li>';
7354 echo '<li>' . wp_kses(__('<strong>API Key</strong> → your Azure OpenAI key (required)', 'mxchat'), array('strong' => array(), 'code' => array())) . '</li>';
7355 echo '<li>' . wp_kses(__('<strong>Auth Scheme</strong> → <code>api-key header (Azure OpenAI)</code>', 'mxchat'), array('strong' => array(), 'code' => array())) . '</li>';
7356 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>';
7357 echo '</ol>';
7358 echo '</div>';
7359
7360 echo '<div class="mxchat-cp-row">';
7361 echo '<label for="custom_provider_base_url">' . esc_html__('Base URL', 'mxchat') . '</label>';
7362 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 . '" />';
7363 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>';
7364 echo '</div>';
7365
7366 echo '<div class="mxchat-cp-row">';
7367 echo '<label for="custom_provider_api_key">' . esc_html__('API Key (optional)', 'mxchat') . '</label>';
7368 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 . '" />';
7369 echo '<p class="description">' . esc_html__('Leave empty for unauthenticated local servers. Required for Azure / vLLM / hosted endpoints.', 'mxchat') . '</p>';
7370 echo '</div>';
7371
7372 echo '<div class="mxchat-cp-row">';
7373 echo '<label for="custom_provider_model">' . esc_html__('Model Name', 'mxchat') . '</label>';
7374 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 . '" />';
7375 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>';
7376 echo '</div>';
7377
7378 echo '<div class="mxchat-cp-row">';
7379 echo '<label for="custom_provider_auth_scheme">' . esc_html__('Auth Scheme', 'mxchat') . '</label>';
7380 echo '<select id="custom_provider_auth_scheme" name="custom_provider_auth_scheme" class="mxchat-autosave-field" data-nonce="' . $nonce . '">';
7381 echo '<option value="bearer"' . selected($auth_scheme, 'bearer', false) . '>' . esc_html__('Authorization: Bearer (OpenAI / Ollama / vLLM / LM Studio)', 'mxchat') . '</option>';
7382 echo '<option value="api-key"' . selected($auth_scheme, 'api-key', false) . '>' . esc_html__('api-key header (Azure OpenAI)', 'mxchat') . '</option>';
7383 echo '</select>';
7384 echo '<p class="description">' . esc_html__('Most OpenAI-compatible servers use Bearer. Azure OpenAI uses the api-key header.', 'mxchat') . '</p>';
7385 echo '</div>';
7386
7387 echo '<div class="mxchat-cp-row">';
7388 echo '<label for="custom_provider_api_version">' . esc_html__('API Version (Azure only)', 'mxchat') . '</label>';
7389 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 . '" />';
7390 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>';
7391 echo '</div>';
7392
7393 // Extended-use checkboxes — opt-in routing of other dispatcher paths through the custom provider.
7394 echo '<div class="mxchat-cp-row">';
7395 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>';
7396 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>';
7397 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>';
7398 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>';
7399 echo '</div>';
7400
7401 echo '<div class="mxchat-cp-row">';
7402 echo '<label for="custom_provider_embedding_model">' . esc_html__('Custom Embedding Model', 'mxchat') . '</label>';
7403 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 . '" />';
7404 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>';
7405 echo '</div>';
7406
7407 echo '<div class="mxchat-cp-test">';
7408 echo '<button type="button" class="button" id="mxchat-test-custom-provider" data-nonce="' . $test_nonce . '">' . esc_html__('Test Connection', 'mxchat') . '</button>';
7409 echo '<span id="mxchat-test-custom-provider-result" class="mxchat-cp-test-status"></span>';
7410 echo '</div>';
7411
7412 echo '<script>(function(){
7413 var btn = document.getElementById("mxchat-test-custom-provider");
7414 if (!btn || btn._wired) { return; } btn._wired = true;
7415 btn.addEventListener("click", function(){
7416 var out = document.getElementById("mxchat-test-custom-provider-result");
7417 out.textContent = "' . esc_js(__('Testing...', 'mxchat')) . '";
7418 out.style.color = "#646970";
7419 var fd = new FormData();
7420 fd.append("action", "mxchat_test_custom_provider");
7421 fd.append("_wpnonce", btn.getAttribute("data-nonce"));
7422 fetch(ajaxurl, { method:"POST", credentials:"same-origin", body: fd })
7423 .then(function(r){ return r.json(); })
7424 .then(function(j){
7425 if (j && j.success) {
7426 out.textContent = "✓ " + (j.data && j.data.message ? j.data.message : "' . esc_js(__('OK', 'mxchat')) . '");
7427 out.style.color = "#00a32a";
7428 } else {
7429 out.textContent = "⚠ " + (j && j.data && j.data.message ? j.data.message : "' . esc_js(__('Failed', 'mxchat')) . '");
7430 out.style.color = "#d63638";
7431 }
7432 })
7433 .catch(function(){
7434 out.textContent = "⚠ ' . esc_js(__('Request failed', 'mxchat')) . '";
7435 out.style.color = "#d63638";
7436 });
7437 });
7438 })();</script>';
7439
7440 echo '</div>';
7441 }
7442
7443 // Voyage API Key
7444 public function voyage_api_key_callback() {
7445 $apiKey = isset($this->options['voyage_api_key']) ? esc_attr($this->options['voyage_api_key']) : '';
7446 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7447
7448 echo '<div class="api-key-wrapper">';
7449 echo '<input type="text" id="voyage_api_key" name="voyage_api_key" value="' . $apiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7450 echo '<button type="button" id="toggleVoyageAPIKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7451 echo '<p class="description">' . esc_html__('Required for Voyage AI embedding models. Get your API key from Voyage AI.', 'mxchat') . '</p>';
7452 echo '</div>';
7453 }
7454
7455 public function mxchat_loops_api_key_callback() {
7456 $loops_api_key = isset($this->options['loops_api_key']) ? esc_attr($this->options['loops_api_key']) : '';
7457 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7458
7459 echo '<div class="api-key-wrapper">';
7460 echo sprintf(
7461 '<input type="text" id="loops_api_key" name="loops_api_key" value="%s" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="%s" />',
7462 $loops_api_key,
7463 $nonce
7464 );
7465 echo '<button type="button" id="toggleLoopsApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7466 echo '</div>';
7467 echo '<p class="description">' . esc_html__('Required for Loops email integration. Get your API key from Loops.so', 'mxchat') . '</p>';
7468 }
7469 public function mxchat_loops_mailing_list_callback() {
7470 // Add error handling and type checking
7471 $loops_api_key = '';
7472 $selected_list = '';
7473 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7474
7475 // Safely get the API key
7476 if (isset($this->options['loops_api_key']) && is_string($this->options['loops_api_key'])) {
7477 $loops_api_key = $this->options['loops_api_key'];
7478 }
7479
7480 // Safely get the selected list
7481 if (isset($this->options['loops_mailing_list']) && is_string($this->options['loops_mailing_list'])) {
7482 $selected_list = $this->options['loops_mailing_list'];
7483 }
7484
7485 if (!empty($loops_api_key)) {
7486 $lists = $this->mxchat_fetch_loops_mailing_lists($loops_api_key);
7487 if (is_array($lists) && !empty($lists)) {
7488 echo '<div class="mxchat-field-wrapper">';
7489 echo '<select id="loops_mailing_list" name="loops_mailing_list" class="mxchat-autosave-field" data-nonce="' . $nonce . '">';
7490
7491 // Add a default "Select a list" option
7492 echo '<option value="" ' . selected($selected_list, '', false) . '>' . esc_html__('Select a list', 'mxchat') . '</option>';
7493
7494 foreach ($lists as $list) {
7495 if (is_array($list) && isset($list['id']) && isset($list['name'])) {
7496 echo sprintf(
7497 '<option value="%s" %s>%s</option>',
7498 esc_attr($list['id']),
7499 selected($selected_list, $list['id'], false),
7500 esc_html($list['name'])
7501 );
7502 }
7503 }
7504 echo '</select>';
7505 echo '</div>';
7506 echo '<p class="description">' . esc_html__('Please select a mailing list to use with Loops.', 'mxchat') . '</p>';
7507 } else {
7508 echo '<p class="description">' . esc_html__('No lists found. Please verify your API Key.', 'mxchat') . '</p>';
7509 }
7510 } else {
7511 echo '<p class="description">' . esc_html__('Enter a valid Loops API Key to load mailing lists.', 'mxchat') . '</p>';
7512 }
7513 }
7514 public function mxchat_triggered_phrase_response_callback() {
7515 $default_response = __('Would you like to join our mailing list? Please provide your email below.', 'mxchat');
7516 $triggered_response = isset($this->options['triggered_phrase_response'])
7517 ? $this->options['triggered_phrase_response']
7518 : $default_response;
7519 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7520
7521 echo '<div class="mxchat-field-wrapper">';
7522 echo sprintf(
7523 '<textarea id="triggered_phrase_response" name="triggered_phrase_response" rows="3" cols="50" class="mxchat-autosave-field" data-nonce="%s">%s</textarea>',
7524 $nonce,
7525 esc_textarea($triggered_response)
7526 );
7527 echo '</div>';
7528 echo '<p class="description">' . esc_html__('Enter the instruction for the AI when a trigger keyword is detected. The AI will use this as guidance to naturally ask for the user\'s email in a conversational way.', 'mxchat') . '</p>';
7529 }
7530
7531 public function mxchat_email_capture_response_callback() {
7532 $default_response = __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
7533 $email_capture_response = isset($this->options['email_capture_response'])
7534 ? $this->options['email_capture_response']
7535 : $default_response;
7536 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7537
7538 echo '<div class="mxchat-field-wrapper">';
7539 echo sprintf(
7540 '<textarea id="email_capture_response" name="email_capture_response" rows="3" cols="50" class="mxchat-autosave-field" data-nonce="%s">%s</textarea>',
7541 $nonce,
7542 esc_textarea($email_capture_response)
7543 );
7544 echo '</div>';
7545 echo '<p class="description">' . esc_html__('Enter the instruction for the AI when a user provides their email. The AI will use this as guidance to naturally confirm the email capture in a conversational way.', 'mxchat') . '</p>';
7546 }
7547 public function mxchat_pre_chat_message_callback() {
7548 // Load the entire 'mxchat_options' array
7549 $all_options = get_option('mxchat_options', []);
7550
7551 // Retrieve the saved message or use the default value
7552 $default_message = __('Hey there! Ask me anything!', 'mxchat');
7553 $pre_chat_message = isset($all_options['pre_chat_message']) ? $all_options['pre_chat_message'] : $default_message;
7554
7555 // Output the textarea
7556 printf(
7557 '<textarea id="pre_chat_message" name="pre_chat_message" rows="5" cols="50">%s</textarea>',
7558 esc_textarea($pre_chat_message)
7559 );
7560 }
7561
7562 // Callback for AI Instructions textarea
7563 public function system_prompt_instructions_callback() {
7564 // Retrieve the current value of the system prompt instructions
7565 $instructions = isset($this->options['system_prompt_instructions']) ? esc_textarea($this->options['system_prompt_instructions']) : '';
7566 // Render the textarea field
7567 printf(
7568 '<textarea id="system_prompt_instructions" name="system_prompt_instructions" rows="5" cols="50">%s</textarea>',
7569 $instructions
7570 );
7571 // Personalization hint
7572 echo '<p class="description" style="margin-top: 8px;">';
7573 echo esc_html__('Use {visitor_name} to personalize AI responses when lead capture is enabled.', 'mxchat') . '<br>';
7574 echo '<code style="font-size: 12px;">' . esc_html__('Example: The visitor\'s name is {visitor_name}. Address them by name.', 'mxchat') . '</code>';
7575 echo '</p>';
7576 // Sample instructions button
7577 echo '<div class="mxchat-instructions-container">';
7578 echo '<button type="button" class="mxchat-instructions-btn" id="mxchatViewSampleBtn">';
7579 echo '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">';
7580 echo '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>';
7581 echo '<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>';
7582 echo '</svg>';
7583 echo esc_html__('View Sample Instructions', 'mxchat');
7584 echo '</button>';
7585 echo '</div>';
7586
7587 // Add modal to WordPress admin footer instead of inline
7588 add_action('admin_footer', array($this, 'render_sample_instructions_modal'));
7589 }
7590
7591 // New method to render modal in admin footer
7592 public function render_sample_instructions_modal() {
7593 static $modal_rendered = false;
7594 if ($modal_rendered) return; // Prevent duplicate modals
7595 $modal_rendered = true;
7596
7597 echo '<div class="mxchat-instructions-modal-overlay" id="mxchatSampleModal">';
7598 echo '<div class="mxchat-instructions-modal-content">';
7599 echo '<div class="mxchat-instructions-modal-header">';
7600 echo '<h3 class="mxchat-instructions-modal-title">';
7601 echo '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">';
7602 echo '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>';
7603 echo '<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>';
7604 echo '</svg>';
7605 echo esc_html__('Sample AI Instructions', 'mxchat');
7606 echo '</h3>';
7607 echo '<button type="button" class="mxchat-instructions-modal-close" id="mxchatModalClose">';
7608 echo '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">';
7609 echo '<line x1="18" y1="6" x2="6" y2="18"/>';
7610 echo '<line x1="6" y1="6" x2="18" y2="18"/>';
7611 echo '</svg>';
7612 echo '</button>';
7613 echo '</div>';
7614 echo '<div class="mxchat-instructions-modal-body">';
7615 echo '<div class="mxchat-instructions-content">';
7616 echo esc_html('You are an AI Chatbot assistant for this website. Your main goal is to assist visitors with questions and provide helpful information. Here are your key guidelines:
7617
7618 # Response Style - CRITICALLY IMPORTANT
7619 - MAXIMUM LENGTH: 1-3 short sentences per response
7620 - Ultra-concise: Get straight to the answer with no filler
7621 - No introductions like "Sure!" or "I\'d be happy to help"
7622 - No phrases like "based on my knowledge" or "according to information"
7623 - No explanatory text before giving the answer
7624 - No summaries or repetition
7625 - Hyperlink all URLs
7626 - Respond in user\'s language
7627 - Minor chit chat or conversation is okay, but try to keep it focused on [insert topic]
7628
7629 # Knowledge Base Requirements - PREVENT HALLUCINATIONS
7630 - ONLY answer using information explicitly provided in OFFICIAL KNOWLEDGE DATABASE CONTENT sections marked with ===== delimiters
7631 - If required information is NOT in the knowledge database: "I don\'t have enough information in my knowledge base to answer that question accurately."
7632 - NEVER invent or hallucinate URLs, links, product specs, procedures, dates, statistics, names, contacts, or company information
7633 - When knowledge base information is unclear or contradictory, acknowledge the limitation rather than guessing
7634 - Better to admit insufficient information than provide inaccurate answers');
7635 echo '</div>';
7636 echo '<button type="button" class="mxchat-instructions-copy-btn" id="mxchatCopyBtn">';
7637 echo '<svg 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">';
7638 echo '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>';
7639 echo '<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>';
7640 echo '</svg>';
7641 echo esc_html__('Copy Instructions', 'mxchat');
7642 echo '</button>';
7643 echo '</div>';
7644 echo '<div class="mxchat-instructions-modal-footer">';
7645 echo '<button type="button" class="mxchat-instructions-btn-secondary" id="mxchatCloseBtn">' . esc_html__('Close', 'mxchat') . '</button>';
7646 echo '</div>';
7647 echo '</div>';
7648 echo '</div>';
7649 }
7650
7651
7652 public function mxchat_model_callback() {
7653 // Catalog refactor (plan-d14e89): single source of truth lives in
7654 // includes/class-mxchat-model-catalog.php. Dropdown groups are the
7655 // provider labels; each group maps model_id => "Label" strings.
7656 if (!class_exists('MxChat_Model_Catalog')) {
7657 require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
7658 }
7659 $models = MxChat_Model_Catalog::settings_dropdown_groups();
7660
7661 // Retrieve the currently selected model from saved options
7662 $selected_model = isset($this->options['model']) ? esc_attr($this->options['model']) : 'gpt-5.1-chat-latest';
7663
7664 // Begin the select dropdown
7665 echo '<select id="model" name="model">';
7666
7667 // Iterate over groups of models
7668 foreach ($models as $group_label => $group_models) {
7669 echo '<optgroup label="' . esc_attr($group_label) . '">';
7670
7671 foreach ($group_models as $model_value => $model_label) {
7672 echo '<option value="' . esc_attr($model_value) . '" ' . selected($selected_model, $model_value, false) . '>' . esc_html($model_label) . '</option>';
7673 }
7674
7675 echo '</optgroup>';
7676 }
7677
7678 echo '</select>';
7679
7680 // Add a note for OpenRouter
7681 echo '<p class="description" id="openrouter-model-note" style="display:none; color: #d63638; font-weight: 500;">';
7682 echo '<span class="dashicons dashicons-info" style="font-size: 16px; vertical-align: middle;"></span> ';
7683 echo esc_html__('After entering your OpenRouter API key above, click the button below to load available models.', 'mxchat');
7684 echo '</p>';
7685
7686 // API Key Status Messages (hidden by default, shown by JS based on selected model)
7687 $has_openai_key = !empty($this->options['api_key']);
7688 $has_claude_key = !empty($this->options['claude_api_key']);
7689 $has_xai_key = !empty($this->options['xai_api_key']);
7690 $has_deepseek_key = !empty($this->options['deepseek_api_key']);
7691 $has_gemini_key = !empty($this->options['gemini_api_key']);
7692 $has_openrouter_key = !empty($this->options['openrouter_api_key']);
7693
7694 // OpenAI/GPT models
7695 echo '<p class="mxchat-api-status" data-provider="openai" style="display:none;">';
7696 if ($has_openai_key) {
7697 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenAI detected', 'mxchat') . '</span>';
7698 } else {
7699 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenAI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7700 }
7701 echo '</p>';
7702
7703 // Claude models
7704 echo '<p class="mxchat-api-status" data-provider="claude" style="display:none;">';
7705 if ($has_claude_key) {
7706 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Anthropic (Claude) detected', 'mxchat') . '</span>';
7707 } else {
7708 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Anthropic (Claude) detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7709 }
7710 echo '</p>';
7711
7712 // X.AI models
7713 echo '<p class="mxchat-api-status" data-provider="xai" style="display:none;">';
7714 if ($has_xai_key) {
7715 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for X.AI (Grok) detected', 'mxchat') . '</span>';
7716 } else {
7717 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for X.AI (Grok) detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7718 }
7719 echo '</p>';
7720
7721 // DeepSeek models
7722 echo '<p class="mxchat-api-status" data-provider="deepseek" style="display:none;">';
7723 if ($has_deepseek_key) {
7724 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for DeepSeek detected', 'mxchat') . '</span>';
7725 } else {
7726 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for DeepSeek detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7727 }
7728 echo '</p>';
7729
7730 // Gemini models
7731 echo '<p class="mxchat-api-status" data-provider="gemini" style="display:none;">';
7732 if ($has_gemini_key) {
7733 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Google Gemini detected', 'mxchat') . '</span>';
7734 } else {
7735 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Google Gemini detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7736 }
7737 echo '</p>';
7738
7739 // OpenRouter models
7740 echo '<p class="mxchat-api-status" data-provider="openrouter" style="display:none;">';
7741 if ($has_openrouter_key) {
7742 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenRouter detected', 'mxchat') . '</span>';
7743 } else {
7744 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenRouter detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7745 }
7746 echo '</p>';
7747
7748 // ADD THESE HIDDEN FIELDS RIGHT HERE:
7749 $openrouter_model = isset($this->options['openrouter_selected_model']) ? esc_attr($this->options['openrouter_selected_model']) : '';
7750 $openrouter_model_name = isset($this->options['openrouter_selected_model_name']) ? esc_attr($this->options['openrouter_selected_model_name']) : '';
7751
7752 echo '<input type="hidden" id="openrouter_selected_model" name="openrouter_selected_model" value="' . $openrouter_model . '" />';
7753 echo '<input type="hidden" id="openrouter_selected_model_name" name="openrouter_selected_model_name" value="' . $openrouter_model_name . '" />';
7754 }
7755
7756 // Update your existing callback method
7757 public function enable_streaming_toggle_callback() {
7758 // Get value from options array, default to 'on'
7759 $enabled = isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on';
7760 $checked = ($enabled === 'on') ? 'checked' : '';
7761
7762 echo '<label class="toggle-switch">';
7763 echo sprintf(
7764 '<input type="checkbox" id="enable_streaming_toggle" name="enable_streaming_toggle" value="on" %s />',
7765 esc_attr($checked)
7766 );
7767 echo '<span class="slider"></span>';
7768 echo '</label>';
7769
7770 // Test button — branded .mxch-btn with inline SVG (IDs preserved for AJAX binding)
7771 echo '<div class="mxch-streaming-test-row">';
7772 echo '<button type="button" id="mxchat-test-streaming-btn" class="mxch-btn mxch-btn-secondary">';
7773 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>';
7774 echo esc_html__('Test Streaming Compatibility', 'mxchat');
7775 echo '</button>';
7776 echo '<p id="mxchat-test-streaming-result" class="mxch-streaming-test-result"></p>';
7777 echo '</div>';
7778 }
7779
7780 // Web Search toggle callback
7781 public function enable_web_search_toggle_callback() {
7782 // Get value from options array, default to 'off'
7783 $enabled = isset($this->options['enable_web_search']) ? $this->options['enable_web_search'] : 'off';
7784 $checked = ($enabled === 'on') ? 'checked' : '';
7785
7786 // Get current model to determine if we should show/enable the toggle
7787 $current_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest';
7788
7789 // Models that DON'T support web search — OpenAI-docs-driven exception list.
7790 // Keep hardcoded; the catalog can't infer "supports web search" per-model, so any
7791 // future OpenAI model that lacks Responses-API web_search support is added here.
7792 $unsupported_models = array('gpt-4.1-nano');
7793
7794 // Web-search-capable chat-model allowlists, derived from the central model catalog
7795 // (class-mxchat-model-catalog.php). When a new OpenAI/Gemini chat model is added there,
7796 // the Web Search toggle picks it up automatically — no edit here.
7797 // OpenAI grounds via the Responses-API web_search tool; Gemini grounds natively via the
7798 // Google Search tool (plan 46b9ea wired the Gemini dispatch — every shipped Gemini chat
7799 // model is 2.x/3.x and grounds, matching that path's empty opt-out list, so all catalog
7800 // Gemini models are supported here).
7801 if (!class_exists('MxChat_Model_Catalog')) {
7802 require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
7803 }
7804 $chat_catalog = MxChat_Model_Catalog::chat_models();
7805 $openai_models = (isset($chat_catalog['openai']['models']) && is_array($chat_catalog['openai']['models']))
7806 ? array_keys($chat_catalog['openai']['models'])
7807 : array();
7808 $gemini_models = (isset($chat_catalog['gemini']['models']) && is_array($chat_catalog['gemini']['models']))
7809 ? array_keys($chat_catalog['gemini']['models'])
7810 : array();
7811
7812 $is_capable = in_array($current_model, $openai_models) || in_array($current_model, $gemini_models);
7813 $is_supported = $is_capable && !in_array($current_model, $unsupported_models);
7814
7815 // Wrapper div with data attributes for JS to show/hide. data-openai-models is kept for
7816 // back-compat; data-gemini-models is the added second provider the JS now also honors.
7817 echo '<div id="web-search-toggle-wrapper" data-openai-models="' . esc_attr(implode(',', $openai_models)) . '" data-gemini-models="' . esc_attr(implode(',', $gemini_models)) . '" data-unsupported-models="' . esc_attr(implode(',', $unsupported_models)) . '"' . (!$is_supported ? ' style="display:none;"' : '') . '>';
7818
7819 echo '<label class="toggle-switch">';
7820 echo sprintf(
7821 '<input type="checkbox" id="enable_web_search" name="enable_web_search" value="on" %s />',
7822 esc_attr($checked)
7823 );
7824 echo '<span class="slider"></span>';
7825 echo '</label>';
7826
7827 echo '</div>';
7828
7829 // Message shown when a model that can't ground (Claude, Grok, DeepSeek, OpenRouter, etc.) is selected
7830 echo '<p id="web-search-unavailable-message" class="description" style="color: #666;' . ($is_supported ? ' display:none;' : '') . '">';
7831 echo '<span class="dashicons dashicons-info" style="font-size: 16px; vertical-align: middle; margin-right: 4px;"></span>';
7832 echo esc_html__('Web search is only available for OpenAI and Gemini models.', 'mxchat');
7833 echo '</p>';
7834 }
7835
7836 // AJAX handler to fetch OpenRouter models
7837 public function fetch_openrouter_models() {
7838 check_ajax_referer('mxchat_fetch_openrouter_models', 'nonce');
7839
7840 $api_key = isset($_POST['api_key']) ? sanitize_text_field($_POST['api_key']) : '';
7841
7842 if (empty($api_key)) {
7843 wp_send_json_error(array('message' => 'API key is required'));
7844 }
7845
7846 $response = wp_remote_get('https://openrouter.ai/api/v1/models', array(
7847 'headers' => array(
7848 'Authorization' => 'Bearer ' . $api_key,
7849 'Content-Type' => 'application/json',
7850 ),
7851 'timeout' => 15,
7852 ));
7853
7854 if (is_wp_error($response)) {
7855 wp_send_json_error(array('message' => $response->get_error_message()));
7856 }
7857
7858 $body = wp_remote_retrieve_body($response);
7859 $data = json_decode($body, true);
7860
7861 if (isset($data['data']) && is_array($data['data'])) {
7862 // Format the models for the frontend
7863 $models = array_map(function($model) {
7864 return array(
7865 'id' => $model['id'],
7866 'name' => $model['name'] ?? $model['id'],
7867 'description' => $model['description'] ?? '',
7868 'context_length' => $model['context_length'] ?? 0,
7869 'pricing' => array(
7870 'prompt' => $model['pricing']['prompt'] ?? 0,
7871 'completion' => $model['pricing']['completion'] ?? 0,
7872 ),
7873 );
7874 }, $data['data']);
7875
7876 wp_send_json_success(array('models' => $models));
7877 } else {
7878 wp_send_json_error(array('message' => 'Invalid response from OpenRouter'));
7879 }
7880 }
7881
7882 // Callback function for embedding model selection
7883 public function embedding_model_callback() {
7884 $models = array(
7885 esc_html__('OpenAI Embeddings', 'mxchat') => array(
7886 'text-embedding-3-small' => esc_html__('TE3 Small (1536, Efficient)', 'mxchat'),
7887 'text-embedding-ada-002' => esc_html__('Ada 2 (1536, Recommended)', 'mxchat'),
7888 'text-embedding-3-large' => esc_html__('TE3 Large (3072, Powerful)', 'mxchat'),
7889 ),
7890 esc_html__('Voyage AI Embeddings', 'mxchat') => array(
7891 'voyage-3-large' => esc_html__('Voyage-3 Large (2048, Most Capable)', 'mxchat'),
7892 ),
7893 esc_html__('Google Gemini Embeddings', 'mxchat') => array(
7894 'gemini-embedding-001' => esc_html__('Gemini Embedding (1536, Stable)', 'mxchat'),
7895 )
7896 );
7897 $selected_model = isset($this->options['embedding_model']) ? esc_attr($this->options['embedding_model']) : 'text-embedding-ada-002';
7898 echo '<select id="embedding_model" name="embedding_model">';
7899 foreach ($models as $group_label => $group_models) {
7900 echo '<optgroup label="' . esc_attr($group_label) . '">';
7901 foreach ($group_models as $model_value => $model_label) {
7902 echo '<option value="' . esc_attr($model_value) . '" ' . selected($selected_model, $model_value, false) . '>' . esc_html($model_label) . '</option>';
7903 }
7904 echo '</optgroup>';
7905 }
7906 echo '</select>';
7907
7908 // API Key Status Messages for Embedding Models
7909 $has_openai_key = !empty($this->options['api_key']);
7910 $has_voyage_key = !empty($this->options['voyage_api_key']);
7911 $has_gemini_key = !empty($this->options['gemini_api_key']);
7912
7913 // OpenAI Embeddings
7914 echo '<p class="mxchat-embedding-api-status" data-provider="openai" style="display:none;">';
7915 if ($has_openai_key) {
7916 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenAI detected', 'mxchat') . '</span>';
7917 } else {
7918 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenAI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7919 }
7920 echo '</p>';
7921
7922 // Voyage AI Embeddings
7923 echo '<p class="mxchat-embedding-api-status" data-provider="voyage" style="display:none;">';
7924 if ($has_voyage_key) {
7925 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Voyage AI detected', 'mxchat') . '</span>';
7926 } else {
7927 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Voyage AI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7928 }
7929 echo '</p>';
7930
7931 // Gemini Embeddings
7932 echo '<p class="mxchat-embedding-api-status" data-provider="gemini" style="display:none;">';
7933 if ($has_gemini_key) {
7934 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Google Gemini detected', 'mxchat') . '</span>';
7935 } else {
7936 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Google Gemini detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7937 }
7938 echo '</p>';
7939
7940 }
7941
7942
7943 public function mxchat_top_bar_title_callback() {
7944 // Retrieve the current value of the top bar title from saved options
7945 $top_bar_title = isset($this->options['top_bar_title']) ? esc_attr($this->options['top_bar_title']) : '';
7946
7947 // Render the input field
7948 echo '<input type="text" id="top_bar_title" name="top_bar_title" value="' . $top_bar_title . '" />';
7949 }
7950 public function mxchat_ai_agent_text_callback() {
7951 // Retrieve the current value of the AI agent text from saved options
7952 $ai_agent_text = isset($this->options['ai_agent_text']) ? esc_attr($this->options['ai_agent_text']) : '';
7953 // Render the input field
7954 echo '<input type="text" id="ai_agent_text" name="ai_agent_text" value="' . $ai_agent_text . '" />';
7955 }
7956
7957
7958 public function enable_email_block_callback() {
7959 // Load full plugin options array
7960 $all_options = get_option('mxchat_options', []);
7961
7962 // Get the value, default to 'off'
7963 $enable_email_block = isset($all_options['enable_email_block']) ? $all_options['enable_email_block'] : 'off';
7964
7965 // Check if it's 'on'
7966 $checked = ($enable_email_block === 'on') ? 'checked' : '';
7967
7968 echo '<label class="toggle-switch">';
7969 echo sprintf(
7970 '<input type="checkbox" id="enable_email_block" name="enable_email_block" value="on" %s />',
7971 esc_attr($checked)
7972 );
7973 echo '<span class="slider"></span>';
7974 echo '</label>';
7975 }
7976
7977 public function email_blocker_header_content_callback() {
7978 // Load the entire 'mxchat_options' array
7979 $all_options = get_option('mxchat_options', []);
7980
7981 // Retrieve the saved content or default to empty
7982 $content = isset($all_options['email_blocker_header_content'])
7983 ? $all_options['email_blocker_header_content']
7984 : '';
7985
7986 // Render the textarea - IMPORTANT: name should be just "email_blocker_header_content"
7987 echo '<textarea
7988 id="email_blocker_header_content"
7989 name="email_blocker_header_content"
7990 rows="5"
7991 cols="70"
7992 data-setting="email_blocker_header_content"
7993 >' . esc_textarea($content) . '</textarea>';
7994 }
7995
7996 public function email_blocker_button_text_callback() {
7997 // Load the entire 'mxchat_options' array
7998 $all_options = get_option('mxchat_options', []);
7999
8000 // Retrieve the saved button text or default to empty
8001 $button_text = isset($all_options['email_blocker_button_text'])
8002 ? $all_options['email_blocker_button_text']
8003 : '';
8004
8005 // Use esc_attr to safely render the existing text
8006 echo '<input type="text" id="email_blocker_button_text" name="email_blocker_button_text" value="' . esc_attr($button_text) . '" style="width: 300px;" />';
8007 }
8008
8009 //Enable name field callback
8010 public function enable_name_field_callback() {
8011 // Load full plugin options array
8012 $all_options = get_option('mxchat_options', []);
8013 // Get the value, default to 'off'
8014 $enable_name_field = isset($all_options['enable_name_field']) ? $all_options['enable_name_field'] : 'off';
8015 // Check if it's 'on'
8016 $checked = ($enable_name_field === 'on') ? 'checked' : '';
8017 echo '<label class="toggle-switch">';
8018 echo sprintf(
8019 '<input type="checkbox" id="enable_name_field" name="enable_name_field" value="on" %s />',
8020 esc_attr($checked)
8021 );
8022 echo '<span class="slider"></span>';
8023 echo '</label>';
8024 }
8025 //Name field placeholder callback
8026 public function name_field_placeholder_callback() {
8027 $all_options = get_option('mxchat_options', []);
8028 $placeholder = isset($all_options['name_field_placeholder'])
8029 ? $all_options['name_field_placeholder']
8030 : esc_html__('Enter your name', 'mxchat');
8031
8032 echo '<input type="text" id="name_field_placeholder" name="name_field_placeholder" value="' . esc_attr($placeholder) . '" style="width: 300px;" />';
8033 }
8034
8035
8036
8037 public function mxchat_intro_message_callback() {
8038 // Load the entire 'mxchat_options' array
8039 $all_options = get_option('mxchat_options', []);
8040 // Retrieve the saved intro message or use the default
8041 $default_message = __('Hello! How can I assist you today?', 'mxchat');
8042 $saved_message = isset($all_options['intro_message']) ? $all_options['intro_message'] : $default_message;
8043 // Escape on output (esc_textarea) — neutralizes any payload already stored before the
8044 // Wordfence Stored-XSS fix (CWE-79, plan-3f8158) and prevents </textarea> context-breakout.
8045 ?>
8046 <textarea id="intro_message" name="intro_message" rows="5" cols="50"><?php echo esc_textarea( $saved_message ); ?></textarea>
8047 <p class="description" style="margin-top: 8px;">
8048 <?php esc_html_e('Use {visitor_name} to personalize greetings when lead capture is enabled.', 'mxchat'); ?><br>
8049 <code style="font-size: 12px;"><?php esc_html_e('Example: Hello {visitor_name}! How can I help you today?', 'mxchat'); ?></code>
8050 </p>
8051 <?php
8052 }
8053
8054 public function mxchat_input_copy_callback() {
8055 // Load the entire 'mxchat_options' array
8056 $all_options = get_option('mxchat_options', []);
8057
8058 // Retrieve the saved input copy or use the default value
8059 $default_copy = __('How can I assist?', 'mxchat');
8060 $input_copy = isset($all_options['input_copy']) ? $all_options['input_copy'] : $default_copy;
8061
8062 // Output the input field with the saved value
8063 printf(
8064 '<input type="text" id="input_copy" name="input_copy" value="%s" placeholder="%s" />',
8065 esc_attr($input_copy),
8066 esc_attr__('How can I assist?', 'mxchat')
8067 );
8068 }
8069
8070
8071 public function mxchat_append_to_body_callback() {
8072 // Fetch fresh options to ensure we have the latest saved values
8073 $options = get_option('mxchat_options', array());
8074
8075 // Get value from options array, default to 'off'
8076 $append_to_body = isset($options['append_to_body']) ? $options['append_to_body'] : 'off';
8077 $checked = ($append_to_body === 'on') ? 'checked' : '';
8078
8079 // Get post type visibility settings
8080 $visibility_mode = isset($options['post_type_visibility_mode']) ? $options['post_type_visibility_mode'] : 'all';
8081 $visibility_list = isset($options['post_type_visibility_list']) ? $options['post_type_visibility_list'] : array();
8082 if (!is_array($visibility_list)) {
8083 $visibility_list = array();
8084 }
8085
8086 echo '<div class="mxchat-autosave-section">';
8087
8088 // Main toggle
8089 echo '<label class="toggle-switch">';
8090 echo sprintf(
8091 '<input type="checkbox" id="append_to_body" name="append_to_body" value="on" %s />',
8092 esc_attr($checked)
8093 );
8094 echo '<span class="slider"></span>';
8095 echo '</label>';
8096
8097 // Post Type Visibility Options (only visible when auto-display is ON)
8098 $display_style = ($append_to_body === 'on') ? '' : 'display: none;';
8099 echo '<div id="post-type-visibility-options" class="mxchat-sub-options" style="' . esc_attr($display_style) . '">';
8100
8101 echo '<div class="mxchat-post-type-visibility-header">';
8102 echo '<h4>' . esc_html__('Post Type Visibility', 'mxchat') . '</h4>';
8103 echo '</div>';
8104
8105 // Mode selector (radio buttons)
8106 echo '<div class="mxchat-visibility-mode">';
8107
8108 echo '<label class="mxchat-radio-label">';
8109 echo '<input type="radio" name="post_type_visibility_mode" value="all" ' . checked($visibility_mode, 'all', false) . ' />';
8110 echo '<span>' . esc_html__('Show on all post types', 'mxchat') . '</span>';
8111 echo '</label>';
8112
8113 echo '<label class="mxchat-radio-label">';
8114 echo '<input type="radio" name="post_type_visibility_mode" value="include" ' . checked($visibility_mode, 'include', false) . ' />';
8115 echo '<span>' . esc_html__('Only show on selected post types', 'mxchat') . '</span>';
8116 echo '</label>';
8117
8118 echo '<label class="mxchat-radio-label">';
8119 echo '<input type="radio" name="post_type_visibility_mode" value="exclude" ' . checked($visibility_mode, 'exclude', false) . ' />';
8120 echo '<span>' . esc_html__('Hide on selected post types', 'mxchat') . '</span>';
8121 echo '</label>';
8122
8123 echo '</div>';
8124
8125 // Post type checkboxes (only visible when mode is include or exclude)
8126 $list_display = ($visibility_mode !== 'all') ? '' : 'display: none;';
8127 echo '<div id="post-type-list" class="mxchat-post-type-list" style="' . esc_attr($list_display) . '">';
8128
8129 // Get all public post types
8130 $post_types = get_post_types(array('public' => true), 'objects');
8131
8132 foreach ($post_types as $post_type) {
8133 // Skip attachments
8134 if ($post_type->name === 'attachment') {
8135 continue;
8136 }
8137
8138 $is_checked = in_array($post_type->name, $visibility_list) ? 'checked' : '';
8139
8140 echo '<label class="mxchat-checkbox-label">';
8141 echo '<input type="checkbox" name="post_type_visibility_list[]" value="' . esc_attr($post_type->name) . '" ' . $is_checked . ' />';
8142 echo '<span>' . esc_html($post_type->label) . '</span>';
8143 echo '</label>';
8144 }
8145
8146 echo '</div>'; // End post-type-list
8147 echo '</div>'; // End post-type-visibility-options
8148 echo '</div>'; // End mxchat-autosave-section
8149 }
8150
8151 public function mxchat_contextual_awareness_callback() {
8152 // Get value from options array, default to 'off'
8153 $contextual_awareness = isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off';
8154 $checked = ($contextual_awareness === 'on') ? 'checked' : '';
8155 echo '<label class="toggle-switch">';
8156 echo sprintf(
8157 '<input type="checkbox" id="contextual_awareness_toggle" name="contextual_awareness_toggle" value="on" %s />',
8158 esc_attr($checked)
8159 );
8160 echo '<span class="slider"></span>';
8161 echo '</label>';
8162 }
8163
8164 public function mxchat_citation_links_toggle_callback() {
8165 // Get value from options array, default to 'on' (enabled by default)
8166 $citation_links = isset($this->options['citation_links_toggle']) ? $this->options['citation_links_toggle'] : 'on';
8167 $checked = ($citation_links === 'on') ? 'checked' : '';
8168 echo '<label class="toggle-switch">';
8169 echo sprintf(
8170 '<input type="checkbox" id="citation_links_toggle" name="citation_links_toggle" value="on" %s />',
8171 esc_attr($checked)
8172 );
8173 echo '<span class="slider"></span>';
8174 echo '</label>';
8175 }
8176
8177 /**
8178 * Toggle for the end-of-session satisfaction rating prompt (plan-a5b006).
8179 * Default ON. The widget reads this through the localized object; the
8180 * mxchat_satisfaction_rating_enabled filter still lets developers force
8181 * the value site-wide.
8182 *
8183 * The 5 customization fields (idle/question/thanks/placeholder/saved) are
8184 * rendered inline here inside a single wrapper div whose initial display
8185 * is set server-side from the toggle value (plan-29caac). Mirrors the
8186 * auto-display chatbot pattern at mxchat_append_to_body_callback — no
8187 * DOMContentLoaded race because rows exist as direct children of this
8188 * callback's output, and the wrapper's display: none is inline at render
8189 * time so refresh shows the correct state with no flash.
8190 */
8191 public function mxchat_satisfaction_rating_toggle_callback() {
8192 $options = $this->options;
8193 $value = isset($options['satisfaction_rating_enabled']) ? $options['satisfaction_rating_enabled'] : 'off';
8194 $checked = ($value === 'on') ? 'checked' : '';
8195
8196 $idle = isset($options['satisfaction_rating_idle_seconds']) ? intval($options['satisfaction_rating_idle_seconds']) : 60;
8197 $idle = max(5, min(600, $idle));
8198 $question = isset($options['satisfaction_rating_question']) ? $options['satisfaction_rating_question'] : '';
8199 $thanks = isset($options['satisfaction_rating_thanks']) ? $options['satisfaction_rating_thanks'] : '';
8200 $placeholder = isset($options['satisfaction_rating_placeholder']) ? $options['satisfaction_rating_placeholder'] : '';
8201 $saved = isset($options['satisfaction_rating_saved']) ? $options['satisfaction_rating_saved'] : '';
8202
8203 echo '<div class="mxchat-autosave-section">';
8204
8205 echo '<label class="toggle-switch">';
8206 echo sprintf(
8207 '<input type="checkbox" id="satisfaction_rating_enabled" name="satisfaction_rating_enabled" value="on" %s />',
8208 esc_attr($checked)
8209 );
8210 echo '<span class="slider"></span>';
8211 echo '</label>';
8212
8213 ?>
8214 <style>
8215 .mxchat-sub-options-field { margin: 12px 0; }
8216 .mxchat-sub-options-field label { display: inline-block; margin-bottom: 4px; }
8217 #satisfaction-rating-sub-options h4 { margin: 16px 0 8px; }
8218 </style>
8219 <?php
8220
8221 $display_style = ($value === 'on') ? '' : 'display: none;';
8222 echo '<div id="satisfaction-rating-sub-options" class="mxchat-sub-options" style="' . esc_attr($display_style) . '">';
8223
8224 echo '<h4>' . esc_html__('Customize the prompt (optional)', 'mxchat') . '</h4>';
8225
8226 echo '<div class="mxchat-sub-options-field">';
8227 echo '<label for="satisfaction_rating_idle_seconds"><strong>' . esc_html__('Idle Timeout', 'mxchat') . '</strong></label><br />';
8228 printf(
8229 '<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>',
8230 (int) $idle,
8231 esc_html__('seconds of user inactivity before the prompt appears (5-600)', 'mxchat')
8232 );
8233 echo '</div>';
8234
8235 echo '<div class="mxchat-sub-options-field">';
8236 echo '<label for="satisfaction_rating_question"><strong>' . esc_html__('Prompt Question', 'mxchat') . '</strong></label><br />';
8237 printf(
8238 '<input type="text" id="satisfaction_rating_question" name="satisfaction_rating_question" value="%s" maxlength="200" class="regular-text" placeholder="%s" />',
8239 esc_attr($question),
8240 esc_attr__('Was this helpful?', 'mxchat')
8241 );
8242 echo '<p class="description">' . esc_html__('Leave blank for the default. Shown above the thumbs up/down.', 'mxchat') . '</p>';
8243 echo '</div>';
8244
8245 echo '<div class="mxchat-sub-options-field">';
8246 echo '<label for="satisfaction_rating_thanks"><strong>' . esc_html__('Thank-You Message', 'mxchat') . '</strong></label><br />';
8247 printf(
8248 '<input type="text" id="satisfaction_rating_thanks" name="satisfaction_rating_thanks" value="%s" maxlength="300" class="regular-text" placeholder="%s" />',
8249 esc_attr($thanks),
8250 esc_attr__('Thanks! Anything we should improve? (optional)', 'mxchat')
8251 );
8252 echo '<p class="description">' . esc_html__('Leave blank for the default. Shown after the user clicks a thumb.', 'mxchat') . '</p>';
8253 echo '</div>';
8254
8255 echo '<div class="mxchat-sub-options-field">';
8256 echo '<label for="satisfaction_rating_placeholder"><strong>' . esc_html__('Feedback Placeholder', 'mxchat') . '</strong></label><br />';
8257 printf(
8258 '<input type="text" id="satisfaction_rating_placeholder" name="satisfaction_rating_placeholder" value="%s" maxlength="200" class="regular-text" placeholder="%s" />',
8259 esc_attr($placeholder),
8260 esc_attr__('Tell us what could be better…', 'mxchat')
8261 );
8262 echo '<p class="description">' . esc_html__('Leave blank for the default. Placeholder text inside the feedback textarea.', 'mxchat') . '</p>';
8263 echo '</div>';
8264
8265 echo '<div class="mxchat-sub-options-field">';
8266 echo '<label for="satisfaction_rating_saved"><strong>' . esc_html__('Saved Confirmation', 'mxchat') . '</strong></label><br />';
8267 printf(
8268 '<input type="text" id="satisfaction_rating_saved" name="satisfaction_rating_saved" value="%s" maxlength="200" class="regular-text" placeholder="%s" />',
8269 esc_attr($saved),
8270 esc_attr__('Thanks for the feedback.', 'mxchat')
8271 );
8272 echo '<p class="description">' . esc_html__('Leave blank for the default. Shown after the feedback is sent.', 'mxchat') . '</p>';
8273 echo '</div>';
8274
8275 echo '</div>'; // #satisfaction-rating-sub-options
8276 echo '</div>'; // .mxchat-autosave-section
8277
8278 ?>
8279 <script>
8280 (function() {
8281 document.addEventListener('DOMContentLoaded', function() {
8282 var toggle = document.getElementById('satisfaction_rating_enabled');
8283 var subOptions = document.getElementById('satisfaction-rating-sub-options');
8284 if (!toggle || !subOptions) return;
8285 toggle.addEventListener('change', function() {
8286 subOptions.style.display = toggle.checked ? '' : 'none';
8287 });
8288 });
8289 })();
8290 </script>
8291 <?php
8292 }
8293
8294 public function mxchat_privacy_toggle_callback() {
8295 // Load from mxchat_options array
8296 $options = get_option('mxchat_options', []);
8297
8298 // Get privacy toggle value with fallback
8299 $privacy_toggle = isset($options['privacy_toggle']) ? $options['privacy_toggle'] : 'off';
8300 $checked = ($privacy_toggle === 'on') ? 'checked' : '';
8301
8302 // Get privacy text with fallback
8303 $privacy_text = isset($options['privacy_text'])
8304 ? $options['privacy_text']
8305 : __('By chatting, you agree to our <a href="https://example.com/privacy-policy" target="_blank">privacy policy</a>.', 'mxchat');
8306
8307 // Output the toggle switch
8308 echo '<label class="toggle-switch">';
8309 echo sprintf(
8310 '<input type="checkbox" id="privacy_toggle" name="privacy_toggle" value="on" %s />',
8311 esc_attr($checked)
8312 );
8313 echo '<span class="slider"></span>';
8314 echo '</label>';
8315
8316 // Output the custom text input field
8317 echo sprintf(
8318 '<textarea id="privacy_text" name="privacy_text" rows="5" cols="50" class="regular-text">%s</textarea>',
8319 esc_textarea($privacy_text)
8320 );
8321 }
8322
8323
8324 public function mxchat_complianz_toggle_callback() {
8325 // Load from mxchat_options array
8326 $options = get_option('mxchat_options', []);
8327
8328 // Get complianz toggle value with fallback
8329 $complianz_toggle = isset($options['complianz_toggle']) ? $options['complianz_toggle'] : 'off';
8330 $checked = ($complianz_toggle === 'on') ? 'checked' : '';
8331
8332 // Output the toggle switch
8333 echo '<label class="toggle-switch">';
8334 echo sprintf(
8335 '<input type="checkbox" id="complianz_toggle" name="complianz_toggle" value="on" %s />',
8336 esc_attr($checked)
8337 );
8338 echo '<span class="slider"></span>';
8339 echo '</label>';
8340 }
8341
8342 public function mxchat_link_target_toggle_callback() {
8343 // Load from mxchat_options array
8344 $options = get_option('mxchat_options', []);
8345
8346 // Get link target toggle value with fallback
8347 $link_target_toggle = isset($options['link_target_toggle']) ? $options['link_target_toggle'] : 'off';
8348 $checked = ($link_target_toggle === 'on') ? 'checked' : '';
8349
8350 // Output the toggle switch
8351 echo '<label class="toggle-switch">';
8352 echo sprintf(
8353 '<input type="checkbox" id="link_target_toggle" name="link_target_toggle" value="on" %s />',
8354 esc_attr($checked)
8355 );
8356 echo '<span class="slider"></span>';
8357 echo '</label>';
8358 }
8359
8360 public function mxchat_chat_persistence_toggle_callback() {
8361 // Load from mxchat_options array
8362 $options = get_option('mxchat_options', []);
8363
8364 // Get chat persistence toggle value with fallback
8365 $chat_persistence_toggle = isset($options['chat_persistence_toggle']) ? $options['chat_persistence_toggle'] : 'off';
8366 $checked = ($chat_persistence_toggle === 'on') ? 'checked' : '';
8367
8368 // Output the toggle switch
8369 echo '<label class="toggle-switch">';
8370 echo sprintf(
8371 '<input type="checkbox" id="chat_persistence_toggle" name="chat_persistence_toggle" value="on" %s />',
8372 esc_attr($checked)
8373 );
8374 echo '<span class="slider"></span>';
8375 echo '</label>';
8376 }
8377
8378 public function mxchat_print_button_toggle_callback() {
8379 // Load from mxchat_options array
8380 $options = get_option('mxchat_options', []);
8381
8382 // Default ON — the option was previously unexposed and the button always showed.
8383 $print_button_enabled = isset($options['print_button_enabled']) ? $options['print_button_enabled'] : 'on';
8384 $checked = ($print_button_enabled === 'on') ? 'checked' : '';
8385
8386 // Output the toggle switch
8387 echo '<label class="toggle-switch">';
8388 echo sprintf(
8389 '<input type="checkbox" id="print_button_enabled" name="print_button_enabled" value="on" %s />',
8390 esc_attr($checked)
8391 );
8392 echo '<span class="slider"></span>';
8393 echo '</label>';
8394 }
8395
8396 /**
8397 * Editor Assistant toggle (plan-8cb0cb). Standalone option, NOT in mxchat_options
8398 * (bypasses the mxchat_sanitize strip-trap + autosave normalization). Default OFF.
8399 * Saved by the mxchat_editor_assistant_enabled case in class-ajax-handler.php.
8400 */
8401 public function mxchat_editor_assistant_toggle_callback() {
8402 $enabled = get_option('mxchat_editor_assistant_enabled', 'off');
8403 $checked = ($enabled === 'on') ? 'checked' : '';
8404
8405 echo '<label class="toggle-switch">';
8406 echo sprintf(
8407 '<input type="checkbox" id="mxchat_editor_assistant_enabled" name="mxchat_editor_assistant_enabled" value="on" %s />',
8408 esc_attr($checked)
8409 );
8410 echo '<span class="slider"></span>';
8411 echo '</label>';
8412 }
8413
8414 public function mxchat_reset_chat_toggle_callback() {
8415 // Load from mxchat_options array. plan ac2e81 — default OFF (new, opt-in).
8416 $options = get_option('mxchat_options', []);
8417 $reset_chat_enabled = isset($options['reset_chat_enabled']) ? $options['reset_chat_enabled'] : 'off';
8418 $checked = ($reset_chat_enabled === 'on') ? 'checked' : '';
8419
8420 echo '<label class="toggle-switch">';
8421 echo sprintf(
8422 '<input type="checkbox" id="reset_chat_enabled" name="reset_chat_enabled" value="on" %s />',
8423 esc_attr($checked)
8424 );
8425 echo '<span class="slider"></span>';
8426 echo '</label>';
8427 }
8428
8429 public function mxchat_reset_chat_label_callback() {
8430 // Editable label for the "Start new chat" menu item. plan ac2e81.
8431 $options = get_option('mxchat_options', []);
8432 $reset_chat_label = isset($options['reset_chat_label']) ? $options['reset_chat_label'] : '';
8433
8434 printf(
8435 '<input type="text" id="reset_chat_label" name="reset_chat_label" value="%s" placeholder="%s" class="regular-text" />',
8436 esc_attr($reset_chat_label),
8437 esc_attr__('Start new chat', 'mxchat')
8438 );
8439 }
8440
8441 public function mxchat_popular_question_1_callback() {
8442 // Load the full plugin options array
8443 $all_options = get_option('mxchat_options', []);
8444
8445 // Retrieve the specific option for popular_question_1
8446 $popular_question_1 = isset($all_options['popular_question_1']) ? $all_options['popular_question_1'] : '';
8447
8448 // Render the input field
8449 printf(
8450 '<input type="text" id="popular_question_1" name="popular_question_1" value="%s" placeholder="%s" class="regular-text" />',
8451 esc_attr($popular_question_1),
8452 esc_attr__('Enter Quick Question 1', 'mxchat')
8453 );
8454 }
8455
8456
8457 public function mxchat_popular_question_2_callback() {
8458 // Load the full plugin options array
8459 $all_options = get_option('mxchat_options', []);
8460
8461 // Retrieve the specific option for popular_question_2
8462 $popular_question_2 = isset($all_options['popular_question_2']) ? $all_options['popular_question_2'] : '';
8463
8464 // Render the input field
8465 printf(
8466 '<input type="text" id="popular_question_2" name="popular_question_2" value="%s" placeholder="%s" class="regular-text" />',
8467 esc_attr($popular_question_2),
8468 esc_attr__('Enter Quick Question 2', 'mxchat')
8469 );
8470 }
8471
8472
8473 public function mxchat_popular_question_3_callback() {
8474 // Load the full plugin options array
8475 $all_options = get_option('mxchat_options', []);
8476
8477 // Retrieve the specific option for popular_question_3
8478 $popular_question_3 = isset($all_options['popular_question_3']) ? $all_options['popular_question_3'] : '';
8479
8480 // Render the input field
8481 printf(
8482 '<input type="text" id="popular_question_3" name="popular_question_3" value="%s" placeholder="%s" class="regular-text" />',
8483 esc_attr($popular_question_3),
8484 esc_attr(__('Enter Quick Question 3', 'mxchat'))
8485 );
8486 }
8487
8488 public function mxchat_additional_popular_questions_callback() {
8489 $options = get_option('mxchat_options', []);
8490 $additional_questions = isset($options['additional_popular_questions'])
8491 ? $options['additional_popular_questions']
8492 : get_option('additional_popular_questions', array());
8493
8494 echo '<div id="mxchat-additional-questions-container">';
8495 if (!empty($additional_questions)) {
8496 foreach ($additional_questions as $index => $question) {
8497 printf(
8498 '<div class="mxchat-question-row">
8499 <input type="text" name="additional_popular_questions[]"
8500 value="%s"
8501 placeholder="%s"
8502 class="regular-text mxchat-question-input"
8503 data-question-index="%d" />
8504 <button type="button" class="button mxchat-remove-question"
8505 aria-label="%s">%s</button>
8506 </div>',
8507 esc_attr($question),
8508 esc_attr(sprintf(__('Enter Additional Quick Question %d', 'mxchat'), $index + 4)),
8509 $index,
8510 esc_attr(__('Remove question', 'mxchat')),
8511 esc_html__('Remove', 'mxchat')
8512 );
8513 }
8514 } else {
8515 printf(
8516 '<div class="mxchat-question-row">
8517 <input type="text" name="additional_popular_questions[]"
8518 value=""
8519 placeholder="%s"
8520 class="regular-text mxchat-question-input"
8521 data-question-index="0" />
8522 <button type="button" class="button mxchat-remove-question"
8523 aria-label="%s">%s</button>
8524 </div>',
8525 esc_attr(__('Enter Additional Quick Question 4', 'mxchat')),
8526 esc_attr(__('Remove question', 'mxchat')),
8527 esc_html__('Remove', 'mxchat')
8528 );
8529 }
8530 echo '</div>';
8531 printf(
8532 '<button type="button" class="button mxchat-add-question" aria-label="%s">%s</button>',
8533 esc_attr(__('Add question', 'mxchat')),
8534 esc_html__('Add Question', 'mxchat')
8535 );
8536 }
8537
8538 public function mxchat_brave_api_key_callback() {
8539 $brave_api_key = isset($this->options['brave_api_key']) ? esc_attr($this->options['brave_api_key']) : '';
8540 $nonce = wp_create_nonce('mxchat_autosave_nonce');
8541
8542 echo '<div class="api-key-wrapper">';
8543 echo sprintf(
8544 '<input type="text" id="brave_api_key" name="brave_api_key" value="%s" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="%s" />',
8545 $brave_api_key,
8546 $nonce
8547 );
8548 echo '<button type="button" id="toggleBraveApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
8549 echo '</div>';
8550 echo '<p class="description">' . __('Required for Brave Search integration. Get your API key from Brave Search API.', 'mxchat') . '</p>';
8551 }
8552
8553 public function mxchat_brave_image_count_callback() {
8554 $brave_image_count = isset($this->options['brave_image_count'])
8555 ? intval($this->options['brave_image_count'])
8556 : 4;
8557 $nonce = wp_create_nonce('mxchat_autosave_nonce');
8558
8559 echo '<div class="mxchat-field-wrapper">';
8560 echo sprintf(
8561 '<input type="number" id="brave_image_count" name="brave_image_count"
8562 value="%d" min="1" max="6" class="small-text mxchat-autosave-field" data-nonce="%s" />',
8563 $brave_image_count,
8564 $nonce
8565 );
8566 echo '</div>';
8567 echo '<p class="description">' . __('Select the number of images to return (1-6).', 'mxchat') . '</p>';
8568 }
8569
8570 public function mxchat_brave_safe_search_callback() {
8571 $brave_safe_search = isset($this->options['brave_safe_search'])
8572 ? esc_attr($this->options['brave_safe_search'])
8573 : 'strict';
8574 $nonce = wp_create_nonce('mxchat_autosave_nonce');
8575
8576 echo '<div class="mxchat-field-wrapper">';
8577 echo '<select id="brave_safe_search" name="brave_safe_search" class="mxchat-autosave-field" data-nonce="' . $nonce . '">';
8578 echo sprintf(
8579 '<option value="strict" %s>%s</option>',
8580 selected($brave_safe_search, 'strict', false),
8581 __('Strict', 'mxchat')
8582 );
8583 echo sprintf(
8584 '<option value="off" %s>%s</option>',
8585 selected($brave_safe_search, 'off', false),
8586 __('Off', 'mxchat')
8587 );
8588 echo '</select>';
8589 echo '</div>';
8590 echo '<p class="description">' .
8591 esc_html__('Set the Safe Search level for image searches. Brave Search only supports "Strict" and "Off" options.', 'mxchat') .
8592 '</p>';
8593 }
8594
8595 public function mxchat_brave_news_count_callback() {
8596 $brave_news_count = isset($this->options['brave_news_count'])
8597 ? intval($this->options['brave_news_count'])
8598 : 3;
8599 $nonce = wp_create_nonce('mxchat_autosave_nonce');
8600
8601 echo '<div class="mxchat-field-wrapper">';
8602 echo sprintf(
8603 '<input type="number" id="brave_news_count" name="brave_news_count"
8604 value="%d" min="1" max="10" class="small-text mxchat-autosave-field" data-nonce="%s" />',
8605 $brave_news_count,
8606 $nonce
8607 );
8608 echo '</div>';
8609 echo '<p class="description">' . esc_html__('Select the number of news articles to retrieve (1-10).', 'mxchat') . '</p>';
8610 }
8611
8612 public function mxchat_brave_country_callback() {
8613 $brave_country = isset($this->options['brave_country'])
8614 ? esc_attr($this->options['brave_country'])
8615 : 'us';
8616 $nonce = wp_create_nonce('mxchat_autosave_nonce');
8617
8618 echo '<div class="mxchat-field-wrapper">';
8619 echo sprintf(
8620 '<input type="text" id="brave_country" name="brave_country"
8621 value="%s" maxlength="2" class="small-text mxchat-autosave-field" data-nonce="%s" />',
8622 $brave_country,
8623 $nonce
8624 );
8625 echo '</div>';
8626 echo '<p class="description">' . esc_html__('Enter the country code (e.g., "us" for United States).', 'mxchat') . '</p>';
8627 }
8628
8629 public function mxchat_brave_language_callback() {
8630 $brave_language = isset($this->options['brave_language'])
8631 ? esc_attr($this->options['brave_language'])
8632 : 'en';
8633 $nonce = wp_create_nonce('mxchat_autosave_nonce');
8634
8635 echo '<div class="mxchat-field-wrapper">';
8636 echo sprintf(
8637 '<input type="text" id="brave_language" name="brave_language"
8638 value="%s" maxlength="2" class="small-text mxchat-autosave-field" data-nonce="%s" />',
8639 $brave_language,
8640 $nonce
8641 );
8642 echo '</div>';
8643 echo '<p class="description">' . esc_html__('Enter the language code (e.g., "en" for English).', 'mxchat') . '</p>';
8644 }
8645
8646
8647
8648
8649
8650 // Section Callback
8651 public function mxchat_pdf_intent_section_callback() {
8652 echo '<p>' . esc_html__('Configure the intent settings for the Chat with PDF feature.', 'mxchat') . '</p>';
8653 }
8654
8655 public function mxchat_chat_toolbar_toggle_callback() {
8656 // Get chat toolbar toggle value with fallback
8657 $chat_toolbar_toggle = isset($this->options['chat_toolbar_toggle']) ? $this->options['chat_toolbar_toggle'] : 'off';
8658 $checked = ($chat_toolbar_toggle === 'on') ? 'checked' : '';
8659
8660 // Output the toggle switch
8661 echo '<label class="toggle-switch">';
8662 echo sprintf(
8663 '<input type="checkbox" id="chat_toolbar_toggle" name="chat_toolbar_toggle" value="on" %s />',
8664 esc_attr($checked)
8665 );
8666 echo '<span class="slider"></span>';
8667 echo '</label>';
8668
8669 echo '<p class="description">' . esc_html__('Enable to display the chat toolbar, adding two icons below the chatbot input field for uploading PDF and Word documents (default is hidden).', 'mxchat') . '</p>';
8670 }
8671
8672 /**
8673 * Callback for PDF upload button toggle setting
8674 */
8675 public function mxchat_show_pdf_upload_button_callback() {
8676 // Get toggle value with fallback
8677 $show_pdf_button = isset($this->options['show_pdf_upload_button']) ? $this->options['show_pdf_upload_button'] : 'on';
8678 $checked = ($show_pdf_button === 'on') ? 'checked' : '';
8679
8680 // Output the toggle switch
8681 echo '<label class="toggle-switch">';
8682 echo sprintf(
8683 '<input type="checkbox" id="show_pdf_upload_button" name="show_pdf_upload_button" value="on" %s />',
8684 esc_attr($checked)
8685 );
8686 echo '<span class="slider"></span>';
8687 echo '</label>';
8688
8689 echo '<p class="description">' . esc_html__('Enable to show the PDF upload button in the chatbot toolbar. Disable to hide it.', 'mxchat') . '</p>';
8690 }
8691
8692 /**
8693 * Callback for Word upload button toggle setting
8694 */
8695 public function mxchat_show_word_upload_button_callback() {
8696 // Get toggle value with fallback
8697 $show_word_button = isset($this->options['show_word_upload_button']) ? $this->options['show_word_upload_button'] : 'on';
8698 $checked = ($show_word_button === 'on') ? 'checked' : '';
8699
8700 // Output the toggle switch
8701 echo '<label class="toggle-switch">';
8702 echo sprintf(
8703 '<input type="checkbox" id="show_word_upload_button" name="show_word_upload_button" value="on" %s />',
8704 esc_attr($checked)
8705 );
8706 echo '<span class="slider"></span>';
8707 echo '</label>';
8708
8709 echo '<p class="description">' . esc_html__('Enable to show the Word document upload button in the chatbot toolbar. Disable to hide it.', 'mxchat') . '</p>';
8710 }
8711
8712 public function mxchat_pdf_intent_trigger_text_callback() {
8713 $default_text = __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat');
8714
8715 echo sprintf(
8716 '<textarea id="pdf_intent_trigger_text"
8717 name="pdf_intent_trigger_text"
8718 rows="3"
8719 cols="50"
8720 placeholder="%s">%s</textarea>',
8721 esc_attr__('Enter trigger text', 'mxchat'),
8722 isset($this->options['pdf_intent_trigger_text'])
8723 ? esc_textarea($this->options['pdf_intent_trigger_text'])
8724 : esc_textarea($default_text)
8725 );
8726 echo '<p class="description">' . esc_html__('Text displayed when the intent is triggered.', 'mxchat') . '</p>';
8727 }
8728
8729 public function mxchat_pdf_intent_success_text_callback() {
8730 $default_text = __("I've processed the PDF. What questions do you have about it?", 'mxchat');
8731
8732 echo sprintf(
8733 '<textarea id="pdf_intent_success_text"
8734 name="pdf_intent_success_text"
8735 rows="3"
8736 cols="50"
8737 placeholder="%s">%s</textarea>',
8738 esc_attr__('Enter success text', 'mxchat'),
8739 isset($this->options['pdf_intent_success_text'])
8740 ? esc_textarea($this->options['pdf_intent_success_text'])
8741 : esc_textarea($default_text)
8742 );
8743 echo '<p class="description">' . esc_html__('Text displayed when the intent is successful.', 'mxchat') . '</p>';
8744 }
8745
8746 public function mxchat_pdf_intent_error_text_callback() {
8747 $default_text = __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
8748
8749 echo sprintf(
8750 '<textarea id="pdf_intent_error_text"
8751 name="pdf_intent_error_text"
8752 rows="3"
8753 cols="50"
8754 placeholder="%s">%s</textarea>',
8755 esc_attr__('Enter error text', 'mxchat'),
8756 isset($this->options['pdf_intent_error_text'])
8757 ? esc_textarea($this->options['pdf_intent_error_text'])
8758 : esc_textarea($default_text)
8759 );
8760 echo '<p class="description">' . esc_html__('Text displayed when an error occurs during the intent.', 'mxchat') . '</p>';
8761 }
8762
8763 public function mxchat_pdf_max_pages_callback() {
8764 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
8765
8766 echo sprintf(
8767 '<input type="range"
8768 id="pdf_max_pages"
8769 name="pdf_max_pages"
8770 min="1"
8771 max="69"
8772 value="%d"
8773 class="range-slider" />',
8774 esc_attr($max_pages)
8775 );
8776 echo '<span id="pdf_max_pages_output">' . esc_html($max_pages) . '</span>';
8777 echo '<p class="description">' . esc_html__('Set the maximum number of document pages users can upload for processing. (1-69 pages)', 'mxchat') . '</p>';
8778 }
8779
8780 public function mxchat_live_agent_status_callback() {
8781 // Always get fresh options instead of using cached $this->options
8782 $fresh_options = get_option('mxchat_options');
8783 $status = isset($fresh_options['live_agent_status']) ? $fresh_options['live_agent_status'] : 'off';
8784
8785 echo '<label class="toggle-switch">';
8786 echo sprintf(
8787 '<input type="checkbox" id="live_agent_status" name="live_agent_status" value="on" %s />',
8788 checked($status, 'on', false)
8789 );
8790 echo '<span class="slider"></span>';
8791 echo '</label>';
8792 echo '<label for="live_agent_status" class="mxchat-status-label">';
8793 echo '<span class="status-text">' . ($status === 'on' ? esc_html__('Online', 'mxchat') : esc_html__('Offline', 'mxchat')) . '</span>';
8794 echo '</label>';
8795 }
8796
8797 /**
8798 * Availability-schedule editor (plans 8ccaa2 + 99d7a4).
8799 *
8800 * ONE callback, parameterized by channel ('slack' | 'telegram' via the
8801 * add_settings_field $args) — the markup and CSS are shared, only the bound
8802 * option and the helper text differ. Each channel's editor renders under its
8803 * own Integrations tab and governs ONLY that channel's handoff. While the
8804 * master toggle is off the day grid is inert and handoff availability stays
8805 * governed solely by that channel's manual status toggle.
8806 *
8807 * The day inputs carry NO name attribute and are marked .mxchat-la-field so the
8808 * generic autosave skips them; the editor JS folds them into the channel's
8809 * hidden live_agent_schedule_<channel> input and fires one change, reusing the
8810 * existing autosave transport rather than growing a second one. All hooks the
8811 * JS needs are CLASSES scoped inside .mxchat-la-schedule, never ids — the
8812 * markup exists twice on the page.
8813 */
8814 public function mxchat_live_agent_schedule_callback($args = array()) {
8815 if (!class_exists('MxChat_Live_Agent_Schedule')) {
8816 return;
8817 }
8818 $channel = (isset($args['channel']) && $args['channel'] === 'telegram') ? 'telegram' : 'slack';
8819 $field = 'live_agent_schedule_' . $channel;
8820 $sched = MxChat_Live_Agent_Schedule::get($channel);
8821 $labels = MxChat_Live_Agent_Schedule::day_labels();
8822 $tz = MxChat_Live_Agent_Schedule::timezone_label();
8823 $enabled = !empty($sched['enabled']);
8824 $channel_label = ($channel === 'telegram') ? __('Telegram', 'mxchat') : __('Slack', 'mxchat');
8825 ?>
8826 <div class="mxchat-la-schedule<?php echo $enabled ? ' is-active' : ''; ?>"
8827 id="mxchat-la-schedule-<?php echo esc_attr($channel); ?>"
8828 data-channel="<?php echo esc_attr($channel); ?>">
8829 <label class="toggle-switch">
8830 <input type="checkbox" id="<?php echo esc_attr($field); ?>_enabled"
8831 class="mxchat-la-field mxchat-la-enabled" <?php checked($enabled); ?> />
8832 <span class="slider"></span>
8833 </label>
8834 <label for="<?php echo esc_attr($field); ?>_enabled" class="mxchat-status-label">
8835 <span class="status-text mxchat-la-status-text">
8836 <?php echo $enabled
8837 ? esc_html__('Scheduled hours', 'mxchat')
8838 : esc_html__('Always available', 'mxchat'); ?>
8839 </span>
8840 </label>
8841
8842 <div class="mxchat-la-days" aria-hidden="<?php echo $enabled ? 'false' : 'true'; ?>">
8843 <?php foreach ($labels as $n => $label) :
8844 $day = $sched['days'][$n];
8845 $on = !empty($day['enabled']);
8846 ?>
8847 <div class="mxchat-la-day<?php echo $on ? ' is-on' : ''; ?>" data-day="<?php echo esc_attr($n); ?>">
8848 <label class="mxchat-la-day-label">
8849 <input type="checkbox" class="mxchat-la-field mxchat-la-day-enabled"
8850 data-day="<?php echo esc_attr($n); ?>" <?php checked($on); ?> />
8851 <span class="mxchat-la-day-name"><?php echo esc_html($label); ?></span>
8852 </label>
8853 <div class="mxchat-la-times">
8854 <input type="time" class="mxchat-la-field mxchat-la-start"
8855 data-day="<?php echo esc_attr($n); ?>"
8856 value="<?php echo esc_attr($day['start']); ?>"
8857 aria-label="<?php echo esc_attr(sprintf(__('%s start time', 'mxchat'), $label)); ?>" />
8858 <span class="mxchat-la-dash">&ndash;</span>
8859 <input type="time" class="mxchat-la-field mxchat-la-end"
8860 data-day="<?php echo esc_attr($n); ?>"
8861 value="<?php echo esc_attr($day['end']); ?>"
8862 aria-label="<?php echo esc_attr(sprintf(__('%s end time', 'mxchat'), $label)); ?>" />
8863 </div>
8864 </div>
8865 <?php endforeach; ?>
8866 </div>
8867
8868 <input type="hidden" name="<?php echo esc_attr($field); ?>" id="<?php echo esc_attr($field); ?>"
8869 class="mxchat-la-hidden" value="<?php echo esc_attr(wp_json_encode($sched)); ?>" />
8870 </div>
8871 <p class="description">
8872 <?php printf(
8873 /* translators: 1: the handoff channel, e.g. Slack or Telegram; 2: the site's timezone, e.g. America/New_York */
8874 esc_html__('When on, visitors are only offered a %1$s human handoff during these hours — outside them the chatbot keeps answering and never offers to fetch an agent. Applies to %1$s only. Times use the site timezone (%2$s). Set a day\'s end time earlier than its start for an overnight shift; identical start and end means all day.', 'mxchat'),
8875 '<strong>' . esc_html($channel_label) . '</strong>',
8876 '<strong>' . esc_html($tz) . '</strong>'
8877 ); ?>
8878 </p>
8879 <?php
8880 }
8881
8882 public function mxchat_live_agent_away_message_callback() {
8883 $message = isset($this->options['live_agent_away_message'])
8884 ? $this->options['live_agent_away_message']
8885 : __('Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.', 'mxchat');
8886
8887 printf(
8888 '<textarea id="live_agent_away_message" name="live_agent_away_message" rows="3" cols="50">%s</textarea>',
8889 esc_textarea($message)
8890 );
8891 echo '<p class="description">' . esc_html__('Message shown when live agents are offline.', 'mxchat') . '</p>';
8892 }
8893
8894 public function mxchat_live_agent_notification_message_callback() {
8895 $message = isset($this->options['live_agent_notification_message'])
8896 ? $this->options['live_agent_notification_message']
8897 : __('Live agent has been notified.', 'mxchat');
8898
8899 printf(
8900 '<textarea id="live_agent_notification_message" name="live_agent_notification_message" rows="3" cols="50">%s</textarea>',
8901 esc_textarea($message)
8902 );
8903 echo '<p class="description">' . esc_html__('Message shown when live transfer activated.', 'mxchat') . '</p>';
8904 }
8905
8906 public function mxchat_live_agent_webhook_url_callback() {
8907 $webhook_url = isset($this->options['live_agent_webhook_url'])
8908 ? esc_url($this->options['live_agent_webhook_url'])
8909 : esc_url(get_option('live_agent_webhook_url', ''));
8910
8911 printf(
8912 '<input type="password" id="live_agent_webhook_url" name="live_agent_webhook_url" value="%s" class="regular-text" />',
8913 $webhook_url
8914 );
8915 echo '<button type="button" id="toggleWebhookUrlVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
8916 echo '<p class="description">' . esc_html__('Enter your Slack webhook URL for live agent notifications.', 'mxchat') . '</p>';
8917 }
8918
8919 public function mxchat_live_agent_secret_key_callback() {
8920 printf(
8921 '<input type="password" id="live_agent_secret_key" name="live_agent_secret_key" value="%s" class="regular-text" />',
8922 isset($this->options['live_agent_secret_key']) ? esc_attr($this->options['live_agent_secret_key']) : ''
8923 );
8924 echo '<button type="button" id="toggleSecretKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
8925 echo '<p class="description">' . esc_html__('Secret key for validating Slack requests. Keep this secure.', 'mxchat') . '</p>';
8926 }
8927
8928 public function mxchat_live_agent_bot_token_callback() {
8929 printf(
8930 '<input type="password" id="live_agent_bot_token" name="live_agent_bot_token" value="%s" class="regular-text" />',
8931 isset($this->options['live_agent_bot_token']) ? esc_attr($this->options['live_agent_bot_token']) : ''
8932 );
8933 echo '<button type="button" id="toggleBotTokenVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
8934 echo '<p class="description">' . esc_html__('Your Slack Bot OAuth Token (starts with xoxb-). Keep this secure.', 'mxchat') . '</p>';
8935 }
8936
8937 public function mxchat_live_agent_user_ids_callback() {
8938 $user_ids = isset($this->options['live_agent_user_ids'])
8939 ? esc_textarea($this->options['live_agent_user_ids'])
8940 : '';
8941
8942 printf(
8943 '<textarea id="live_agent_user_ids" name="live_agent_user_ids" rows="4" class="large-text">%s</textarea>',
8944 $user_ids
8945 );
8946 echo '<p class="description">' . esc_html__('Enter Slack User IDs of agents who should be automatically invited to chat channels (one per line). Find user IDs in Slack profiles under "More" → "Copy member ID". Example: U1234567890', 'mxchat') . '</p>';
8947 }
8948
8949 public function mxchat_live_agent_shared_channel_callback() {
8950 $value = isset($this->options['live_agent_shared_channel']) ? $this->options['live_agent_shared_channel'] : '';
8951 printf(
8952 '<input type="text" id="live_agent_shared_channel" name="live_agent_shared_channel" value="%s" class="regular-text" placeholder="#support or C0123456789" />',
8953 esc_attr($value)
8954 );
8955 echo '<p class="description">' . esc_html__('Optional. Route ALL live agent handoffs into this one existing Slack channel — each conversation becomes its own thread there. Enter a channel ID (starts with C) or a #channel-name, and invite your bot to the channel first (/invite @YourBot). Agents reply inside a conversation\'s thread; !endchat inside the thread ends that chat. Leave blank to keep creating a separate chat- channel per conversation.', 'mxchat') . '</p>';
8956
8957 // Surface the last failed handoff so a wrong name / missing invite is
8958 // visible right where it gets fixed. A successful handoff clears this.
8959 $shared_error = get_option('mxchat_slack_shared_channel_error');
8960 if (!empty($shared_error['error']) && trim((string) $value) !== '' && ($shared_error['configured'] ?? '') === trim((string) $value)) {
8961 echo '<p class="description"><strong>' . esc_html__('⚠ Last handoff could not reach this channel', 'mxchat') . '</strong> — '
8962 . esc_html(sprintf(
8963 /* translators: %s: Slack API error code */
8964 __('Slack said "%s". Handoffs are falling back to per-conversation channels until this is fixed. Check the channel name or ID and make sure the bot has been invited to it.', 'mxchat'),
8965 $shared_error['error']
8966 )) . '</p>';
8967 }
8968 }
8969
8970 public function mxchat_live_agent_archive_on_end_callback() {
8971 $value = isset($this->options['live_agent_archive_on_end_toggle']) ? $this->options['live_agent_archive_on_end_toggle'] : 'off';
8972 printf(
8973 '<input type="checkbox" id="live_agent_archive_on_end_toggle" name="live_agent_archive_on_end_toggle" value="on" %s />',
8974 checked('on', $value, false)
8975 );
8976 echo '<label for="live_agent_archive_on_end_toggle" class="mxchat-status-label">' . esc_html__('Archive the conversation\'s chat- channel when an agent ends the chat with !endchat', 'mxchat') . '</label>';
8977 echo '<p class="description">' . esc_html__('Keeps your Slack sidebar tidy on busy sites — each ended conversation\'s channel is archived instead of living forever. Archived channels stay searchable in Slack, so the record is never lost. Only applies to per-conversation chat- channels; a Shared Handoff Channel is never archived. If a returning visitor requests an agent again, a fresh channel is created automatically.', 'mxchat') . '</p>';
8978 }
8979
8980 /**
8981 * Telegram Integration Callbacks
8982 */
8983 public function mxchat_telegram_section_callback() {
8984 echo '<p>' . esc_html__('Configure Telegram integration for live agent support.', 'mxchat') . '</p>';
8985 }
8986
8987 public function mxchat_telegram_status_callback() {
8988 $fresh_options = get_option('mxchat_options');
8989 $status = isset($fresh_options['telegram_status']) ? $fresh_options['telegram_status'] : 'off';
8990
8991 echo '<label class="toggle-switch">';
8992 echo sprintf(
8993 '<input type="checkbox" id="telegram_status" name="telegram_status" value="on" %s />',
8994 checked($status, 'on', false)
8995 );
8996 echo '<span class="slider"></span>';
8997 echo '</label>';
8998 echo '<label for="telegram_status" class="mxchat-status-label">';
8999 echo '<span class="status-text">' . ($status === 'on' ? esc_html__('Online', 'mxchat') : esc_html__('Offline', 'mxchat')) . '</span>';
9000 echo '</label>';
9001 }
9002
9003 public function mxchat_telegram_notification_message_callback() {
9004 $message = isset($this->options['telegram_notification_message'])
9005 ? $this->options['telegram_notification_message']
9006 : __("I've notified a support agent. Please allow a moment for them to respond.", 'mxchat');
9007
9008 printf(
9009 '<textarea id="telegram_notification_message" name="telegram_notification_message" rows="3" cols="50">%s</textarea>',
9010 esc_textarea($message)
9011 );
9012 echo '<p class="description">' . esc_html__('Message shown when live transfer activated.', 'mxchat') . '</p>';
9013 }
9014
9015 public function mxchat_telegram_away_message_callback() {
9016 $message = isset($this->options['telegram_away_message'])
9017 ? $this->options['telegram_away_message']
9018 : __('Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.', 'mxchat');
9019
9020 printf(
9021 '<textarea id="telegram_away_message" name="telegram_away_message" rows="3" cols="50">%s</textarea>',
9022 esc_textarea($message)
9023 );
9024 echo '<p class="description">' . esc_html__('Message shown when live agents are offline.', 'mxchat') . '</p>';
9025 }
9026
9027 public function mxchat_telegram_bot_token_callback() {
9028 printf(
9029 '<input type="password" id="telegram_bot_token" name="telegram_bot_token" value="%s" class="regular-text" />',
9030 isset($this->options['telegram_bot_token']) ? esc_attr($this->options['telegram_bot_token']) : ''
9031 );
9032 echo '<button type="button" class="button button-secondary" onclick="var f=document.getElementById(\'telegram_bot_token\'); if(f.type===\'password\'){f.type=\'text\';this.textContent=\'' . esc_js(__('Hide', 'mxchat')) . '\';}else{f.type=\'password\';this.textContent=\'' . esc_js(__('Show', 'mxchat')) . '\';}">' . esc_html__('Show', 'mxchat') . '</button>';
9033 echo '<p class="description">' . esc_html__('Your Telegram Bot Token from @BotFather (e.g., 123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ).', 'mxchat') . '</p>';
9034 }
9035
9036 public function mxchat_telegram_group_id_callback() {
9037 printf(
9038 '<input type="text" id="telegram_group_id" name="telegram_group_id" value="%s" class="regular-text" />',
9039 isset($this->options['telegram_group_id']) ? esc_attr($this->options['telegram_group_id']) : ''
9040 );
9041 echo '<p class="description">' . esc_html__('Your Telegram supergroup ID (starts with -100). The group must have forum topics enabled.', 'mxchat') . '</p>';
9042 }
9043
9044 public function mxchat_telegram_webhook_secret_callback() {
9045 $secret = isset($this->options['telegram_webhook_secret']) ? $this->options['telegram_webhook_secret'] : '';
9046
9047 // Auto-generate secret if empty
9048 if (empty($secret)) {
9049 $secret = wp_generate_password(64, false, false);
9050 $options = get_option('mxchat_options', []);
9051 $options['telegram_webhook_secret'] = $secret;
9052 update_option('mxchat_options', $options);
9053 $this->options['telegram_webhook_secret'] = $secret;
9054 }
9055
9056 printf(
9057 '<input type="password" id="telegram_webhook_secret" name="telegram_webhook_secret" value="%s" class="regular-text" />',
9058 esc_attr($secret)
9059 );
9060 echo '<button type="button" class="button button-secondary" onclick="var f=document.getElementById(\'telegram_webhook_secret\'); if(f.type===\'password\'){f.type=\'text\';this.textContent=\'' . esc_js(__('Hide', 'mxchat')) . '\';}else{f.type=\'password\';this.textContent=\'' . esc_js(__('Show', 'mxchat')) . '\';}">' . esc_html__('Show', 'mxchat') . '</button>';
9061 echo '<p class="description">' . esc_html__('Secret token for webhook verification. Use this when setting up your Telegram webhook with the secret_token parameter.', 'mxchat') . '</p>';
9062 }
9063
9064 public function mxchat_similarity_threshold_callback() {
9065 // Load from mxchat_options array
9066 $options = get_option('mxchat_options', []);
9067
9068 // Get value from options array with default of 35
9069 $threshold = isset($options['similarity_threshold']) ? $options['similarity_threshold'] : 35;
9070
9071 echo '<div class="slider-container">';
9072 echo sprintf(
9073 '<input type="range"
9074 id="similarity_threshold"
9075 name="similarity_threshold"
9076 min="20"
9077 max="85"
9078 step="1"
9079 value="%s"
9080 class="range-slider" />',
9081 esc_attr($threshold)
9082 );
9083 echo sprintf(
9084 '<span id="threshold_value" class="range-value">%s</span>',
9085 esc_html($threshold)
9086 );
9087 echo '</div>';
9088 }
9089
9090 public function mxchat_max_input_length_callback() {
9091 // Load from mxchat_options array (plan a3fae2 part C).
9092 $options = get_option('mxchat_options', []);
9093
9094 // 0 = unlimited (default — preserves current behavior, no cap).
9095 $max_input_length = isset($options['max_input_length']) ? intval($options['max_input_length']) : 0;
9096
9097 echo sprintf(
9098 '<input type="number"
9099 id="max_input_length"
9100 name="max_input_length"
9101 min="0"
9102 max="100000"
9103 step="1"
9104 value="%s"
9105 placeholder="0"
9106 class="mxch-input mxch-input-sm" />',
9107 esc_attr($max_input_length)
9108 );
9109 }
9110
9111 public function mxchat_rag_sources_limit_callback() {
9112 // Load from mxchat_options array
9113 $options = get_option('mxchat_options', []);
9114
9115 // Get value from options array with default of 3
9116 $rag_sources_limit = isset($options['rag_sources_limit']) ? intval($options['rag_sources_limit']) : 3;
9117
9118 echo '<div class="slider-container">';
9119 echo sprintf(
9120 '<input type="range"
9121 id="rag_sources_limit"
9122 name="rag_sources_limit"
9123 min="3"
9124 max="10"
9125 step="1"
9126 value="%s"
9127 class="range-slider" />',
9128 esc_attr($rag_sources_limit)
9129 );
9130 echo sprintf(
9131 '<span id="rag_sources_limit_value" class="range-value">%s</span>',
9132 esc_html($rag_sources_limit)
9133 );
9134 echo '</div>';
9135 }
9136
9137 public function mxchat_rag_chunks_limit_callback() {
9138 // Load from mxchat_options array
9139 $options = get_option('mxchat_options', []);
9140
9141 // Get value from options array with default of 15
9142 $rag_chunks_limit = isset($options['rag_chunks_limit']) ? intval($options['rag_chunks_limit']) : 15;
9143
9144 echo '<div class="slider-container">';
9145 echo sprintf(
9146 '<input type="range"
9147 id="rag_chunks_limit"
9148 name="rag_chunks_limit"
9149 min="8"
9150 max="20"
9151 step="1"
9152 value="%s"
9153 class="range-slider" />',
9154 esc_attr($rag_chunks_limit)
9155 );
9156 echo sprintf(
9157 '<span id="rag_chunks_limit_value" class="range-value">%s</span>',
9158 esc_html($rag_chunks_limit)
9159 );
9160 echo '</div>';
9161 }
9162
9163 /**
9164 * Add body class on the Onboarding wizard page so CSS-only chrome surgery
9165 * (collapsing the WP admin sidebar) only applies on this page.
9166 * Plan: plan-mxchat-20260527-905439.
9167 */
9168 public function mxchat_add_onboarding_body_class($classes) {
9169 if (isset($_GET['page']) && $_GET['page'] === 'mxchat-onboarding') {
9170 $classes .= ' mxchat-onboarding-focused';
9171 }
9172 return $classes;
9173 }
9174
9175 public function mxchat_enqueue_admin_assets() {
9176 // Get plugin version
9177 $version = MXCHAT_VERSION;
9178
9179 // Use file modification time for development (remove in production)
9180 if (defined('WP_DEBUG') && WP_DEBUG) {
9181 $version = filemtime(plugin_dir_path(__FILE__) . '../mxchat-basic.php');
9182 }
9183
9184 $current_page = isset($_GET['page']) ? $_GET['page'] : '';
9185 $plugin_url = plugin_dir_url(__FILE__) . '../';
9186
9187 // Only load on MxChat pages
9188 if (strpos($current_page, 'mxchat') === false) {
9189 return;
9190 }
9191
9192 // Always load these on all MxChat pages
9193 $this->enqueue_core_admin_assets($plugin_url, $version);
9194 $this->enqueue_page_specific_assets($current_page, $plugin_url, $version);
9195 $this->localize_admin_scripts($current_page);
9196 }
9197 private function enqueue_core_admin_assets($plugin_url, $version) {
9198 // Core admin styles
9199 wp_enqueue_style('mxchat-admin-css', $plugin_url . 'css/admin-style.css', array(), $version);
9200 wp_enqueue_style('mxchat-knowledge-css', $plugin_url . 'css/knowledge-style.css', array(), $version);
9201
9202 // New sidebar navigation styles (for main settings page)
9203 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
9204
9205 // Core admin scripts
9206 wp_enqueue_script('mxchat-admin-js', $plugin_url . 'js/mxchat-admin.js', array('jquery'), $version, true);
9207 }
9208 private function enqueue_page_specific_assets($current_page, $plugin_url, $version) {
9209 switch ($current_page) {
9210 case 'mxchat-prompts':
9211 // Knowledge processing page assets
9212 wp_enqueue_style('mxchat-content-selector-css', $plugin_url . 'css/content-selector.css', array(), $version);
9213 wp_enqueue_script('mxchat-content-selector-js', $plugin_url . 'js/content-selector.js', array('jquery'), $version, true);
9214 // Add the knowledge processing script (common script needed for WordPress dismiss functionality)
9215 wp_enqueue_script('mxchat-knowledge-processing', $plugin_url . 'js/knowledge-processing.js', array('jquery', 'common'), $version, true);
9216 // Per-entry "View indexed content" inspector (plan-d8cb4b) reuses the
9217 // Testing tab's match-card / chunk-detail components, which are scoped
9218 // under .mxch-testing-results. Load that stylesheet here so the inspector
9219 // modal renders identically to the Testing tab.
9220 wp_enqueue_style('mxchat-admin-testing-css', $plugin_url . 'css/admin-testing-tab.css', array('mxchat-admin-sidebar-css'), $version);
9221 break;
9222
9223 case 'mxchat-transcripts':
9224 // Load admin sidebar CSS first (shared styles for sidebar navigation)
9225 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
9226 // Load transcripts-specific styles
9227 wp_enqueue_style('mxchat-chat-transcripts-css', $plugin_url . 'css/chat-transcripts.css', array('mxchat-admin-sidebar-css'), $version);
9228 wp_enqueue_script('mxchat-transcripts-js', $plugin_url . 'js/mxchat_transcripts.js', array('jquery'), $version, true);
9229 break;
9230
9231 case 'mxchat-actions':
9232 // Load admin sidebar CSS first (shared styles for sidebar navigation)
9233 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
9234 // Load actions-specific styles
9235 wp_enqueue_style('mxchat-actions-css', $plugin_url . 'css/actions.css', array('mxchat-admin-sidebar-css'), $version);
9236 wp_enqueue_script('mxchat-actions-js', $plugin_url . 'js/mxchat_actions.js', array('jquery'), $version, true);
9237
9238 // Localize script data for actions page
9239 $is_activated = get_option('mxchat_pro_license_status') === 'active';
9240 wp_localize_script('mxchat-actions-js', 'mxchActionsData', array(
9241 'ajaxUrl' => admin_url('admin-ajax.php'),
9242 'nonce' => wp_create_nonce('mxchat_actions_nonce'),
9243 'addNonce' => wp_create_nonce('mxchat_add_intent_nonce'),
9244 'editNonce' => wp_create_nonce('mxchat_edit_intent'),
9245 'deleteNonce' => wp_create_nonce('mxchat_delete_intent_nonce'),
9246 'toggleNonce' => wp_create_nonce('mxchat_actions_nonce'),
9247 'addPhraseNonce' => wp_create_nonce('mxchat_add_phrase_nonce'),
9248 'deletePhraseNonce' => wp_create_nonce('mxchat_delete_phrase_nonce'),
9249 'getPhrasesNonce' => wp_create_nonce('mxchat_get_phrases_nonce'),
9250 'deleteLegacyNonce' => wp_create_nonce('mxchat_delete_legacy_nonce'),
9251 'isActivated' => $is_activated,
9252 'i18n' => array(
9253 'confirmDelete' => __('Are you sure you want to delete this trigger phrase?', 'mxchat'),
9254 'confirmBulkDelete' => __('Are you sure you want to delete the selected trigger phrases?', 'mxchat'),
9255 'saving' => __('Saving...', 'mxchat'),
9256 'saved' => __('Saved successfully!', 'mxchat'),
9257 'error' => __('An error occurred. Please try again.', 'mxchat'),
9258 'proRequired' => __('This feature requires MxChat Pro.', 'mxchat'),
9259 'addonRequired' => __('This feature requires an add-on.', 'mxchat')
9260 )
9261 ));
9262 break;
9263
9264 case 'mxchat-activation':
9265 // Load admin sidebar CSS first (shared styles for sidebar navigation)
9266 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
9267 // Load pro page-specific styles
9268 wp_enqueue_style('mxchat-pro-css', $plugin_url . 'css/admin-pro.css', array('mxchat-admin-sidebar-css'), $version);
9269 // Load pro page JavaScript
9270 wp_enqueue_script('mxchat-pro-js', $plugin_url . 'js/mxchat_pro.js', array('jquery'), $version, true);
9271 // Load activation script for license activation/deactivation
9272 wp_enqueue_script('mxchat-activation-js', $plugin_url . 'js/activation-script.js', array('jquery'), $version, true);
9273 break;
9274
9275 case 'mxchat-content':
9276 // Load admin sidebar CSS (shared styles for sidebar navigation)
9277 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
9278 // Load content page-specific styles
9279 wp_enqueue_style('mxchat-content-css', $plugin_url . 'css/admin-content.css', array('mxchat-admin-sidebar-css'), $version);
9280 // Load content page JavaScript
9281 wp_enqueue_script('mxchat-content-js', $plugin_url . 'js/mxchat-content.js', array('jquery'), $version, true);
9282 break;
9283
9284 case 'mxchat-api-access':
9285 // Shared sidebar shell (CSS + JS for tab switching / mobile menu / copy buttons).
9286 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
9287 wp_enqueue_script('mxchat-admin-sidebar-js', $plugin_url . 'js/admin-sidebar.js', array(), $version, true);
9288 wp_localize_script('mxchat-admin-sidebar-js', 'MxChatAdminSidebarI18n', array(
9289 'copied' => __('Copied', 'mxchat'),
9290 ));
9291 break;
9292
9293 case 'mxchat-max':
9294 case 'mxchat-onboarding':
9295 // Onboarding page — uses the shared admin shell PLUS the wizard
9296 // overlay (plan-905439). admin-onboarding-wizard.css scopes the
9297 // WP-chrome surgery to body.mxchat-onboarding-focused so it only
9298 // applies on THIS page. The body class is added below via the
9299 // admin_body_class filter.
9300 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
9301 wp_enqueue_script('mxchat-admin-sidebar-js', $plugin_url . 'js/admin-sidebar.js', array(), $version, true);
9302 wp_localize_script('mxchat-admin-sidebar-js', 'MxChatAdminSidebarI18n', array(
9303 'copied' => __('Copied', 'mxchat'),
9304 ));
9305 // admin-style.css provides the .mxchat-instructions-modal-* classes
9306 // the new Behavior step's "View Sample Instructions" modal needs.
9307 // Enqueued AFTER admin-sidebar.css but BEFORE the wizard overlay
9308 // so the wizard's own rules win where they collide. plan-a2e4d6.
9309 wp_enqueue_style('mxchat-admin-style-css', $plugin_url . 'css/admin-style.css', array('mxchat-admin-sidebar-css'), $version);
9310 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);
9311 wp_enqueue_script('mxchat-admin-onboarding-wizard-js', $plugin_url . 'js/admin-onboarding-wizard.js', array(), $version, true);
9312 break;
9313 default:
9314 wp_enqueue_script(
9315 'mxchat-test-streaming-js',
9316 $plugin_url . 'js/mxchat-test-streaming.js',
9317 ['jquery'],
9318 $version,
9319 true
9320 );
9321
9322 wp_localize_script('mxchat-test-streaming-js', 'mxchatTestStreamingAjax', [
9323 'ajax_url' => admin_url('admin-ajax.php'),
9324 'nonce' => wp_create_nonce('mxchat_test_streaming_nonce'),
9325 'settings_nonce' => wp_create_nonce('mxchat_save_setting_nonce')
9326 ]);
9327
9328 // Testing Tab: Load chatbot assets for the embedded testing chatbot
9329 wp_enqueue_style('mxchat-chat-css', $plugin_url . 'css/chat-style.css', array(), $version);
9330 wp_enqueue_style('mxchat-admin-testing-css', $plugin_url . 'css/admin-testing-tab.css', array('mxchat-admin-sidebar-css', 'mxchat-chat-css'), $version);
9331
9332 wp_enqueue_script('mxchat-chat-js', $plugin_url . 'js/chat-script.js', array('jquery'), $version, true);
9333 wp_enqueue_script('mxchat-admin-testing-js', $plugin_url . 'js/admin-testing-tab.js', array('jquery', 'mxchat-chat-js'), $version, true);
9334
9335 // Allow add-ons to enqueue their public CSS/JS for the testing chatbot
9336 do_action('mxchat_enqueue_testing_tab_assets');
9337
9338 // Localize mxchatChat for the chatbot JS (same settings as frontend)
9339 $options = get_option('mxchat_options', array());
9340 $prompts_options = get_option('mxchat_prompts_options', array());
9341 $theme_options = get_option('mxchat_theme_options', array());
9342 $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
9343 $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
9344 $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
9345
9346 wp_localize_script('mxchat-chat-js', 'mxchatChat', array(
9347 'ajax_url' => admin_url('admin-ajax.php'),
9348 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
9349 'model' => isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest',
9350 'enable_streaming_toggle' => isset($options['enable_streaming_toggle']) ? $options['enable_streaming_toggle'] : 'on',
9351 'contextual_awareness_toggle' => isset($options['contextual_awareness_toggle']) ? $options['contextual_awareness_toggle'] : 'off',
9352 'link_target_toggle' => $options['link_target_toggle'] ?? 'off',
9353 'rate_limit_message' => $options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
9354 'complianz_toggle' => isset($options['complianz_toggle']) && $options['complianz_toggle'] === 'on',
9355 'user_message_bg_color' => $options['user_message_bg_color'] ?? '#fff',
9356 'user_message_font_color' => $options['user_message_font_color'] ?? '#212121',
9357 'bot_message_bg_color' => $options['bot_message_bg_color'] ?? '#212121',
9358 'bot_message_font_color' => $options['bot_message_font_color'] ?? '#fff',
9359 'top_bar_bg_color' => $options['top_bar_bg_color'] ?? '#212121',
9360 'send_button_font_color' => $options['send_button_font_color'] ?? '#212121',
9361 'close_button_color' => $options['close_button_color'] ?? '#fff',
9362 'chatbot_background_color' => $options['chatbot_background_color'] ?? '#212121',
9363 'chatbot_bg_color' => $options['chatbot_bg_color'] ?? '#fff',
9364 'icon_color' => $options['icon_color'] ?? '#fff',
9365 'chat_input_font_color' => $options['chat_input_font_color'] ?? '#212121',
9366 'chat_persistence_toggle' => 'off', // Always off for testing chatbot
9367 'appendWidgetToBody' => 'off',
9368 'live_agent_message_bg_color' => $options['live_agent_message_bg_color'] ?? '#ffffff',
9369 'live_agent_message_font_color' => $options['live_agent_message_font_color'] ?? '#333333',
9370 'chat_toolbar_toggle' => $options['chat_toolbar_toggle'] ?? 'off',
9371 'mode_indicator_bg_color' => $options['mode_indicator_bg_color'] ?? '#767676',
9372 'mode_indicator_font_color' => $options['mode_indicator_font_color'] ?? '#ffffff',
9373 'toolbar_icon_color' => $options['toolbar_icon_color'] ?? '#212121',
9374 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
9375 'email_collection_enabled' => 'off', // No email collection in testing
9376 'initial_email_state' => null,
9377 'skip_email_check' => true,
9378 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
9379 'skip_inline_colors' => $skip_inline_colors,
9380 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array()
9381 ));
9382
9383 // Localize testing tab script data
9384 wp_localize_script('mxchat-admin-testing-js', 'mxchatAdminTestData', array(
9385 'ajaxUrl' => admin_url('admin-ajax.php'),
9386 'nonce' => wp_create_nonce('mxchat_test_nonce'),
9387 'isAdmin' => true,
9388 'testingEnabled' => true
9389 ));
9390
9391 // Add testing enabled flag for the chatbot to return debug data
9392 add_action('admin_footer', function() {
9393 echo '<script>window.mxchatTestingEnabled = true;</script>';
9394 });
9395
9396 break;
9397 }
9398 }
9399 private function localize_admin_scripts($current_page) {
9400 // Base localization data for main admin script
9401 $base_data = array(
9402 'ajax_url' => admin_url('admin-ajax.php'),
9403 'nonce' => wp_create_nonce('mxchat_admin_nonce'),
9404 'admin_url' => admin_url(),
9405 'license_nonce' => wp_create_nonce('mxchat_activate_license_nonce'),
9406 'inline_edit_nonce' => wp_create_nonce('mxchat_save_inline_nonce'),
9407 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
9408 'export_nonce' => wp_create_nonce('mxchat_export_transcripts'),
9409 'actions_nonce' => wp_create_nonce('mxchat_actions_nonce'),
9410 'add_intent_nonce' => wp_create_nonce('mxchat_add_intent_nonce'),
9411 'edit_intent_nonce' => wp_create_nonce('mxchat_edit_intent'),
9412 'add_phrase_nonce' => wp_create_nonce('mxchat_add_phrase_nonce'),
9413 'delete_phrase_nonce' => wp_create_nonce('mxchat_delete_phrase_nonce'),
9414 'get_phrases_nonce' => wp_create_nonce('mxchat_get_phrases_nonce'),
9415 'delete_legacy_nonce' => wp_create_nonce('mxchat_delete_legacy_nonce'),
9416 'toggle_action_nonce' => wp_create_nonce('mxchat_actions_nonce'),
9417 'fetch_openrouter_models_nonce' => wp_create_nonce('mxchat_fetch_openrouter_models'),
9418 'is_activated' => $this->is_activated ? '1' : '0',
9419 'status_refresh_interval' => 5000,
9420 'discard_changes_confirm' => __('Discard your unsaved changes?', 'mxchat'),
9421 // Live-agent schedule status text (plan 8ccaa2).
9422 'i18n_scheduled_hours' => __('Scheduled hours', 'mxchat'),
9423 'i18n_always_available' => __('Always available', 'mxchat'),
9424 'prompts_setting_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
9425 'ajaxurl' => admin_url('admin-ajax.php')
9426 );
9427
9428 // Localize main admin script with base data
9429 wp_localize_script('mxchat-admin-js', 'mxchatAdmin', $base_data);
9430
9431 // Canonical chat-model catalog for the modal picker grid
9432 // (plan-d14e89). Adding a model in class-mxchat-model-catalog.php
9433 // automatically appears here.
9434 if (!class_exists('MxChat_Model_Catalog')) {
9435 require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
9436 }
9437 wp_localize_script('mxchat-admin-js', 'mxchatChatModelCatalog', MxChat_Model_Catalog::js_picker_shape());
9438
9439 // Page-specific localizations
9440 $this->localize_page_specific_scripts($current_page);
9441 }
9442 private function localize_page_specific_scripts($current_page) {
9443 switch ($current_page) {
9444 case 'mxchat-prompts':
9445 // Status updater localization
9446 wp_localize_script('mxchat-status-updater', 'mxchat_status_data', array(
9447 'ajax_url' => admin_url('admin-ajax.php'),
9448 'nonce' => wp_create_nonce('mxchat_status_nonce')
9449 ));
9450
9451 // Content selector localization
9452 $mxchat_options_for_selector = get_option('mxchat_options', array());
9453 $acf_pdf_extract_default = !empty($mxchat_options_for_selector['acf_pdf_extract_default']);
9454 wp_localize_script('mxchat-content-selector-js', 'mxchatSelector', array(
9455 'ajaxurl' => admin_url('admin-ajax.php'),
9456 'nonce' => wp_create_nonce('mxchat_content_selector_nonce'),
9457 'acfPdfExtractDefault' => $acf_pdf_extract_default ? 1 : 0,
9458 'i18n' => array(
9459 'searchPlaceholder' => __('Search posts and pages...', 'mxchat'),
9460 'selectAll' => __('Select All', 'mxchat'),
9461 'process' => __('Process Selected', 'mxchat'),
9462 'cancel' => __('Cancel', 'mxchat'),
9463 'noResults' => __('No content found.', 'mxchat'),
9464 'extractingPdfs' => __('extracting %d PDF(s)...', 'mxchat'),
9465 'pdfExtractedSuffix' => __(' (%d PDF(s) extracted)', 'mxchat')
9466 )
9467 ));
9468
9469 wp_localize_script('mxchat-knowledge-processing', 'mxchatAdmin', array(
9470 'ajax_url' => admin_url('admin-ajax.php'),
9471 'status_nonce' => wp_create_nonce('mxchat_status_nonce'),
9472 'queue_nonce' => wp_create_nonce('mxchat_queue_nonce'),
9473 'stop_nonce' => wp_create_nonce('mxchat_stop_processing_action'),
9474 'settings_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
9475 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
9476 'entries_nonce' => wp_create_nonce('mxchat_entries_nonce'),
9477 'admin_url' => admin_url(),
9478 'ajaxurl' => admin_url('admin-ajax.php'),
9479 'status_refresh_interval' => 2000,
9480 'bot_id' => isset($_GET['bot_id']) ? sanitize_text_field($_GET['bot_id']) : 'default'
9481 ));
9482 break;
9483
9484 case 'mxchat-activation':
9485 wp_localize_script('mxchat-activation-js', 'mxchatAdmin', array(
9486 'ajax_url' => admin_url('admin-ajax.php'),
9487 'license_nonce' => wp_create_nonce('mxchat_activate_license_nonce')
9488 ));
9489 break;
9490
9491 case 'mxchat-content':
9492 wp_localize_script('mxchat-content-js', 'mxchatContent', array(
9493 'ajaxUrl' => admin_url('admin-ajax.php'),
9494 'nonce' => wp_create_nonce('mxchat_content_nonce'),
9495 'settingNonce' => wp_create_nonce('mxchat_save_setting_nonce'),
9496 'previewUrl' => home_url('/?p='),
9497 'isActivated' => $this->is_activated(),
9498 'hasAdvancedContent' => apply_filters('mxchat_content_pro_feature', false, 'seo_readability'),
9499 'hasGSC' => apply_filters('mxchat_content_pro_feature', false, 'gsc_integration'),
9500 // Whether Search Console is actually LINKED — distinct from hasGSC
9501 // (add-on active). Reads the same option the add-on's settings
9502 // card keys off; harmless false when the add-on is absent.
9503 'gscLinked' => (bool) get_option('mxchat_gsc_connected', false),
9504 'seoOptimize' => array(
9505 'meta_description' => ($options['seo_optimize_meta_desc'] ?? 'on') === 'on',
9506 'seo_title' => ($options['seo_optimize_seo_title'] ?? 'on') === 'on',
9507 'slug' => ($options['seo_optimize_slug'] ?? 'on') === 'on',
9508 'readability' => ($options['seo_optimize_readability'] ?? 'on') === 'on',
9509 'internal_links' => ($options['seo_optimize_internal_links'] ?? 'on') === 'on',
9510 'img_alt' => ($options['seo_optimize_img_alt'] ?? 'on') === 'on',
9511 'featured_img' => ($options['seo_optimize_featured_img'] ?? 'on') === 'on',
9512 ),
9513 'i18n' => array(
9514 'generating' => __('Generating...', 'mxchat'),
9515 'planning' => __('Planning content structure...', 'mxchat'),
9516 'images' => __('Generating images...', 'mxchat'),
9517 'writing' => __('Writing full content...', 'mxchat'),
9518 'creating' => __('Creating WordPress post...', 'mxchat'),
9519 'done' => __('Content generated successfully!', 'mxchat'),
9520 'error' => __('An error occurred. Please try again.', 'mxchat'),
9521 'editSuccess' => __('Content updated successfully.', 'mxchat'),
9522 'promptEmpty' => __('Please enter a prompt.', 'mxchat'),
9523 )
9524 ));
9525 break;
9526
9527 case 'mxchat-transcripts':
9528 // Get chart data for localization
9529 $chart_data = $this->get_transcripts_chart_data();
9530
9531 wp_localize_script('mxchat-transcripts-js', 'mxchatAdmin', array(
9532 'ajax_url' => admin_url('admin-ajax.php'),
9533 'export_nonce' => wp_create_nonce('mxchat_export_transcripts'),
9534 'delete_nonce' => wp_create_nonce('mxchat_delete_chat_history'),
9535 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
9536 'translate_nonce' => wp_create_nonce('mxchat_translate_messages')
9537 ));
9538
9539 // Localize chart data separately - use array_values to ensure proper JSON array encoding
9540 wp_localize_script('mxchat-transcripts-js', 'mxchatChartData', array(
9541 'labels' => array_values($chart_data['labels']),
9542 'chats' => array_values($chart_data['chats']),
9543 'messages' => array_values($chart_data['messages'])
9544 ));
9545 break;
9546
9547 case 'mxchat-settings':
9548 default:
9549 // Color picker and settings localization
9550 wp_localize_script('mxchat-color-picker', 'mxchatStyleSettings', array(
9551 'ajax_url' => admin_url('admin-ajax.php'),
9552 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
9553 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
9554 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
9555 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
9556 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
9557 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
9558 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
9559 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
9560 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
9561 'icon_color' => $this->options['icon_color'] ?? '#fff',
9562 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
9563 'pre_chat_message' => $this->options['pre_chat_message'] ?? esc_html__('Hey there! Ask me anything!', 'mxchat'),
9564 'rate_limit_message' => $this->options['rate_limit_message'] ?? esc_html__('Rate limit exceeded. Please try again later.', 'mxchat'),
9565 'loops_api_key' => $this->options['loops_api_key'] ?? '',
9566 'loops_mailing_list' => $this->options['loops_mailing_list'] ?? '',
9567 'triggered_phrase_response' => $this->options['triggered_phrase_response'] ?? esc_html__('Would you like to join our mailing list? Please provide your email below.', 'mxchat'),
9568 'email_capture_response' => $this->options['email_capture_response'] ?? esc_html__('Thank you for providing your email! You\'ve been added to our list.', 'mxchat'),
9569 'pdf_intent_trigger_text' => $this->options['pdf_intent_trigger_text'] ?? esc_html__("Please provide the URL to the PDF you'd like to discuss.", 'mxchat'),
9570 'pdf_intent_success_text' => $this->options['pdf_intent_success_text'] ?? esc_html__("I've processed the PDF. What questions do you have about it?", 'mxchat'),
9571 'pdf_intent_error_text' => $this->options['pdf_intent_error_text'] ?? esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat'),
9572 'pdf_max_pages' => $this->options['pdf_max_pages'] ?? 69,
9573 'live_agent_webhook_url' => $this->options['live_agent_webhook_url'] ?? '',
9574 'live_agent_secret_key' => $this->options['live_agent_secret_key'] ?? '',
9575 'live_agent_bot_token' => $this->options['live_agent_bot_token'] ?? '',
9576 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
9577 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
9578 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
9579 'show_pdf_upload_button' => $this->options['show_pdf_upload_button'] ?? 'on',
9580 'show_word_upload_button' => $this->options['show_word_upload_button'] ?? 'on',
9581 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
9582 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
9583 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
9584 ));
9585 break;
9586 }
9587
9588 // Additional localization that was in the original code
9589 wp_localize_script('mxchat-admin-js', 'mxchatPromptsAdmin', array(
9590 'ajax_url' => admin_url('admin-ajax.php'),
9591 'prompts_setting_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
9592 ));
9593 }
9594
9595 public function mxchat_sanitize($input) {
9596 $new_input = array();
9597
9598 if (isset($input['api_key'])) {
9599 $new_input['api_key'] = sanitize_text_field($input['api_key']);
9600 }
9601
9602 if (isset($input['similarity_threshold'])) {
9603 $new_input['similarity_threshold'] = absint($input['similarity_threshold']); // Ensure it's an integer
9604 $new_input['similarity_threshold'] = min(max($new_input['similarity_threshold'], 20), 85); // Enforce range
9605 }
9606
9607 if (isset($input['rag_sources_limit'])) {
9608 $new_input['rag_sources_limit'] = absint($input['rag_sources_limit']); // Ensure it's an integer
9609 $new_input['rag_sources_limit'] = min(max($new_input['rag_sources_limit'], 3), 10); // Enforce range 3-10
9610 }
9611
9612 if (isset($input['rag_chunks_limit'])) {
9613 $new_input['rag_chunks_limit'] = absint($input['rag_chunks_limit']); // Ensure it's an integer
9614 $new_input['rag_chunks_limit'] = min(max($new_input['rag_chunks_limit'], 8), 20); // Enforce range 8-20
9615 }
9616
9617 if (isset($input['xai_api_key'])) {
9618 $new_input['xai_api_key'] = sanitize_text_field($input['xai_api_key']);
9619 }
9620
9621 if (isset($input['claude_api_key'])) {
9622 $new_input['claude_api_key'] = sanitize_text_field($input['claude_api_key']);
9623 }
9624
9625 if (isset($input['enable_streaming_toggle'])) {
9626 $new_input['enable_streaming_toggle'] = ($input['enable_streaming_toggle'] === 'on') ? 'on' : 'off';
9627 } else {
9628 // If checkbox not checked, it won't be in $input, so set to 'off'
9629 $new_input['enable_streaming_toggle'] = 'off';
9630 }
9631
9632 if (isset($input['enable_web_search'])) {
9633 $new_input['enable_web_search'] = ($input['enable_web_search'] === 'on') ? 'on' : 'off';
9634 } else {
9635 // If checkbox not checked, it won't be in $input, so set to 'off'
9636 $new_input['enable_web_search'] = 'off';
9637 }
9638
9639 if (isset($input['deepseek_api_key'])) {
9640 $new_input['deepseek_api_key'] = sanitize_text_field($input['deepseek_api_key']);
9641 }
9642
9643 if (isset($input['gemini_api_key'])) {
9644 $new_input['gemini_api_key'] = sanitize_text_field($input['gemini_api_key']);
9645 }
9646
9647 if (isset($input['enable_woocommerce_integration'])) {
9648 $new_input['enable_woocommerce_integration'] = $input['enable_woocommerce_integration'] === 'on' ? 'on' : 'off';
9649 }
9650
9651 if (isset($input['privacy_toggle'])) {
9652 $new_input['privacy_toggle'] = $input['privacy_toggle'];
9653 }
9654
9655 if (isset($input['complianz_toggle'])) {
9656 $new_input['complianz_toggle'] = $input['complianz_toggle'];
9657 }
9658
9659 // Handle custom privacy text input
9660 if (isset($input['privacy_text'])) {
9661 // Allow basic HTML for links
9662 $new_input['privacy_text'] = wp_kses_post($input['privacy_text']);
9663 }
9664
9665 if (isset($input['system_prompt_instructions'])) {
9666 $new_input['system_prompt_instructions'] = sanitize_textarea_field($input['system_prompt_instructions']);
9667 }
9668
9669 if (isset($input['mxchat_pro_email'])) {
9670 $new_input['mxchat_pro_email'] = sanitize_email($input['mxchat_pro_email']);
9671 }
9672
9673 if (isset($input['mxchat_activation_key'])) {
9674 $new_input['mxchat_activation_key'] = sanitize_text_field($input['mxchat_activation_key']);
9675 }
9676
9677 if (isset($input['append_to_body'])) {
9678 $new_input['append_to_body'] = $input['append_to_body'] === 'on' ? 'on' : 'off';
9679 }
9680
9681 // Post type visibility settings
9682 if (isset($input['post_type_visibility_mode'])) {
9683 $allowed_modes = array('all', 'include', 'exclude');
9684 $new_input['post_type_visibility_mode'] = in_array($input['post_type_visibility_mode'], $allowed_modes)
9685 ? $input['post_type_visibility_mode']
9686 : 'all';
9687 }
9688
9689 if (isset($input['post_type_visibility_list'])) {
9690 if (is_array($input['post_type_visibility_list'])) {
9691 $new_input['post_type_visibility_list'] = array_map('sanitize_key', $input['post_type_visibility_list']);
9692 } else {
9693 $new_input['post_type_visibility_list'] = array();
9694 }
9695 }
9696
9697 if (isset($input['contextual_awareness_toggle'])) {
9698 $new_input['contextual_awareness_toggle'] = $input['contextual_awareness_toggle'] === 'on' ? 'on' : 'off';
9699 }
9700
9701 if (isset($input['citation_links_toggle'])) {
9702 $new_input['citation_links_toggle'] = $input['citation_links_toggle'] === 'on' ? 'on' : 'off';
9703 }
9704
9705 // Satisfaction rating prompt — defaults ON when unchecked (so first save
9706 // doesn't accidentally disable it). The form posts 'on' when checked;
9707 // unchecked checkboxes don't post a value at all, so we infer 'off' only
9708 // when the autosave/PHP request explicitly clears it via empty string.
9709 if (array_key_exists('satisfaction_rating_enabled', $input)) {
9710 $new_input['satisfaction_rating_enabled'] = $input['satisfaction_rating_enabled'] === 'on' ? 'on' : 'off';
9711 }
9712
9713 // Satisfaction rating customization (plan-141a12). Idle is clamped 5-600;
9714 // the 4 text strings are sanitized + capped server-side. Blank values are
9715 // preserved so the integrator falls back to translated defaults.
9716 if (array_key_exists('satisfaction_rating_idle_seconds', $input)) {
9717 $new_input['satisfaction_rating_idle_seconds'] = max(5, min(600, intval($input['satisfaction_rating_idle_seconds'])));
9718 }
9719
9720 // Max input length in characters (plan a3fae2 part C). 0 = unlimited (default,
9721 // preserves current behavior). Clamp to a sane ceiling so a typo can't lock out
9722 // all input. REQUIRED here — mxchat_sanitize() rebuilds the array from a whitelist,
9723 // so without this isset-handler the key is stripped on the next save of ANY field
9724 // (the 2c02ea global-rate-limit footgun).
9725 if (array_key_exists('max_input_length', $input)) {
9726 $new_input['max_input_length'] = max(0, min(100000, intval($input['max_input_length'])));
9727 }
9728 if (array_key_exists('satisfaction_rating_question', $input)) {
9729 $new_input['satisfaction_rating_question'] = mb_substr(sanitize_text_field($input['satisfaction_rating_question']), 0, 200);
9730 }
9731 if (array_key_exists('satisfaction_rating_thanks', $input)) {
9732 $new_input['satisfaction_rating_thanks'] = mb_substr(sanitize_text_field($input['satisfaction_rating_thanks']), 0, 300);
9733 }
9734 if (array_key_exists('satisfaction_rating_placeholder', $input)) {
9735 $new_input['satisfaction_rating_placeholder'] = mb_substr(sanitize_text_field($input['satisfaction_rating_placeholder']), 0, 200);
9736 }
9737 if (array_key_exists('satisfaction_rating_saved', $input)) {
9738 $new_input['satisfaction_rating_saved'] = mb_substr(sanitize_text_field($input['satisfaction_rating_saved']), 0, 200);
9739 }
9740
9741 if (isset($input['top_bar_title'])) {
9742 $new_input['top_bar_title'] = sanitize_text_field($input['top_bar_title']);
9743 }
9744
9745 if (isset($input['ai_agent_text'])) {
9746 $new_input['ai_agent_text'] = sanitize_text_field($input['ai_agent_text']);
9747 }
9748
9749 if (isset($input['enable_email_block'])) {
9750 $new_input['enable_email_block'] = sanitize_text_field($input['enable_email_block']);
9751 }
9752
9753 if (isset($input['email_blocker_header_content'])) {
9754 // wp_kses_post() allows standard HTML tags permitted by WordPress
9755 $new_input['email_blocker_header_content'] = wp_kses_post($input['email_blocker_header_content']);
9756 }
9757 if (isset($input['email_blocker_button_text'])) {
9758 $new_input['email_blocker_button_text'] = sanitize_text_field($input['email_blocker_button_text']);
9759 }
9760 // Sanitize name field toggle
9761 if (isset($input['enable_name_field'])) {
9762 $new_input['enable_name_field'] = ($input['enable_name_field'] === 'on') ? 'on' : 'off';
9763 } else {
9764 $new_input['enable_name_field'] = 'off';
9765 }
9766 // Sanitize name field placeholder
9767 if (isset($input['name_field_placeholder'])) {
9768 $new_input['name_field_placeholder'] = sanitize_text_field($input['name_field_placeholder']);
9769 }
9770 if (isset($input['intro_message'])) {
9771 $new_input['intro_message'] = wp_kses_post($input['intro_message']); // Use wp_kses_post instead
9772 }
9773
9774 if (isset($input['input_copy'])) {
9775 $new_input['input_copy'] = sanitize_text_field($input['input_copy']);
9776 }
9777
9778 if (isset($input['rate_limit_message'])) {
9779 $new_input['rate_limit_message'] = sanitize_text_field($input['rate_limit_message']);
9780 }
9781
9782 // Handle the new rate limits format
9783 if (isset($input['rate_limits']) && is_array($input['rate_limits'])) {
9784 $new_input['rate_limits'] = array();
9785 $allowed_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited');
9786 $allowed_timeframes = array('hourly', 'daily', 'weekly', 'monthly');
9787
9788 foreach ($input['rate_limits'] as $role_id => $settings) {
9789 $new_input['rate_limits'][$role_id] = array();
9790
9791 // Sanitize limit
9792 if (isset($settings['limit'])) {
9793 $limit = sanitize_text_field($settings['limit']);
9794 // Accept presets, 'unlimited', the '__custom__' sentinel, OR any positive
9795 // integer (custom value) — mirrors the global branch (plan-2c02ea). Without
9796 // the custom path the per-role custom input was dropped and reset to the role
9797 // default on every save (plan-7e23e7).
9798 if (in_array($limit, $allowed_limits, true) || $limit === '__custom__' || (ctype_digit($limit) && (int) $limit >= 1)) {
9799 $new_input['rate_limits'][$role_id]['limit'] = $limit;
9800 } else {
9801 $new_input['rate_limits'][$role_id]['limit'] = ($role_id === 'logged_out') ? '10' : '100'; // Default
9802 }
9803 }
9804
9805 // Preserve the per-role custom value (mirrors the global branch's limit_custom).
9806 if (isset($settings['limit_custom'])) {
9807 $new_input['rate_limits'][$role_id]['limit_custom'] = preg_replace('/[^0-9]/', '', (string) $settings['limit_custom']);
9808 }
9809
9810 // Sanitize timeframe
9811 if (isset($settings['timeframe'])) {
9812 $timeframe = sanitize_text_field($settings['timeframe']);
9813 if (in_array($timeframe, $allowed_timeframes, true)) {
9814 $new_input['rate_limits'][$role_id]['timeframe'] = $timeframe;
9815 } else {
9816 $new_input['rate_limits'][$role_id]['timeframe'] = 'daily'; // Default
9817 }
9818 }
9819
9820 // Sanitize message
9821 if (isset($settings['message'])) {
9822 $new_input['rate_limits'][$role_id]['message'] = sanitize_textarea_field($settings['message']);
9823 }
9824 }
9825 }
9826
9827 // Handle the whole-chatbot global rate limit (plan-mxchat-20260603-2c02ea).
9828 // Mirrors the per-role block above, adapted to the single global shape. Without
9829 // this branch the whitelist-rebuild dropped rate_limits_global entirely on every
9830 // save, so the global cap silently fell back to its 'unlimited' default.
9831 // MUST accept arbitrary positive integers so the custom-value path (d55f65) is not regressed.
9832 if (isset($input['rate_limits_global']) && is_array($input['rate_limits_global'])) {
9833 $g = $input['rate_limits_global'];
9834 $allowed_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited');
9835 $allowed_timeframes = array('hourly', 'daily', 'weekly', 'monthly');
9836 $global_out = array();
9837
9838 if (isset($g['limit'])) {
9839 $limit = sanitize_text_field($g['limit']);
9840 // Accept presets, 'unlimited', the '__custom__' sentinel (resolved by the
9841 // autosave handler / renderer), OR any positive integer (custom value).
9842 if (in_array($limit, $allowed_limits, true) || $limit === '__custom__' || (ctype_digit($limit) && (int) $limit >= 1)) {
9843 $global_out['limit'] = $limit;
9844 } else {
9845 $global_out['limit'] = 'unlimited';
9846 }
9847 }
9848
9849 if (isset($g['limit_custom'])) {
9850 $global_out['limit_custom'] = preg_replace('/[^0-9]/', '', (string) $g['limit_custom']);
9851 }
9852
9853 if (isset($g['timeframe'])) {
9854 $timeframe = sanitize_text_field($g['timeframe']);
9855 $global_out['timeframe'] = in_array($timeframe, $allowed_timeframes, true) ? $timeframe : 'daily';
9856 }
9857
9858 if (isset($g['message'])) {
9859 $global_out['message'] = sanitize_textarea_field($g['message']);
9860 }
9861
9862 $new_input['rate_limits_global'] = $global_out;
9863 }
9864
9865 if (isset($input['pre_chat_message'])) {
9866 $new_input['pre_chat_message'] = sanitize_textarea_field($input['pre_chat_message']);
9867 }
9868
9869 if (isset($input['voyage_api_key'])) {
9870 $new_input['voyage_api_key'] = sanitize_text_field($input['voyage_api_key']);
9871 }
9872
9873 // Add to your sanitize function
9874 if (isset($input['embedding_model'])) {
9875 // Catalog refactor (plan-d14e89): allowlist derived from the canonical
9876 // catalog in includes/class-mxchat-model-catalog.php.
9877 if (!class_exists('MxChat_Model_Catalog')) {
9878 require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
9879 }
9880 $allowed_models = MxChat_Model_Catalog::embedding_model_ids();
9881 if (in_array($input['embedding_model'], $allowed_models)) {
9882 $new_input['embedding_model'] = sanitize_text_field($input['embedding_model']);
9883 }
9884 }
9885
9886 if (isset($input['model'])) {
9887 if ($input['model'] === 'openrouter') {
9888 $new_input['model'] = 'openrouter';
9889 } else {
9890 if (!class_exists('MxChat_Model_Catalog')) {
9891 require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
9892 }
9893 $allowed_models = MxChat_Model_Catalog::chat_model_ids();
9894
9895 if (in_array($input['model'], $allowed_models)) {
9896 $new_input['model'] = sanitize_text_field($input['model']);
9897 } else {
9898 // Fallback for any deprecated model
9899 $new_input['model'] = 'gpt-5.1-chat-latest';
9900 }
9901 }
9902 }
9903
9904 if (isset($input['openrouter_selected_model'])) {
9905 // Just sanitize it, don't validate against a whitelist
9906 $new_input['openrouter_selected_model'] = sanitize_text_field($input['openrouter_selected_model']);
9907 }
9908
9909 // ADD THIS:
9910 if (isset($input['openrouter_selected_model_name'])) {
9911 $new_input['openrouter_selected_model_name'] = sanitize_text_field($input['openrouter_selected_model_name']);
9912 }
9913
9914 if (isset($input['openrouter_api_key'])) {
9915 $new_input['openrouter_api_key'] = sanitize_text_field($input['openrouter_api_key']);
9916 }
9917
9918 // Custom (OpenAI-compatible) Provider — Ollama, LM Studio, vLLM, Azure OpenAI, etc.
9919 if (isset($input['custom_provider_base_url'])) {
9920 $new_input['custom_provider_base_url'] = esc_url_raw(rtrim(trim((string) $input['custom_provider_base_url']), '/'));
9921 }
9922 if (isset($input['custom_provider_api_key'])) {
9923 $new_input['custom_provider_api_key'] = sanitize_text_field($input['custom_provider_api_key']);
9924 }
9925 if (isset($input['custom_provider_model'])) {
9926 $new_input['custom_provider_model'] = sanitize_text_field($input['custom_provider_model']);
9927 }
9928 if (isset($input['custom_provider_auth_scheme'])) {
9929 $scheme = sanitize_text_field($input['custom_provider_auth_scheme']);
9930 $new_input['custom_provider_auth_scheme'] = in_array($scheme, array('bearer', 'api-key'), true) ? $scheme : 'bearer';
9931 }
9932 if (isset($input['custom_provider_for_embeddings'])) {
9933 $new_input['custom_provider_for_embeddings'] = ($input['custom_provider_for_embeddings'] === 'on') ? 'on' : 'off';
9934 }
9935 if (isset($input['custom_provider_for_images'])) {
9936 $new_input['custom_provider_for_images'] = ($input['custom_provider_for_images'] === 'on') ? 'on' : 'off';
9937 }
9938 if (isset($input['custom_provider_embedding_model'])) {
9939 $new_input['custom_provider_embedding_model'] = sanitize_text_field($input['custom_provider_embedding_model']);
9940 }
9941 if (isset($input['custom_provider_api_version'])) {
9942 $new_input['custom_provider_api_version'] = sanitize_text_field($input['custom_provider_api_version']);
9943 }
9944
9945
9946
9947 if (isset($input['woocommerce_consumer_key'])) {
9948 $new_input['woocommerce_consumer_key'] = sanitize_text_field($input['woocommerce_consumer_key']);
9949 }
9950
9951 if (isset($input['woocommerce_consumer_secret'])) {
9952 $new_input['woocommerce_consumer_secret'] = sanitize_text_field($input['woocommerce_consumer_secret']);
9953 }
9954
9955
9956 // Sanitize link_target_toggle
9957 if (isset($input['link_target_toggle'])) {
9958 $new_input['link_target_toggle'] = $input['link_target_toggle'] === 'on' ? 'on' : 'off';
9959 }
9960
9961 // Sanitize Loops API Key
9962 if (isset($input['loops_api_key'])) {
9963 $new_input['loops_api_key'] = sanitize_text_field($input['loops_api_key']);
9964 }
9965
9966 if (isset($input['chat_persistence_toggle'])) {
9967 $new_input['chat_persistence_toggle'] = $input['chat_persistence_toggle'] === 'on' ? 'on' : 'off';
9968 }
9969
9970 // No else clause: an absent key stays absent, so the front-end default ('on') applies
9971 // and the rebuild never strips a saved 'off' (autosave passes the full options array back through here).
9972 if (isset($input['print_button_enabled'])) {
9973 $new_input['print_button_enabled'] = $input['print_button_enabled'] === 'on' ? 'on' : 'off';
9974 }
9975
9976 // plan ac2e81 — "Start new chat" toggle (default OFF) + editable label.
9977 // No else on the toggle: an absent key stays absent so the front-end '?? off'
9978 // default applies, and the full-array autosave rebuild preserves a saved value.
9979 if (isset($input['reset_chat_enabled'])) {
9980 $new_input['reset_chat_enabled'] = $input['reset_chat_enabled'] === 'on' ? 'on' : 'off';
9981 }
9982
9983 if (isset($input['reset_chat_label'])) {
9984 $new_input['reset_chat_label'] = sanitize_text_field($input['reset_chat_label']);
9985 }
9986
9987 if (isset($input['popular_question_1'])) {
9988 $new_input['popular_question_1'] = sanitize_text_field($input['popular_question_1']);
9989 }
9990
9991 if (isset($input['popular_question_2'])) {
9992 $new_input['popular_question_2'] = sanitize_text_field($input['popular_question_2']);
9993 }
9994
9995 if (isset($input['popular_question_3'])) {
9996 $new_input['popular_question_3'] = sanitize_text_field($input['popular_question_3']);
9997 }
9998
9999 if (isset($input['additional_popular_questions']) && is_array($input['additional_popular_questions'])) {
10000 $new_input['additional_popular_questions'] = array_map('sanitize_text_field', $input['additional_popular_questions']);
10001 }
10002
10003 // Sanitize Loops Mailing List
10004 if (isset($input['loops_mailing_list'])) {
10005 $new_input['loops_mailing_list'] = sanitize_text_field($input['loops_mailing_list']);
10006 }
10007
10008 // Sanitize Triggered Phrase Response
10009 if (isset($input['triggered_phrase_response'])) {
10010 $new_input['triggered_phrase_response'] = wp_kses_post($input['triggered_phrase_response']);
10011 }
10012 if (isset($input['email_capture_response'])) {
10013 $new_input['email_capture_response'] = wp_kses_post($input['email_capture_response']);
10014 }
10015
10016 // Sanitize Brave Search Settings
10017 if (isset($input['brave_api_key'])) {
10018 $new_input['brave_api_key'] = sanitize_text_field($input['brave_api_key']);
10019 }
10020
10021 if (isset($input['brave_image_count'])) {
10022 $image_count = intval($input['brave_image_count']);
10023 $new_input['brave_image_count'] = ($image_count >=1 && $image_count <=6) ? $image_count : 4;
10024 }
10025
10026 if (isset($input['brave_safe_search'])) {
10027 $allowed = array('strict', 'off');
10028 $new_input['brave_safe_search'] = in_array($input['brave_safe_search'], $allowed, true) ? $input['brave_safe_search'] : 'strict';
10029 }
10030
10031 if (isset($input['brave_news_count'])) {
10032 $news_count = intval($input['brave_news_count']);
10033 $new_input['brave_news_count'] = ($news_count >=1 && $news_count <=10) ? $news_count : 3;
10034 }
10035
10036 if (isset($input['brave_country'])) {
10037 $new_input['brave_country'] = sanitize_text_field($input['brave_country']);
10038 }
10039
10040 if (isset($input['brave_language'])) {
10041 $new_input['brave_language'] = sanitize_text_field($input['brave_language']);
10042 }
10043
10044 if (isset($input['chat_toolbar_toggle'])) {
10045 $new_input['chat_toolbar_toggle'] = $input['chat_toolbar_toggle'] === 'on' ? 'on' : 'off';
10046 }
10047
10048 // Sanitize PDF upload button toggle
10049 if (isset($input['show_pdf_upload_button'])) {
10050 $new_input['show_pdf_upload_button'] = $input['show_pdf_upload_button'] === 'on' ? 'on' : 'off';
10051 } else {
10052 $new_input['show_pdf_upload_button'] = 'off'; // If checkbox is unchecked
10053 }
10054
10055 // Sanitize Word upload button toggle
10056 if (isset($input['show_word_upload_button'])) {
10057 $new_input['show_word_upload_button'] = $input['show_word_upload_button'] === 'on' ? 'on' : 'off';
10058 } else {
10059 $new_input['show_word_upload_button'] = 'off'; // If checkbox is unchecked
10060 }
10061
10062 if (isset($input['pdf_intent_trigger_text'])) {
10063 $new_input['pdf_intent_trigger_text'] = sanitize_text_field($input['pdf_intent_trigger_text']);
10064 }
10065
10066 if (isset($input['pdf_intent_success_text'])) {
10067 $new_input['pdf_intent_success_text'] = sanitize_text_field($input['pdf_intent_success_text']);
10068 }
10069
10070 if (isset($input['pdf_intent_error_text'])) {
10071 $new_input['pdf_intent_error_text'] = sanitize_text_field($input['pdf_intent_error_text']);
10072 }
10073
10074 if (isset($input['pdf_max_pages'])) {
10075 $new_input['pdf_max_pages'] = intval($input['pdf_max_pages']);
10076 if ($new_input['pdf_max_pages'] < 1 || $new_input['pdf_max_pages'] > 69) {
10077 $new_input['pdf_max_pages'] = 69; // Default to 69 if out of range
10078 }
10079 }
10080
10081 if (isset($input['live_agent_webhook_url'])) {
10082 $new_input['live_agent_webhook_url'] = esc_url_raw($input['live_agent_webhook_url']);
10083 }
10084 if (isset($input['live_agent_secret_key'])) {
10085 $new_input['live_agent_secret_key'] = sanitize_text_field($input['live_agent_secret_key']);
10086 }
10087
10088 // Live Agent Integration
10089 if (isset($input['live_agent_bot_token'])) {
10090 $new_input['live_agent_bot_token'] = sanitize_text_field($input['live_agent_bot_token']);
10091 }
10092
10093 if (isset($input['live_agent_shared_channel'])) {
10094 $new_input['live_agent_shared_channel'] = sanitize_text_field($input['live_agent_shared_channel']);
10095 }
10096
10097 // Default-OFF toggle: absent key stays absent (unchecked box = off).
10098 if (isset($input['live_agent_archive_on_end_toggle'])) {
10099 $new_input['live_agent_archive_on_end_toggle'] = ($input['live_agent_archive_on_end_toggle'] === 'on') ? 'on' : 'off';
10100 }
10101
10102 if (isset($input['live_agent_user_ids'])) {
10103 $new_input['live_agent_user_ids'] = sanitize_textarea_field($input['live_agent_user_ids']);
10104 }
10105
10106 if (isset($input['live_agent_status'])) {
10107 $new_input['live_agent_status'] = ($input['live_agent_status'] === 'on') ? 'on' : 'off';
10108 }
10109 if (isset($input['live_agent_away_message'])) {
10110 $new_input['live_agent_away_message'] = sanitize_textarea_field($input['live_agent_away_message']);
10111 }
10112 if (isset($input['live_agent_notification_message'])) {
10113 $new_input['live_agent_notification_message'] = sanitize_textarea_field($input['live_agent_notification_message']);
10114 }
10115
10116 // Telegram Integration
10117 if (isset($input['telegram_status'])) {
10118 $new_input['telegram_status'] = ($input['telegram_status'] === 'on') ? 'on' : 'off';
10119 }
10120 if (isset($input['telegram_bot_token'])) {
10121 $new_input['telegram_bot_token'] = sanitize_text_field($input['telegram_bot_token']);
10122 }
10123 if (isset($input['telegram_group_id'])) {
10124 $new_input['telegram_group_id'] = sanitize_text_field($input['telegram_group_id']);
10125 }
10126 if (isset($input['telegram_webhook_secret'])) {
10127 $new_input['telegram_webhook_secret'] = sanitize_text_field($input['telegram_webhook_secret']);
10128 }
10129 if (isset($input['telegram_notification_message'])) {
10130 $new_input['telegram_notification_message'] = sanitize_textarea_field($input['telegram_notification_message']);
10131 }
10132 if (isset($input['telegram_away_message'])) {
10133 $new_input['telegram_away_message'] = sanitize_textarea_field($input['telegram_away_message']);
10134 }
10135
10136 // Sanitize script loading strategy
10137 if (isset($input['script_loading_strategy'])) {
10138 $allowed_strategies = array('default', 'defer', 'delay_1s', 'delay_3s', 'delay_5s', 'on_interaction');
10139 $new_input['script_loading_strategy'] = in_array($input['script_loading_strategy'], $allowed_strategies)
10140 ? $input['script_loading_strategy']
10141 : 'default';
10142 }
10143
10144 // Sanitize debug mode
10145 if (isset($input['debug_mode'])) {
10146 $new_input['debug_mode'] = ($input['debug_mode'] === 'on') ? 'on' : 'off';
10147 }
10148
10149 // Content Generator Settings
10150 if (isset($input['content_model'])) {
10151 $new_input['content_model'] = sanitize_text_field($input['content_model']);
10152 }
10153 if (isset($input['content_image_model'])) {
10154 $new_input['content_image_model'] = sanitize_text_field($input['content_image_model']);
10155 }
10156 if (isset($input['content_image_quality'])) {
10157 $q = sanitize_text_field($input['content_image_quality']);
10158 $new_input['content_image_quality'] = in_array($q, array('auto', 'low', 'medium', 'high'), true) ? $q : 'auto';
10159 }
10160 if (isset($input['content_enable_images'])) {
10161 $new_input['content_enable_images'] = ($input['content_enable_images'] === 'on') ? 'on' : 'off';
10162 }
10163 if (isset($input['content_use_placeholders'])) {
10164 $new_input['content_use_placeholders'] = ($input['content_use_placeholders'] === 'on') ? 'on' : 'off';
10165 }
10166 if (isset($input['content_internal_linking'])) {
10167 $new_input['content_internal_linking'] = ($input['content_internal_linking'] === 'on') ? 'on' : 'off';
10168 }
10169 if (isset($input['content_tool_use'])) {
10170 $new_input['content_tool_use'] = ($input['content_tool_use'] === 'on') ? 'on' : 'off';
10171 }
10172 if (isset($input['content_image_count'])) {
10173 $new_input['content_image_count'] = (string) max(1, min(5, (int) $input['content_image_count']));
10174 }
10175
10176 // SEO Optimize toggle fields
10177 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) {
10178 if (isset($input[$seo_key])) {
10179 $new_input[$seo_key] = ($input[$seo_key] === 'on') ? 'on' : 'off';
10180 }
10181 }
10182
10183 // Preserve content generator settings when saving from main settings page
10184 // (where content fields are not in the form submission)
10185 $existing = get_option('mxchat_options', array());
10186 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) {
10187 if (!isset($new_input[$key]) && isset($existing[$key])) {
10188 $new_input[$key] = $existing[$key];
10189 }
10190 }
10191
10192 return $new_input;
10193 }
10194
10195 /**
10196 * Log a debug message if debug mode is enabled
10197 *
10198 * @param string $type The type of log entry (settings_save, api_error, activation, etc.)
10199 * @param string $message The log message
10200 * @param array $data Optional additional data to log
10201 * @return bool Whether the message was logged
10202 */
10203 public static function mxchat_log_debug( $type, $message, $data = array() ) {
10204 $options = get_option( 'mxchat_options', array() );
10205
10206 // Check if debug mode is enabled
10207 if ( ! isset( $options['debug_mode'] ) || $options['debug_mode'] !== 'on' ) {
10208 return false;
10209 }
10210
10211 // Get current log
10212 $log = get_option( 'mxchat_debug_log', array() );
10213 if ( ! is_array( $log ) ) {
10214 $log = array();
10215 }
10216
10217 // Add new entry
10218 $entry = array(
10219 'time' => current_time( 'Y-m-d H:i:s' ),
10220 'type' => sanitize_key( $type ),
10221 'message' => sanitize_text_field( $message ),
10222 );
10223
10224 if ( ! empty( $data ) ) {
10225 $entry['data'] = $data;
10226 }
10227
10228 // Add to beginning of array (newest first)
10229 array_unshift( $log, $entry );
10230
10231 // Keep only last 100 entries
10232 if ( count( $log ) > 100 ) {
10233 $log = array_slice( $log, 0, 100 );
10234 }
10235
10236 // Save log
10237 update_option( 'mxchat_debug_log', $log, false );
10238
10239 return true;
10240 }
10241
10242 /**
10243 * Get the debug log entries
10244 *
10245 * @return array The debug log entries
10246 */
10247 public static function mxchat_get_debug_log() {
10248 $log = get_option( 'mxchat_debug_log', array() );
10249 return is_array( $log ) ? $log : array();
10250 }
10251
10252 /**
10253 * Clear the debug log
10254 *
10255 * @return bool Whether the log was cleared
10256 */
10257 public static function mxchat_clear_debug_log() {
10258 return delete_option( 'mxchat_debug_log' );
10259 }
10260
10261 /**
10262 * Export settings as JSON with masked API keys
10263 *
10264 * @return array The sanitized settings array
10265 */
10266 public static function mxchat_export_settings() {
10267 $options = get_option( 'mxchat_options', array() );
10268
10269 if ( ! is_array( $options ) ) {
10270 return array();
10271 }
10272
10273 // List of API key fields to mask
10274 $api_key_fields = array(
10275 'api_key',
10276 'xai_api_key',
10277 'claude_api_key',
10278 'deepseek_api_key',
10279 'voyage_api_key',
10280 'gemini_api_key',
10281 'openrouter_api_key',
10282 'loops_api_key',
10283 'brave_api_key',
10284 'live_agent_secret_key',
10285 'live_agent_bot_token',
10286 'telegram_bot_token',
10287 'telegram_webhook_secret',
10288 'woocommerce_consumer_key',
10289 'woocommerce_consumer_secret',
10290 );
10291
10292 // Mask API keys (show only last 4 characters)
10293 foreach ( $api_key_fields as $field ) {
10294 if ( isset( $options[ $field ] ) && ! empty( $options[ $field ] ) ) {
10295 $value = $options[ $field ];
10296 if ( strlen( $value ) > 4 ) {
10297 $options[ $field ] = str_repeat( '*', strlen( $value ) - 4 ) . substr( $value, -4 );
10298 } else {
10299 $options[ $field ] = '****';
10300 }
10301 }
10302 }
10303
10304 // Add metadata
10305 $export = array(
10306 'plugin_version' => defined( 'MXCHAT_VERSION' ) ? MXCHAT_VERSION : 'unknown',
10307 'export_date' => current_time( 'Y-m-d H:i:s' ),
10308 'wordpress_version' => get_bloginfo( 'version' ),
10309 'php_version' => phpversion(),
10310 'settings' => $options,
10311 );
10312
10313 return $export;
10314 }
10315
10316 /**
10317 * Reset all settings to defaults
10318 *
10319 * @return bool Whether the reset was successful
10320 */
10321 public static function mxchat_reset_all_settings() {
10322 // Delete main options
10323 $deleted = delete_option( 'mxchat_options' );
10324
10325 // Also clear the debug log
10326 delete_option( 'mxchat_debug_log' );
10327
10328 // Log the reset (will create new log since we just cleared it)
10329 // We need to temporarily enable debug mode to log this
10330 $temp_options = array( 'debug_mode' => 'on' );
10331 update_option( 'mxchat_options', $temp_options );
10332
10333 self::mxchat_log_debug( 'reset', 'All settings have been reset to defaults' );
10334
10335 // Now delete again to trigger re-initialization
10336 delete_option( 'mxchat_options' );
10337
10338 return $deleted;
10339 }
10340
10341 // Method to append the chatbot to the body
10342 public function mxchat_append_chatbot_to_body() {
10343 $options = get_option('mxchat_options');
10344 if (isset($options['append_to_body']) && $options['append_to_body'] === 'on') {
10345 echo do_shortcode('[mxchat_chatbot floating="yes"]');
10346 }
10347 }
10348
10349
10350
10351
10352
10353 private function mxchat_fetch_loops_mailing_lists($api_key) {
10354 $url = 'https://app.loops.so/api/v1/lists';
10355 $response = wp_remote_get($url, array(
10356 'headers' => array(
10357 'Authorization' => 'Bearer ' . $api_key,
10358 'Content-Type' => 'application/json'
10359 )
10360 ));
10361
10362 if (is_wp_error($response)) {
10363 return array();
10364 }
10365
10366 $body = wp_remote_retrieve_body($response);
10367 $lists = json_decode($body, true);
10368
10369 return isset($lists) && is_array($lists) ? $lists : array();
10370 }
10371
10372 function mxchat_calculate_cosine_similarity($vec1, $vec2) {
10373 if (empty($vec1) || empty($vec2)) {
10374 return 0.0;
10375 }
10376
10377 $dot_product = 0.0;
10378 $norm_a = 0.0;
10379 $norm_b = 0.0;
10380
10381 for ($i = 0; $i < count($vec1); $i++) {
10382 $dot_product += $vec1[$i] * $vec2[$i];
10383 $norm_a += pow($vec1[$i], 2);
10384 $norm_b += pow($vec2[$i], 2);
10385 }
10386
10387 if ($norm_a == 0.0 || $norm_b == 0.0) {
10388 return 0.0;
10389 } else {
10390 return $dot_product / (sqrt($norm_a) * sqrt($norm_b));
10391 }
10392 }
10393
10394 /**
10395 * Validates nonce and deletes all chat prompts
10396 */
10397 public function mxchat_handle_delete_all_prompts() {
10398 //error_log('=== DELETE ALL DEBUG START ===');
10399
10400 // Verify nonce
10401 if (!isset($_POST['mxchat_delete_all_prompts_nonce']) || !wp_verify_nonce($_POST['mxchat_delete_all_prompts_nonce'], 'mxchat_delete_all_prompts_action')) {
10402 //error_log('DEBUG: Nonce verification failed');
10403 wp_die(__('Nonce verification failed.', 'mxchat'));
10404 }
10405
10406 // Check permissions
10407 if (!current_user_can('manage_options')) {
10408 //error_log('DEBUG: Permission check failed');
10409 wp_die(__('You do not have sufficient permissions to delete all prompts.', 'mxchat'));
10410 }
10411
10412 // Get bot_id and content type filter from POST data
10413 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
10414 $content_type_filter = isset($_POST['content_type_filter']) ? sanitize_text_field($_POST['content_type_filter']) : '';
10415
10416 $success = true;
10417 $error_messages = array();
10418
10419 // Get bot-specific Pinecone configuration
10420 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
10421 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
10422
10423 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
10424
10425 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
10426 // Delete from Pinecone (with optional content type filter)
10427 $result = $pinecone_manager->mxchat_delete_all_from_pinecone($pinecone_options, $content_type_filter);
10428
10429 if (!$result['success']) {
10430 $success = false;
10431 $error_messages[] = $result['message'];
10432 }
10433
10434 } else {
10435 // Delete from WordPress database
10436 global $wpdb;
10437 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
10438
10439 // Build WHERE conditions
10440 $where_clauses = array();
10441 $where_values = array();
10442
10443 // Bot filter
10444 if ($bot_id !== 'default' && class_exists('MxChat_Multi_Bot_Manager')) {
10445 $where_clauses[] = 'bot_id = %s';
10446 $where_values[] = $bot_id;
10447 }
10448
10449 // Content type filter
10450 if (!empty($content_type_filter)) {
10451 $where_clauses[] = 'content_type = %s';
10452 $where_values[] = $content_type_filter;
10453 }
10454
10455 if (!empty($where_clauses)) {
10456 $where_sql = implode(' AND ', $where_clauses);
10457 $result = $wpdb->query($wpdb->prepare("DELETE FROM {$table_name} WHERE {$where_sql}", $where_values));
10458 } else {
10459 // No filters — delete all
10460 $result = $wpdb->query("DELETE FROM {$table_name}");
10461 }
10462
10463 if ($result === false) {
10464 $success = false;
10465 $error_messages[] = 'Failed to delete from WordPress database';
10466 }
10467 }
10468
10469 // Redirect back with a success message and bot_id
10470 $redirect_url = add_query_arg(array(
10471 'page' => 'mxchat-prompts',
10472 'bot_id' => $bot_id,
10473 'all_deleted' => $success ? 'true' : 'false'
10474 ), admin_url('admin.php'));
10475
10476 wp_safe_redirect($redirect_url);
10477 exit;
10478 }
10479
10480
10481
10482 /**
10483 * Handles deletion prompt with nonce validation
10484 */
10485 public function mxchat_handle_delete_prompt() {
10486 // Sanitize and validate nonce
10487 $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : '';
10488 if (empty($nonce) || !wp_verify_nonce($nonce, 'mxchat_delete_prompt_nonce')) {
10489 wp_die(esc_html__('Nonce verification failed.', 'mxchat'));
10490 }
10491
10492 // Check permissions
10493 if (!current_user_can('manage_options')) {
10494 wp_die(esc_html__('You do not have sufficient permissions to delete prompts.', 'mxchat'));
10495 }
10496
10497 // Get ID and source parameters
10498 $id = isset($_GET['id']) ? sanitize_text_field($_GET['id']) : '';
10499 $source = isset($_GET['source']) ? sanitize_text_field($_GET['source']) : '';
10500
10501 if (empty($id)) {
10502 wp_die(esc_html__('Invalid prompt ID.', 'mxchat'));
10503 }
10504
10505 $success = false;
10506 $error_message = '';
10507
10508 // Check if Pinecone is enabled and determine source automatically if not specified
10509 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
10510 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
10511
10512 // If source is not specified, determine based on Pinecone configuration
10513 if (empty($source)) {
10514 $source = $use_pinecone ? 'pinecone' : 'wordpress';
10515 }
10516
10517 if ($source === 'pinecone' || $use_pinecone) {
10518 //error_log('[MXCHAT-DELETE] Deleting from Pinecone, ID: ' . $id);
10519
10520 // Handle Pinecone deletion
10521 if (empty($pinecone_options['mxchat_pinecone_host']) ||
10522 empty($pinecone_options['mxchat_pinecone_api_key'])) {
10523 wp_die(esc_html__('Pinecone configuration is missing.', 'mxchat'));
10524 }
10525
10526 // Delete from Pinecone using the vector ID directly
10527 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
10528 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
10529 $id,
10530 $pinecone_options['mxchat_pinecone_api_key'],
10531 $pinecone_options['mxchat_pinecone_host']
10532 );
10533 if ($result['success']) {
10534 $success = true;
10535
10536 // Remove from vector cache
10537 $pinecone_manager->mxchat_remove_from_pinecone_vector_cache($id);
10538 $pinecone_manager->mxchat_remove_from_processed_content_caches($id);
10539
10540 set_transient('mxchat_admin_notice_success',
10541 esc_html__('Vector deleted successfully from Pinecone.', 'mxchat'), 30);
10542 } else {
10543 $error_message = $result['message'];
10544 set_transient('mxchat_admin_notice_error',
10545 esc_html__('Failed to delete from Pinecone: ', 'mxchat') . esc_html($error_message), 30);
10546 }
10547 } else {
10548 //error_log('[MXCHAT-DELETE] Deleting from WordPress database, ID: ' . $id);
10549
10550 // Handle WordPress database deletion
10551 global $wpdb;
10552 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
10553
10554 // Clear cache and delete prompt
10555 wp_cache_delete('prompt_' . $id, 'mxchat_prompts');
10556
10557 $result = $wpdb->delete(
10558 $table_name,
10559 array('id' => intval($id)),
10560 array('%d')
10561 );
10562
10563 if ($result !== false) {
10564 $success = true;
10565 set_transient('mxchat_admin_notice_success',
10566 esc_html__('Entry deleted successfully.', 'mxchat'), 30);
10567 } else {
10568 set_transient('mxchat_admin_notice_error',
10569 esc_html__('Failed to delete entry from database.', 'mxchat'), 30);
10570 }
10571 }
10572
10573 // Redirect back to the prompts page
10574 wp_safe_redirect(add_query_arg(
10575 array(
10576 'page' => 'mxchat-prompts',
10577 'deleted' => $success ? 'true' : 'false'
10578 ),
10579 admin_url('admin.php')
10580 ));
10581 exit;
10582 }
10583
10584
10585
10586
10587 /**
10588 * Averages multiple vectors into a single vector
10589 */
10590 private function mxchat_average_vectors($vectors) {
10591 $vector_length = count($vectors[0]);
10592 $sum_vector = array_fill(0, $vector_length, 0);
10593
10594 foreach ($vectors as $vector) {
10595 for ($i = 0; $i < $vector_length; $i++) {
10596 $sum_vector[$i] += $vector[$i];
10597 }
10598 }
10599
10600 // Divide each component by the number of vectors to get the average
10601 $num_vectors = count($vectors);
10602 for ($i = 0; $i < $vector_length; $i++) {
10603 $sum_vector[$i] /= $num_vectors;
10604 }
10605
10606 return $sum_vector;
10607 }
10608
10609
10610
10611
10612 /**
10613 * Generates embeddings from input text for MXChat
10614 */
10615 public function mxchat_generate_embedding($text) {
10616 // Enable detailed logging for debugging
10617 //error_log('[MXCHAT-EMBED] Starting embedding generation. Text length: ' . strlen($text) . ' bytes');
10618 //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
10619
10620 $options = get_option('mxchat_options');
10621 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
10622 //error_log('[MXCHAT-EMBED] Selected embedding model: ' . $selected_model);
10623
10624 // Determine provider and endpoint
10625 if (strpos($selected_model, 'voyage') === 0) {
10626 $api_key = $options['voyage_api_key'] ?? '';
10627 $endpoint = 'https://api.voyageai.com/v1/embeddings';
10628 $provider_name = 'Voyage AI';
10629 //error_log('[MXCHAT-EMBED] Using Voyage AI API');
10630 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
10631 $api_key = $options['gemini_api_key'] ?? '';
10632 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
10633 $provider_name = 'Google Gemini';
10634 //error_log('[MXCHAT-EMBED] Using Google Gemini API');
10635 } else {
10636 $api_key = $options['api_key'] ?? '';
10637 $endpoint = 'https://api.openai.com/v1/embeddings';
10638 $provider_name = 'OpenAI';
10639 //error_log('[MXCHAT-EMBED] Using OpenAI API');
10640 }
10641
10642 //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
10643
10644 if (empty($api_key)) {
10645 $error_message = sprintf('Missing %s API key. Please configure your API key in the MxChat settings.', $provider_name);
10646 //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
10647 return $error_message;
10648 }
10649
10650 // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
10651 $estimated_tokens = ceil(str_word_count($text) / 0.75);
10652 //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
10653
10654 if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
10655 //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
10656 // Consider truncating text here
10657 }
10658
10659 // Prepare request body based on provider
10660 if (strpos($selected_model, 'gemini-embedding') === 0) {
10661 // Gemini API format
10662 $request_body = array(
10663 'model' => 'models/' . $selected_model,
10664 'content' => array(
10665 'parts' => array(
10666 array('text' => $text)
10667 )
10668 )
10669 );
10670
10671 // Set output dimensionality to 1536 for consistency with other models
10672 $request_body['outputDimensionality'] = 1536;
10673 } else {
10674 // OpenAI/Voyage API format
10675 $request_body = array(
10676 'model' => $selected_model,
10677 'input' => $text
10678 );
10679
10680 // Add output_dimension for voyage-3-large model
10681 if ($selected_model === 'voyage-3-large') {
10682 $request_body['output_dimension'] = 2048;
10683 }
10684 }
10685
10686 //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
10687
10688 // Prepare headers based on provider
10689 if (strpos($selected_model, 'gemini-embedding') === 0) {
10690 // Gemini uses API key as query parameter
10691 $endpoint .= '?key=' . $api_key;
10692 $headers = array(
10693 'Content-Type' => 'application/json'
10694 );
10695 } else {
10696 // OpenAI/Voyage use Bearer token
10697 $headers = array(
10698 'Authorization' => 'Bearer ' . $api_key,
10699 'Content-Type' => 'application/json'
10700 );
10701 }
10702
10703 // Make API request
10704 //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
10705 $response = wp_remote_post($endpoint, array(
10706 'body' => wp_json_encode($request_body),
10707 'headers' => $headers,
10708 'timeout' => 60 // Increased timeout for large inputs
10709 ));
10710
10711 // Handle wp_remote_post errors
10712 if (is_wp_error($response)) {
10713 $error_message = $response->get_error_message();
10714 //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
10715 return 'Connection error: ' . $error_message;
10716 }
10717
10718 // Get and check HTTP response code
10719 $http_code = wp_remote_retrieve_response_code($response);
10720 //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
10721
10722 if ($http_code !== 200) {
10723 $error_body = wp_remote_retrieve_body($response);
10724 //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
10725
10726 // Try to parse error for more details
10727 $error_json = json_decode($error_body, true);
10728 if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
10729 $error_type = $error_json['error']['type'] ?? 'unknown';
10730 $error_message = $error_json['error']['message'] ?? 'No message';
10731 //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
10732 //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
10733
10734 // Customize error message for common API errors
10735 if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
10736 $error_message = sprintf('Invalid %s API key. Please check your API key in the MxChat settings.', $provider_name);
10737 } elseif ($error_type === 'authentication_error') {
10738 $error_message = sprintf('%s authentication failed. Please verify your API key in the MxChat settings.', $provider_name);
10739 }
10740
10741 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
10742 return $error_message;
10743 }
10744
10745 $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding", $http_code);
10746 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
10747 return $error_message;
10748 }
10749
10750 // Parse response body
10751 $response_body = wp_remote_retrieve_body($response);
10752 //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
10753
10754 $response_data = json_decode($response_body, true);
10755
10756 if (json_last_error() !== JSON_ERROR_NONE) {
10757 $error = json_last_error_msg();
10758 //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
10759 //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
10760 return "Failed to parse API response: $error";
10761 }
10762
10763 // Handle different response formats based on provider
10764 if (strpos($selected_model, 'gemini-embedding') === 0) {
10765 // Gemini API response format
10766 if (isset($response_data['embedding']['values'])) {
10767 $embedding_dimensions = count($response_data['embedding']['values']);
10768 //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
10769
10770 // Check if embedding dimensions are as expected (should be 1536)
10771 if ($embedding_dimensions !== 1536) {
10772 //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
10773 }
10774
10775 MxChat_Utils::stamp_active_embedding_model($selected_model);
10776 return $response_data['embedding']['values'];
10777 } else {
10778 //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
10779 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
10780
10781 if (isset($response_data['error'])) {
10782 $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
10783 //error_log('[MXCHAT-EMBED] ' . $error_message);
10784 return $error_message;
10785 }
10786
10787 $error_message = "Invalid Gemini API response format: No embedding found";
10788 //error_log('[MXCHAT-EMBED] ' . $error_message);
10789 return $error_message;
10790 }
10791 } else {
10792 // OpenAI/Voyage API response format
10793 if (isset($response_data['data'][0]['embedding'])) {
10794 $embedding_dimensions = count($response_data['data'][0]['embedding']);
10795 //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
10796
10797 // Check if embedding dimensions are as expected
10798 if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
10799 ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
10800 //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
10801 }
10802
10803 MxChat_Utils::stamp_active_embedding_model($selected_model);
10804 return $response_data['data'][0]['embedding'];
10805 } else {
10806 //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
10807 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
10808
10809 if (isset($response_data['error'])) {
10810 $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
10811 //error_log('[MXCHAT-EMBED] ' . $error_message);
10812 return $error_message;
10813 }
10814
10815 $error_message = "Invalid API response format: No embedding found";
10816 //error_log('[MXCHAT-EMBED] ' . $error_message);
10817 return $error_message;
10818 }
10819 }
10820 }
10821
10822
10823
10824 }
10825 ?>
10826