PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.13
MxChat – AI Chatbot & Content Generation for WordPress v3.2.13
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.13, at includes/class-mxchat-admin.php

10,518 lines 443.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
80 add_action('admin_init', array($this, 'register_pinecone_settings'));
81 add_action('admin_init', array($this, 'register_openai_vectorstore_settings'));
82
83 add_action('admin_notices', array($this, 'display_admin_notices'));
84
85 add_action('wp_ajax_mxchat_test_streaming_actual', [$this, 'mxchat_handle_test_streaming_actual']);
86 add_action('wp_ajax_mxchat_test_streaming', [$this, 'mxchat_handle_test_streaming']); // Keep existing as fallback
87 add_action('wp_ajax_mxchat_test_vectorstore_connection', array($this, 'mxchat_test_vectorstore_connection'));
88
89 add_action('wp_ajax_mxchat_save_selected_bot', array($this, 'mxchat_save_selected_bot'));
90 add_action('wp_ajax_mxchat_fetch_openrouter_models', array($this, 'fetch_openrouter_models'));
91 add_action('wp_ajax_mxchat_get_rag_context', array($this, 'mxchat_get_rag_context'));
92
93 // Actions page AJAX handlers
94 add_action('wp_ajax_mxchat_fetch_actions_list', array($this, 'mxchat_fetch_actions_list'));
95 add_action('wp_ajax_mxchat_toggle_action_status', array($this, 'mxchat_toggle_action_status'));
96 add_action('wp_ajax_mxchat_bulk_delete_actions', array($this, 'mxchat_bulk_delete_actions'));
97 add_action('wp_ajax_mxchat_add_intent_ajax', array($this, 'mxchat_add_intent_ajax'));
98 add_action('wp_ajax_mxchat_edit_intent_ajax', array($this, 'mxchat_edit_intent_ajax'));
99 add_action('wp_ajax_mxchat_delete_intent_ajax', array($this, 'mxchat_delete_intent_ajax'));
100 add_action('wp_ajax_mxchat_add_phrase', array($this, 'mxchat_add_phrase_ajax'));
101 add_action('wp_ajax_mxchat_delete_phrase', array($this, 'mxchat_delete_phrase_ajax'));
102 add_action('wp_ajax_mxchat_get_phrases', array($this, 'mxchat_get_phrases_ajax'));
103 add_action('wp_ajax_mxchat_delete_legacy_phrases', array($this, 'mxchat_delete_legacy_phrases_ajax'));
104
105 // Slack test connection
106 add_action('wp_ajax_mxchat_test_slack_connection', array($this, 'mxchat_test_slack_connection'));
107
108 // Translation handlers
109 add_action('wp_ajax_mxchat_translate_messages', array($this, 'mxchat_translate_messages'));
110 add_action('wp_ajax_mxchat_get_transcript_translation', array($this, 'mxchat_get_transcript_translation'));
111
112 // 3.2.3: Embedding model switch protection
113 add_action('wp_ajax_mxchat_check_embedding_switch', array($this, 'mxchat_check_embedding_switch_ajax'));
114 add_action('wp_ajax_mxchat_dismiss_embedding_mismatch', array($this, 'mxchat_dismiss_embedding_mismatch_ajax'));
115 add_action('admin_notices', array($this, 'mxchat_embedding_mismatch_notice'));
116 }
117
118 /**
119 * 3.2.3: Preflight check before allowing the embedding model dropdown to
120 * switch. Pure option comparison — no counts, no DB queries beyond the
121 * cached options. The dialog is shown whenever the user has previously
122 * embedded with a different model than the one they're switching to.
123 */
124 public function mxchat_check_embedding_switch_ajax() {
125 check_ajax_referer('mxchat_admin_nonce', 'security');
126 if (!current_user_can('manage_options')) {
127 wp_send_json_error(__('Unauthorized', 'mxchat'));
128 }
129
130 $new_model = isset($_POST['new_model']) ? sanitize_text_field(wp_unslash($_POST['new_model'])) : '';
131 $active_model = MxChat_Utils::get_active_embedding_model();
132
133 $is_mismatch = !empty($active_model) && !empty($new_model) && $active_model !== $new_model;
134
135 $active_dims = MxChat_Utils::embedding_model_dimensions($active_model);
136 $new_dims = MxChat_Utils::embedding_model_dimensions($new_model);
137
138 wp_send_json_success(array(
139 'is_mismatch' => $is_mismatch,
140 'active_model' => $active_model,
141 'active_label' => MxChat_Utils::embedding_model_label($active_model),
142 'new_model' => $new_model,
143 'new_label' => MxChat_Utils::embedding_model_label($new_model),
144 'dims_differ' => ($active_dims > 0 && $new_dims > 0 && $active_dims !== $new_dims),
145 'active_dims' => $active_dims,
146 'new_dims' => $new_dims,
147 ));
148 }
149
150 /**
151 * 3.2.3: Dismiss the persistent mismatch banner. Tied to the active+selected
152 * pair so the banner reappears on the next switch event.
153 */
154 public function mxchat_dismiss_embedding_mismatch_ajax() {
155 check_ajax_referer('mxchat_admin_nonce', 'security');
156 if (!current_user_can('manage_options')) {
157 wp_send_json_error(__('Unauthorized', 'mxchat'));
158 }
159
160 $options = get_option('mxchat_options', array());
161 $selected = $options['embedding_model'] ?? '';
162 $active = MxChat_Utils::get_active_embedding_model();
163 update_option('mxchat_dismissed_embedding_mismatch', $active . '|' . $selected, false);
164 wp_send_json_success();
165 }
166
167 /**
168 * 3.2.3: Persistent admin banner shown whenever the active embedding model
169 * (last used to actually embed something) differs from the currently
170 * selected model. Pure option comparison — no DB queries on every page
171 * load. The banner auto-clears once both match again, i.e. after a delete
172 * + re-embed cycle.
173 */
174 public function mxchat_embedding_mismatch_notice() {
175 if (!current_user_can('manage_options')) {
176 return;
177 }
178
179 $options = get_option('mxchat_options', array());
180 $selected = $options['embedding_model'] ?? '';
181 $active = MxChat_Utils::get_active_embedding_model();
182
183 if (empty($active) || empty($selected) || $active === $selected) {
184 return;
185 }
186
187 $dismissed = get_option('mxchat_dismissed_embedding_mismatch', '');
188 if ($dismissed === $active . '|' . $selected) {
189 return;
190 }
191
192 $active_label = MxChat_Utils::embedding_model_label($active);
193 $selected_label = MxChat_Utils::embedding_model_label($selected);
194 $active_dims = MxChat_Utils::embedding_model_dimensions($active);
195 $selected_dims = MxChat_Utils::embedding_model_dimensions($selected);
196 $dims_differ = ($active_dims > 0 && $selected_dims > 0 && $active_dims !== $selected_dims);
197
198 $kb_url = admin_url('admin.php?page=mxchat-prompts');
199 $actions_url = admin_url('admin.php?page=mxchat-actions');
200
201 ?>
202 <div class="notice notice-error is-dismissible mxchat-embedding-mismatch-notice"
203 data-active="<?php echo esc_attr($active); ?>"
204 data-selected="<?php echo esc_attr($selected); ?>">
205 <p><strong><?php esc_html_e('MxChat: Embedding model mismatch detected', 'mxchat'); ?></strong></p>
206 <p>
207 <?php
208 printf(
209 /* translators: 1: previously-used model name, 2: currently-selected model name */
210 esc_html__('Your knowledge base and actions were embedded with %1$s, but %2$s is now selected. Similarity matching will return inaccurate or empty results until you delete all existing embeddings and re-embed your content with the new model.', 'mxchat'),
211 '<code>' . esc_html($active_label) . '</code>',
212 '<code>' . esc_html($selected_label) . '</code>'
213 );
214 ?>
215 </p>
216 <?php if ($dims_differ) : ?>
217 <p>
218 <strong><?php esc_html_e('Dimension mismatch:', 'mxchat'); ?></strong>
219 <?php
220 printf(
221 /* translators: 1: old dim count, 2: new dim count */
222 esc_html__('Existing vectors are %1$d-dimensional but the new model produces %2$d-dimensional vectors. If you use Pinecone, your index will reject queries entirely until re-embedded.', 'mxchat'),
223 (int) $active_dims,
224 (int) $selected_dims
225 );
226 ?>
227 </p>
228 <?php endif; ?>
229 <p>
230 <?php esc_html_e('To fix this:', 'mxchat'); ?>
231 <a href="<?php echo esc_url($kb_url); ?>"><?php esc_html_e('Delete all knowledge base entries', 'mxchat'); ?></a> ·
232 <a href="<?php echo esc_url($actions_url); ?>"><?php esc_html_e('Delete all actions', 'mxchat'); ?></a> ·
233 <?php esc_html_e('then re-import / re-add them with the new model selected.', 'mxchat'); ?>
234 </p>
235 </div>
236 <script>
237 (function($){
238 $(document).on('click', '.mxchat-embedding-mismatch-notice .notice-dismiss', function(){
239 $.post(ajaxurl, {
240 action: 'mxchat_dismiss_embedding_mismatch',
241 security: '<?php echo esc_js(wp_create_nonce('mxchat_admin_nonce')); ?>'
242 });
243 });
244 })(jQuery);
245 </script>
246 <?php
247 }
248
249 private function is_license_active() {
250 $license_status = get_option('mxchat_license_status', 'inactive');
251 return ($license_status === 'active');
252 }
253
254 private function initialize_default_options() {
255 $default_options = array(
256 'api_key' => '',
257 'xai_api_key' => '',
258 'claude_api_key' => '',
259 'deepseek_api_key' => '',
260 'voyage_api_key' => '',
261 'gemini_api_key' => '',
262 'enable_streaming_toggle' => 'off',
263 'enable_web_search' => 'off',
264 'embedding_model' => 'text-embedding-ada-002',
265 '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:
266
267 # Response Style - CRITICALLY IMPORTANT
268 - MAXIMUM LENGTH: 1-3 short sentences per response
269 - Ultra-concise: Get straight to the answer with no filler
270 - No introductions like "Sure!" or "I\'d be happy to help"
271 - No phrases like "based on my knowledge" or "according to information"
272 - No explanatory text before giving the answer
273 - No summaries or repetition
274 - Hyperlink all URLs
275 - Respond in user\'s language
276 - Minor chit chat or conversation is okay, but try to keep it focused on [insert topic]
277
278 # Knowledge Base Requirements - PREVENT HALLUCINATIONS
279 - ONLY answer questions using information explicitly provided in OFFICIAL KNOWLEDGE DATABASE CONTENT sections marked with ===== delimiters
280 - If required information is NOT in the knowledge database: "I don\'t have enough information in my knowledge base to answer that question accurately."
281 - NEVER invent or hallucinate URLs, links, product specs, procedures, dates, statistics, names, contacts, or company information
282 - When knowledge base information is unclear or contradictory, acknowledge the limitation rather than guessing
283 - Better to admit insufficient information than provide inaccurate answers',
284 'model' => esc_html__('gpt-5.1-chat-latest', 'mxchat'),
285 'rate_limit_logged_out' => esc_html__('100', 'mxchat'),
286 'role_rate_limits' => array(),
287 'rate_limit_message' => esc_html__('Rate limit exceeded. Please try again later.', 'mxchat'),
288 'enable_email_block' => '',
289 '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'),
290 'email_blocker_button_text' => esc_html__('Start Chat', 'mxchat'),
291 'enable_name_field' => 'off', // NEW
292 'name_field_placeholder' => esc_html__('Enter your name', 'mxchat'), // NEW
293 'top_bar_title' => esc_html__('MxChat', 'mxchat'),
294 'intro_message' => __('Hello! How can I assist you today?', 'mxchat'),
295 'ai_agent_text' => esc_html__('AI Agent', 'mxchat'),
296 'input_copy' => esc_html__('How can I assist?', 'mxchat'),
297 'append_to_body' => esc_html__('off', 'mxchat'),
298 'post_type_visibility_mode' => 'all', // 'all', 'include', 'exclude'
299 'post_type_visibility_list' => array(), // Array of post type slugs
300 'contextual_awareness_toggle' => 'off',
301 'citation_links_toggle' => 'on',
302 'satisfaction_rating_enabled' => 'off',
303 'satisfaction_rating_idle_seconds' => 60,
304 'satisfaction_rating_question' => '',
305 'satisfaction_rating_thanks' => '',
306 'satisfaction_rating_placeholder' => '',
307 'satisfaction_rating_saved' => '',
308 'close_button_color' => esc_html__('#fff', 'mxchat'),
309 'chatbot_bg_color' => esc_html__('#fff', 'mxchat'),
310 'user_message_bg_color' => esc_html__('#fff', 'mxchat'),
311 'user_message_font_color' => esc_html__('#212121', 'mxchat'),
312 'bot_message_bg_color' => esc_html__('#212121', 'mxchat'),
313 'bot_message_font_color' => esc_html__('#fff', 'mxchat'),
314 'top_bar_bg_color' => esc_html__('#212121', 'mxchat'),
315 'send_button_font_color' => esc_html__('#212121', 'mxchat'),
316 'chat_input_font_color' => esc_html__('#212121', 'mxchat'),
317 'chatbot_background_color' => esc_html__('#212121', 'mxchat'),
318 'icon_color' => esc_html__('#fff', 'mxchat'),
319 'enable_woocommerce_integration' => esc_html__('0', 'mxchat'),
320 'link_target_toggle' => esc_html__('off', 'mxchat'),
321 'pre_chat_message' => esc_html__('Hey there! Ask me anything!', 'mxchat'),
322
323 // New fields for Loops Integration
324 'loops_api_key' => '',
325 'loops_mailing_list' => '',
326 'triggered_phrase_response' => __('Would you like to join our mailing list? Please provide your email below.', 'mxchat'),
327 'email_capture_response' => __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat'),
328 'popular_question_1' => '',
329 'popular_question_2' => '',
330 'popular_question_3' => '',
331 'pdf_intent_trigger_text' => __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat'),
332 'pdf_intent_success_text' => __("I've processed the PDF. What questions do you have about it?", 'mxchat'),
333 'pdf_intent_error_text' => __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat'),
334 'pdf_max_pages' => 69,
335 'show_pdf_upload_button' => 'on',
336 'show_word_upload_button' => 'on',
337
338 // Live Agent Integration (Slack)
339 'live_agent_webhook_url' => '',
340 'live_agent_secret_key' => '',
341 'live_agent_bot_token' => '',
342 'live_agent_message_bg_color' => esc_html__('#ffffff', 'mxchat'),
343 'live_agent_message_font_color' => esc_html__('#333333', 'mxchat'),
344
345 // Telegram Integration
346 'telegram_status' => 'off',
347 'telegram_bot_token' => '',
348 'telegram_group_id' => '',
349 'telegram_webhook_secret' => '',
350 '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'),
351 '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'),
352
353 'chat_toolbar_toggle' => esc_html__('off', 'mxchat'),
354 'mode_indicator_bg_color' => esc_html__('#767676', 'mxchat'),
355 'mode_indicator_font_color' => esc_html__('#ffffff', 'mxchat'),
356 'toolbar_icon_color' => esc_html__('#212121', 'mxchat'),
357
358 // Optimization settings
359 'script_loading_strategy' => 'default',
360
361 // Debug settings
362 'debug_mode' => 'off',
363 );
364
365
366 // Merge existing options with defaults
367 $existing_options = get_option('mxchat_options', array());
368 $merged_options = wp_parse_args($existing_options, $default_options);
369
370 // Update the options if they have changed
371 if ($existing_options !== $merged_options) {
372 update_option('mxchat_options', $merged_options);
373 }
374
375 // Add default limits for each role
376 $roles = wp_roles()->get_names();
377 foreach ($roles as $role_id => $role_name) {
378 $default_options['role_rate_limits'][$role_id] = esc_html__('100', 'mxchat');
379 }
380
381 return $default_options;
382
383 // Update the $this->options property
384 $this->options = $merged_options;
385 }
386
387 public function mxchat_add_plugin_page() {
388 // Onboarding lifecycle helpers (admin_init redirect + ajax handlers live in the file).
389 require_once plugin_dir_path(__FILE__) . 'admin-onboarding-page.php';
390
391 // Main menu page — `mxchat-max` remains the parent slug for every MxChat submenu
392 // (Settings, Knowledge, Transcripts, …). Hitting `?page=mxchat-max` directly now
393 // dispatches to the Onboarding page (or Settings if the user has dismissed onboarding).
394 add_menu_page(
395 esc_html__('MxChat', 'mxchat'),
396 esc_html__('MxChat', 'mxchat'),
397 'manage_options',
398 'mxchat-max',
399 array($this, 'mxchat_create_dashboard_page'),
400 'dashicons-testimonial',
401 6
402 );
403
404 // Onboarding submenu — first child under MxChat (plan-d14e89).
405 // First registration uses menu_slug === parent slug 'mxchat-max' →
406 // WP-canonical override of the auto-duplicate "MxChat" entry. Result: the
407 // first child shows as "Onboarding" instead of a redundant pair.
408 add_submenu_page(
409 'mxchat-max',
410 esc_html__('MxChat Onboarding', 'mxchat'),
411 esc_html__('Onboarding', 'mxchat'),
412 'manage_options',
413 'mxchat-max',
414 array($this, 'mxchat_create_dashboard_page')
415 );
416 // Hidden route for `?page=mxchat-onboarding` (Settings "Show again" link
417 // + legacy redirects still target this slug). Parent === null keeps it
418 // out of the menu while remaining accessible by URL.
419 add_submenu_page(
420 null,
421 esc_html__('MxChat Onboarding', 'mxchat'),
422 esc_html__('Onboarding', 'mxchat'),
423 'manage_options',
424 'mxchat-onboarding',
425 array($this, 'mxchat_create_dashboard_page')
426 );
427
428 // Settings submenu — same callback as before, just at a new slug.
429 add_submenu_page(
430 'mxchat-max',
431 esc_html__('MxChat Settings', 'mxchat'),
432 esc_html__('Settings', 'mxchat'),
433 'manage_options',
434 'mxchat-settings',
435 array($this, 'mxchat_create_admin_page')
436 );
437
438 // Submenu page for Knowledge
439 add_submenu_page(
440 'mxchat-max',
441 esc_html__('Prompts', 'mxchat'),
442 esc_html__('Knowledge', 'mxchat'),
443 'manage_options',
444 'mxchat-prompts',
445 array($this, 'mxchat_create_prompts_page')
446 );
447
448 add_submenu_page(
449 'mxchat-max',
450 esc_html__('Chat Transcripts', 'mxchat'),
451 esc_html__('Transcripts', 'mxchat'),
452 'manage_options',
453 'mxchat-transcripts',
454 array($this, 'mxchat_create_transcripts_page')
455 );
456
457 add_submenu_page(
458 'mxchat-max',
459 esc_html__('MxChat Actions', 'mxchat'),
460 esc_html__('Actions', 'mxchat'),
461 'manage_options',
462 'mxchat-actions',
463 array($this, 'mxchat_actions_page_html')
464 );
465
466 // Content Generator page
467 add_submenu_page(
468 'mxchat-max',
469 esc_html__('Content', 'mxchat'),
470 esc_html__('Content', 'mxchat'),
471 'manage_options',
472 'mxchat-content',
473 array($this, 'mxchat_create_content_page')
474 );
475
476 }
477
478 /**
479 * Register the Pro & Extensions submenu on a later admin_menu priority so it
480 * always renders as the bottom-most item in the MxChat sidebar — below
481 * configuration pages like API Access (priority 20).
482 */
483 public function mxchat_add_pro_extensions_page() {
484 add_submenu_page(
485 'mxchat-max',
486 esc_html__('Pro & Extensions', 'mxchat'),
487 esc_html__('Pro & Extensions', 'mxchat'),
488 'manage_options',
489 'mxchat-activation',
490 array($this, 'mxchat_create_activation_page')
491 );
492 }
493
494 public function mxchat_create_addons_page() {
495 require_once plugin_dir_path(__FILE__) . 'class-mxchat-addons.php';
496 $addons_page = new MxChat_Addons();
497 $addons_page->render_page();
498 }
499
500 /**
501 * Render the Content Generator admin page
502 */
503 public function mxchat_create_content_page() {
504 require_once plugin_dir_path(__FILE__) . 'admin-content-page.php';
505 mxchat_render_content_page($this);
506 }
507
508 /**
509 * Test actual streaming functionality in the WordPress environment
510 */
511 public function mxchat_handle_test_streaming_actual() {
512 check_ajax_referer('mxchat_test_streaming_nonce', 'nonce');
513
514 // Check if headers have already been sent
515 if (headers_sent()) {
516 wp_send_json_error(['message' => 'Headers already sent - streaming not possible']);
517 return;
518 }
519
520 // Check for required functions
521 if (!function_exists('curl_init')) {
522 wp_send_json_error(['message' => 'cURL not available - streaming requires cURL']);
523 return;
524 }
525
526 // Get user's selected model and API key
527 $options = get_option('mxchat_options', []);
528 $selected_model = $options['model'] ?? 'gpt-5.1-chat-latest';
529
530 // Get the provider from the model
531 $model_parts = explode('-', $selected_model);
532 $provider = strtolower($model_parts[0]);
533
534 // Get the appropriate API key
535 $api_key = '';
536 switch ($provider) {
537 case 'gpt':
538 case 'o1':
539 $api_key = $options['api_key'] ?? '';
540 break;
541 case 'claude':
542 $api_key = $options['claude_api_key'] ?? '';
543 break;
544 case 'grok':
545 $api_key = $options['xai_api_key'] ?? '';
546 break;
547 case 'deepseek':
548 $api_key = $options['deepseek_api_key'] ?? '';
549 break;
550 case 'gemini':
551 $api_key = $options['gemini_api_key'] ?? '';
552 break;
553 default:
554 // Default to OpenAI for unknown models
555 $api_key = $options['api_key'] ?? '';
556 $provider = 'gpt';
557 break;
558 }
559
560 if (empty($api_key)) {
561 wp_send_json_error(['message' => "API key not configured for {$provider} provider"]);
562 return;
563 }
564
565 // Test streaming with the selected model and provider
566 try {
567 $this->perform_streaming_test($provider, $selected_model, $api_key);
568 } catch (Exception $e) {
569 wp_send_json_error(['message' => 'Streaming test exception: ' . $e->getMessage()]);
570 }
571 }
572
573 /**
574 * Perform the actual streaming test
575 */
576 private function perform_streaming_test($provider, $model, $api_key) {
577 // Set streaming headers
578 header('Content-Type: text/event-stream');
579 header('Cache-Control: no-cache');
580 header('X-Accel-Buffering: no'); // Disable nginx buffering
581
582 // Prepare test message
583 $test_message = "Please respond with exactly: 'Streaming test successful!' - send this as a short response for testing.";
584
585 // Configure API request based on provider
586 $url = '';
587 $headers = [];
588 $body = [];
589
590 switch ($provider) {
591 case 'gpt':
592 case 'o1':
593 $url = 'https://api.openai.com/v1/chat/completions';
594 $headers = [
595 'Content-Type: application/json',
596 'Authorization: Bearer ' . $api_key
597 ];
598 $body = [
599 'model' => $model,
600 'messages' => [['role' => 'user', 'content' => $test_message]],
601 'max_tokens' => 50,
602 'temperature' => 0.3,
603 'stream' => true
604 ];
605 break;
606
607 case 'claude':
608 $url = 'https://api.anthropic.com/v1/messages';
609 $headers = [
610 'Content-Type: application/json',
611 'x-api-key: ' . $api_key,
612 'anthropic-version: 2023-06-01'
613 ];
614 $body = [
615 'model' => $model,
616 'messages' => [['role' => 'user', 'content' => $test_message]],
617 'max_tokens' => 50,
618 'temperature' => 0.3,
619 'stream' => true
620 ];
621 break;
622
623 case 'grok':
624 $url = 'https://api.x.ai/v1/chat/completions';
625 $headers = [
626 'Content-Type: application/json',
627 'Authorization: Bearer ' . $api_key
628 ];
629 $body = [
630 'model' => $model,
631 'messages' => [['role' => 'user', 'content' => $test_message]],
632 'max_tokens' => 50,
633 'temperature' => 0.3,
634 'stream' => true
635 ];
636 break;
637
638 case 'deepseek':
639 $url = 'https://api.deepseek.com/v1/chat/completions';
640 $headers = [
641 'Content-Type: application/json',
642 'Authorization: Bearer ' . $api_key
643 ];
644 $body = [
645 'model' => $model,
646 'messages' => [['role' => 'user', 'content' => $test_message]],
647 'max_tokens' => 50,
648 'temperature' => 0.3,
649 'stream' => true
650 ];
651 break;
652
653 default:
654 echo "data: " . json_encode(['error' => 'Unsupported provider for streaming test: ' . $provider]) . "\n\n";
655 flush();
656 return;
657 }
658
659 // Initialize cURL
660 $ch = curl_init();
661 curl_setopt($ch, CURLOPT_URL, $url);
662 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
663 curl_setopt($ch, CURLOPT_POST, true);
664 curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
665 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
666 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
667 curl_setopt($ch, CURLOPT_TIMEOUT, 30);
668 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use ($provider) {
669 return $this->process_streaming_test_data($data, $provider);
670 });
671
672 // Send initial test message
673 echo "data: " . json_encode(['content' => '[Starting streaming test...]']) . "\n\n";
674 flush();
675
676 $result = curl_exec($ch);
677 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
678 $curl_error = curl_error($ch);
679 curl_close($ch);
680
681 if ($curl_error) {
682 echo "data: " . json_encode(['error' => 'cURL Error: ' . $curl_error]) . "\n\n";
683 flush();
684 return;
685 }
686
687 if ($http_code !== 200) {
688 echo "data: " . json_encode(['error' => 'API returned HTTP ' . $http_code]) . "\n\n";
689 flush();
690 return;
691 }
692
693 // Send completion signal
694 echo "data: [DONE]\n\n";
695 flush();
696 }
697
698 /**
699 * Process streaming data for the test
700 */
701 private function process_streaming_test_data($data, $provider) {
702 static $chunk_count = 0;
703
704 $lines = explode("\n", $data);
705
706 foreach ($lines as $line) {
707 if (trim($line) === '') {
708 continue;
709 }
710
711 // Handle different provider formats
712 if ($provider === 'claude') {
713 // Claude uses event: and data: format
714 if (strpos($line, 'data: ') === 0) {
715 $json_str = substr($line, 6);
716 $json = json_decode($json_str, true);
717
718 if (isset($json['type']) && $json['type'] === 'content_block_delta') {
719 if (isset($json['delta']['text'])) {
720 $chunk_count++;
721 echo "data: " . json_encode([
722 'content' => $json['delta']['text'],
723 'test_chunk' => $chunk_count
724 ]) . "\n\n";
725 flush();
726 }
727 }
728 }
729 } else {
730 // OpenAI, X.AI, DeepSeek format
731 if (strpos($line, 'data: ') === 0) {
732 $json_str = substr($line, 6);
733
734 if ($json_str === '[DONE]') {
735 // Don't echo [DONE] here, let the main function handle it
736 continue;
737 }
738
739 $json = json_decode($json_str, true);
740 if (isset($json['choices'][0]['delta']['content'])) {
741 $chunk_count++;
742 echo "data: " . json_encode([
743 'content' => $json['choices'][0]['delta']['content'],
744 'test_chunk' => $chunk_count
745 ]) . "\n\n";
746 flush();
747 }
748 }
749 }
750 }
751
752 return strlen($data);
753 }
754
755 /**
756 * Updated version of your existing test method (keep this as a fallback)
757 */
758 public function mxchat_handle_test_streaming() {
759 check_ajax_referer('mxchat_test_streaming_nonce', 'nonce');
760
761 // Use the actual streaming test instead
762 $this->mxchat_handle_test_streaming_actual();
763 }
764
765
766 public function register_pinecone_settings() {
767 register_setting(
768 'mxchat_pinecone_addon_options',
769 'mxchat_pinecone_addon_options',
770 array(
771 'type' => 'array',
772 'sanitize_callback' => array($this, 'sanitize_pinecone_settings'),
773 'default' => array(
774 'mxchat_use_pinecone' => '0',
775 'mxchat_pinecone_api_key' => '',
776 'mxchat_pinecone_host' => '',
777 'mxchat_pinecone_index' => '',
778 'mxchat_pinecone_environment' => ''
779 )
780 )
781 );
782 }
783
784 public function sanitize_pinecone_settings($input) {
785 $sanitized = array();
786
787 $sanitized['mxchat_use_pinecone'] = isset($input['mxchat_use_pinecone']) ? '1' : '0';
788 $sanitized['mxchat_pinecone_api_key'] = sanitize_text_field($input['mxchat_pinecone_api_key'] ?? '');
789 $sanitized['mxchat_pinecone_host'] = sanitize_text_field($input['mxchat_pinecone_host'] ?? '');
790 $sanitized['mxchat_pinecone_index'] = sanitize_text_field($input['mxchat_pinecone_index'] ?? '');
791 $sanitized['mxchat_pinecone_environment'] = sanitize_text_field($input['mxchat_pinecone_environment'] ?? '');
792
793 // Remove https:// from host if present
794 $sanitized['mxchat_pinecone_host'] = str_replace(['https://', 'http://'], '', $sanitized['mxchat_pinecone_host']);
795
796 return $sanitized;
797 }
798
799 public function register_openai_vectorstore_settings() {
800 register_setting(
801 'mxchat_openai_vectorstore_options',
802 'mxchat_openai_vectorstore_options',
803 array(
804 'type' => 'array',
805 'sanitize_callback' => array($this, 'sanitize_openai_vectorstore_settings'),
806 'default' => array(
807 'mxchat_use_openai_vectorstore' => '0',
808 'mxchat_vectorstore_ids' => '',
809 'mxchat_vectorstore_max_results' => 5
810 )
811 )
812 );
813 }
814
815 public function sanitize_openai_vectorstore_settings($input) {
816 $sanitized = array();
817
818 $sanitized['mxchat_use_openai_vectorstore'] = isset($input['mxchat_use_openai_vectorstore']) ? '1' : '0';
819 $sanitized['mxchat_vectorstore_ids'] = sanitize_text_field($input['mxchat_vectorstore_ids'] ?? '');
820 $sanitized['mxchat_vectorstore_max_results'] = absint($input['mxchat_vectorstore_max_results'] ?? 5);
821
822 // Ensure max results is within reasonable range
823 if ($sanitized['mxchat_vectorstore_max_results'] < 1) {
824 $sanitized['mxchat_vectorstore_max_results'] = 1;
825 }
826 if ($sanitized['mxchat_vectorstore_max_results'] > 20) {
827 $sanitized['mxchat_vectorstore_max_results'] = 20;
828 }
829
830 return $sanitized;
831 }
832
833 /**
834 * AJAX handler to test OpenAI Vector Store connection
835 */
836 public function mxchat_test_vectorstore_connection() {
837 check_ajax_referer('mxchat_admin_nonce', 'nonce');
838
839 if (!current_user_can('manage_options')) {
840 wp_send_json_error(array('message' => __('Permission denied.', 'mxchat')));
841 return;
842 }
843
844 $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
845 $vectorstore_ids = $vectorstore_options['mxchat_vectorstore_ids'] ?? '';
846 $mxchat_options = get_option('mxchat_options', array());
847 $api_key = $mxchat_options['api_key'] ?? '';
848
849 if (empty($api_key)) {
850 wp_send_json_error(array('message' => __('OpenAI API key is not configured.', 'mxchat')));
851 return;
852 }
853
854 if (empty($vectorstore_ids)) {
855 wp_send_json_error(array('message' => __('No Vector Store ID configured.', 'mxchat')));
856 return;
857 }
858
859 // Get the first Vector Store ID for testing
860 $ids_array = array_map('trim', explode(',', $vectorstore_ids));
861 $test_id = $ids_array[0];
862
863 // Test by retrieving the Vector Store info
864 $response = wp_remote_get(
865 'https://api.openai.com/v1/vector_stores/' . $test_id,
866 array(
867 'headers' => array(
868 'Authorization' => 'Bearer ' . $api_key,
869 'Content-Type' => 'application/json',
870 'OpenAI-Beta' => 'assistants=v2'
871 ),
872 'timeout' => 30
873 )
874 );
875
876 if (is_wp_error($response)) {
877 wp_send_json_error(array('message' => __('Connection failed: ', 'mxchat') . $response->get_error_message()));
878 return;
879 }
880
881 $status_code = wp_remote_retrieve_response_code($response);
882 $body = json_decode(wp_remote_retrieve_body($response), true);
883
884 if ($status_code === 200 && isset($body['id'])) {
885 $file_count = $body['file_counts']['completed'] ?? 0;
886 $name = $body['name'] ?? $test_id;
887 wp_send_json_success(array(
888 'message' => sprintf(
889 __('Connected successfully! Vector Store: %s (%d files)', 'mxchat'),
890 esc_html($name),
891 $file_count
892 )
893 ));
894 } elseif ($status_code === 404) {
895 wp_send_json_error(array('message' => __('Vector Store not found. Please check the ID.', 'mxchat')));
896 } elseif ($status_code === 401) {
897 wp_send_json_error(array('message' => __('Invalid API key.', 'mxchat')));
898 } else {
899 $error_message = $body['error']['message'] ?? __('Unknown error occurred.', 'mxchat');
900 wp_send_json_error(array('message' => $error_message));
901 }
902 }
903
904 /**
905 * AJAX handler to test Slack connection and validate scopes
906 */
907 public function mxchat_test_slack_connection() {
908 check_ajax_referer('mxchat_admin_nonce', 'nonce');
909
910 if (!current_user_can('manage_options')) {
911 wp_send_json_error(array('message' => __('Permission denied.', 'mxchat')));
912 return;
913 }
914
915 $bot_token = $this->options['live_agent_bot_token'] ?? '';
916
917 if (empty($bot_token)) {
918 wp_send_json_error(array('message' => __('Slack Bot Token is not configured. Please enter your token and save settings first.', 'mxchat')));
919 return;
920 }
921
922 // Test 1: Validate bot token with auth.test
923 $auth_response = wp_remote_post('https://slack.com/api/auth.test', array(
924 'headers' => array(
925 'Authorization' => 'Bearer ' . $bot_token,
926 'Content-Type' => 'application/json'
927 ),
928 'timeout' => 15
929 ));
930
931 if (is_wp_error($auth_response)) {
932 wp_send_json_error(array('message' => __('Connection failed: ', 'mxchat') . $auth_response->get_error_message()));
933 return;
934 }
935
936 $auth_body = json_decode(wp_remote_retrieve_body($auth_response), true);
937
938 if (!isset($auth_body['ok']) || !$auth_body['ok']) {
939 $error = $auth_body['error'] ?? 'unknown_error';
940 $error_messages = array(
941 'invalid_auth' => __('Invalid bot token. Please check your token starts with xoxb-', 'mxchat'),
942 'not_authed' => __('No authentication token provided.', 'mxchat'),
943 'account_inactive' => __('The Slack workspace has been deactivated.', 'mxchat'),
944 'token_revoked' => __('The bot token has been revoked. Please generate a new one.', 'mxchat'),
945 );
946 $message = $error_messages[$error] ?? sprintf(__('Authentication failed: %s', 'mxchat'), $error);
947 wp_send_json_error(array('message' => $message));
948 return;
949 }
950
951 $team_name = $auth_body['team'] ?? 'Unknown Workspace';
952 $bot_name = $auth_body['user'] ?? 'Unknown Bot';
953
954 // Test 2: Check if we can list channels (tests channels:read scope)
955 $channels_response = wp_remote_post('https://slack.com/api/conversations.list', array(
956 'headers' => array(
957 'Authorization' => 'Bearer ' . $bot_token,
958 'Content-Type' => 'application/json'
959 ),
960 'body' => json_encode(array('limit' => 1)),
961 'timeout' => 15
962 ));
963
964 $channels_body = json_decode(wp_remote_retrieve_body($channels_response), true);
965 $can_read_channels = isset($channels_body['ok']) && $channels_body['ok'];
966
967 // Test 3: Check if we can create channels (tests channels:manage scope)
968 // We'll just check the error message without actually creating
969 $create_response = wp_remote_post('https://slack.com/api/conversations.create', array(
970 'headers' => array(
971 'Authorization' => 'Bearer ' . $bot_token,
972 'Content-Type' => 'application/json'
973 ),
974 'body' => json_encode(array('name' => 'mxchat-test-' . time(), 'is_private' => false)),
975 'timeout' => 15
976 ));
977
978 $create_body = json_decode(wp_remote_retrieve_body($create_response), true);
979
980 // If channel was created, delete it immediately
981 if (isset($create_body['ok']) && $create_body['ok'] && isset($create_body['channel']['id'])) {
982 wp_remote_post('https://slack.com/api/conversations.archive', array(
983 'headers' => array(
984 'Authorization' => 'Bearer ' . $bot_token,
985 'Content-Type' => 'application/json'
986 ),
987 'body' => json_encode(array('channel' => $create_body['channel']['id'])),
988 'timeout' => 15
989 ));
990 $can_create_channels = true;
991 } else {
992 $create_error = $create_body['error'] ?? '';
993 // name_taken means we have permission but channel exists
994 $can_create_channels = ($create_error === 'name_taken' || (isset($create_body['ok']) && $create_body['ok']));
995
996 // Check for missing scope errors
997 if ($create_error === 'missing_scope') {
998 $can_create_channels = false;
999 }
1000 }
1001
1002 // Build result message
1003 $results = array();
1004 $results[] = sprintf(__('Workspace: %s', 'mxchat'), esc_html($team_name));
1005 $results[] = sprintf(__('Bot: %s', 'mxchat'), esc_html($bot_name));
1006 $results[] = '';
1007 $results[] = ($can_read_channels ? '✓' : '✗') . ' ' . __('channels:read - List channels', 'mxchat');
1008 $results[] = ($can_create_channels ? '✓' : '✗') . ' ' . __('channels:manage - Create channels', 'mxchat');
1009
1010 $missing_scopes = array();
1011 if (!$can_read_channels) $missing_scopes[] = 'channels:read';
1012 if (!$can_create_channels) $missing_scopes[] = 'channels:manage';
1013
1014 if (!empty($missing_scopes)) {
1015 wp_send_json_error(array(
1016 'message' => implode("\n", $results),
1017 'missing_scopes' => $missing_scopes,
1018 'partial' => true
1019 ));
1020 } else {
1021 wp_send_json_success(array(
1022 'message' => implode("\n", $results)
1023 ));
1024 }
1025 }
1026
1027 public function mxchat_display_admin_notice() {
1028 // Success notice
1029 if ($message = get_transient('mxchat_admin_notice_success')) {
1030 ?>
1031 <div class="notice notice-success is-dismissible">
1032 <p><?php echo esc_html($message); ?></p>
1033 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php echo esc_html__('Dismiss this notice.', 'mxchat'); ?></span></button>
1034 </div>
1035 <?php
1036 delete_transient('mxchat_admin_notice_success'); // Clear the transient after displaying
1037 }
1038
1039 // Error notice
1040 if ($message = get_transient('mxchat_admin_notice_error')) {
1041 ?>
1042 <div class="notice notice-error is-dismissible">
1043 <p><?php echo esc_html($message); ?></p>
1044 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php echo esc_html__('Dismiss this notice.', 'mxchat'); ?></span></button>
1045 </div>
1046 <?php
1047 delete_transient('mxchat_admin_notice_error'); // Clear the transient after displaying
1048 }
1049 }
1050
1051 public function show_live_agent_disabled_banner() {
1052 $show_disabled_notice = get_option('mxchat_show_live_agent_disabled_notice', false);
1053
1054 if ($show_disabled_notice) {
1055 ?>
1056 <div class="mxchat-live-agent-disabled-notice" id="mxchat-disabled-notice">
1057 <div class="mxchat-pro-notification">
1058 <button type="button" class="mxchat-dismiss-btn" onclick="dismissLiveAgentNotice()" aria-label="Dismiss notification">
1059 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1060 <line x1="18" y1="6" x2="6" y2="18"></line>
1061 <line x1="6" y1="6" x2="18" y2="18"></line>
1062 </svg>
1063 </button>
1064 <div class="mxchat-live-agent-content">
1065 <h3>🔧 Live Agent Integration Updated!</h3>
1066 <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>
1067 </div>
1068 </div>
1069 </div>
1070 <?php
1071 }
1072 }
1073 /**
1074 * Render the Onboarding page. Delegates to the procedural renderer in
1075 * includes/admin-onboarding-page.php. Wired to both `?page=mxchat-max`
1076 * (legacy top-level URL) and `?page=mxchat-onboarding` (the canonical
1077 * Onboarding submenu).
1078 *
1079 * When the user has dismissed onboarding and lands on `mxchat-max` (the
1080 * legacy URL, since the Onboarding submenu has been removed), redirect to
1081 * Settings instead — the page is "graduated" and we shouldn't dump them
1082 * back onto it. They can still navigate here directly via the unhide link.
1083 */
1084 public function mxchat_create_dashboard_page() {
1085 require_once plugin_dir_path(__FILE__) . 'admin-onboarding-page.php';
1086
1087 $current = isset($_GET['page']) ? sanitize_key($_GET['page']) : '';
1088 if ($current === 'mxchat-max' && function_exists('mxchat_onboarding_is_dismissed') && mxchat_onboarding_is_dismissed()) {
1089 wp_safe_redirect(admin_url('admin.php?page=mxchat-settings'));
1090 exit;
1091 }
1092
1093 if (function_exists('mxchat_render_onboarding_page')) {
1094 mxchat_render_onboarding_page();
1095 return;
1096 }
1097 // Defensive: if the include failed to load, fall back to the old Settings page
1098 // so the top-level menu never lands on an empty screen.
1099 $this->mxchat_create_admin_page();
1100 }
1101
1102 /**
1103 * Hide the Onboarding submenu when the user has dismissed it (either
1104 * manually or via auto-graduation). The page itself remains routable so
1105 * the Settings "Show MxChat Onboarding again" link can navigate back to it.
1106 */
1107 public function mxchat_apply_onboarding_visibility() {
1108 if (!function_exists('mxchat_onboarding_is_dismissed')) {
1109 return;
1110 }
1111 if (mxchat_onboarding_is_dismissed()) {
1112 // The first MxChat child is the same-slug-as-parent registration
1113 // (slug 'mxchat-max', labelled "Onboarding") added in plan-d14e89.
1114 // Remove it so the menu opens straight to Settings after dismiss.
1115 remove_submenu_page('mxchat-max', 'mxchat-max');
1116 }
1117 }
1118
1119 public function mxchat_create_admin_page() {
1120 $this->add_live_agent_nonce();
1121 $this->add_theme_migration_nonce();
1122
1123 // Include and render the new sidebar-based settings page
1124 require_once plugin_dir_path(__FILE__) . 'admin-settings-page.php';
1125 mxchat_render_settings_page($this);
1126 }
1127
1128 public function dismiss_live_agent_notice() {
1129 // Add debugging
1130 //error_log('dismiss_live_agent_notice called');
1131 //error_log('POST data: ' . print_r($_POST, true));
1132
1133 // Verify nonce
1134 if (!wp_verify_nonce($_POST['nonce'], 'dismiss_live_agent_notice')) {
1135 //error_log('Nonce verification failed');
1136 wp_die('Security check failed');
1137 }
1138
1139 // Remove the notice flag
1140 $deleted = delete_option('mxchat_show_live_agent_disabled_notice');
1141 //error_log('Option deleted: ' . ($deleted ? 'yes' : 'no'));
1142
1143 wp_send_json_success();
1144 }
1145 public function add_live_agent_nonce() {
1146 if (get_option('mxchat_show_live_agent_disabled_notice', false)) {
1147 // Make sure your admin script is enqueued and localize the data
1148 wp_localize_script('mxchat-admin-js', 'mxchatLiveAgent', array(
1149 'nonce' => wp_create_nonce('dismiss_live_agent_notice'),
1150 'ajaxurl' => admin_url('admin-ajax.php')
1151 ));
1152 }
1153 }
1154
1155 /**
1156 * Show theme migration notice for Pro users with AI-generated themes
1157 * Only shown once - dismissible and stored in options
1158 */
1159 public function show_theme_migration_banner() {
1160 // Only show if Pro is activated
1161 if (!$this->is_activated) {
1162 return;
1163 }
1164
1165 // Check if notice should be shown
1166 $show_notice = get_option('mxchat_show_theme_migration_notice', false);
1167
1168 if ($show_notice) {
1169 ?>
1170 <div class="mxchat-theme-migration-notice" id="mxchat-theme-migration-notice">
1171 <div class="mxchat-pro-notification">
1172 <button type="button" class="mxchat-dismiss-btn" onclick="dismissThemeMigrationNotice()" aria-label="Dismiss notification">
1173 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1174 <line x1="18" y1="6" x2="6" y2="18"></line>
1175 <line x1="6" y1="6" x2="18" y2="18"></line>
1176 </svg>
1177 </button>
1178 <div class="mxchat-theme-migration-content">
1179 <h3>🎨 AI Theme Migration Required</h3>
1180 <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>
1181 </div>
1182 </div>
1183 </div>
1184 <?php
1185 }
1186 }
1187
1188 /**
1189 * Dismiss theme migration notice via AJAX
1190 */
1191 public function dismiss_theme_migration_notice() {
1192 // Verify nonce
1193 if (!wp_verify_nonce($_POST['nonce'], 'dismiss_theme_migration_notice')) {
1194 wp_die('Security check failed');
1195 }
1196
1197 // Remove the notice flag
1198 delete_option('mxchat_show_theme_migration_notice');
1199
1200 wp_send_json_success();
1201 }
1202
1203 /**
1204 * Add nonce for theme migration notice dismiss
1205 */
1206 public function add_theme_migration_nonce() {
1207 if (get_option('mxchat_show_theme_migration_notice', false) && $this->is_activated) {
1208 wp_localize_script('mxchat-admin-js', 'mxchatThemeMigration', array(
1209 'nonce' => wp_create_nonce('dismiss_theme_migration_notice'),
1210 'ajaxurl' => admin_url('admin-ajax.php')
1211 ));
1212 }
1213 }
1214
1215 public function mxchat_create_transcripts_page() {
1216 global $wpdb;
1217 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1218
1219 // Get basic stats
1220 $total_chats = $wpdb->get_var("SELECT COUNT(DISTINCT session_id) FROM $table_name") ?: 0;
1221 $total_messages = $wpdb->get_var("SELECT COUNT(*) FROM $table_name") ?: 0;
1222
1223 // Count unique users with detailed breakdown
1224 $total_users = $wpdb->get_var("
1225 SELECT COUNT(DISTINCT
1226 CASE
1227 WHEN user_email != '' AND user_email IS NOT NULL THEN user_email
1228 WHEN user_id != 0 THEN CONCAT('user_', user_id)
1229 WHEN user_identifier NOT LIKE 'Tech-Savvy User'
1230 AND user_identifier NOT LIKE 'Detail-Oriented User'
1231 AND user_identifier NOT LIKE 'Language Learner'
1232 AND user_identifier NOT LIKE 'Casual Browser'
1233 AND user_identifier NOT LIKE 'Policy Enforcer'
1234 AND user_identifier NOT LIKE 'Researcher'
1235 AND user_identifier NOT LIKE 'Loyalty Member'
1236 AND user_identifier NOT LIKE 'Gift Buyer'
1237 AND user_identifier NOT LIKE 'Parent or Caregiver'
1238 THEN user_identifier
1239 ELSE session_id
1240 END
1241 )
1242 FROM $table_name
1243 WHERE role != 'assistant'
1244 ");
1245
1246 // Get user type breakdown
1247 $registered_users = $wpdb->get_var("
1248 SELECT COUNT(DISTINCT user_email)
1249 FROM $table_name
1250 WHERE user_email != '' AND user_email IS NOT NULL
1251 ");
1252
1253 $guest_users = $wpdb->get_var("
1254 SELECT COUNT(DISTINCT user_identifier)
1255 FROM $table_name
1256 WHERE (user_email = '' OR user_email IS NULL)
1257 AND role != 'assistant'
1258 AND user_identifier NOT LIKE 'Tech-Savvy User'
1259 AND user_identifier NOT LIKE 'Detail-Oriented User'
1260 AND user_identifier NOT LIKE 'Language Learner'
1261 AND user_identifier NOT LIKE 'Casual Browser'
1262 AND user_identifier NOT LIKE 'Policy Enforcer'
1263 AND user_identifier NOT LIKE 'Researcher'
1264 AND user_identifier NOT LIKE 'Loyalty Member'
1265 AND user_identifier NOT LIKE 'Gift Buyer'
1266 AND user_identifier NOT LIKE 'Parent or Caregiver'
1267 ");
1268
1269 // Get agent test messages count
1270 $agent_tests = $wpdb->get_var("
1271 SELECT COUNT(DISTINCT session_id)
1272 FROM $table_name
1273 WHERE user_identifier IN (
1274 'Tech-Savvy User',
1275 'Detail-Oriented User',
1276 'Language Learner',
1277 'Casual Browser',
1278 'Policy Enforcer',
1279 'Researcher',
1280 'Loyalty Member',
1281 'Gift Buyer',
1282 'Parent or Caregiver'
1283 )
1284 ");
1285 // Get activity metrics
1286 $today_chats = $wpdb->get_var("
1287 SELECT COUNT(DISTINCT session_id)
1288 FROM $table_name
1289 WHERE DATE(timestamp) = CURDATE()
1290 ");
1291
1292 $week_chats = $wpdb->get_var("
1293 SELECT COUNT(DISTINCT session_id)
1294 FROM $table_name
1295 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)
1296 ");
1297
1298 $month_chats = $wpdb->get_var("
1299 SELECT COUNT(DISTINCT session_id)
1300 FROM $table_name
1301 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 30 DAY)
1302 ");
1303
1304 // Get daily chat data for last 7 days
1305 $daily_stats = $wpdb->get_results("
1306 SELECT
1307 DATE(timestamp) as date,
1308 COUNT(DISTINCT session_id) as chats,
1309 COUNT(*) as messages
1310 FROM $table_name
1311 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)
1312 GROUP BY DATE(timestamp)
1313 ORDER BY date ASC
1314 ");
1315
1316 // Get average messages per chat
1317 $avg_messages = $wpdb->get_var("
1318 SELECT AVG(message_count)
1319 FROM (
1320 SELECT session_id, COUNT(*) as message_count
1321 FROM $table_name
1322 GROUP BY session_id
1323 ) as chat_counts
1324 ");
1325 $avg_messages = $avg_messages ? round($avg_messages, 1) : 0;
1326
1327 // Get busiest hour
1328 $busiest_hour = $wpdb->get_row("
1329 SELECT HOUR(timestamp) as hour, COUNT(DISTINCT session_id) as chat_count
1330 FROM $table_name
1331 GROUP BY HOUR(timestamp)
1332 ORDER BY chat_count DESC
1333 LIMIT 1
1334 ");
1335
1336 // Prepare chart data
1337 $chart_labels = array();
1338 $chart_chats = array();
1339 $chart_messages = array();
1340
1341 // Fill last 7 days with data
1342 for ($i = 6; $i >= 0; $i--) {
1343 $date = date('Y-m-d', strtotime("-$i days"));
1344 $day_name = date('D', strtotime("-$i days"));
1345 $chart_labels[] = $day_name;
1346
1347 $found = false;
1348 foreach ($daily_stats as $stat) {
1349 if ($stat->date === $date) {
1350 $chart_chats[] = (int)$stat->chats;
1351 $chart_messages[] = (int)$stat->messages;
1352 $found = true;
1353 break;
1354 }
1355 }
1356 if (!$found) {
1357 $chart_chats[] = 0;
1358 $chart_messages[] = 0;
1359 }
1360 }
1361
1362 // Satisfaction rating rollup — last 30 days, grouped by bot (plan-a5b006).
1363 $satisfaction_stats = $this->get_satisfaction_rating_stats(30);
1364
1365 // Prepare page data for the template
1366 $page_data = array(
1367 'total_chats' => $total_chats,
1368 'total_messages' => $total_messages,
1369 'total_users' => $total_users,
1370 'registered_users' => $registered_users,
1371 'guest_users' => $guest_users,
1372 'agent_tests' => $agent_tests,
1373 'today_chats' => $today_chats,
1374 'week_chats' => $week_chats,
1375 'month_chats' => $month_chats,
1376 'avg_messages' => $avg_messages,
1377 'busiest_hour' => $busiest_hour,
1378 'chart_labels' => $chart_labels,
1379 'chart_chats' => $chart_chats,
1380 'chart_messages' => $chart_messages,
1381 'satisfaction_stats' => $satisfaction_stats,
1382 );
1383
1384 // Include and render the new template
1385 require_once plugin_dir_path(__FILE__) . 'admin-transcripts-page.php';
1386 mxchat_render_transcripts_page($this, $page_data);
1387 }
1388
1389 /**
1390 * Per-bot satisfaction rating rollup over the last $days days. Used by the
1391 * Satisfaction card on the Transcripts dashboard (plan-a5b006).
1392 *
1393 * @param int $days Window in days.
1394 * @return array Each entry: ['bot_id', 'total', 'positive', 'negative', 'positive_pct', 'negative_pct'].
1395 */
1396 public function get_satisfaction_rating_stats($days = 30) {
1397 global $wpdb;
1398 $table = $wpdb->prefix . 'mxchat_session_ratings';
1399 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table)) !== $table) {
1400 return array();
1401 }
1402 $days = max(1, (int) $days);
1403 $rows = $wpdb->get_results($wpdb->prepare(
1404 "SELECT bot_id,
1405 COUNT(*) AS total,
1406 SUM(CASE WHEN rating_value = 1 THEN 1 ELSE 0 END) AS positive,
1407 SUM(CASE WHEN rating_value = -1 THEN 1 ELSE 0 END) AS negative
1408 FROM {$table}
1409 WHERE created_at >= DATE_SUB(NOW(), INTERVAL %d DAY)
1410 GROUP BY bot_id
1411 ORDER BY total DESC",
1412 $days
1413 ));
1414 $out = array();
1415 foreach ((array) $rows as $row) {
1416 $total = (int) $row->total;
1417 $positive = (int) $row->positive;
1418 $negative = (int) $row->negative;
1419 $out[] = array(
1420 'bot_id' => $row->bot_id ?: 'default',
1421 'total' => $total,
1422 'positive' => $positive,
1423 'negative' => $negative,
1424 'positive_pct' => $total > 0 ? (int) round(($positive / $total) * 100) : 0,
1425 'negative_pct' => $total > 0 ? (int) round(($negative / $total) * 100) : 0,
1426 );
1427 }
1428 return $out;
1429 }
1430
1431 /**
1432 * Get chart data for transcripts page
1433 * Used by both page render and script localization
1434 *
1435 * @return array Chart data with labels, chats, and messages arrays
1436 */
1437 private function get_transcripts_chart_data() {
1438 global $wpdb;
1439 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1440
1441 // Get daily chat data for last 7 days - same query as mxchat_transcripts_page()
1442 $daily_stats = $wpdb->get_results("
1443 SELECT
1444 DATE(timestamp) as date,
1445 COUNT(DISTINCT session_id) as chats,
1446 COUNT(*) as messages
1447 FROM $table_name
1448 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)
1449 GROUP BY DATE(timestamp)
1450 ORDER BY date ASC
1451 ");
1452
1453 // Prepare chart data
1454 $chart_labels = array();
1455 $chart_chats = array();
1456 $chart_messages = array();
1457
1458 // Fill last 7 days with data - same logic as mxchat_transcripts_page()
1459 for ($i = 6; $i >= 0; $i--) {
1460 $date = date('Y-m-d', strtotime("-$i days"));
1461 $day_name = date('D', strtotime("-$i days"));
1462 $chart_labels[] = $day_name;
1463
1464 $found = false;
1465 if ($daily_stats) {
1466 foreach ($daily_stats as $stat) {
1467 if ($stat->date === $date) {
1468 $chart_chats[] = (int)$stat->chats;
1469 $chart_messages[] = (int)$stat->messages;
1470 $found = true;
1471 break;
1472 }
1473 }
1474 }
1475 if (!$found) {
1476 $chart_chats[] = 0;
1477 $chart_messages[] = 0;
1478 }
1479 }
1480
1481 return array(
1482 'labels' => $chart_labels,
1483 'chats' => $chart_chats,
1484 'messages' => $chart_messages
1485 );
1486 }
1487
1488 public function mxchat_transcripts_notification_section_callback() {
1489 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>';
1490 }
1491 public function mxchat_enable_notifications_callback() {
1492 $options = get_option('mxchat_transcripts_options', array());
1493 $enabled = isset($options['mxchat_enable_notifications']) ? $options['mxchat_enable_notifications'] : 0;
1494 ?>
1495 <label for="mxchat_enable_notifications">
1496 <input type="checkbox" id="mxchat_enable_notifications"
1497 name="mxchat_transcripts_options[mxchat_enable_notifications]"
1498 value="1" <?php checked(1, $enabled); ?>>
1499 <?php esc_html_e('Send email notification when a new chat session starts', 'mxchat'); ?>
1500 </label>
1501 <p class="description">
1502 <?php esc_html_e('Enable this option to receive email notifications for new chat sessions.', 'mxchat'); ?>
1503 </p>
1504 <?php
1505 }
1506 public function mxchat_notification_email_callback() {
1507 $options = get_option('mxchat_transcripts_options', array());
1508 $email = isset($options['mxchat_notification_email']) ? $options['mxchat_notification_email'] : get_option('admin_email');
1509 ?>
1510 <input type="email" id="mxchat_notification_email"
1511 name="mxchat_transcripts_options[mxchat_notification_email]"
1512 value="<?php echo esc_attr($email); ?>"
1513 class="regular-text">
1514 <p class="description">
1515 <?php esc_html_e('Enter the email address where notifications should be sent. Defaults to the admin email address.', 'mxchat'); ?>
1516 </p>
1517 <?php
1518 }
1519
1520 public function mxchat_auto_delete_transcripts_callback() {
1521 $options = get_option('mxchat_transcripts_options', array());
1522 $interval = isset($options['mxchat_auto_delete_transcripts']) ? $options['mxchat_auto_delete_transcripts'] : 'never';
1523 ?>
1524 <select id="mxchat_auto_delete_transcripts"
1525 name="mxchat_transcripts_options[mxchat_auto_delete_transcripts]">
1526 <option value="never" <?php selected($interval, 'never'); ?>>
1527 <?php esc_html_e('Never (Keep All Transcripts)', 'mxchat'); ?>
1528 </option>
1529 <option value="1week" <?php selected($interval, '1week'); ?>>
1530 <?php esc_html_e('After 1 Week', 'mxchat'); ?>
1531 </option>
1532 <option value="2weeks" <?php selected($interval, '2weeks'); ?>>
1533 <?php esc_html_e('After 2 Weeks', 'mxchat'); ?>
1534 </option>
1535 <option value="1month" <?php selected($interval, '1month'); ?>>
1536 <?php esc_html_e('After 1 Month', 'mxchat'); ?>
1537 </option>
1538 </select>
1539 <p class="description">
1540 <?php esc_html_e('Automatically delete old chat transcripts after the selected time period. This helps manage database size and privacy.', 'mxchat'); ?>
1541 </p>
1542 <?php
1543 }
1544
1545 /**
1546 * Custom retention-days input. When > 0 it overrides the bucket dropdown above
1547 * and deletes transcripts older than the given number of days. Set to 0 to fall
1548 * back to the dropdown (or "Never" if the dropdown is also Never).
1549 *
1550 * Devs can override the final day count via the `mxchat_transcript_retention_days`
1551 * filter — runs in `cleanup_old_transcripts()` after this option is read.
1552 *
1553 * (plan-mxchat-20260509-9b80b1)
1554 */
1555 public function mxchat_retention_days_callback() {
1556 $options = get_option('mxchat_transcripts_options', array());
1557 $days = isset($options['mxchat_retention_days']) ? (int) $options['mxchat_retention_days'] : 0;
1558 ?>
1559 <input type="number"
1560 id="mxchat_retention_days"
1561 name="mxchat_transcripts_options[mxchat_retention_days]"
1562 value="<?php echo esc_attr($days); ?>"
1563 min="0"
1564 max="3650"
1565 step="1"
1566 style="width: 90px;" />
1567 <p class="description">
1568 <?php esc_html_e('Number of days to retain transcripts. When set to a value greater than 0, this overrides the dropdown above. Set to 0 to use the dropdown. Maximum 3650 (10 years). A daily wp-cron task removes anything older, cascading to translations and click-tracking rows.', 'mxchat'); ?>
1569 <br>
1570 <code>apply_filters( 'mxchat_transcript_retention_days', $days )</code>
1571 <?php esc_html_e('lets developers override the final day count programmatically.', 'mxchat'); ?>
1572 </p>
1573 <?php
1574 }
1575
1576 public function mxchat_auto_email_transcript_callback() {
1577 $options = get_option('mxchat_transcripts_options', array());
1578 $enabled = isset($options['mxchat_auto_email_transcript_enabled']) ? $options['mxchat_auto_email_transcript_enabled'] : 0;
1579 $delay = isset($options['mxchat_auto_email_transcript_delay']) ? $options['mxchat_auto_email_transcript_delay'] : '30';
1580 $require_contact = isset($options['mxchat_auto_email_transcript_require_contact']) ? $options['mxchat_auto_email_transcript_require_contact'] : 0;
1581 ?>
1582 <label>
1583 <input type="checkbox"
1584 id="mxchat_auto_email_transcript_enabled"
1585 name="mxchat_transcripts_options[mxchat_auto_email_transcript_enabled]"
1586 value="1"
1587 <?php checked($enabled, 1); ?>>
1588 <?php esc_html_e('Enable Auto-Email of Full Transcript', 'mxchat'); ?>
1589 </label>
1590 <br><br>
1591 <label for="mxchat_auto_email_transcript_delay">
1592 <?php esc_html_e('Send transcript after:', 'mxchat'); ?>
1593 </label>
1594 <select id="mxchat_auto_email_transcript_delay"
1595 name="mxchat_transcripts_options[mxchat_auto_email_transcript_delay]">
1596 <option value="15" <?php selected($delay, '15'); ?>>
1597 <?php esc_html_e('15 minutes', 'mxchat'); ?>
1598 </option>
1599 <option value="30" <?php selected($delay, '30'); ?>>
1600 <?php esc_html_e('30 minutes', 'mxchat'); ?>
1601 </option>
1602 <option value="60" <?php selected($delay, '60'); ?>>
1603 <?php esc_html_e('1 hour', 'mxchat'); ?>
1604 </option>
1605 </select>
1606 <br><br>
1607 <label>
1608 <input type="checkbox"
1609 id="mxchat_auto_email_transcript_require_contact"
1610 name="mxchat_transcripts_options[mxchat_auto_email_transcript_require_contact]"
1611 value="1"
1612 <?php checked($require_contact, 1); ?>>
1613 <?php esc_html_e('Only send if visitor provided contact info', 'mxchat'); ?>
1614 </label>
1615 <p class="description">
1616 <?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'); ?>
1617 <br>
1618 <?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'); ?>
1619 </p>
1620 <?php
1621 }
1622
1623
1624
1625 public function sanitize_transcripts_options($input) {
1626 $sanitized = array();
1627
1628 $sanitized['mxchat_enable_notifications'] = isset($input['mxchat_enable_notifications']) ? 1 : 0;
1629
1630 if (isset($input['mxchat_notification_email'])) {
1631 $sanitized['mxchat_notification_email'] = sanitize_email($input['mxchat_notification_email']);
1632 if (!is_email($sanitized['mxchat_notification_email'])) {
1633 add_settings_error(
1634 'mxchat_transcripts_options',
1635 'invalid_email',
1636 __('Please enter a valid email address for notifications.', 'mxchat'),
1637 'error'
1638 );
1639 $sanitized['mxchat_notification_email'] = get_option('admin_email');
1640 }
1641 }
1642
1643 // Sanitize auto-delete setting
1644 $valid_intervals = array('never', '1week', '2weeks', '1month');
1645 if (isset($input['mxchat_auto_delete_transcripts'])) {
1646 $sanitized['mxchat_auto_delete_transcripts'] = in_array($input['mxchat_auto_delete_transcripts'], $valid_intervals)
1647 ? $input['mxchat_auto_delete_transcripts']
1648 : 'never';
1649 } else {
1650 $sanitized['mxchat_auto_delete_transcripts'] = 'never';
1651 }
1652
1653 // Sanitize custom retention-days override (plan-9b80b1).
1654 if (isset($input['mxchat_retention_days'])) {
1655 $days = (int) $input['mxchat_retention_days'];
1656 $sanitized['mxchat_retention_days'] = max(0, min(3650, $days));
1657 } else {
1658 $sanitized['mxchat_retention_days'] = 0;
1659 }
1660
1661 // Get old values to check if auto-delete or retention-days changed.
1662 $old_options = get_option('mxchat_transcripts_options');
1663 $old_interval = isset($old_options['mxchat_auto_delete_transcripts']) ? $old_options['mxchat_auto_delete_transcripts'] : 'never';
1664 $old_retention = isset($old_options['mxchat_retention_days']) ? (int) $old_options['mxchat_retention_days'] : 0;
1665
1666 // If either setting changed, reschedule the cron job. The schedule_transcript_cleanup
1667 // helper now treats "any active retention" (dropdown != never OR custom days > 0) as a
1668 // reason to keep the daily cron registered.
1669 $interval_changed = ($old_interval !== $sanitized['mxchat_auto_delete_transcripts']);
1670 $retention_changed = ($old_retention !== $sanitized['mxchat_retention_days']);
1671 if ($interval_changed || $retention_changed) {
1672 $any_active = ($sanitized['mxchat_auto_delete_transcripts'] !== 'never') || ($sanitized['mxchat_retention_days'] > 0);
1673 $this->schedule_transcript_cleanup($any_active ? 'active' : 'never');
1674 }
1675
1676 // Sanitize auto-email transcript settings
1677 $sanitized['mxchat_auto_email_transcript_enabled'] = isset($input['mxchat_auto_email_transcript_enabled']) ? 1 : 0;
1678
1679 $valid_delays = array('15', '30', '60');
1680 if (isset($input['mxchat_auto_email_transcript_delay'])) {
1681 $sanitized['mxchat_auto_email_transcript_delay'] = in_array($input['mxchat_auto_email_transcript_delay'], $valid_delays)
1682 ? $input['mxchat_auto_email_transcript_delay']
1683 : '30';
1684 } else {
1685 $sanitized['mxchat_auto_email_transcript_delay'] = '30';
1686 }
1687
1688 // Sanitize require contact info setting
1689 $sanitized['mxchat_auto_email_transcript_require_contact'] = isset($input['mxchat_auto_email_transcript_require_contact']) ? 1 : 0;
1690
1691 return $sanitized;
1692 }
1693
1694 /**
1695 * Schedule or unschedule the transcript cleanup cron job
1696 */
1697 public function schedule_transcript_cleanup($interval) {
1698 // Clear any existing scheduled event
1699 $timestamp = wp_next_scheduled('mxchat_cleanup_old_transcripts');
1700 if ($timestamp) {
1701 wp_unschedule_event($timestamp, 'mxchat_cleanup_old_transcripts');
1702 }
1703
1704 // Schedule new event whenever retention is active. "active" is the canonical
1705 // value passed by sanitize_transcripts_options when either the dropdown != never
1706 // OR the custom retention-days > 0; "never" turns the cron off. Any other value
1707 // (the legacy "1week" / "2weeks" / "1month" strings) is also treated as active.
1708 if ($interval !== 'never') {
1709 // Schedule to run daily at 3 AM
1710 $next_run = strtotime('tomorrow 3:00 AM');
1711 wp_schedule_event($next_run, 'daily', 'mxchat_cleanup_old_transcripts');
1712 }
1713 }
1714
1715 /**
1716 * Delete old transcripts based on the configured interval
1717 */
1718 public function cleanup_old_transcripts() {
1719 $options = get_option('mxchat_transcripts_options', array());
1720 $interval = isset($options['mxchat_auto_delete_transcripts']) ? $options['mxchat_auto_delete_transcripts'] : 'never';
1721 $custom_days = isset($options['mxchat_retention_days']) ? (int) $options['mxchat_retention_days'] : 0;
1722
1723 // Custom retention-days (plan-9b80b1) takes precedence over the bucket dropdown.
1724 $days = 0;
1725 if ($custom_days > 0) {
1726 $days = $custom_days;
1727 } else {
1728 switch ($interval) {
1729 case '1week': $days = 7; break;
1730 case '2weeks': $days = 14; break;
1731 case '1month': $days = 30; break;
1732 case 'never':
1733 default:
1734 $days = 0;
1735 }
1736 }
1737
1738 // Devs can override the final day count programmatically.
1739 $days = (int) apply_filters('mxchat_transcript_retention_days', $days);
1740
1741 if ($days <= 0) {
1742 return; // Retention disabled — bail.
1743 }
1744
1745 global $wpdb;
1746 $transcripts_table = $wpdb->prefix . 'mxchat_chat_transcripts';
1747 $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
1748 $url_clicks_table = $wpdb->prefix . 'mxchat_url_clicks';
1749
1750 // Defensive: if the main table doesn't exist (fresh-ish install), bail.
1751 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $transcripts_table)) !== $transcripts_table) {
1752 return;
1753 }
1754
1755 $cutoff_date = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
1756
1757 // Cap at 5000 session_ids per run so a large unattended site doesn't OOM —
1758 // the cron will pick up where it left off on the next tick (within hours).
1759 $batch_cap = (int) apply_filters('mxchat_transcript_retention_batch_cap', 5000);
1760
1761 $sessions_to_delete = $wpdb->get_col(
1762 $wpdb->prepare(
1763 "SELECT DISTINCT session_id FROM {$transcripts_table} WHERE timestamp IS NOT NULL AND timestamp < %s LIMIT %d",
1764 $cutoff_date,
1765 $batch_cap
1766 )
1767 );
1768
1769 if (empty($sessions_to_delete)) {
1770 return;
1771 }
1772
1773 $placeholders = implode(',', array_fill(0, count($sessions_to_delete), '%s'));
1774
1775 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1776 // $placeholders is a server-built list of literal "%s" tokens.
1777 $deleted_transcripts = (int) $wpdb->query(
1778 $wpdb->prepare(
1779 "DELETE FROM {$transcripts_table} WHERE session_id IN ($placeholders)",
1780 $sessions_to_delete
1781 )
1782 );
1783
1784 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $translations_table)) === $translations_table) {
1785 $wpdb->query(
1786 $wpdb->prepare(
1787 "DELETE FROM {$translations_table} WHERE session_id IN ($placeholders)",
1788 $sessions_to_delete
1789 )
1790 );
1791 }
1792
1793 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $url_clicks_table)) === $url_clicks_table) {
1794 $wpdb->query(
1795 $wpdb->prepare(
1796 "DELETE FROM {$url_clicks_table} WHERE session_id IN ($placeholders)",
1797 $sessions_to_delete
1798 )
1799 );
1800 }
1801 // phpcs:enable
1802
1803 update_option('mxchat_retention_last_swept_at', time(), false);
1804 update_option('mxchat_retention_rows_last_deleted', $deleted_transcripts, false);
1805 }
1806
1807 public function export_chat_transcripts() {
1808 if (!current_user_can('manage_options')) {
1809 wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'mxchat'));
1810 }
1811
1812 check_ajax_referer('mxchat_export_transcripts', 'security');
1813
1814 global $wpdb;
1815 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1816
1817 // Get all transcripts ordered by session and timestamp
1818 $results = $wpdb->get_results(
1819 "SELECT session_id, user_email, user_identifier, role, message, timestamp
1820 FROM {$table_name}
1821 ORDER BY session_id, timestamp ASC"
1822 );
1823
1824 if (empty($results)) {
1825 wp_send_json_error(array('message' => 'No transcripts found.'));
1826 wp_die();
1827 }
1828
1829 // Set headers for CSV download
1830 header('Content-Type: text/csv');
1831 header('Content-Disposition: attachment; filename="chat-transcripts-' . date('Y-m-d') . '.csv"');
1832 header('Pragma: no-cache');
1833 header('Expires: 0');
1834
1835 // Create output stream
1836 $output = fopen('php://output', 'w');
1837
1838 // Add UTF-8 BOM for proper Excel encoding
1839 fputs($output, "\xEF\xBB\xBF");
1840
1841 // Add CSV headers
1842 fputcsv($output, array(
1843 'Session ID',
1844 'Email',
1845 'User Identifier',
1846 'Role',
1847 'Message',
1848 'Timestamp'
1849 ));
1850
1851 // Add data rows
1852 foreach ($results as $row) {
1853 fputcsv($output, array(
1854 $row->session_id,
1855 $row->user_email,
1856 $row->user_identifier,
1857 $row->role,
1858 $row->message,
1859 $row->timestamp
1860 ));
1861 }
1862
1863 fclose($output);
1864 wp_die();
1865 }
1866
1867 // ============================================================================
1868 // Leads tab (inside Transcripts)
1869 //
1870 // Leads are derived from existing data — no dedicated table. Primary source:
1871 // wp_mxchat_chat_transcripts rows where user_email is populated. Secondary
1872 // source: wp_options entries `mxchat_email_{session_id}` / `mxchat_name_{sid}`
1873 // for "orphan" leads who submitted the pre-chat form but never chatted.
1874 // ============================================================================
1875
1876 /**
1877 * Fetch leads: dedup-by-email rows, stats strip, and top pages in one call.
1878 */
1879 public function mxchat_fetch_leads() {
1880 if (!current_user_can('manage_options')) {
1881 wp_send_json_error(['message' => 'Insufficient permissions']);
1882 wp_die();
1883 }
1884
1885 global $wpdb;
1886 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
1887
1888 $page = isset($_POST['page']) ? max(1, absint($_POST['page'])) : 1;
1889 $per_page = isset($_POST['per_page']) ? min(100, max(10, absint($_POST['per_page']))) : 25;
1890 $offset = ($page - 1) * $per_page;
1891 $search = isset($_POST['search']) ? sanitize_text_field(wp_unslash($_POST['search'])) : '';
1892 $date_range = isset($_POST['date_range']) ? sanitize_key($_POST['date_range']) : 'all';
1893 $status = isset($_POST['status']) ? sanitize_key($_POST['status']) : 'all';
1894 $page_filter = isset($_POST['page_url']) ? esc_url_raw(wp_unslash($_POST['page_url'])) : '';
1895 $sort = isset($_POST['sort']) ? sanitize_key($_POST['sort']) : 'last_seen';
1896 $sort_dir = (isset($_POST['sort_dir']) && $_POST['sort_dir'] === 'asc') ? 'ASC' : 'DESC';
1897
1898 $date_cutoff = self::mxchat_leads_date_cutoff($date_range);
1899 $has_page_url_column = !empty($wpdb->get_results("SHOW COLUMNS FROM $table LIKE 'originating_page_url'"));
1900
1901 // Base WHERE for transcripts leads.
1902 $where_clauses = ["user_email IS NOT NULL", "user_email != ''"];
1903 $where_params = [];
1904
1905 if ($date_cutoff) {
1906 $where_clauses[] = 'timestamp >= %s';
1907 $where_params[] = $date_cutoff;
1908 }
1909 if ($page_filter && $has_page_url_column) {
1910 $where_clauses[] = 'originating_page_url = %s';
1911 $where_params[] = $page_filter;
1912 }
1913 if ($search !== '') {
1914 $like = '%' . $wpdb->esc_like($search) . '%';
1915 $where_clauses[] = '(user_email LIKE %s OR user_name LIKE %s)';
1916 $where_params[] = $like;
1917 $where_params[] = $like;
1918 }
1919 $where_sql = 'WHERE ' . implode(' AND ', $where_clauses);
1920
1921 // Aggregate query grouped by email.
1922 $select_sql = $has_page_url_column
1923 ? "SELECT user_email, MAX(timestamp) AS last_seen, MIN(timestamp) AS first_seen,
1924 COUNT(DISTINCT session_id) AS conversation_count"
1925 : "SELECT user_email, MAX(timestamp) AS last_seen, MIN(timestamp) AS first_seen,
1926 COUNT(DISTINCT session_id) AS conversation_count";
1927
1928 $order_column = in_array($sort, ['last_seen', 'conversation_count', 'first_seen'], true) ? $sort : 'last_seen';
1929 $group_order_limit = " GROUP BY user_email ORDER BY {$order_column} {$sort_dir} LIMIT %d OFFSET %d";
1930
1931 $transcripts_sql = $wpdb->prepare(
1932 "{$select_sql} FROM {$table} {$where_sql}{$group_order_limit}",
1933 array_merge($where_params, [$per_page, $offset])
1934 );
1935 $transcript_rows = $wpdb->get_results($transcripts_sql);
1936
1937 // Count of unique transcript-based leads under the same filters.
1938 $count_sql = $wpdb->prepare(
1939 "SELECT COUNT(DISTINCT user_email) FROM {$table} {$where_sql}",
1940 $where_params
1941 );
1942 $transcripts_lead_count = (int) $wpdb->get_var($count_sql);
1943
1944 // Hydrate each row: name, latest_session_id, top page.
1945 $leads = [];
1946 foreach ($transcript_rows as $row) {
1947 $detail = $has_page_url_column
1948 ? $wpdb->get_row($wpdb->prepare(
1949 "SELECT session_id, user_name, originating_page_url, originating_page_title
1950 FROM {$table} WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
1951 $row->user_email
1952 ))
1953 : $wpdb->get_row($wpdb->prepare(
1954 "SELECT session_id, user_name FROM {$table}
1955 WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
1956 $row->user_email
1957 ));
1958
1959 $leads[] = [
1960 'email' => $row->user_email,
1961 'name' => isset($detail->user_name) ? (string) $detail->user_name : '',
1962 'conversation_count' => (int) $row->conversation_count,
1963 'last_seen' => $row->last_seen,
1964 'last_seen_display' => self::mxchat_leads_format_relative($row->last_seen),
1965 'first_seen' => $row->first_seen,
1966 'latest_session_id' => isset($detail->session_id) ? $detail->session_id : '',
1967 'top_page_url' => isset($detail->originating_page_url) ? $detail->originating_page_url : '',
1968 'top_page_title' => isset($detail->originating_page_title) ? $detail->originating_page_title : '',
1969 'is_orphan' => false,
1970 'status' => 'active',
1971 ];
1972 }
1973
1974 // Non-transcript lead sources. Built once here, then filtered/merged based on the
1975 // status filter below. Deduplication priority when the same email appears in multiple
1976 // sources: transcripts > chat_deleted > orphan.
1977 $transcripts_emails_seen = array_flip(array_map(
1978 function ($r) { return strtolower($r['email']); },
1979 $leads
1980 ));
1981
1982 // Chat-deleted leads: had a conversation that an admin removed. Preserved via
1983 // mxchat_lead_del_* options, with timestamps so they respect date filters.
1984 $chat_deleted_leads_all = [];
1985 if ($status === 'all' || $status === 'chat_deleted') {
1986 $chat_deleted_leads_all = self::mxchat_collect_chat_deleted_leads($search, $date_cutoff);
1987 // Dedup: drop any chat_deleted row whose email is already in the transcripts set.
1988 $chat_deleted_leads_all = array_values(array_filter(
1989 $chat_deleted_leads_all,
1990 function ($row) use ($transcripts_emails_seen) {
1991 return !isset($transcripts_emails_seen[strtolower($row['email'])]);
1992 }
1993 ));
1994 foreach ($chat_deleted_leads_all as $row) {
1995 $transcripts_emails_seen[strtolower($row['email'])] = true;
1996 }
1997 }
1998
1999 // Orphans: pre-chat form captures with no conversation. No timestamp, so skipped
2000 // when a date filter is active.
2001 $orphan_leads_all = [];
2002 if (($status === 'all' || $status === 'orphan') && !$date_cutoff && !$page_filter) {
2003 $orphan_leads_all = self::mxchat_collect_orphan_leads($search);
2004 $orphan_leads_all = array_values(array_filter(
2005 $orphan_leads_all,
2006 function ($row) use ($transcripts_emails_seen) {
2007 return !isset($transcripts_emails_seen[strtolower($row['email'])]);
2008 }
2009 ));
2010 }
2011
2012 // Apply status filter to the transcripts-derived list.
2013 if ($status === 'orphan' || $status === 'chat_deleted') {
2014 $leads = [];
2015 $transcripts_lead_count = 0;
2016 }
2017
2018 // Stitch the current page from the three buckets in priority order.
2019 $total_count = $transcripts_lead_count + count($chat_deleted_leads_all) + count($orphan_leads_all);
2020 $remaining_slots = $per_page - count($leads);
2021
2022 if ($remaining_slots > 0 && !empty($chat_deleted_leads_all)) {
2023 $start = max(0, ($page - 1) * $per_page - $transcripts_lead_count);
2024 if ($start < count($chat_deleted_leads_all)) {
2025 $leads = array_merge($leads, array_slice($chat_deleted_leads_all, $start, $remaining_slots));
2026 $remaining_slots = $per_page - count($leads);
2027 }
2028 }
2029
2030 if ($remaining_slots > 0 && !empty($orphan_leads_all)) {
2031 $before = $transcripts_lead_count + count($chat_deleted_leads_all);
2032 $start = max(0, ($page - 1) * $per_page - $before);
2033 if ($start < count($orphan_leads_all)) {
2034 $leads = array_merge($leads, array_slice($orphan_leads_all, $start, $remaining_slots));
2035 }
2036 }
2037
2038 $total_pages = $per_page > 0 ? (int) ceil($total_count / $per_page) : 1;
2039
2040 // Stats strip: always computed over full dataset, unaffected by filters.
2041 $stats = self::mxchat_leads_stats($table, $has_page_url_column);
2042
2043 // Top pages: top 5 by distinct emails captured.
2044 $top_pages = [];
2045 if ($has_page_url_column) {
2046 $top_pages_rows = $wpdb->get_results(
2047 "SELECT originating_page_url AS url,
2048 MAX(originating_page_title) AS title,
2049 COUNT(DISTINCT user_email) AS lead_count
2050 FROM {$table}
2051 WHERE user_email IS NOT NULL AND user_email != ''
2052 AND originating_page_url IS NOT NULL AND originating_page_url != ''
2053 GROUP BY originating_page_url
2054 ORDER BY lead_count DESC, url ASC
2055 LIMIT 5"
2056 );
2057 foreach ($top_pages_rows as $p) {
2058 $top_pages[] = [
2059 'url' => $p->url,
2060 'title' => $p->title ?: $p->url,
2061 'lead_count' => (int) $p->lead_count,
2062 ];
2063 }
2064 }
2065
2066 wp_send_json([
2067 'success' => true,
2068 'leads' => $leads,
2069 'page' => $page,
2070 'per_page' => $per_page,
2071 'total_count' => $total_count,
2072 'total_pages' => $total_pages,
2073 'showing_start' => $total_count === 0 ? 0 : ($offset + 1),
2074 'showing_end' => min($offset + $per_page, $total_count),
2075 'stats' => $stats,
2076 'top_pages' => $top_pages,
2077 ]);
2078 wp_die();
2079 }
2080
2081 /**
2082 * Delete one or more leads by email. Removes every transcripts row for that
2083 * email and cleans up related wp_options (mxchat_email_{sid}, mxchat_name_{sid},
2084 * mxchat_history_{sid}) and any orphan option entries matching the email.
2085 */
2086 public function mxchat_delete_leads() {
2087 if (!current_user_can('manage_options')) {
2088 wp_send_json_error(['message' => 'Insufficient permissions']);
2089 wp_die();
2090 }
2091 check_ajax_referer('mxchat_delete_leads', 'security');
2092
2093 $emails_raw = isset($_POST['emails']) ? (array) wp_unslash($_POST['emails']) : [];
2094 $emails = [];
2095 foreach ($emails_raw as $e) {
2096 $clean = sanitize_email((string) $e);
2097 if ($clean) {
2098 $emails[] = $clean;
2099 }
2100 }
2101 if (empty($emails)) {
2102 wp_send_json_error(['message' => 'No emails provided']);
2103 wp_die();
2104 }
2105
2106 $summary = self::mxchat_wipe_leads_by_email($emails);
2107
2108 wp_send_json([
2109 'success' => true,
2110 'deleted_leads' => count($emails),
2111 'deleted_sessions' => $summary['deleted_sessions'],
2112 'deleted_rows' => $summary['deleted_rows'],
2113 ]);
2114 wp_die();
2115 }
2116
2117 /**
2118 * Fully wipe one or more leads by email: every transcripts row, every related wp_options
2119 * entry (history, pre-chat capture, chat_deleted preservation, agent name, translations).
2120 *
2121 * Shared between the Leads-tab Delete button and the transcript-delete opt-in checkbox.
2122 * Input emails must already be sanitized with sanitize_email().
2123 */
2124 private static function mxchat_wipe_leads_by_email(array $emails) {
2125 global $wpdb;
2126 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2127 $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
2128 $has_translations = $wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table;
2129
2130 $deleted_sessions = 0;
2131 $deleted_rows = 0;
2132 $emails_lc = array_map('strtolower', $emails);
2133
2134 foreach ($emails as $email) {
2135 $session_ids = $wpdb->get_col($wpdb->prepare(
2136 "SELECT DISTINCT session_id FROM {$table} WHERE user_email = %s",
2137 $email
2138 ));
2139
2140 $rows_removed = $wpdb->delete($table, ['user_email' => $email], ['%s']);
2141 if ($rows_removed !== false) {
2142 $deleted_rows += (int) $rows_removed;
2143 }
2144
2145 foreach ($session_ids as $sid) {
2146 $deleted_sessions++;
2147 wp_cache_delete('chat_session_' . $sid, 'mxchat_chat_sessions');
2148 delete_option('mxchat_history_' . $sid);
2149 delete_option('mxchat_email_' . $sid);
2150 delete_option('mxchat_name_' . $sid);
2151 delete_option('mxchat_agent_name_' . $sid);
2152 delete_option('mxchat_lead_del_email_' . $sid);
2153 delete_option('mxchat_lead_del_name_' . $sid);
2154 delete_option('mxchat_lead_del_ts_' . $sid);
2155 if ($has_translations) {
2156 $wpdb->delete($translations_table, ['session_id' => $sid], ['%s']);
2157 }
2158 }
2159 }
2160
2161 // Clean up any lingering option entries (orphan pre-chat captures + chat_deleted
2162 // preservations) whose stored value matches one of the emails being wiped.
2163 $lingering = $wpdb->get_results(
2164 "SELECT option_name, option_value FROM {$wpdb->options}
2165 WHERE option_name LIKE 'mxchat_email_%' OR option_name LIKE 'mxchat_lead_del_email_%'"
2166 );
2167 foreach ($lingering as $opt) {
2168 if (!in_array(strtolower(trim($opt->option_value)), $emails_lc, true)) {
2169 continue;
2170 }
2171 if (strpos($opt->option_name, 'mxchat_lead_del_email_') === 0) {
2172 $sid = substr($opt->option_name, strlen('mxchat_lead_del_email_'));
2173 delete_option('mxchat_lead_del_email_' . $sid);
2174 delete_option('mxchat_lead_del_name_' . $sid);
2175 delete_option('mxchat_lead_del_ts_' . $sid);
2176 } else {
2177 $sid = substr($opt->option_name, strlen('mxchat_email_'));
2178 delete_option('mxchat_email_' . $sid);
2179 delete_option('mxchat_name_' . $sid);
2180 }
2181 }
2182
2183 wp_cache_delete('all_chat_sessions', 'mxchat_chat_sessions');
2184
2185 return [
2186 'deleted_sessions' => $deleted_sessions,
2187 'deleted_rows' => $deleted_rows,
2188 ];
2189 }
2190
2191 /**
2192 * Stream a leads CSV. scope=all exports every lead under current filters is not
2193 * supported to keep semantics simple; caller either exports all leads or a
2194 * specific set of selected emails.
2195 */
2196 public function mxchat_export_leads() {
2197 if (!current_user_can('manage_options')) {
2198 wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'mxchat'));
2199 }
2200 check_ajax_referer('mxchat_export_leads', 'security');
2201
2202 $scope = isset($_POST['scope']) ? sanitize_key($_POST['scope']) : 'all';
2203 $fields_mode = isset($_POST['fields']) ? sanitize_key($_POST['fields']) : 'email_and_name';
2204 $emails_in = isset($_POST['emails']) ? (array) wp_unslash($_POST['emails']) : [];
2205
2206 $emails_in_clean = [];
2207 foreach ($emails_in as $e) {
2208 $clean = sanitize_email((string) $e);
2209 if ($clean) {
2210 $emails_in_clean[] = $clean;
2211 }
2212 }
2213
2214 global $wpdb;
2215 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2216 $has_page_url_column = !empty($wpdb->get_results("SHOW COLUMNS FROM $table LIKE 'originating_page_url'"));
2217
2218 // Collect leads from transcripts.
2219 $transcripts_sql = "SELECT user_email AS email,
2220 MAX(timestamp) AS last_seen,
2221 COUNT(DISTINCT session_id) AS conversation_count
2222 FROM {$table}
2223 WHERE user_email IS NOT NULL AND user_email != ''";
2224 $params = [];
2225 if ($scope === 'selected' && !empty($emails_in_clean)) {
2226 $placeholders = implode(',', array_fill(0, count($emails_in_clean), '%s'));
2227 $transcripts_sql .= " AND user_email IN ({$placeholders})";
2228 $params = $emails_in_clean;
2229 }
2230 $transcripts_sql .= " GROUP BY user_email ORDER BY last_seen DESC";
2231
2232 $rows = !empty($params)
2233 ? $wpdb->get_results($wpdb->prepare($transcripts_sql, $params))
2234 : $wpdb->get_results($transcripts_sql);
2235
2236 // Hydrate each row with name + top page.
2237 $export_rows = [];
2238 foreach ($rows as $row) {
2239 $detail = $has_page_url_column
2240 ? $wpdb->get_row($wpdb->prepare(
2241 "SELECT user_name, originating_page_url FROM {$table}
2242 WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
2243 $row->email
2244 ))
2245 : $wpdb->get_row($wpdb->prepare(
2246 "SELECT user_name FROM {$table}
2247 WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
2248 $row->email
2249 ));
2250 $export_rows[] = [
2251 'email' => $row->email,
2252 'name' => isset($detail->user_name) ? (string) $detail->user_name : '',
2253 'conversation_count' => (int) $row->conversation_count,
2254 'last_seen' => $row->last_seen,
2255 'top_page_url' => isset($detail->originating_page_url) ? $detail->originating_page_url : '',
2256 ];
2257 }
2258
2259 // Include orphan + chat_deleted leads when exporting all.
2260 if ($scope === 'all') {
2261 $transcripts_emails_lc = array_flip(array_map(
2262 function ($r) { return strtolower($r['email']); },
2263 $export_rows
2264 ));
2265 foreach (self::mxchat_collect_chat_deleted_leads('') as $cd) {
2266 if (isset($transcripts_emails_lc[strtolower($cd['email'])])) continue;
2267 $transcripts_emails_lc[strtolower($cd['email'])] = true;
2268 $export_rows[] = [
2269 'email' => $cd['email'],
2270 'name' => $cd['name'],
2271 'conversation_count' => 0,
2272 'last_seen' => $cd['last_seen'],
2273 'top_page_url' => '',
2274 ];
2275 }
2276 foreach (self::mxchat_collect_orphan_leads('') as $orphan) {
2277 if (isset($transcripts_emails_lc[strtolower($orphan['email'])])) continue;
2278 $transcripts_emails_lc[strtolower($orphan['email'])] = true;
2279 $export_rows[] = [
2280 'email' => $orphan['email'],
2281 'name' => $orphan['name'],
2282 'conversation_count' => 0,
2283 'last_seen' => '',
2284 'top_page_url' => '',
2285 ];
2286 }
2287 }
2288
2289 if (empty($export_rows)) {
2290 wp_send_json_error(['message' => 'No leads to export.']);
2291 wp_die();
2292 }
2293
2294 $filename = 'mxchat-leads-' . date('Y-m-d') . '.csv';
2295 header('Content-Type: text/csv');
2296 header('Content-Disposition: attachment; filename="' . $filename . '"');
2297 header('Pragma: no-cache');
2298 header('Expires: 0');
2299
2300 $output = fopen('php://output', 'w');
2301 fputs($output, "\xEF\xBB\xBF"); // UTF-8 BOM for Excel
2302
2303 if ($fields_mode === 'email_only') {
2304 fputcsv($output, ['Email']);
2305 foreach ($export_rows as $r) {
2306 fputcsv($output, [$r['email']]);
2307 }
2308 } else {
2309 fputcsv($output, ['Email', 'Name', 'Conversations', 'Last seen', 'Top page']);
2310 foreach ($export_rows as $r) {
2311 fputcsv($output, [
2312 $r['email'],
2313 $r['name'],
2314 $r['conversation_count'],
2315 $r['last_seen'],
2316 $r['top_page_url'],
2317 ]);
2318 }
2319 }
2320
2321 fclose($output);
2322 wp_die();
2323 }
2324
2325 /**
2326 * Stats strip payload (independent of filters).
2327 */
2328 private static function mxchat_leads_stats($table, $has_page_url_column) {
2329 global $wpdb;
2330
2331 $total_transcripts_emails = (int) $wpdb->get_var(
2332 "SELECT COUNT(DISTINCT user_email) FROM {$table}
2333 WHERE user_email IS NOT NULL AND user_email != ''"
2334 );
2335
2336 $new_this_week = (int) $wpdb->get_var($wpdb->prepare(
2337 "SELECT COUNT(*) FROM (
2338 SELECT user_email FROM {$table}
2339 WHERE user_email IS NOT NULL AND user_email != ''
2340 GROUP BY user_email
2341 HAVING MIN(timestamp) >= %s
2342 ) AS new_leads",
2343 gmdate('Y-m-d H:i:s', strtotime('-7 days'))
2344 ));
2345
2346 $total_convos = (int) $wpdb->get_var(
2347 "SELECT COUNT(DISTINCT session_id) FROM {$table}
2348 WHERE user_email IS NOT NULL AND user_email != ''"
2349 );
2350
2351 $orphan_count = count(self::mxchat_collect_orphan_leads(''));
2352 $chat_deleted_count = self::mxchat_count_chat_deleted_leads();
2353
2354 // Total leads = unique emails across all three sources (dedup priority: transcripts > chat_deleted > orphan
2355 // is already enforced at collection time in mxchat_fetch_leads; stats re-apply it here).
2356 $total_leads = $total_transcripts_emails + $chat_deleted_count + $orphan_count;
2357
2358 $avg = $total_transcripts_emails > 0
2359 ? round($total_convos / $total_transcripts_emails, 1)
2360 : 0;
2361
2362 // Orphan % reflects *true* orphans only (pre-chat dropoffs). Chat-deleted leads are
2363 // excluded so the metric stays meaningful — admins shouldn't see their cleanups
2364 // inflate this number.
2365 $orphan_pct = $total_leads > 0
2366 ? (int) round(($orphan_count / $total_leads) * 100)
2367 : 0;
2368
2369 return [
2370 'total_leads' => $total_leads,
2371 'new_this_week' => $new_this_week,
2372 'avg_convos' => $avg,
2373 'orphan_pct' => $orphan_pct,
2374 'orphan_count' => $orphan_count,
2375 'chat_deleted_count' => $chat_deleted_count,
2376 ];
2377 }
2378
2379 /**
2380 * Collect leads who had a conversation that an admin later deleted (preserved via
2381 * mxchat_lead_del_* options). Returns rows tagged status='chat_deleted' with the
2382 * original last-seen timestamp so they still sort and filter sensibly.
2383 *
2384 * @param string $search Optional email/name substring filter.
2385 * @param string $date_cutoff Optional 'Y-m-d H:i:s' cutoff — only rows with last_ts >= cutoff.
2386 * @return array
2387 */
2388 private static function mxchat_collect_chat_deleted_leads($search = '', $date_cutoff = '') {
2389 global $wpdb;
2390 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2391
2392 $rows = $wpdb->get_results(
2393 "SELECT option_name, option_value FROM {$wpdb->options}
2394 WHERE option_name LIKE 'mxchat_lead_del_email_%'"
2395 );
2396 if (empty($rows)) {
2397 return [];
2398 }
2399
2400 // Emails that currently have transcripts rows should not appear as chat_deleted —
2401 // they've come back and chatted, so they're active leads again.
2402 $emails_in_transcripts = array_map(
2403 'strtolower',
2404 (array) $wpdb->get_col(
2405 "SELECT DISTINCT user_email FROM {$table}
2406 WHERE user_email IS NOT NULL AND user_email != ''"
2407 )
2408 );
2409 $emails_in_transcripts = array_flip($emails_in_transcripts);
2410
2411 $needle = strtolower(trim((string) $search));
2412 $by_email = [];
2413
2414 foreach ($rows as $opt) {
2415 $email = sanitize_email(trim((string) $opt->option_value));
2416 if (!$email) {
2417 continue;
2418 }
2419 if (isset($emails_in_transcripts[strtolower($email)])) {
2420 continue;
2421 }
2422 $sid = substr($opt->option_name, strlen('mxchat_lead_del_email_'));
2423 if (!$sid) {
2424 continue;
2425 }
2426 $name = (string) get_option('mxchat_lead_del_name_' . $sid, '');
2427 $ts = (string) get_option('mxchat_lead_del_ts_' . $sid, '');
2428
2429 if ($date_cutoff !== '' && ($ts === '' || $ts < $date_cutoff)) {
2430 continue;
2431 }
2432 if ($needle !== '') {
2433 $hay = strtolower($email . ' ' . $name);
2434 if (strpos($hay, $needle) === false) {
2435 continue;
2436 }
2437 }
2438
2439 $key = strtolower($email);
2440 if (!isset($by_email[$key]) || (isset($by_email[$key]['last_seen']) && $ts > $by_email[$key]['last_seen'])) {
2441 $by_email[$key] = [
2442 'email' => $email,
2443 'name' => $name,
2444 'conversation_count' => 0,
2445 'last_seen' => $ts,
2446 'last_seen_display' => $ts ? self::mxchat_leads_format_relative($ts) : __('Chat deleted', 'mxchat'),
2447 'first_seen' => $ts,
2448 'latest_session_id' => '',
2449 'top_page_url' => '',
2450 'top_page_title' => '',
2451 'is_orphan' => false,
2452 'status' => 'chat_deleted',
2453 ];
2454 }
2455 }
2456
2457 // Newest chat_deleted first.
2458 usort($by_email, function ($a, $b) {
2459 return strcmp((string) $b['last_seen'], (string) $a['last_seen']);
2460 });
2461 return array_values($by_email);
2462 }
2463
2464 /**
2465 * Count unique emails preserved as "chat deleted" (for the stats strip).
2466 */
2467 private static function mxchat_count_chat_deleted_leads() {
2468 global $wpdb;
2469 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2470
2471 $emails = $wpdb->get_col(
2472 "SELECT DISTINCT option_value FROM {$wpdb->options}
2473 WHERE option_name LIKE 'mxchat_lead_del_email_%'"
2474 );
2475 if (empty($emails)) {
2476 return 0;
2477 }
2478
2479 $transcripts_emails = array_map(
2480 'strtolower',
2481 (array) $wpdb->get_col(
2482 "SELECT DISTINCT user_email FROM {$table}
2483 WHERE user_email IS NOT NULL AND user_email != ''"
2484 )
2485 );
2486 $transcripts_emails = array_flip($transcripts_emails);
2487
2488 $count = 0;
2489 $seen = [];
2490 foreach ($emails as $raw) {
2491 $email = strtolower(trim((string) $raw));
2492 if (!$email || isset($seen[$email]) || isset($transcripts_emails[$email])) {
2493 continue;
2494 }
2495 $seen[$email] = true;
2496 $count++;
2497 }
2498 return $count;
2499 }
2500
2501 /**
2502 * Find leads who submitted the pre-chat form but never produced a transcripts row.
2503 * Returned rows have no conversation_count, no timestamp.
2504 *
2505 * @param string $search Optional email/name substring filter.
2506 * @return array
2507 */
2508 private static function mxchat_collect_orphan_leads($search = '') {
2509 global $wpdb;
2510 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2511
2512 $option_rows = $wpdb->get_results(
2513 "SELECT option_name, option_value FROM {$wpdb->options}
2514 WHERE option_name LIKE 'mxchat_email_%'"
2515 );
2516 if (empty($option_rows)) {
2517 return [];
2518 }
2519
2520 // Collect all session_ids that have real transcripts rows so we can exclude them.
2521 $session_ids_with_rows = $wpdb->get_col(
2522 "SELECT DISTINCT session_id FROM {$table}
2523 WHERE user_email IS NOT NULL AND user_email != ''"
2524 );
2525 $session_ids_with_rows = array_flip($session_ids_with_rows);
2526
2527 // Seen emails in transcripts (so orphans only include truly never-chatted leads).
2528 $emails_in_transcripts = array_map(
2529 'strtolower',
2530 (array) $wpdb->get_col(
2531 "SELECT DISTINCT user_email FROM {$table}
2532 WHERE user_email IS NOT NULL AND user_email != ''"
2533 )
2534 );
2535 $emails_in_transcripts = array_flip($emails_in_transcripts);
2536
2537 $orphans_by_email = [];
2538 $needle = strtolower(trim((string) $search));
2539
2540 foreach ($option_rows as $opt) {
2541 $email = sanitize_email(trim((string) $opt->option_value));
2542 if (!$email) {
2543 continue;
2544 }
2545 $sid = substr($opt->option_name, strlen('mxchat_email_'));
2546 if (!$sid) {
2547 continue;
2548 }
2549 // Exclude leads who have any transcripts rows (they appear in the main list).
2550 if (isset($emails_in_transcripts[strtolower($email)])) {
2551 continue;
2552 }
2553 if (isset($session_ids_with_rows[$sid])) {
2554 continue;
2555 }
2556
2557 $name_option = get_option('mxchat_name_' . $sid, '');
2558 $name = is_string($name_option) ? trim($name_option) : '';
2559
2560 if ($needle !== '') {
2561 $hay = strtolower($email . ' ' . $name);
2562 if (strpos($hay, $needle) === false) {
2563 continue;
2564 }
2565 }
2566
2567 $key = strtolower($email);
2568 if (!isset($orphans_by_email[$key])) {
2569 $orphans_by_email[$key] = [
2570 'email' => $email,
2571 'name' => $name,
2572 'conversation_count' => 0,
2573 'last_seen' => '',
2574 'last_seen_display' => __('No conversation yet', 'mxchat'),
2575 'first_seen' => '',
2576 'latest_session_id' => '',
2577 'top_page_url' => '',
2578 'top_page_title' => '',
2579 'is_orphan' => true,
2580 'status' => 'orphan',
2581 ];
2582 }
2583 }
2584
2585 return array_values($orphans_by_email);
2586 }
2587
2588 /**
2589 * Map a date_range key to a SQL-comparable cutoff string, or '' for all-time.
2590 */
2591 private static function mxchat_leads_date_cutoff($date_range) {
2592 switch ($date_range) {
2593 case 'today': return gmdate('Y-m-d H:i:s', strtotime('-24 hours'));
2594 case '7d': return gmdate('Y-m-d H:i:s', strtotime('-7 days'));
2595 case '30d': return gmdate('Y-m-d H:i:s', strtotime('-30 days'));
2596 case '90d': return gmdate('Y-m-d H:i:s', strtotime('-90 days'));
2597 case 'all':
2598 default: return '';
2599 }
2600 }
2601
2602 /**
2603 * Turn a UTC timestamp into a short relative display like "2h ago" or "Apr 12".
2604 */
2605 private static function mxchat_leads_format_relative($timestamp) {
2606 if (!$timestamp) {
2607 return '';
2608 }
2609 $ts = strtotime($timestamp . ' UTC');
2610 if (!$ts) {
2611 return '';
2612 }
2613 $diff = time() - $ts;
2614 if ($diff < 60) return __('just now', 'mxchat');
2615 if ($diff < 3600) return floor($diff / 60) . __('m ago', 'mxchat');
2616 if ($diff < 86400) return floor($diff / 3600) . __('h ago', 'mxchat');
2617 if ($diff < 604800) return floor($diff / 86400) . __('d ago', 'mxchat');
2618 return wp_date('M j', $ts);
2619 }
2620
2621 /**
2622 * Handle translation of chat messages via AJAX
2623 */
2624 public function mxchat_translate_messages() {
2625 if (!current_user_can('manage_options')) {
2626 wp_send_json_error(['error' => 'Insufficient permissions']);
2627 wp_die();
2628 }
2629
2630 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
2631 $target_lang = isset($_POST['target_lang']) ? sanitize_text_field($_POST['target_lang']) : 'en';
2632 $messages_json = isset($_POST['messages']) ? wp_unslash($_POST['messages']) : '[]';
2633 $messages = json_decode($messages_json, true);
2634
2635 if (empty($session_id)) {
2636 wp_send_json_error(['error' => 'No session ID provided']);
2637 wp_die();
2638 }
2639
2640 if (empty($messages) || !is_array($messages)) {
2641 wp_send_json_error(['error' => 'No messages to translate']);
2642 wp_die();
2643 }
2644
2645 // Language names for prompting
2646 $languages = [
2647 'en' => 'English',
2648 'es' => 'Spanish',
2649 'fr' => 'French',
2650 'de' => 'German',
2651 'it' => 'Italian',
2652 'pt' => 'Portuguese',
2653 'nl' => 'Dutch',
2654 'ru' => 'Russian',
2655 'zh' => 'Chinese',
2656 'ja' => 'Japanese',
2657 'ko' => 'Korean',
2658 'ar' => 'Arabic',
2659 'hi' => 'Hindi',
2660 'tr' => 'Turkish',
2661 'pl' => 'Polish',
2662 'vi' => 'Vietnamese',
2663 'th' => 'Thai',
2664 'id' => 'Indonesian',
2665 'sv' => 'Swedish',
2666 'da' => 'Danish'
2667 ];
2668
2669 $target_lang_name = isset($languages[$target_lang]) ? $languages[$target_lang] : 'English';
2670
2671 // Build combined text for translation (numbered for parsing)
2672 $numbered_messages = [];
2673 foreach ($messages as $i => $msg) {
2674 $content = isset($msg['content']) ? trim($msg['content']) : '';
2675 if (!empty($content)) {
2676 $numbered_messages[] = "[MSG" . $i . "]" . $content . "[/MSG" . $i . "]";
2677 }
2678 }
2679
2680 if (empty($numbered_messages)) {
2681 wp_send_json_error(['error' => 'No valid messages to translate']);
2682 wp_die();
2683 }
2684
2685 $combined_text = implode("\n\n", $numbered_messages);
2686
2687 // Prepare the translation prompt
2688 $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.";
2689
2690 // Get user's selected model and determine provider
2691 $options = get_option('mxchat_options', []);
2692 $selected_model = $options['model'] ?? 'gpt-5.1-chat-latest';
2693
2694 // Check if using OpenRouter
2695 if ($selected_model === 'openrouter') {
2696 $provider = 'openrouter';
2697 $selected_model = $options['openrouter_selected_model'] ?? '';
2698 $api_key = $options['openrouter_api_key'] ?? '';
2699
2700 if (empty($selected_model)) {
2701 wp_send_json_error(['error' => 'No OpenRouter model selected']);
2702 wp_die();
2703 }
2704 } else {
2705 // Determine provider from model name
2706 $model_parts = explode('-', $selected_model);
2707 $provider = strtolower($model_parts[0]);
2708
2709 // Get the appropriate API key based on provider
2710 $api_key = '';
2711 switch ($provider) {
2712 case 'gpt':
2713 case 'o1':
2714 $api_key = $options['api_key'] ?? '';
2715 break;
2716 case 'claude':
2717 $api_key = $options['claude_api_key'] ?? '';
2718 break;
2719 case 'grok':
2720 $api_key = $options['xai_api_key'] ?? '';
2721 break;
2722 case 'deepseek':
2723 $api_key = $options['deepseek_api_key'] ?? '';
2724 break;
2725 case 'gemini':
2726 $api_key = $options['gemini_api_key'] ?? '';
2727 break;
2728 default:
2729 // Default to OpenAI
2730 $api_key = $options['api_key'] ?? '';
2731 $provider = 'gpt';
2732 break;
2733 }
2734 }
2735
2736 if (empty($api_key)) {
2737 wp_send_json_error(['error' => 'No API key configured for ' . $provider]);
2738 wp_die();
2739 }
2740
2741 // Make API request based on provider
2742 $response = $this->translate_with_provider($provider, $selected_model, $api_key, $system_prompt, $combined_text);
2743
2744 if (is_wp_error($response)) {
2745 wp_send_json_error(['error' => $response->get_error_message()]);
2746 wp_die();
2747 }
2748
2749 // Parse the response to extract translated messages
2750 $translations = [];
2751 foreach ($messages as $msg) {
2752 $index = $msg['index'];
2753 $pattern = '/\[MSG' . $index . '\](.*?)\[\/MSG' . $index . '\]/s';
2754 if (preg_match($pattern, $response, $matches)) {
2755 $translations[] = [
2756 'index' => $index,
2757 'translated' => trim($matches[1])
2758 ];
2759 }
2760 }
2761
2762 // Save translations to database
2763 if (!empty($translations)) {
2764 $this->save_transcript_translation($session_id, $target_lang, $translations);
2765 }
2766
2767 wp_send_json(['success' => true, 'translations' => $translations, 'language' => $target_lang]);
2768 wp_die();
2769 }
2770
2771 /**
2772 * Save transcript translation to database
2773 */
2774 private function save_transcript_translation($session_id, $language_code, $translations) {
2775 global $wpdb;
2776 $table_name = $wpdb->prefix . 'mxchat_transcript_translations';
2777
2778 // Check if table exists, create if not
2779 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") !== $table_name) {
2780 mxchat_create_translations_table();
2781 }
2782
2783 $now = current_time('mysql');
2784 $translations_json = wp_json_encode($translations);
2785
2786 // Use REPLACE to insert or update
2787 $wpdb->query($wpdb->prepare(
2788 "REPLACE INTO $table_name (session_id, language_code, translations, created_at, updated_at)
2789 VALUES (%s, %s, %s, %s, %s)",
2790 $session_id,
2791 $language_code,
2792 $translations_json,
2793 $now,
2794 $now
2795 ));
2796 }
2797
2798 /**
2799 * Get saved translation for a session
2800 */
2801 public function mxchat_get_transcript_translation() {
2802 if (!current_user_can('manage_options')) {
2803 wp_send_json_error(['error' => 'Insufficient permissions']);
2804 wp_die();
2805 }
2806
2807 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
2808
2809 if (empty($session_id)) {
2810 wp_send_json_error(['error' => 'No session ID provided']);
2811 wp_die();
2812 }
2813
2814 global $wpdb;
2815 $table_name = $wpdb->prefix . 'mxchat_transcript_translations';
2816
2817 // Check if table exists
2818 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") !== $table_name) {
2819 wp_send_json(['success' => true, 'has_translation' => false]);
2820 wp_die();
2821 }
2822
2823 // Get the most recent translation for this session
2824 $result = $wpdb->get_row($wpdb->prepare(
2825 "SELECT language_code, translations FROM $table_name WHERE session_id = %s ORDER BY updated_at DESC LIMIT 1",
2826 $session_id
2827 ));
2828
2829 if ($result) {
2830 $translations = json_decode($result->translations, true);
2831 wp_send_json([
2832 'success' => true,
2833 'has_translation' => true,
2834 'language' => $result->language_code,
2835 'translations' => $translations
2836 ]);
2837 } else {
2838 wp_send_json(['success' => true, 'has_translation' => false]);
2839 }
2840 wp_die();
2841 }
2842
2843 /**
2844 * Translate text using the user's selected provider and model
2845 */
2846 private function translate_with_provider($provider, $model, $api_key, $system_prompt, $text) {
2847 switch ($provider) {
2848 case 'claude':
2849 return $this->translate_with_claude($api_key, $model, $system_prompt, $text);
2850 case 'grok':
2851 return $this->translate_with_xai($api_key, $model, $system_prompt, $text);
2852 case 'deepseek':
2853 return $this->translate_with_deepseek($api_key, $model, $system_prompt, $text);
2854 case 'gemini':
2855 return $this->translate_with_gemini($api_key, $model, $system_prompt, $text);
2856 case 'openrouter':
2857 return $this->translate_with_openrouter($api_key, $model, $system_prompt, $text);
2858 case 'gpt':
2859 case 'o1':
2860 default:
2861 return $this->translate_with_openai($api_key, $model, $system_prompt, $text);
2862 }
2863 }
2864
2865 /**
2866 * Translate text using OpenAI API
2867 */
2868 private function translate_with_openai($api_key, $model, $system_prompt, $text) {
2869 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', [
2870 'timeout' => 60,
2871 'headers' => [
2872 'Authorization' => 'Bearer ' . $api_key,
2873 'Content-Type' => 'application/json'
2874 ],
2875 'body' => wp_json_encode([
2876 'model' => $model,
2877 'messages' => [
2878 ['role' => 'system', 'content' => $system_prompt],
2879 ['role' => 'user', 'content' => $text]
2880 ],
2881 'temperature' => 0.3
2882 ])
2883 ]);
2884
2885 if (is_wp_error($response)) {
2886 return $response;
2887 }
2888
2889 $body = json_decode(wp_remote_retrieve_body($response), true);
2890
2891 if (isset($body['error'])) {
2892 return new WP_Error('api_error', $body['error']['message']);
2893 }
2894
2895 if (isset($body['choices'][0]['message']['content'])) {
2896 return $body['choices'][0]['message']['content'];
2897 }
2898
2899 return new WP_Error('api_error', 'Invalid API response');
2900 }
2901
2902 /**
2903 * Translate text using Claude API
2904 */
2905 private function translate_with_claude($api_key, $model, $system_prompt, $text) {
2906 // Anthropic retired claude-opus-4-20250514 / claude-sonnet-4-20250514 on 2026-06-15.
2907 // Read-time rescue: remap a saved dead ID to the current equivalent before the API call.
2908 if ($model === 'claude-opus-4-20250514') { $model = 'claude-opus-4-8'; }
2909 elseif ($model === 'claude-sonnet-4-20250514') { $model = 'claude-sonnet-4-6'; }
2910 $response = wp_remote_post('https://api.anthropic.com/v1/messages', [
2911 'timeout' => 60,
2912 'headers' => [
2913 'x-api-key' => $api_key,
2914 'anthropic-version' => '2023-06-01',
2915 'Content-Type' => 'application/json'
2916 ],
2917 'body' => wp_json_encode([
2918 'model' => $model,
2919 'max_tokens' => 4096,
2920 'system' => $system_prompt,
2921 'messages' => [
2922 ['role' => 'user', 'content' => $text]
2923 ]
2924 ])
2925 ]);
2926
2927 if (is_wp_error($response)) {
2928 return $response;
2929 }
2930
2931 $body = json_decode(wp_remote_retrieve_body($response), true);
2932
2933 if (isset($body['error'])) {
2934 return new WP_Error('api_error', $body['error']['message']);
2935 }
2936
2937 if (isset($body['content'][0]['text'])) {
2938 return $body['content'][0]['text'];
2939 }
2940
2941 return new WP_Error('api_error', 'Invalid API response');
2942 }
2943
2944 /**
2945 * Translate text using xAI (Grok) API
2946 */
2947 private function translate_with_xai($api_key, $model, $system_prompt, $text) {
2948 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', [
2949 'timeout' => 60,
2950 'headers' => [
2951 'Authorization' => 'Bearer ' . $api_key,
2952 'Content-Type' => 'application/json'
2953 ],
2954 'body' => wp_json_encode([
2955 'model' => $model,
2956 'messages' => [
2957 ['role' => 'system', 'content' => $system_prompt],
2958 ['role' => 'user', 'content' => $text]
2959 ],
2960 'temperature' => 0.3
2961 ])
2962 ]);
2963
2964 if (is_wp_error($response)) {
2965 return $response;
2966 }
2967
2968 $body = json_decode(wp_remote_retrieve_body($response), true);
2969
2970 if (isset($body['error'])) {
2971 return new WP_Error('api_error', $body['error']['message']);
2972 }
2973
2974 if (isset($body['choices'][0]['message']['content'])) {
2975 return $body['choices'][0]['message']['content'];
2976 }
2977
2978 return new WP_Error('api_error', 'Invalid API response');
2979 }
2980
2981 /**
2982 * Translate text using DeepSeek API
2983 */
2984 private function translate_with_deepseek($api_key, $model, $system_prompt, $text) {
2985 $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', [
2986 'timeout' => 60,
2987 'headers' => [
2988 'Authorization' => 'Bearer ' . $api_key,
2989 'Content-Type' => 'application/json'
2990 ],
2991 'body' => wp_json_encode([
2992 'model' => $model,
2993 'messages' => [
2994 ['role' => 'system', 'content' => $system_prompt],
2995 ['role' => 'user', 'content' => $text]
2996 ],
2997 'temperature' => 0.3
2998 ])
2999 ]);
3000
3001 if (is_wp_error($response)) {
3002 return $response;
3003 }
3004
3005 $body = json_decode(wp_remote_retrieve_body($response), true);
3006
3007 if (isset($body['error'])) {
3008 return new WP_Error('api_error', $body['error']['message']);
3009 }
3010
3011 if (isset($body['choices'][0]['message']['content'])) {
3012 return $body['choices'][0]['message']['content'];
3013 }
3014
3015 return new WP_Error('api_error', 'Invalid API response');
3016 }
3017
3018 /**
3019 * Translate text using Google Gemini API
3020 */
3021 private function translate_with_gemini($api_key, $model, $system_prompt, $text) {
3022 if ($model === 'gemini-3-pro-preview') {
3023 $model = 'gemini-3.1-pro-preview';
3024 }
3025 $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':generateContent?key=' . $api_key;
3026
3027 $response = wp_remote_post($url, [
3028 'timeout' => 60,
3029 'headers' => [
3030 'Content-Type' => 'application/json'
3031 ],
3032 'body' => wp_json_encode([
3033 'contents' => [
3034 [
3035 'parts' => [
3036 ['text' => $system_prompt . "\n\n" . $text]
3037 ]
3038 ]
3039 ],
3040 'generationConfig' => [
3041 'temperature' => 0.3
3042 ]
3043 ])
3044 ]);
3045
3046 if (is_wp_error($response)) {
3047 return $response;
3048 }
3049
3050 $body = json_decode(wp_remote_retrieve_body($response), true);
3051
3052 if (isset($body['error'])) {
3053 return new WP_Error('api_error', $body['error']['message']);
3054 }
3055
3056 if (isset($body['candidates'][0]['content']['parts'][0]['text'])) {
3057 return $body['candidates'][0]['content']['parts'][0]['text'];
3058 }
3059
3060 return new WP_Error('api_error', 'Invalid API response');
3061 }
3062
3063 /**
3064 * Translate text using OpenRouter API
3065 */
3066 private function translate_with_openrouter($api_key, $model, $system_prompt, $text) {
3067 $response = wp_remote_post('https://openrouter.ai/api/v1/chat/completions', [
3068 'timeout' => 60,
3069 'headers' => [
3070 'Authorization' => 'Bearer ' . $api_key,
3071 'Content-Type' => 'application/json',
3072 'HTTP-Referer' => home_url(),
3073 'X-Title' => 'MxChat Translation'
3074 ],
3075 'body' => wp_json_encode([
3076 'model' => $model,
3077 'messages' => [
3078 ['role' => 'system', 'content' => $system_prompt],
3079 ['role' => 'user', 'content' => $text]
3080 ],
3081 'temperature' => 0.3
3082 ])
3083 ]);
3084
3085 if (is_wp_error($response)) {
3086 return $response;
3087 }
3088
3089 $body = json_decode(wp_remote_retrieve_body($response), true);
3090
3091 if (isset($body['error'])) {
3092 return new WP_Error('api_error', $body['error']['message']);
3093 }
3094
3095 if (isset($body['choices'][0]['message']['content'])) {
3096 return $body['choices'][0]['message']['content'];
3097 }
3098
3099 return new WP_Error('api_error', 'Invalid API response');
3100 }
3101
3102 public function mxchat_fetch_chat_history() {
3103 global $wpdb;
3104 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
3105 $url_clicks_table = $wpdb->prefix . 'mxchat_url_clicks';
3106
3107 if (!current_user_can('manage_options')) {
3108 wp_send_json_error(['message' => 'Insufficient permissions']);
3109 wp_die();
3110 }
3111
3112 $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
3113 $per_page = isset($_POST['per_page']) ? absint($_POST['per_page']) : 50;
3114 $offset = ($page - 1) * $per_page;
3115 $search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
3116 $sort_raw = isset($_POST['sort_order']) ? sanitize_key($_POST['sort_order']) : 'desc';
3117 $allowed_sorts = array('asc', 'desc', 'rating_positive', 'rating_negative');
3118 if (!in_array($sort_raw, $allowed_sorts, true)) { $sort_raw = 'desc'; }
3119 $sort_order = ($sort_raw === 'asc') ? 'ASC' : 'DESC';
3120 $ratings_table = $wpdb->prefix . 'mxchat_session_ratings';
3121 $ratings_join = '';
3122 $rating_table_exists = ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $ratings_table)) === $ratings_table);
3123 if ($rating_table_exists && ($sort_raw === 'rating_positive' || $sort_raw === 'rating_negative')) {
3124 // We sort by rating then recency. Wrap rating_value so NULL sorts last.
3125 $rating_dir = ($sort_raw === 'rating_positive') ? 'DESC' : 'ASC';
3126 $ratings_join = " LEFT JOIN {$ratings_table} r ON r.session_id = t.session_id ";
3127 }
3128
3129 // Build search condition
3130 $search_condition = '';
3131 $search_params = [];
3132 if (!empty($search)) {
3133 $search_condition = "WHERE (
3134 session_id LIKE %s
3135 OR user_email LIKE %s
3136 OR user_name LIKE %s
3137 OR user_identifier LIKE %s
3138 OR message LIKE %s
3139 )";
3140 $search_params = array_fill(0, 5, '%' . $wpdb->esc_like($search) . '%');
3141 }
3142
3143 // Get total count
3144 $count_query = !empty($search)
3145 ? $wpdb->prepare("SELECT COUNT(DISTINCT session_id) FROM {$table_name} {$search_condition}", $search_params)
3146 : "SELECT COUNT(DISTINCT session_id) FROM {$table_name}";
3147 $total_sessions = (int) $wpdb->get_var($count_query);
3148
3149 // Get session IDs for current page. Default sort is recency; rating sorts join the ratings table
3150 // and order by rating value (NULLs last) with recency as tiebreaker.
3151 if ($ratings_join) {
3152 $order_by = "ORDER BY (r.rating_value IS NULL), r.rating_value {$rating_dir}, MAX(t.timestamp) DESC";
3153 } else {
3154 $order_by = "ORDER BY MAX(t.timestamp) {$sort_order}";
3155 }
3156 $session_query = !empty($search)
3157 ? $wpdb->prepare(
3158 "SELECT DISTINCT t.session_id FROM {$table_name} t {$ratings_join} {$search_condition}
3159 GROUP BY t.session_id {$order_by} LIMIT %d OFFSET %d",
3160 array_merge($search_params, [$per_page, $offset])
3161 )
3162 : $wpdb->prepare(
3163 "SELECT DISTINCT t.session_id FROM {$table_name} t {$ratings_join}
3164 GROUP BY t.session_id {$order_by} LIMIT %d OFFSET %d",
3165 $per_page, $offset
3166 );
3167 $session_ids = $wpdb->get_col($session_query);
3168
3169 $total_pages = ceil($total_sessions / $per_page);
3170
3171 if (empty($session_ids)) {
3172 wp_send_json([
3173 'success' => true,
3174 'sessions' => [],
3175 'page' => $page,
3176 'total_pages' => 0,
3177 'total_sessions' => 0,
3178 'showing_start' => 0,
3179 'showing_end' => 0
3180 ]);
3181 wp_die();
3182 }
3183
3184 // Check for optional columns/tables
3185 $url_table_exists = $wpdb->get_var("SHOW TABLES LIKE '$url_clicks_table'") === $url_clicks_table;
3186 $originating_columns_exist = !empty($wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'"));
3187
3188 // Batch-fetch session ratings for this page (plan-a5b006).
3189 $ratings_map = array();
3190 if ($rating_table_exists && !empty($session_ids)) {
3191 $placeholders = implode(',', array_fill(0, count($session_ids), '%s'));
3192 $rating_rows = $wpdb->get_results($wpdb->prepare(
3193 "SELECT session_id, rating_value, rating_feedback FROM {$ratings_table} WHERE session_id IN ($placeholders)",
3194 $session_ids
3195 ));
3196 foreach ($rating_rows as $row) {
3197 $ratings_map[$row->session_id] = array(
3198 'value' => (int) $row->rating_value,
3199 'feedback' => (string) $row->rating_feedback,
3200 );
3201 }
3202 }
3203
3204 // Build session list data
3205 $sessions = [];
3206 foreach ($session_ids as $session_id) {
3207 // Get session metadata
3208 $session_data = $originating_columns_exist
3209 ? $wpdb->get_row($wpdb->prepare(
3210 "SELECT user_email, user_name, user_identifier, originating_page_url, originating_page_title, timestamp
3211 FROM {$table_name} WHERE session_id = %s ORDER BY timestamp ASC LIMIT 1",
3212 $session_id
3213 ))
3214 : $wpdb->get_row($wpdb->prepare(
3215 "SELECT user_email, user_name, user_identifier, timestamp FROM {$table_name}
3216 WHERE session_id = %s LIMIT 1",
3217 $session_id
3218 ));
3219
3220 // Get message count and latest timestamp
3221 $message_stats = $wpdb->get_row($wpdb->prepare(
3222 "SELECT COUNT(*) as count, MAX(timestamp) as latest FROM {$table_name} WHERE session_id = %s",
3223 $session_id
3224 ));
3225
3226 // Get first user message as preview
3227 $first_user_msg = $wpdb->get_var($wpdb->prepare(
3228 "SELECT message FROM {$table_name} WHERE session_id = %s AND role = 'user' ORDER BY timestamp ASC LIMIT 1",
3229 $session_id
3230 ));
3231 $preview = $first_user_msg ? wp_trim_words(wp_strip_all_tags(stripslashes($first_user_msg)), 12, '...') : 'No messages';
3232
3233 // Build display name
3234 $user_email = !empty($session_data->user_email) ? $session_data->user_email : '';
3235 $user_name = !empty($session_data->user_name) ? $session_data->user_name : '';
3236 $user_identifier = !empty($session_data->user_identifier) ? $session_data->user_identifier : 'Guest';
3237
3238 $display_name = $user_name ?: ($user_email ? explode('@', $user_email)[0] : $user_identifier);
3239 $display_sub = $user_email ?: ('ID: ' . $user_identifier);
3240
3241 // Format time - show relative for recent, date for older
3242 $timestamp = strtotime($message_stats->latest . ' UTC');
3243 $now = time();
3244 $diff = $now - $timestamp;
3245 if ($diff < 3600) {
3246 $time_display = floor($diff / 60) . 'm ago';
3247 } elseif ($diff < 86400) {
3248 $time_display = floor($diff / 3600) . 'h ago';
3249 } elseif ($diff < 604800) {
3250 $time_display = floor($diff / 86400) . 'd ago';
3251 } else {
3252 $time_display = wp_date('M j', $timestamp);
3253 }
3254
3255 // Get initials for avatar
3256 $initials = strtoupper(substr($display_name, 0, 2));
3257 if (strlen($display_name) > 2 && strpos($display_name, ' ') !== false) {
3258 $parts = explode(' ', $display_name);
3259 $initials = strtoupper(substr($parts[0], 0, 1) . substr(end($parts), 0, 1));
3260 }
3261
3262 $rating_entry = isset($ratings_map[$session_id]) ? $ratings_map[$session_id] : null;
3263 $rating_value = $rating_entry ? $rating_entry['value'] : null;
3264 $rating_feedback = $rating_entry ? $rating_entry['feedback'] : '';
3265
3266 $sessions[] = [
3267 'session_id' => $session_id,
3268 'display_name' => $display_name,
3269 'display_sub' => $display_sub,
3270 'initials' => $initials,
3271 'preview' => $preview,
3272 'message_count' => (int) $message_stats->count,
3273 'time_display' => $time_display,
3274 'timestamp' => $message_stats->latest,
3275 'rating_value' => $rating_value,
3276 'rating_feedback' => $rating_feedback,
3277 ];
3278 }
3279
3280 wp_send_json([
3281 'success' => true,
3282 'sessions' => $sessions,
3283 'page' => $page,
3284 'total_pages' => $total_pages,
3285 'total_sessions' => $total_sessions,
3286 'showing_start' => $offset + 1,
3287 'showing_end' => min($offset + $per_page, $total_sessions)
3288 ]);
3289 wp_die();
3290 }
3291
3292 /**
3293 * Fetch single conversation details for split-panel view
3294 */
3295 public function mxchat_fetch_conversation() {
3296 global $wpdb;
3297 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
3298 $url_clicks_table = $wpdb->prefix . 'mxchat_url_clicks';
3299
3300 if (!current_user_can('manage_options')) {
3301 wp_send_json_error(['message' => 'Insufficient permissions']);
3302 wp_die();
3303 }
3304
3305 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
3306 if (empty($session_id)) {
3307 wp_send_json_error(['message' => 'No session ID provided']);
3308 wp_die();
3309 }
3310
3311 // Check for optional columns/tables
3312 $url_table_exists = $wpdb->get_var("SHOW TABLES LIKE '$url_clicks_table'") === $url_clicks_table;
3313 $originating_columns_exist = !empty($wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'"));
3314
3315 // Get session metadata
3316 $session_data = $originating_columns_exist
3317 ? $wpdb->get_row($wpdb->prepare(
3318 "SELECT user_email, user_name, user_identifier, originating_page_url, originating_page_title, timestamp
3319 FROM {$table_name} WHERE session_id = %s ORDER BY timestamp ASC LIMIT 1",
3320 $session_id
3321 ))
3322 : $wpdb->get_row($wpdb->prepare(
3323 "SELECT user_email, user_name, user_identifier, timestamp FROM {$table_name}
3324 WHERE session_id = %s LIMIT 1",
3325 $session_id
3326 ));
3327
3328 if (!$session_data) {
3329 wp_send_json_error(['message' => 'Session not found']);
3330 wp_die();
3331 }
3332
3333 // Get clicked URLs
3334 $clicked_urls = [];
3335 if ($url_table_exists) {
3336 $url_clicks = $wpdb->get_results($wpdb->prepare(
3337 "SELECT DISTINCT clicked_url FROM {$url_clicks_table}
3338 WHERE session_id = %s ORDER BY click_timestamp ASC",
3339 $session_id
3340 ));
3341 foreach ($url_clicks as $click) {
3342 $clicked_urls[] = $click->clicked_url;
3343 }
3344 }
3345
3346 // Get all messages
3347 $messages = $wpdb->get_results($wpdb->prepare(
3348 "SELECT * FROM {$table_name} WHERE session_id = %s ORDER BY timestamp ASC",
3349 $session_id
3350 ));
3351
3352 // Build user info
3353 $user_email = !empty($session_data->user_email) ? $session_data->user_email : '';
3354 $user_name = !empty($session_data->user_name) ? $session_data->user_name : '';
3355 $user_identifier = !empty($session_data->user_identifier) ? $session_data->user_identifier : 'Guest';
3356
3357 $display_name = $user_name ?: ($user_email ? explode('@', $user_email)[0] : $user_identifier);
3358 $display_sub = $user_email ?: ('ID: ' . $user_identifier);
3359
3360 // Get initials
3361 $initials = strtoupper(substr($display_name, 0, 2));
3362 if (strlen($display_name) > 2 && strpos($display_name, ' ') !== false) {
3363 $parts = explode(' ', $display_name);
3364 $initials = strtoupper(substr($parts[0], 0, 1) . substr(end($parts), 0, 1));
3365 }
3366
3367 // Page info
3368 $page_url = $originating_columns_exist && !empty($session_data->originating_page_url) ? $session_data->originating_page_url : '';
3369 $page_title = '';
3370 if ($page_url) {
3371 $page_title = !empty($session_data->originating_page_title) ? $session_data->originating_page_title : parse_url($page_url, PHP_URL_PATH);
3372 }
3373
3374 // Format messages for output
3375 $formatted_messages = [];
3376 foreach ($messages as $msg) {
3377 $is_user = ($msg->role === 'user');
3378 $is_bot = ($msg->role === 'bot' || $msg->role === 'assistant');
3379
3380 $content = wp_kses(
3381 stripslashes($msg->message),
3382 [
3383 'b' => [], 'strong' => [], 'i' => [], 'em' => [], 'u' => [],
3384 'br' => [], 'p' => [], 'ul' => [], 'ol' => [], 'li' => [],
3385 'a' => ['href' => [], 'title' => [], 'target' => [], 'class' => []],
3386 'div' => ['class' => [], 'id' => [], 'data-nonce' => []],
3387 'img' => ['src' => [], 'alt' => [], 'class' => []],
3388 'h3' => ['class' => []],
3389 'h4' => ['class' => []],
3390 'button' => ['type' => [], 'class' => [], 'data-product-id' => [], 'data-nonce' => [], 'data-product-type' => [], 'data-original-text' => [], 'data-mxchat-action' => []],
3391 'select' => ['class' => [], 'data-attribute' => []],
3392 'option' => ['value' => []],
3393 'span' => ['class' => []],
3394 'del' => [], 'ins' => [],
3395 ]
3396 );
3397 $formatted_content = $this->format_transcript_message($content);
3398
3399 $has_rag = $is_bot && !empty($msg->rag_context);
3400
3401 $formatted_messages[] = [
3402 'id' => $msg->id,
3403 'role' => $msg->role,
3404 'is_user' => $is_user,
3405 'is_bot' => $is_bot,
3406 'content' => $formatted_content,
3407 'timestamp' => wp_date('g:i A', strtotime($msg->timestamp . ' UTC')),
3408 'full_timestamp' => wp_date('F j, Y g:i A', strtotime($msg->timestamp . ' UTC')),
3409 'has_rag' => $has_rag
3410 ];
3411 }
3412
3413 // First message timestamp for "started" display
3414 $started = !empty($messages) ? wp_date('M j, Y g:i A', strtotime($messages[0]->timestamp . ' UTC')) : '-';
3415
3416 // Pull rating_feedback for this session (if any) so the details drawer can show it.
3417 $rating_feedback = '';
3418 $ratings_table_det = $wpdb->prefix . 'mxchat_session_ratings';
3419 if ($wpdb->get_var("SHOW TABLES LIKE '$ratings_table_det'") === $ratings_table_det) {
3420 $rating_feedback = (string) $wpdb->get_var($wpdb->prepare(
3421 "SELECT rating_feedback FROM {$ratings_table_det} WHERE session_id = %s LIMIT 1",
3422 $session_id
3423 ));
3424 }
3425
3426 wp_send_json([
3427 'success' => true,
3428 'session_id' => $session_id,
3429 'user' => [
3430 'name' => $display_name,
3431 'sub' => $display_sub,
3432 'initials' => $initials,
3433 'email' => $user_email,
3434 'identifier' => $user_identifier
3435 ],
3436 'page' => [
3437 'url' => $page_url,
3438 'title' => $page_title
3439 ],
3440 'clicked_urls' => $clicked_urls,
3441 'messages' => $formatted_messages,
3442 'message_count' => count($messages),
3443 'started' => $started,
3444 'rating_feedback' => $rating_feedback
3445 ]);
3446 wp_die();
3447 }
3448
3449
3450 /**
3451 * ALTERNATIVE: Simpler helper method using string replacement
3452 */
3453 private function highlight_clicked_links($message_content, $clicked_urls) {
3454 if (empty($clicked_urls)) {
3455 return wp_kses(
3456 $message_content,
3457 [
3458 'b' => [], 'strong' => [], 'i' => [], 'em' => [], 'u' => [],
3459 'br' => [], 'p' => [], 'ul' => [], 'ol' => [], 'li' => [],
3460 'a' => ['href' => [], 'title' => [], 'target' => [], 'class' => []]
3461 ]
3462 );
3463 }
3464
3465 // First apply standard sanitization
3466 $message_content = wp_kses(
3467 $message_content,
3468 [
3469 'b' => [], 'strong' => [], 'i' => [], 'em' => [], 'u' => [],
3470 'br' => [], 'p' => [], 'ul' => [], 'ol' => [], 'li' => [],
3471 'a' => ['href' => [], 'title' => [], 'target' => [], 'class' => []]
3472 ]
3473 );
3474
3475 // Process each clicked URL
3476 foreach ($clicked_urls as $clicked_url) {
3477 // Try multiple patterns to catch different link formats
3478 $patterns = [
3479 // Standard link format
3480 '/<a([^>]*href=["\']' . preg_quote($clicked_url, '/') . '["\'][^>]*)>/i',
3481 // Link with trailing slash
3482 '/<a([^>]*href=["\']' . preg_quote(rtrim($clicked_url, '/'), '/') . '\/?["\'][^>]*)>/i',
3483 // Encoded entities version
3484 '/<a([^>]*href=["\']' . preg_quote(htmlentities($clicked_url), '/') . '["\'][^>]*)>/i',
3485 ];
3486
3487 foreach ($patterns as $pattern) {
3488 if (preg_match($pattern, $message_content)) {
3489 $message_content = preg_replace_callback(
3490 $pattern,
3491 function($matches) {
3492 $full_match = $matches[0];
3493 $attributes = $matches[1];
3494
3495 // Check if it already has the class
3496 if (strpos($full_match, 'mxchat-clicked-link') !== false) {
3497 return $full_match;
3498 }
3499
3500 // Check if class attribute exists
3501 if (preg_match('/class=["\']([^"\']*)["\']/', $attributes, $class_matches)) {
3502 // Add to existing class
3503 $new_attributes = preg_replace(
3504 '/class=["\']([^"\']*)["\']/',
3505 'class="$1 mxchat-clicked-link"',
3506 $attributes
3507 );
3508 } else {
3509 // Add new class attribute
3510 $new_attributes = $attributes . ' class="mxchat-clicked-link"';
3511 }
3512
3513 // Add title if not present
3514 if (strpos($new_attributes, 'title=') === false) {
3515 $new_attributes .= ' title="User clicked this link"';
3516 }
3517
3518 return '<a' . $new_attributes . '>';
3519 },
3520 $message_content
3521 );
3522
3523 // If we found and replaced, break out of the patterns loop
3524 break;
3525 }
3526 }
3527 }
3528
3529 return $message_content;
3530 }
3531
3532 public function mxchat_create_prompts_page() {
3533 //error_log('=== DEBUG: mxchat_create_prompts_page started ===');
3534
3535 global $wpdb;
3536 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3537
3538 $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
3539 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
3540
3541 // Display success message if all prompts were deleted
3542 if (isset($_GET['all_deleted']) && $_GET['all_deleted'] === 'true') {
3543 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>';
3544 }
3545
3546 // Set up pagination, search query, and content type filter
3547 $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field($_GET['_wpnonce']) : '';
3548 $search_query = (!empty($nonce) && wp_verify_nonce($nonce, 'mxchat_prompts_search_nonce') && isset($_GET['search'])) ? sanitize_text_field($_GET['search']) : '';
3549 $content_type_filter = isset($_GET['content_type']) ? sanitize_key($_GET['content_type']) : ''; // ADDED 2.5.6
3550 $current_page = isset($_GET['paged']) ? absint($_GET['paged']) : 1;
3551 $per_page = 25;
3552
3553 //error_log('DEBUG: Search query: ' . $search_query);
3554 //error_log('DEBUG: Content type filter: ' . $content_type_filter);
3555 //error_log('DEBUG: Current page: ' . $current_page);
3556 //error_log('DEBUG: Per page: ' . $per_page);
3557
3558 // ================================
3559 // MULTI-BOT CONFIGURATION
3560 // ================================
3561
3562 // Check for multi-bot and set up bot selection
3563 if (class_exists('MxChat_Multi_Bot_Manager')) {
3564 $multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
3565 $available_bots = $multi_bot_manager->get_available_bots();
3566
3567 // Get saved bot selection (user-specific first, then site-wide default)
3568 $user_id = get_current_user_id();
3569 $saved_bot_id = get_user_meta($user_id, 'mxchat_selected_knowledge_bot', true);
3570 if (empty($saved_bot_id)) {
3571 $saved_bot_id = get_option('mxchat_current_knowledge_bot', 'default');
3572 }
3573
3574 // Allow URL override but default to saved selection
3575 $current_bot_id = isset($_GET['bot_id']) ? sanitize_key($_GET['bot_id']) : $saved_bot_id;
3576 $multibot_active = true;
3577 } else {
3578 $current_bot_id = 'default';
3579 $multibot_active = false;
3580 }
3581
3582 // ================================
3583 // DATA SOURCE CONFIGURATION
3584 // ================================
3585
3586 // Get bot-specific Pinecone settings
3587 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($current_bot_id);
3588 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3589 $pinecone_api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3590
3591 // Get OpenAI Vector Store settings
3592 $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
3593 $use_vectorstore = ($vectorstore_options['mxchat_use_openai_vectorstore'] ?? '0') === '1';
3594
3595 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Use Pinecone: ' . ($use_pinecone ? 'YES' : 'NO'));
3596 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Has API Key: ' . (!empty($pinecone_api_key) ? 'YES' : 'NO'));
3597 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Host: ' . ($pinecone_options['mxchat_pinecone_host'] ?? 'NOT SET'));
3598 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Namespace: ' . ($pinecone_options['mxchat_pinecone_namespace'] ?? 'NOT SET'));
3599
3600 if ($use_pinecone && !empty($pinecone_api_key)) {
3601 //error_log('DEBUG: Using PINECONE data source with bot-specific config');
3602 // PINECONE DATA SOURCE
3603 $data_source = 'pinecone';
3604
3605 // IMPORTANT: Pass the bot-specific $pinecone_options, not default options!
3606 // UPDATED 2.5.6: Added content_type_filter parameter
3607 $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, $search_query, $current_page, $per_page, $current_bot_id, $content_type_filter);
3608
3609 // TEMPORARY DEBUG - Add this right after the fetch call
3610 //error_log('=== DEBUG: Bot switching issue ===');
3611 //error_log('Current bot ID: ' . $current_bot_id);
3612 //error_log('Use Pinecone: ' . ($use_pinecone ? 'YES' : 'NO'));
3613 //error_log('Records returned: ' . count($records['data'] ?? []));
3614 //error_log('Total records: ' . ($records['total'] ?? 0));
3615
3616 // Check first few records to see their bot_id
3617 if (!empty($records['data'])) {
3618 foreach (array_slice($records['data'], 0, 3) as $i => $record) {
3619 $record_bot_id = $record->bot_id ?? 'NOT_SET';
3620 //error_log('Record ' . ($i+1) . ' bot_id: ' . $record_bot_id . ', content preview: ' . substr($record->article_content ?? '', 0, 30) . '...');
3621 }
3622 }
3623 //error_log('=== END DEBUG ===');
3624
3625 $total_records = $records['total'] ?? 0;
3626 $prompts = $records['data'] ?? array();
3627 $total_in_database = $records['total_in_database'] ?? 0;
3628 $showing_recent_only = $records['showing_recent_only'] ?? false;
3629
3630 $total_pages = ceil($total_records / $per_page);
3631
3632 } else {
3633 //error_log('DEBUG: Using WORDPRESS DB data source');
3634 // WORDPRESS DB DATA SOURCE (your existing logic)
3635 $data_source = 'wordpress';
3636
3637 // Initialize these variables for WordPress DB
3638 $total_in_database = 0;
3639 $showing_recent_only = false;
3640
3641 $offset = ($current_page - 1) * $per_page;
3642
3643 // UPDATED 2.5.6: Build WHERE clause for search and content type filtering
3644 $where_clauses = array();
3645 $where_values = array();
3646
3647 if ($search_query) {
3648 $where_clauses[] = "article_content LIKE %s";
3649 $where_values[] = '%' . $wpdb->esc_like($search_query) . '%';
3650 }
3651
3652 if ($content_type_filter) {
3653 $where_clauses[] = "content_type = %s";
3654 $where_values[] = $content_type_filter;
3655 }
3656
3657 $sql_where = "";
3658 if (!empty($where_clauses)) {
3659 $sql_where = "WHERE " . implode(" AND ", $where_clauses);
3660 }
3661
3662 // UPDATED 2.6.3: Count unique entries (by source_url) instead of individual rows
3663 // This ensures pagination shows X entries per page, not X chunks
3664 // Entries with empty source_url are counted individually
3665 if (!empty($where_values)) {
3666 // Count unique source_urls + count of rows with empty source_url
3667 $count_query = $wpdb->prepare(
3668 "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$sql_where} AND source_url != '') +
3669 (SELECT COUNT(*) FROM {$table_name} {$sql_where} AND (source_url = '' OR source_url IS NULL))",
3670 array_merge($where_values, $where_values)
3671 );
3672 } else {
3673 $count_query = "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '') +
3674 (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL)";
3675 }
3676 $total_records = $wpdb->get_var($count_query);
3677 $total_pages = ceil($total_records / $per_page);
3678
3679 // UPDATED 2.6.3: Get unique source_urls for pagination, then fetch all their rows
3680 // Step 1: Get the source_urls for this page (distinct URLs ordered by latest timestamp)
3681 if (!empty($where_values)) {
3682 $urls_query = $wpdb->prepare(
3683 "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name} {$sql_where}
3684 GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
3685 array_merge($where_values, array($per_page, $offset))
3686 );
3687 } else {
3688 $urls_query = $wpdb->prepare(
3689 "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
3690 GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
3691 $per_page, $offset
3692 );
3693 }
3694 $page_urls = $wpdb->get_results($urls_query);
3695
3696 // Step 2: Fetch all rows for these source_urls
3697 $prompts = array();
3698 if (!empty($page_urls)) {
3699 // Build URL order map to preserve newest-first ordering from step 1
3700 $url_order_map = array();
3701 $url_list = array();
3702 $has_empty_url = false;
3703 $order_index = 0;
3704 foreach ($page_urls as $url_row) {
3705 if (empty($url_row->source_url)) {
3706 $has_empty_url = true;
3707 $url_order_map['__empty__'] = $order_index++;
3708 } else {
3709 $url_list[] = $url_row->source_url;
3710 $url_order_map[$url_row->source_url] = $order_index++;
3711 }
3712 }
3713
3714 // Build query to fetch all rows for these URLs
3715 $url_conditions = array();
3716 $url_values = array();
3717
3718 if (!empty($url_list)) {
3719 $placeholders = implode(',', array_fill(0, count($url_list), '%s'));
3720 $url_conditions[] = "source_url IN ($placeholders)";
3721 $url_values = array_merge($url_values, $url_list);
3722 }
3723
3724 if ($has_empty_url) {
3725 $url_conditions[] = "(source_url = '' OR source_url IS NULL)";
3726 }
3727
3728 if (!empty($url_conditions)) {
3729 $url_where = "WHERE (" . implode(" OR ", $url_conditions) . ")";
3730
3731 // Add original filters back
3732 if (!empty($where_clauses)) {
3733 $url_where .= " AND " . implode(" AND ", $where_clauses);
3734 $url_values = array_merge($url_values, $where_values);
3735 }
3736
3737 if (!empty($url_values)) {
3738 $prompts_query = $wpdb->prepare(
3739 "SELECT * FROM {$table_name} {$url_where} ORDER BY timestamp DESC",
3740 $url_values
3741 );
3742 } else {
3743 $prompts_query = "SELECT * FROM {$table_name} {$url_where} ORDER BY timestamp DESC";
3744 }
3745
3746 $prompts = $wpdb->get_results($prompts_query);
3747
3748 // Sort prompts by the original URL order (newest first), then by timestamp within each URL group
3749 usort($prompts, function($a, $b) use ($url_order_map) {
3750 $url_a = empty($a->source_url) ? '__empty__' : $a->source_url;
3751 $url_b = empty($b->source_url) ? '__empty__' : $b->source_url;
3752 $order_a = $url_order_map[$url_a] ?? PHP_INT_MAX;
3753 $order_b = $url_order_map[$url_b] ?? PHP_INT_MAX;
3754
3755 // First sort by URL order (newest URLs first)
3756 if ($order_a !== $order_b) {
3757 return $order_a - $order_b;
3758 }
3759
3760 // Within same URL, sort by timestamp DESC (newest chunks first)
3761 return strtotime($b->timestamp) - strtotime($a->timestamp);
3762 });
3763 }
3764 }
3765 }
3766
3767 // ================================
3768 // PAGINATION GENERATION
3769 // ================================
3770
3771 // Generate pagination links
3772 if ($total_pages > 1) {
3773 // Build a clean base URL with only necessary parameters
3774 $pagination_args = array('page' => 'mxchat-prompts');
3775
3776 // Preserve bot_id parameter if multi-bot is active
3777 if ($multibot_active && !empty($current_bot_id) && $current_bot_id !== 'default') {
3778 $pagination_args['bot_id'] = $current_bot_id;
3779 }
3780
3781 // Preserve search query and nonce if present
3782 if ($search_query) {
3783 $pagination_args['search'] = $search_query;
3784 if (!empty($nonce)) {
3785 $pagination_args['_wpnonce'] = $nonce;
3786 }
3787 }
3788
3789 // Preserve content type filter if present
3790 if ($content_type_filter) {
3791 $pagination_args['content_type'] = $content_type_filter;
3792 }
3793
3794 // Build clean base URL
3795 $base_url = add_query_arg($pagination_args, admin_url('admin.php'));
3796
3797 $page_links = paginate_links(array(
3798 'base' => $base_url . '%_%',
3799 'format' => '&paged=%#%',
3800 'prev_text' => __('&laquo; Previous', 'mxchat'),
3801 'next_text' => __('Next &raquo;', 'mxchat'),
3802 'total' => $total_pages,
3803 'current' => $current_page,
3804 'type' => 'plain',
3805 ));
3806 } else {
3807 $page_links = '';
3808 }
3809
3810 // ================================
3811 // PROCESSING STATUS RETRIEVAL
3812 // ================================
3813
3814 // Retrieve processing statuses using queue-based method
3815 $processing_statuses = $knowledge_manager->mxchat_get_processing_statuses();
3816 $pdf_status = $processing_statuses['pdf_status'];
3817 $sitemap_status = $processing_statuses['sitemap_status'];
3818 $is_processing = $processing_statuses['is_processing'];
3819
3820 //error_log('=== DEBUG: mxchat_create_prompts_page data preparation completed ===');
3821
3822 // ================================
3823 // RENDER PAGE WITH NEW SIDEBAR LAYOUT
3824 // ================================
3825
3826 // Include the new knowledge page template
3827 require_once plugin_dir_path(__FILE__) . 'admin-knowledge-page.php';
3828
3829 // Package all page data for the render function
3830 $page_data = array(
3831 'prompts' => $prompts,
3832 'total_records' => $total_records,
3833 'total_pages' => $total_pages,
3834 'current_page' => $current_page,
3835 'per_page' => $per_page,
3836 'page_links' => $page_links,
3837 'search_query' => $search_query,
3838 'content_type_filter' => $content_type_filter,
3839 'data_source' => $data_source,
3840 'use_pinecone' => $use_pinecone,
3841 'use_vectorstore' => $use_vectorstore,
3842 'multibot_active' => $multibot_active,
3843 'current_bot_id' => $current_bot_id,
3844 'pdf_status' => $pdf_status,
3845 'sitemap_status' => $sitemap_status,
3846 'is_processing' => $is_processing,
3847 'total_in_database' => $total_in_database ?? 0,
3848 'showing_recent_only' => $showing_recent_only ?? false,
3849 );
3850
3851 // Render the new sidebar-based page
3852 mxchat_render_knowledge_page($this, $knowledge_manager, $page_data);
3853 }
3854
3855 /**
3856 * Get bot-specific Pinecone configuration
3857 * Used in the knowledge retrieval functions
3858 */
3859 private function get_bot_pinecone_config($bot_id = 'default') {
3860 //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
3861
3862 // If default bot or multi-bot add-on not active, use default Pinecone config
3863 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
3864 //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
3865 $addon_options = get_option('mxchat_pinecone_addon_options', array());
3866 $config = array(
3867 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
3868 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
3869 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
3870 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
3871 );
3872 //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
3873 return $config;
3874 }
3875
3876 //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
3877
3878 // Hook for multi-bot add-on to provide bot-specific Pinecone config
3879 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
3880
3881 if (!empty($bot_pinecone_config)) {
3882 //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
3883 //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
3884 //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
3885 //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
3886 } else {
3887 //error_log("MXCHAT DEBUG: Filter returned empty config!");
3888 }
3889
3890 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
3891 }
3892
3893 public function mxchat_delete_chat_history() {
3894 if (!current_user_can('manage_options')) {
3895 echo wp_json_encode(['error' => esc_html__('You do not have sufficient permissions.', 'mxchat')]);
3896 wp_die();
3897 }
3898 check_ajax_referer('mxchat_delete_chat_history', 'security');
3899
3900 if (!isset($_POST['delete_session_ids']) || !is_array($_POST['delete_session_ids'])) {
3901 echo wp_json_encode(['error' => esc_html__('No chat sessions selected for deletion.', 'mxchat')]);
3902 wp_die();
3903 }
3904
3905 global $wpdb;
3906 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
3907 $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
3908 $has_translations = $wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table;
3909
3910 // When true, any lead attached to these sessions is fully wiped (all their sessions,
3911 // across the whole table). Default false: the chat rows go away but the lead is
3912 // preserved as a separate "chat deleted" lead in the Leads tab.
3913 $also_delete_lead = !empty($_POST['also_delete_lead']) && $_POST['also_delete_lead'] !== 'false';
3914
3915 $deleted_count = 0;
3916 $preserved_as_deleted_leads = 0;
3917 $emails_to_fully_wipe = [];
3918
3919 foreach ((array) $_POST['delete_session_ids'] as $session_id) {
3920 $session_id_sanitized = sanitize_text_field($session_id);
3921 if ($session_id_sanitized === '') {
3922 continue;
3923 }
3924
3925 // Capture the lead info attached to this session *before* we delete the rows.
3926 $lead_row = $wpdb->get_row($wpdb->prepare(
3927 "SELECT user_email, user_name, MAX(timestamp) AS last_ts
3928 FROM {$table_name}
3929 WHERE session_id = %s AND user_email IS NOT NULL AND user_email != ''
3930 GROUP BY user_email, user_name
3931 ORDER BY last_ts DESC LIMIT 1",
3932 $session_id_sanitized
3933 ));
3934
3935 wp_cache_delete('chat_session_' . $session_id_sanitized, 'mxchat_chat_sessions');
3936 $wpdb->delete($table_name, ['session_id' => $session_id_sanitized]);
3937
3938 if ($has_translations) {
3939 $wpdb->delete($translations_table, ['session_id' => $session_id_sanitized]);
3940 }
3941
3942 delete_option('mxchat_history_' . $session_id_sanitized);
3943 delete_option('mxchat_agent_name_' . $session_id_sanitized);
3944
3945 if ($lead_row && !empty($lead_row->user_email)) {
3946 if ($also_delete_lead) {
3947 // Full-wipe requested — queue the email so that all their sessions and
3948 // related options get swept below. Also clear this session's pre-chat
3949 // capture options (they're no longer meaningful).
3950 $emails_to_fully_wipe[strtolower($lead_row->user_email)] = $lead_row->user_email;
3951 delete_option('mxchat_email_' . $session_id_sanitized);
3952 delete_option('mxchat_name_' . $session_id_sanitized);
3953 } else {
3954 // Preserve the lead in a "Chat deleted" state via distinct option keys so
3955 // they stay out of the orphan bucket (orphan = pre-chat form dropoff).
3956 update_option('mxchat_lead_del_email_' . $session_id_sanitized, $lead_row->user_email, false);
3957 if (!empty($lead_row->user_name)) {
3958 update_option('mxchat_lead_del_name_' . $session_id_sanitized, $lead_row->user_name, false);
3959 }
3960 if (!empty($lead_row->last_ts)) {
3961 update_option('mxchat_lead_del_ts_' . $session_id_sanitized, $lead_row->last_ts, false);
3962 }
3963 // Clean up pre-chat capture options for this session — chat_deleted supersedes.
3964 delete_option('mxchat_email_' . $session_id_sanitized);
3965 delete_option('mxchat_name_' . $session_id_sanitized);
3966 $preserved_as_deleted_leads++;
3967 }
3968 } else {
3969 // No lead attached — nothing to preserve. Clean up any orphan options anyway.
3970 delete_option('mxchat_email_' . $session_id_sanitized);
3971 delete_option('mxchat_name_' . $session_id_sanitized);
3972 }
3973
3974 $deleted_count++;
3975 }
3976
3977 // Opt-in full-lead wipe: sweep every remaining row + every option key (including
3978 // chat_deleted preservation) for each affected email. Reuses the same internal
3979 // helper as the Leads-tab Delete button for consistency.
3980 if (!empty($emails_to_fully_wipe)) {
3981 self::mxchat_wipe_leads_by_email(array_values($emails_to_fully_wipe));
3982 }
3983
3984 wp_cache_delete('all_chat_sessions', 'mxchat_chat_sessions');
3985
3986 echo wp_json_encode([
3987 'success' => sprintf(
3988 esc_html__('%d chat session(s) have been deleted.', 'mxchat'),
3989 $deleted_count
3990 ),
3991 'preserved_as_deleted_leads' => $preserved_as_deleted_leads,
3992 'leads_fully_wiped' => count($emails_to_fully_wipe),
3993 ]);
3994 wp_die();
3995 }
3996
3997 /**
3998 * Format transcript message content with markdown processing
3999 * Converts markdown links, bold, italic, code blocks, and plain URLs to HTML
4000 */
4001 private function format_transcript_message($text) {
4002 if (empty($text)) {
4003 return '';
4004 }
4005
4006 // Normalize line endings and clean up excessive whitespace
4007 $text = str_replace("\r\n", "\n", $text);
4008 $text = str_replace("\r", "\n", $text);
4009
4010 // Clean up existing <br> tags that may have been saved (legacy data)
4011 // Convert <br>, <br/>, <br /> back to newlines for consistent processing
4012 $text = preg_replace('/<br\s*\/?>\s*/i', "\n", $text);
4013
4014 // Collapse 3+ consecutive newlines to just 2 (paragraph break)
4015 $text = preg_replace('/\n{3,}/', "\n\n", $text);
4016
4017 // Process markdown headers (# Header)
4018 $text = preg_replace_callback('/^(#{1,6})\s+(.+)$/m', function($matches) {
4019 $level = strlen($matches[1]);
4020 $content = esc_html(trim($matches[2]));
4021 return "<h{$level}>{$content}</h{$level}>";
4022 }, $text);
4023
4024 // Process code blocks with triple backticks
4025 $text = preg_replace_callback('/```(\w+)?\n?([\s\S]*?)```/', function($matches) {
4026 $language = !empty($matches[1]) ? ' class="language-' . esc_attr($matches[1]) . '"' : '';
4027 $code = esc_html($matches[2]);
4028 return "<pre><code{$language}>{$code}</code></pre>";
4029 }, $text);
4030
4031 // Process inline code with single backticks
4032 $text = preg_replace('/`([^`]+)`/', '<code>$1</code>', $text);
4033
4034 // Process bold text **text** or __text__
4035 $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
4036 $text = preg_replace('/__(.+?)__/', '<strong>$1</strong>', $text);
4037
4038 // Process italic text *text* or _text_ (but not if part of URL)
4039 $text = preg_replace('/(?<![*_\w])\*([^*]+)\*(?![*\w])/', '<em>$1</em>', $text);
4040 $text = preg_replace('/(?<![*_\w])_([^_]+)_(?![*\w])/', '<em>$1</em>', $text);
4041
4042 // Process markdown links [text](url)
4043 $text = preg_replace_callback('/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/', function($matches) {
4044 $link_text = esc_html($matches[1]);
4045 $url = esc_url($matches[2]);
4046 return "<a href=\"{$url}\" target=\"_blank\" rel=\"noopener\">{$link_text}</a>";
4047 }, $text);
4048
4049 // Process citation-style brackets [URL]
4050 $text = preg_replace_callback('/\[(https?:\/\/[^\]]+)\]/', function($matches) {
4051 $url = esc_url($matches[1]);
4052 return "<a href=\"{$url}\" target=\"_blank\" rel=\"noopener\">{$url}</a>";
4053 }, $text);
4054
4055 // Process standalone URLs (not already in links or img src)
4056 $text = preg_replace_callback(
4057 '/(?<!href="|src="|">)(https?:\/\/[^\s<>"]+)(?![^<]*<\/a>)/',
4058 function($matches) {
4059 $url = esc_url($matches[1]);
4060 // Truncate display URL if too long
4061 $display = strlen($matches[1]) > 50 ? substr($matches[1], 0, 47) . '...' : $matches[1];
4062 return "<a href=\"{$url}\" target=\"_blank\" rel=\"noopener\">{$display}</a>";
4063 },
4064 $text
4065 );
4066
4067 // Process mailto links
4068 $text = preg_replace_callback('/\[([^\]]+)\]\((mailto:[^)]+)\)/', function($matches) {
4069 $link_text = esc_html($matches[1]);
4070 $mailto = esc_url($matches[2]);
4071 return "<a href=\"{$mailto}\">{$link_text}</a>";
4072 }, $text);
4073
4074 // Convert paragraphs: split by double newlines, wrap in <p> tags
4075 // This creates proper paragraph structure instead of excessive <br> tags
4076 $paragraphs = preg_split('/\n\n+/', $text);
4077
4078 // Filter out empty paragraphs but preserve content like "0"
4079 $paragraphs = array_values(array_filter(array_map('trim', $paragraphs), function($p) {
4080 return $p !== '';
4081 }));
4082
4083 if (empty($paragraphs)) {
4084 // No content after filtering
4085 return '';
4086 } elseif (count($paragraphs) > 1) {
4087 // Multiple paragraphs - wrap each in <p> tags, convert single newlines to <br>
4088 $formatted_paragraphs = array_map(function($p) {
4089 return nl2br($p);
4090 }, $paragraphs);
4091 $text = '<p>' . implode('</p><p>', $formatted_paragraphs) . '</p>';
4092 } else {
4093 // Single paragraph - just convert newlines to <br>
4094 $text = nl2br($paragraphs[0]);
4095 }
4096
4097 return $text;
4098 }
4099
4100 /**
4101 * AJAX handler to fetch RAG context for a specific message
4102 * Used by the transcript viewer to show retrieved documents
4103 */
4104 public function mxchat_get_rag_context() {
4105 // Check permissions
4106 if (!current_user_can('manage_options')) {
4107 wp_send_json_error(['message' => esc_html__('You do not have sufficient permissions.', 'mxchat')]);
4108 wp_die();
4109 }
4110
4111 // Validate message ID
4112 if (!isset($_POST['message_id']) || empty($_POST['message_id'])) {
4113 wp_send_json_error(['message' => esc_html__('Message ID is required.', 'mxchat')]);
4114 wp_die();
4115 }
4116
4117 $message_id = absint($_POST['message_id']);
4118
4119 global $wpdb;
4120 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
4121
4122 // Fetch the RAG context for this message
4123 $result = $wpdb->get_row($wpdb->prepare(
4124 "SELECT rag_context FROM {$table_name} WHERE id = %d",
4125 $message_id
4126 ));
4127
4128 if (!$result || empty($result->rag_context)) {
4129 wp_send_json_error(['message' => esc_html__('No RAG context found for this message.', 'mxchat')]);
4130 wp_die();
4131 }
4132
4133 // Decode the JSON data
4134 $rag_context = json_decode($result->rag_context, true);
4135
4136 if (json_last_error() !== JSON_ERROR_NONE) {
4137 wp_send_json_error(['message' => esc_html__('Invalid RAG context data.', 'mxchat')]);
4138 wp_die();
4139 }
4140
4141 wp_send_json_success($rag_context);
4142 wp_die();
4143 }
4144
4145 public function display_admin_notices() {
4146 // Check if we're on a MXChat admin page
4147 $screen = get_current_screen();
4148 if (!$screen || strpos($screen->base, 'mxchat') === false) {
4149 return;
4150 }
4151
4152 $dismiss_button = '<button type="button" class="notice-dismiss"><span class="screen-reader-text">' . esc_html__('Dismiss this notice.', 'mxchat') . '</span></button>';
4153
4154 // Check for error notices
4155 $error_notice = get_transient('mxchat_admin_notice_error');
4156 if ($error_notice) {
4157 echo '<div class="notice notice-error is-dismissible"><p>' . wp_kses_post($error_notice) . '</p>' . $dismiss_button . '</div>';
4158 delete_transient('mxchat_admin_notice_error');
4159 }
4160
4161 // Check for success notices
4162 $success_notice = get_transient('mxchat_admin_notice_success');
4163 if ($success_notice) {
4164 echo '<div class="notice notice-success is-dismissible"><p>' . wp_kses_post($success_notice) . '</p>' . $dismiss_button . '</div>';
4165 delete_transient('mxchat_admin_notice_success');
4166 }
4167
4168 // Check for info notices
4169 $info_notice = get_transient('mxchat_admin_notice_info');
4170 if ($info_notice) {
4171 echo '<div class="notice notice-info is-dismissible"><p>' . wp_kses_post($info_notice) . '</p>' . $dismiss_button . '</div>';
4172 delete_transient('mxchat_admin_notice_info');
4173 }
4174 }
4175
4176
4177 public function mxchat_create_activation_page() {
4178 // Include the new Pro & Extensions page template
4179 require_once plugin_dir_path(__FILE__) . 'admin-pro-page.php';
4180
4181 // Get addons configuration from the MxChat_Addons class
4182 require_once plugin_dir_path(__FILE__) . 'class-mxchat-addons.php';
4183 $addons_instance = new MxChat_Addons();
4184 $addons_config = $addons_instance->get_addons_config();
4185
4186 // Render the consolidated Pro & Extensions page
4187 mxchat_render_pro_page($this, $addons_config);
4188 }
4189
4190 /**
4191 * Check if current activation is linked to a domain
4192 * This checks YOUR website's database, not the user's local database
4193 */
4194 public function is_current_activation_linked($domain) {
4195 $license_key = get_option('mxchat_activation_key');
4196 $email = get_option('mxchat_pro_email');
4197
4198 if (empty($license_key) || empty($email)) {
4199 return false;
4200 }
4201
4202 // Check with YOUR website's API
4203 $response = wp_remote_post('https://mxchat.ai/mxchat-api/check-domain', array(
4204 'body' => array(
4205 'license_key' => $license_key,
4206 'email' => $email,
4207 'domain' => $domain
4208 ),
4209 'timeout' => 10,
4210 'sslverify' => false
4211 ));
4212
4213 if (is_wp_error($response)) {
4214 return false;
4215 }
4216
4217 $body = json_decode(wp_remote_retrieve_body($response), true);
4218 return isset($body['success']) && $body['success'] && isset($body['data']['linked']) && $body['data']['linked'];
4219 }
4220
4221
4222 public function mxchat_actions_page_html() {
4223 if (!current_user_can('manage_options')) {
4224 return;
4225 }
4226
4227 global $wpdb;
4228 $table_name = $wpdb->prefix . 'mxchat_intents';
4229
4230 // Get stats for dashboard
4231 $total_actions = $wpdb->get_var("SELECT COUNT(*) FROM $table_name");
4232 $enabled_actions = $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE enabled = 1");
4233 $disabled_actions = $total_actions - $enabled_actions;
4234
4235 // Get unique action types count
4236 $action_types_count = $wpdb->get_var("SELECT COUNT(DISTINCT callback_function) FROM $table_name");
4237
4238 // Get action type distribution
4239 $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");
4240 $available_callbacks = $this->mxchat_get_available_callbacks();
4241 $callback_groups = $this->mxchat_get_available_callbacks(true, true);
4242
4243 $action_type_distribution = array();
4244 foreach ($type_distribution_raw as $row) {
4245 $label = isset($available_callbacks[$row->callback_function]['label'])
4246 ? $available_callbacks[$row->callback_function]['label']
4247 : $row->callback_function;
4248 $action_type_distribution[$label] = $row->count;
4249 }
4250
4251 // Native function-calling (AI Tools) data — plan-mxchat-20260617-a41dee.
4252 // The AI Tools checklist reads from MxChat_Tool_Registry, the SAME single
4253 // source the chat-time function-calling loop reads, so the two never drift.
4254 if (!class_exists('MxChat_Tool_Registry')) {
4255 require_once plugin_dir_path(__FILE__) . 'class-mxchat-tool-registry.php';
4256 }
4257 if (!class_exists('MxChat_Model_Catalog')) {
4258 require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
4259 }
4260 $fc_options = get_option('mxchat_options', array());
4261 $fc_current_model = isset($fc_options['model']) ? $fc_options['model'] : 'gpt-5.1-chat-latest';
4262 $fc_model_capable = class_exists('MxChat_Model_Catalog')
4263 ? MxChat_Model_Catalog::supports_tools($fc_current_model) : true;
4264 $active_tab = (isset($_GET['tab']) && $_GET['tab'] === 'ai-tools') ? 'ai-tools' : 'dashboard';
4265
4266 // Enrich each AI Tool with the dashicon its callback already uses on the
4267 // Trigger Phrases "Choose what it does" grid, so the AI Tools cards/modal
4268 // share the same iconography (plan 8bbf98 part 4). View-layer only — the
4269 // registry's model-facing data is untouched. Default admin-generic.
4270 $fc_tools = MxChat_Tool_Registry::available_tools();
4271 foreach ($fc_tools as &$fc_tool_ref) {
4272 $fc_cb_ref = $fc_tool_ref['callback'];
4273 $fc_tool_ref['icon'] = isset($available_callbacks[$fc_cb_ref]['icon'])
4274 ? $available_callbacks[$fc_cb_ref]['icon']
4275 : 'admin-generic';
4276 }
4277 unset($fc_tool_ref);
4278
4279 // Count of ACTIVE tools — drives the AI Tools sidebar nav badge, mirroring
4280 // $total_actions for Trigger Phrases. A tool in the list = active (plan
4281 // d450a7), so the badge shows the same number the list pane shows (plan 5f7409).
4282 $total_tools = count(array_filter($fc_tools, function ($t) {
4283 return !empty($t['enabled']);
4284 }));
4285
4286 // Brave-key dependency check (plan 183856): Web Search + Image Search run on
4287 // the Brave Search API. If either is enabled as a tool but no brave_api_key is
4288 // configured, surface a graceful "key not set" notice so the admin isn't met
4289 // with a silently no-firing tool.
4290 $fc_brave_missing = false;
4291 $brave_key = isset($fc_options['brave_api_key']) ? trim($fc_options['brave_api_key']) : '';
4292 if ($brave_key === '') {
4293 foreach ($fc_tools as $fc_t) {
4294 if (!empty($fc_t['enabled']) && isset($fc_t['requires_key']) && $fc_t['requires_key'] === 'brave_api_key') {
4295 $fc_brave_missing = true;
4296 break;
4297 }
4298 }
4299 }
4300
4301 // Prepare page data
4302 $page_data = array(
4303 'total_actions' => $total_actions,
4304 'enabled_actions' => $enabled_actions,
4305 'disabled_actions' => $disabled_actions,
4306 'action_types_count' => $action_types_count,
4307 'action_type_distribution' => $action_type_distribution,
4308 'available_callbacks' => $available_callbacks,
4309 'callback_groups' => $callback_groups,
4310 // AI Tools section
4311 'active_tab' => $active_tab,
4312 'fc_enabled' => MxChat_Tool_Registry::is_enabled(),
4313 'total_tools' => $total_tools,
4314 'fc_tools' => $fc_tools,
4315 'fc_current_model' => $fc_current_model,
4316 'fc_model_capable' => $fc_model_capable,
4317 'fc_brave_missing' => $fc_brave_missing,
4318 'fc_saved' => isset($_GET['mxchat_fc_saved']),
4319 );
4320
4321 // Include and render the new template
4322 require_once plugin_dir_path(__FILE__) . 'admin-actions-page.php';
4323 mxchat_render_actions_page($this, $page_data);
4324 }
4325
4326 /**
4327 * LEGACY HTML - Preserved below for reference, to be removed in future update
4328 */
4329 function mxchat_actions_page_legacy_html() {
4330 // This function is deprecated and no longer used
4331 // The new template is in includes/admin-actions-page.php
4332 ?>
4333 <div class="wrap mxchat-wrapper">
4334 <!-- Hero Section -->
4335 <div class="mxchat-hero">
4336 <h1 class="mxchat-main-title">
4337 <span class="mxchat-gradient-text">Actions</span> Manager
4338 </h1>
4339 <p class="mxchat-hero-subtitle">
4340 <?php esc_html_e('Create and manage custom actions to enhance your chatbot\'s capabilities.', 'mxchat'); ?>
4341 </p>
4342 </div>
4343
4344 <!-- Actions Header with Search and Filter -->
4345 <div class="mxchat-actions-header">
4346 <div class="mxchat-actions-filters">
4347 <form method="get" class="mxchat-search-form">
4348 <input type="hidden" name="page" value="mxchat-actions">
4349 <div class="mxchat-search-group">
4350 <span class="dashicons dashicons-search"></span>
4351 <input type="text" name="s" class="mxchat-search-input"
4352 placeholder="<?php esc_attr_e('Search Actions', 'mxchat'); ?>"
4353 value="<?php echo esc_attr($search_term); ?>">
4354 </div>
4355 <select name="callback_filter" class="mxchat-action-filter">
4356 <option value=""><?php esc_html_e('All Action Types', 'mxchat'); ?></option>
4357 <?php foreach ($available_callbacks as $function => $callback_data) :
4358 $label = $callback_data['label']; ?>
4359 <option value="<?php echo esc_attr($function); ?>"
4360 <?php selected($callback_filter, $function); ?>>
4361 <?php echo esc_html($label); ?>
4362 </option>
4363 <?php endforeach; ?>
4364 </select>
4365 <button type="submit" class="mxchat-button-secondary">
4366 <?php esc_html_e('Filter', 'mxchat'); ?>
4367 </button>
4368 </form>
4369 </div>
4370 <div class="mxchat-actions-controls">
4371 <button type="button" id="mxchat-add-action-btn" class="mxchat-button-primary">
4372 <span class="dashicons dashicons-plus-alt"></span>
4373 <?php esc_html_e('Add New Action', 'mxchat'); ?>
4374 </button>
4375 </div>
4376 </div>
4377
4378 <!-- Actions Grid Layout - All actions in a single grid -->
4379 <div class="mxchat-actions-grid">
4380 <div class="mxchat-cards-container">
4381 <?php if (!empty($actions)) : ?>
4382 <?php foreach ($actions as $action) :
4383 $callback_function = $action->callback_function;
4384 $callback_label = isset($available_callbacks[$callback_function]['label'])
4385 ? $available_callbacks[$callback_function]['label']
4386 : $callback_function;
4387 $threshold_value = isset($action->similarity_threshold)
4388 ? round($action->similarity_threshold * 100)
4389 : 85;
4390
4391 // Check if this is a form action
4392 $is_form_action = strpos($action->intent_label, 'Form ') === 0;
4393
4394 // Get action status (enabled/disabled) - default to true if column doesn't exist
4395 $is_enabled = isset($action->enabled) ? (bool)$action->enabled : true;
4396
4397 // Get enabled bots for display
4398 $enabled_bots = [];
4399 if (isset($action->enabled_bots) && !empty($action->enabled_bots)) {
4400 $enabled_bots = json_decode($action->enabled_bots, true);
4401 if (!is_array($enabled_bots)) {
4402 $enabled_bots = ['default'];
4403 }
4404 } else {
4405 $enabled_bots = ['default']; // Backward compatibility
4406 }
4407 ?>
4408 <div class="mxchat-action-card <?php echo $is_form_action ? 'mxchat-form-action' : ''; ?>">
4409 <div class="mxchat-card-header">
4410 <div class="mxchat-card-title"><?php echo esc_html($action->intent_label); ?></div>
4411 <div class="mxchat-card-toggle">
4412 <label class="mxchat-switch">
4413 <input type="checkbox" class="mxchat-action-toggle"
4414 data-action-id="<?php echo esc_attr($action->id); ?>"
4415 <?php checked($is_enabled); ?>>
4416 <span class="mxchat-slider round"></span>
4417 </label>
4418 </div>
4419 </div>
4420
4421 <div class="mxchat-card-body">
4422 <div class="mxchat-card-description">
4423 <strong><?php esc_html_e('Type:', 'mxchat'); ?></strong>
4424 <?php echo esc_html($callback_label); ?>
4425 </div>
4426
4427 <div class="mxchat-card-phrases">
4428 <strong><?php esc_html_e('Trigger phrases:', 'mxchat'); ?></strong>
4429 <div class="mxchat-phrases-preview">
4430 <?php
4431 // Check if the helper function exists, otherwise use a simple substring
4432 if (method_exists($this, 'get_trimmed_phrases')) {
4433 echo esc_html($this->get_trimmed_phrases($action->phrases));
4434 } else {
4435 echo esc_html(strlen($action->phrases) > 100 ?
4436 substr($action->phrases, 0, 97) . '...' :
4437 $action->phrases);
4438 }
4439 ?>
4440 </div>
4441 </div>
4442
4443 <div class="mxchat-threshold-control">
4444 <div class="mxchat-threshold-label">
4445 <?php esc_html_e('Similarity Threshold:', 'mxchat'); ?>
4446 <span class="mxchat-threshold-value"><?php echo esc_html($threshold_value); ?>%</span>
4447 </div>
4448 </div>
4449
4450 <!-- Bot Availability Display (Read-only) -->
4451 <div class="mxchat-card-bots">
4452 <strong><?php esc_html_e('Assigned bots:', 'mxchat'); ?></strong>
4453 <div class="mxchat-bot-badges">
4454 <?php
4455 foreach ($enabled_bots as $bot_id) {
4456 if ($bot_id === 'default') {
4457 echo '<span class="mxchat-bot-badge">' . esc_html__('Default Bot', 'mxchat') . '</span>';
4458 } else {
4459 // Try to get bot name from multi-bot manager
4460 if (class_exists('MxChat_Multi_Bot_Core_Manager')) {
4461 $multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
4462 $available_bots = $multi_bot_manager->get_available_bots();
4463 $bot_name = isset($available_bots[$bot_id]) ? $available_bots[$bot_id] : $bot_id;
4464 echo '<span class="mxchat-bot-badge">' . esc_html($bot_name) . '</span>';
4465 } else {
4466 echo '<span class="mxchat-bot-badge">' . esc_html($bot_id) . '</span>';
4467 }
4468 }
4469 }
4470 ?>
4471 </div>
4472 </div>
4473 </div>
4474
4475 <div class="mxchat-card-footer">
4476 <?php
4477 // Check if it's a form action
4478 $is_form_action = preg_match('/Form (\d+)/', $action->intent_label, $form_matches);
4479
4480 // Check if it's a recommendation flow action
4481 $is_flow_action = preg_match('/Recommendation Flow (\d+)/', $action->intent_label, $flow_matches);
4482
4483 if ($is_form_action) {
4484 $form_id = isset($form_matches[1]) ? $form_matches[1] : '';
4485 ?>
4486 <a href="<?php echo esc_url(admin_url('admin.php?page=mxchat-forms&action=edit&form_id=' . $form_id)); ?>"
4487 class="mxchat-button-primary">
4488 <span class="dashicons dashicons-feedback"></span>
4489 <?php esc_html_e('Edit Form', 'mxchat'); ?>
4490 </a>
4491 <?php } elseif ($is_flow_action) {
4492 ?>
4493 <a href="<?php echo esc_url(admin_url('admin.php?page=mxchat-smart-recommender')); ?>"
4494 class="mxchat-button-primary">
4495 <span class="dashicons dashicons-list-view"></span>
4496 <?php esc_html_e('Manage Flows', 'mxchat'); ?>
4497 </a>
4498 <?php } else { ?>
4499 <button type="button"
4500 class="mxchat-button-secondary mxchat-edit-button"
4501 data-action-id="<?php echo esc_attr($action->id); ?>"
4502 data-phrases="<?php echo esc_attr($action->phrases); ?>"
4503 data-label="<?php echo esc_attr($action->intent_label); ?>"
4504 data-threshold="<?php echo esc_attr(round($action->similarity_threshold * 100)); ?>"
4505 data-callback-function="<?php echo esc_attr($action->callback_function); ?>"
4506 data-enabled-bots="<?php echo esc_attr(json_encode($enabled_bots)); ?>">
4507 <span class="dashicons dashicons-edit"></span>
4508 <?php esc_html_e('Edit', 'mxchat'); ?>
4509 </button>
4510 <form method="post"
4511 action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
4512 class="mxchat-delete-form"
4513 onsubmit="return confirm('<?php esc_attr_e('Are you sure you want to delete this action?', 'mxchat'); ?>');">
4514 <?php wp_nonce_field('mxchat_delete_intent_nonce'); ?>
4515 <input type="hidden" name="action" value="mxchat_delete_intent">
4516 <input type="hidden" name="intent_id" value="<?php echo esc_attr($action->id); ?>">
4517 <button type="submit" class="mxchat-button-text mxchat-delete-button">
4518 <span class="dashicons dashicons-trash"></span>
4519 <?php esc_html_e('Delete', 'mxchat'); ?>
4520 </button>
4521 </form>
4522 <?php } ?>
4523 </div>
4524 </div>
4525 <?php endforeach; ?>
4526 <?php else : ?>
4527 <!-- If no actions found -->
4528 <div class="mxchat-no-actions">
4529 <div class="mxchat-empty-state">
4530 <span class="dashicons dashicons-format-chat"></span>
4531 <h2><?php esc_html_e('No actions found', 'mxchat'); ?></h2>
4532 <p><?php esc_html_e('Get started by creating your first action to enhance your chatbot.', 'mxchat'); ?></p>
4533 <button type="button" id="mxchat-create-first-action" class="mxchat-button-primary">
4534 <?php esc_html_e('Create Your First Action', 'mxchat'); ?>
4535 </button>
4536 </div>
4537 </div>
4538 <?php endif; ?>
4539 </div>
4540 </div>
4541
4542 <?php if ($total_pages > 1) : ?>
4543 <div class="mxchat-pagination">
4544 <?php
4545 echo paginate_links(array(
4546 'base' => add_query_arg('paged', '%#%'),
4547 'format' => '',
4548 'prev_text' => __('&laquo; Previous', 'mxchat'),
4549 'next_text' => __('Next &raquo;', 'mxchat'),
4550 'total' => $total_pages,
4551 'current' => $page
4552 ));
4553 ?>
4554 </div>
4555 <?php endif; ?>
4556
4557 <!-- Add/Edit Action Modal with Step-Based Approach -->
4558 <!-- Complete Modal HTML with Defined Groups Variable -->
4559 <div id="mxchat-action-modal" class="mxchat-modal" style="display: none;">
4560 <div class="mxchat-modal-content">
4561 <span class="mxchat-modal-close">&times;</span>
4562
4563 <form id="mxchat-action-form" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
4564 <!-- Dynamic nonce field -->
4565 <div id="action-nonce-container">
4566 <?php wp_nonce_field('mxchat_add_intent_nonce', 'add_intent_nonce'); ?>
4567 </div>
4568 <input type="hidden" name="action" id="form_action_type" value="mxchat_add_intent">
4569 <input type="hidden" name="intent_id" id="edit_action_id" value="">
4570 <input type="hidden" name="callback_function" id="callback_function" value="">
4571
4572 <!-- Step 1: Action Type Selection -->
4573 <div id="mxchat-action-step-1" class="mxchat-action-step active">
4574 <div class="mxchat-step-indicator">
4575 <div class="mxchat-step-number">1</div>
4576 <div class="mxchat-step-title"><?php esc_html_e('Select Action Type', 'mxchat'); ?></div>
4577 </div>
4578
4579 <div id="mxchat-action-type-selector" class="mxchat-action-type-selector">
4580 <div class="mxchat-action-type-search">
4581 <span class="dashicons dashicons-search"></span>
4582 <input type="text" id="action-type-search" placeholder="<?php esc_attr_e('Search action types...', 'mxchat'); ?>" class="mxchat-action-type-search-input">
4583 </div>
4584
4585 <?php
4586 // Get the callbacks - IMPORTANT: Define the $groups variable here
4587 $groups = $this->mxchat_get_available_callbacks(true, true);
4588 ?>
4589
4590 <div class="mxchat-action-type-categories">
4591 <button type="button" class="mxchat-category-button active" data-category="all"><?php esc_html_e('All', 'mxchat'); ?></button>
4592 <?php
4593 // Get unique categories from the defined groups
4594 foreach ($groups as $group_label => $group_callbacks) :
4595 $category_slug = sanitize_title($group_label);
4596 ?>
4597 <button type="button" class="mxchat-category-button" data-category="<?php echo esc_attr($category_slug); ?>"><?php echo esc_html($group_label); ?></button>
4598 <?php endforeach; ?>
4599 </div>
4600
4601 <div class="mxchat-action-types-grid">
4602 <?php
4603 // Generate action cards from available callbacks
4604 foreach ($groups as $group_label => $group_callbacks) :
4605 $category_slug = sanitize_title($group_label);
4606
4607 foreach ($group_callbacks as $function => $data) :
4608 $label = $data['label'];
4609 $icon = isset($data['icon']) ? $data['icon'] : 'admin-generic';
4610 $description = isset($data['description']) ? $data['description'] : '';
4611 $is_addon = isset($data['addon']) && $data['addon'] !== false;
4612 $addon_name = isset($data['addon_name']) ? $data['addon_name'] : '';
4613 $is_installed = isset($data['installed']) ? $data['installed'] : true;
4614 $is_promo = !empty($data['addon_promo']) && !$is_installed;
4615
4616 // Determine card status and styling
4617 $card_class = 'mxchat-action-type-card';
4618 $icon_class = 'mxchat-action-type-icon';
4619 $status_badge = '';
4620
4621 if ($is_promo) {
4622 // Promotional card — not selectable, just informational
4623 $card_class .= ' not-installed mxchat-promo-card';
4624 $status_badge = '<span class="mxchat-addon-badge">' . esc_html__('Add-on Required', 'mxchat') . '</span>';
4625 } elseif ($is_addon && !$is_installed) {
4626 // Add-on not installed
4627 $card_class .= ' not-installed';
4628 $status_badge .= '<span class="mxchat-addon-badge">' . esc_html__('Add-on Required', 'mxchat') . '</span>';
4629 }
4630
4631 // Default description if none provided
4632 if (empty($description)) {
4633 $description = sprintf(
4634 esc_html__('Use the %s action in your chatbot', 'mxchat'),
4635 $label
4636 );
4637 }
4638 ?>
4639 <div class="<?php echo esc_attr($card_class); ?>"
4640 data-category="<?php echo esc_attr($category_slug); ?>"
4641 <?php if (!$is_promo) : ?>
4642 data-value="<?php echo esc_attr($function); ?>"
4643 data-label="<?php echo esc_attr($label); ?>"
4644 <?php endif; ?>
4645 data-pro="false"
4646 data-addon="<?php echo esc_attr($is_addon ? $data['addon'] : ''); ?>"
4647 data-installed="<?php echo $is_installed ? 'true' : 'false'; ?>"
4648 <?php if ($is_promo) : ?>data-promo="true"<?php endif; ?>>
4649 <div class="<?php echo esc_attr($icon_class); ?>">
4650 <span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span>
4651 </div>
4652 <div class="mxchat-action-type-info">
4653 <h4><?php echo esc_html($label); ?></h4>
4654 <p><?php echo esc_html($description); ?></p>
4655 <?php if (!empty($status_badge)) : ?>
4656 <?php echo $status_badge; ?>
4657 <?php endif; ?>
4658
4659 <?php if ($is_promo || ($is_addon && !$is_installed)) : ?>
4660 <div class="mxchat-addon-info">
4661 <?php echo esc_html(sprintf(
4662 __('Requires %s', 'mxchat'),
4663 $addon_name
4664 )); ?>
4665 — <a href="https://mxchat.ai/" target="_blank"><?php esc_html_e('Get Add-on', 'mxchat'); ?></a>
4666 </div>
4667 <?php endif; ?>
4668 </div>
4669 </div>
4670 <?php endforeach;
4671 endforeach; ?>
4672 </div>
4673 </div>
4674
4675 <div class="mxchat-modal-actions">
4676 <button type="button" class="mxchat-button-secondary mxchat-modal-cancel">
4677 <?php esc_html_e('Cancel', 'mxchat'); ?>
4678 </button>
4679 </div>
4680 </div>
4681
4682 <!-- Step 2: Action Configuration -->
4683 <div id="mxchat-action-step-2" class="mxchat-action-step">
4684 <div class="mxchat-step-indicator">
4685 <div class="mxchat-step-number">2</div>
4686 <div class="mxchat-step-title"><?php esc_html_e('Configure Action', 'mxchat'); ?></div>
4687 </div>
4688
4689 <div class="mxchat-selected-action">
4690 <button type="button" class="mxchat-back-button" id="mxchat-back-to-step-1">
4691 <span class="dashicons dashicons-arrow-left-alt"></span>
4692 <?php esc_html_e('Back to Action Types', 'mxchat'); ?>
4693 </button>
4694 <div class="mxchat-selected-action-info">
4695 <div id="selected-action-icon" class="mxchat-action-type-icon">
4696 <span class="dashicons dashicons-admin-generic"></span>
4697 </div>
4698 <div class="mxchat-selected-action-details">
4699 <h3 id="selected-action-title"><?php esc_html_e('Selected Action', 'mxchat'); ?></h3>
4700 <p id="selected-action-description"><?php esc_html_e('Configure this action for your chatbot', 'mxchat'); ?></p>
4701 </div>
4702 </div>
4703 </div>
4704
4705 <div class="mxchat-form-group">
4706 <label for="intent_label">
4707 <?php esc_html_e('Action Label (For your reference only)', 'mxchat'); ?>
4708 </label>
4709 <input name="intent_label" type="text" id="intent_label" required
4710 class="mxchat-intent-input"
4711 placeholder="<?php esc_attr_e('Example: Newsletter Signup', 'mxchat'); ?>">
4712 </div>
4713
4714 <div class="mxchat-form-group">
4715 <label for="phrases">
4716 <?php esc_html_e('Trigger Phrases (comma-separated)', 'mxchat'); ?>
4717 </label>
4718 <textarea name="phrases" id="action_phrases" rows="5" required
4719 class="mxchat-intent-textarea"
4720 placeholder="<?php esc_attr_e('Example: sign me up, subscribe me, I want to join, add me to the newsletter', 'mxchat'); ?>"></textarea>
4721 </div>
4722
4723 <div class="mxchat-form-group">
4724 <label for="similarity_threshold">
4725 <?php esc_html_e('Similarity Threshold', 'mxchat'); ?>
4726 <span class="mxchat-threshold-value-display">85%</span>
4727 </label>
4728 <div class="mxchat-slider-group modal-slider">
4729 <input type="range"
4730 name="similarity_threshold"
4731 id="similarity_threshold"
4732 min="10"
4733 max="95"
4734 value="85"
4735 class="mxchat-intent-slider"
4736 oninput="document.querySelector('.mxchat-threshold-value-display').textContent = this.value + '%'">
4737 </div>
4738 <div class="mxchat-threshold-hint">
4739 <?php esc_html_e('Lower values (10-30) make the action trigger more easily. Higher values (70-95) require more exact matches.', 'mxchat'); ?>
4740 </div>
4741 </div>
4742
4743 <!-- Bot Selection Section -->
4744 <div class="mxchat-form-group">
4745 <label for="enabled_bots">
4746 <?php esc_html_e('Which bots should we enable this action for?', 'mxchat'); ?>
4747 </label>
4748 <div class="mxchat-bot-selector">
4749 <div class="mxchat-bot-option">
4750 <label class="mxchat-checkbox-label">
4751 <input type="checkbox"
4752 name="enabled_bots[]"
4753 value="default"
4754 id="bot_default"
4755 checked="checked">
4756 <span class="mxchat-checkmark"></span>
4757 <?php esc_html_e('Default Bot', 'mxchat'); ?>
4758 </label>
4759 </div>
4760
4761 <?php if (class_exists('MxChat_Multi_Bot_Core_Manager')) :
4762 $multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
4763 $available_bots = $multi_bot_manager->get_available_bots();
4764
4765 foreach ($available_bots as $bot_id => $bot_name) :
4766 if ($bot_id === 'default') continue; // Skip default, already shown above
4767 ?>
4768 <div class="mxchat-bot-option">
4769 <label class="mxchat-checkbox-label">
4770 <input type="checkbox"
4771 name="enabled_bots[]"
4772 value="<?php echo esc_attr($bot_id); ?>"
4773 id="bot_<?php echo esc_attr($bot_id); ?>">
4774 <span class="mxchat-checkmark"></span>
4775 <?php echo esc_html($bot_name); ?>
4776 </label>
4777 </div>
4778 <?php
4779 endforeach;
4780 endif; ?>
4781 </div>
4782 </div>
4783
4784 <div class="mxchat-modal-actions">
4785 <button type="button" class="mxchat-button-secondary mxchat-modal-cancel">
4786 <?php esc_html_e('Cancel', 'mxchat'); ?>
4787 </button>
4788 <button type="submit" class="mxchat-button-primary" id="mxchat-save-action-btn">
4789 <?php esc_html_e('Save Action', 'mxchat'); ?>
4790 </button>
4791 </div>
4792 </div>
4793 </form>
4794 </div>
4795 </div>
4796
4797 <div id="mxchat-action-loading" class="mxchat-action-loading" style="display: none;">
4798 <div class="mxchat-action-loading-spinner"></div>
4799 <div class="mxchat-action-loading-text">
4800 <?php esc_html_e('Saving action, please wait...', 'mxchat'); ?>
4801 </div>
4802 </div>
4803 </div><!-- .mxchat-wrapper -->
4804 <?php
4805 }
4806 private function get_trimmed_phrases($phrases, $max_length = 100) {
4807 if (strlen($phrases) <= $max_length) {
4808 return $phrases;
4809 }
4810
4811 $trimmed = substr($phrases, 0, $max_length);
4812 $last_comma = strrpos($trimmed, ',');
4813
4814 if ($last_comma !== false) {
4815 $trimmed = substr($trimmed, 0, $last_comma);
4816 }
4817
4818 return $trimmed . '...';
4819 }
4820 public function mxchat_add_enabled_column_to_intents() {
4821 global $wpdb;
4822 $table_name = $wpdb->prefix . 'mxchat_intents';
4823
4824 // Check if the column already exists
4825 $columns = $wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'enabled'");
4826
4827 if (empty($columns)) {
4828 // Add the column with default value of 1 (enabled)
4829 $wpdb->query("ALTER TABLE $table_name ADD COLUMN enabled TINYINT(1) NOT NULL DEFAULT 1");
4830 }
4831 }
4832
4833 public function mxchat_handle_delete_intent() {
4834 if ( ! current_user_can( 'manage_options' ) ) {
4835 wp_die( esc_html__('Unauthorized user', 'mxchat') );
4836 }
4837
4838 check_admin_referer('mxchat_delete_intent_nonce');
4839
4840 if (isset($_POST['intent_id'])) {
4841 global $wpdb;
4842 $table_name = $wpdb->prefix . 'mxchat_intents';
4843 $intent_id = intval($_POST['intent_id']);
4844
4845 $wpdb->delete($table_name, ['id' => $intent_id], ['%d']);
4846 }
4847
4848 wp_safe_redirect(admin_url('admin.php?page=mxchat-actions'));
4849 exit;
4850 }
4851 public function mxchat_handle_edit_intent() {
4852 // Security checks (nonce and permissions)
4853 if (!current_user_can('manage_options')) {
4854 wp_die(esc_html__('Unauthorized user', 'mxchat'));
4855 }
4856 check_admin_referer('mxchat_edit_intent');
4857
4858 // Get POST data
4859 $intent_id = isset($_POST['intent_id']) ? absint($_POST['intent_id']) : 0;
4860 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
4861 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
4862 $threshold_percentage = isset($_POST['similarity_threshold']) ? intval($_POST['similarity_threshold']) : 85;
4863 $similarity_threshold = min(95, max(10, $threshold_percentage)) / 100; // Convert to 0.10–0.95
4864
4865 // Handle enabled_bots
4866 $enabled_bots = isset($_POST['enabled_bots']) ? $_POST['enabled_bots'] : array('default');
4867 $enabled_bots = array_map('sanitize_text_field', $enabled_bots);
4868
4869 // Ensure default is always included for backward compatibility
4870 if (!in_array('default', $enabled_bots)) {
4871 $enabled_bots[] = 'default';
4872 }
4873
4874 $enabled_bots_json = json_encode($enabled_bots);
4875
4876 // Validate inputs
4877 if (!$intent_id || empty($intent_label) || empty($phrases_input)) {
4878 $this->handle_embedding_error(__('Invalid input. Please ensure all fields are filled out.', 'mxchat'));
4879 return;
4880 }
4881
4882 $phrases_array = array_map('sanitize_text_field', array_filter(array_map('trim', explode(',', $phrases_input))));
4883 if (empty($phrases_array)) {
4884 $this->handle_embedding_error(__('Please enter at least one valid phrase.', 'mxchat'));
4885 return;
4886 }
4887
4888 // Generate embeddings with improved error handling
4889 $vectors = [];
4890 $failed_phrases = [];
4891
4892 foreach ($phrases_array as $phrase) {
4893 $embedding_vector = $this->mxchat_generate_embedding($phrase, $this->options['api_key']);
4894 if (is_array($embedding_vector)) {
4895 $vectors[] = $embedding_vector;
4896 } else {
4897 $failed_phrases[] = $phrase;
4898 }
4899 }
4900
4901 if (!empty($failed_phrases)) {
4902 $this->handle_embedding_error(
4903 sprintf(
4904 __('Error generating embeddings for phrases: %s. Check your embedding API.', 'mxchat'),
4905 implode(', ', $failed_phrases)
4906 )
4907 );
4908 return;
4909 }
4910
4911 if (empty($vectors)) {
4912 $this->handle_embedding_error(__('No valid embeddings generated. Please check your phrases.', 'mxchat'));
4913 return;
4914 }
4915
4916 $combined_vector = $this->mxchat_average_vectors($vectors);
4917 $serialized_vector = maybe_serialize($combined_vector);
4918
4919 // Update the database
4920 global $wpdb;
4921 $table_name = $wpdb->prefix . 'mxchat_intents';
4922
4923 $result = $wpdb->update(
4924 $table_name,
4925 array(
4926 'intent_label' => $intent_label,
4927 'phrases' => implode(', ', $phrases_array),
4928 'embedding_vector' => $serialized_vector,
4929 'similarity_threshold' => $similarity_threshold,
4930 'enabled_bots' => $enabled_bots_json, // Include enabled_bots in update
4931 ),
4932 array('id' => $intent_id),
4933 array('%s', '%s', '%s', '%f', '%s'), // Format: string, string, string, float, string
4934 array('%d') // Where format: integer
4935 );
4936
4937 if (false === $result) {
4938 $this->handle_embedding_error(__('Failed to update action in database.', 'mxchat'));
4939 return;
4940 }
4941
4942 // Set success message and redirect
4943 set_transient('mxchat_admin_notice_success', __('Intent updated successfully!', 'mxchat'), 60);
4944
4945 $redirect_url = add_query_arg(
4946 array(
4947 'page' => 'mxchat-actions'
4948 ),
4949 admin_url('admin.php')
4950 );
4951 wp_safe_redirect($redirect_url);
4952 exit;
4953 }
4954 public function mxchat_handle_add_intent() {
4955 if (!current_user_can('manage_options')) {
4956 wp_die(esc_html__('Unauthorized user', 'mxchat'));
4957 }
4958 check_admin_referer('mxchat_add_intent_nonce');
4959 global $wpdb;
4960 $table_name = $wpdb->prefix . 'mxchat_intents';
4961
4962 // Sanitize and get form data
4963 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
4964 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
4965 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
4966
4967 // Get similarity threshold from form (convert percentage to decimal)
4968 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
4969
4970 // Handle enabled_bots
4971 $enabled_bots = isset($_POST['enabled_bots']) ? $_POST['enabled_bots'] : array('default');
4972 $enabled_bots = array_map('sanitize_text_field', $enabled_bots);
4973
4974 // Ensure default is always included for backward compatibility with existing actions
4975 if (!in_array('default', $enabled_bots)) {
4976 $enabled_bots[] = 'default';
4977 }
4978
4979 $enabled_bots_json = json_encode($enabled_bots);
4980
4981 // Validate required fields
4982 if (empty($intent_label) || empty($callback_function) || empty($phrases_input)) {
4983 $this->handle_embedding_error(__('Invalid input. Please ensure all fields are filled out.', 'mxchat'));
4984 return;
4985 }
4986
4987 // Validate callback function
4988 $available_callbacks = $this->mxchat_get_available_callbacks();
4989 if (!array_key_exists($callback_function, $available_callbacks)) {
4990 $this->handle_embedding_error(__('Invalid callback function selected.', 'mxchat'));
4991 return;
4992 }
4993
4994 // Check if this is an add-on promotional placeholder (not a real action)
4995 if (!empty($available_callbacks[$callback_function]['addon_promo'])) {
4996 $addon_name = isset($available_callbacks[$callback_function]['addon_name']) ? $available_callbacks[$callback_function]['addon_name'] : __('an add-on', 'mxchat');
4997 $this->handle_embedding_error(sprintf(
4998 __('This action requires the %s to be installed and activated.', 'mxchat'),
4999 $addon_name
5000 ));
5001 return;
5002 }
5003
5004 // Process phrases
5005 $phrases_array = array_map('sanitize_text_field', array_filter(array_map('trim', explode(',', $phrases_input))));
5006 if (empty($phrases_array)) {
5007 $this->handle_embedding_error(__('Please enter at least one valid phrase.', 'mxchat'));
5008 return;
5009 }
5010
5011 // Generate embeddings with improved error handling
5012 $vectors = [];
5013 $failed_phrases = [];
5014 foreach ($phrases_array as $phrase) {
5015 $embedding_vector = $this->mxchat_generate_embedding($phrase, $this->options['api_key']);
5016 if (is_array($embedding_vector)) {
5017 $vectors[] = $embedding_vector;
5018 } else {
5019 $failed_phrases[] = $phrase;
5020 }
5021 }
5022
5023 // Check for embedding failures
5024 if (!empty($failed_phrases)) {
5025 $this->handle_embedding_error(
5026 sprintf(
5027 __('Error generating embeddings for phrases: %s. Check your embedding API.', 'mxchat'),
5028 implode(', ', $failed_phrases)
5029 )
5030 );
5031 return;
5032 }
5033
5034 if (empty($vectors)) {
5035 $this->handle_embedding_error(__('No valid embeddings generated. Please check your phrases.', 'mxchat'));
5036 return;
5037 }
5038
5039 // Create combined vector and insert into database
5040 $combined_vector = $this->mxchat_average_vectors($vectors);
5041 $serialized_vector = maybe_serialize($combined_vector);
5042
5043 $result = $wpdb->insert($table_name, [
5044 'intent_label' => $intent_label,
5045 'phrases' => implode(', ', $phrases_array),
5046 'embedding_vector' => $serialized_vector,
5047 'callback_function' => $callback_function,
5048 'similarity_threshold' => $similarity_threshold,
5049 'enabled_bots' => $enabled_bots_json, // NEW field
5050 ]);
5051
5052 if ($result === false) {
5053 $this->handle_embedding_error(__('Database error: ', 'mxchat') . $wpdb->last_error);
5054 return;
5055 }
5056
5057 // Set success message and redirect
5058 set_transient('mxchat_admin_notice_success', __('New intent added successfully!', 'mxchat'), 60);
5059 wp_safe_redirect(admin_url('admin.php?page=mxchat-actions'));
5060 exit;
5061 }
5062
5063
5064
5065
5066 private function handle_embedding_error($message, $redirect = true) {
5067 // Store the error message in the existing transient
5068 set_transient('mxchat_admin_notice_error', $message, 60);
5069
5070 if ($redirect) {
5071 // Redirect back to the actions page
5072 $redirect_url = add_query_arg(
5073 array(
5074 'page' => 'mxchat-actions'
5075 ),
5076 admin_url('admin.php')
5077 );
5078 wp_safe_redirect($redirect_url);
5079 exit;
5080 }
5081 }
5082
5083 /**
5084 * AJAX handler to fetch actions list for the new split-panel UI
5085 */
5086 public function mxchat_fetch_actions_list() {
5087 check_ajax_referer('mxchat_actions_nonce', 'security');
5088
5089 if (!current_user_can('manage_options')) {
5090 wp_send_json_error(__('Unauthorized', 'mxchat'));
5091 }
5092
5093 global $wpdb;
5094 $table_name = $wpdb->prefix . 'mxchat_intents';
5095
5096 $page = isset($_POST['page']) ? max(1, intval($_POST['page'])) : 1;
5097 $per_page = isset($_POST['per_page']) ? min(100, max(1, intval($_POST['per_page']))) : 50;
5098 $offset = ($page - 1) * $per_page;
5099 $search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
5100 $callback_filter = isset($_POST['callback_filter']) ? sanitize_text_field($_POST['callback_filter']) : '';
5101 $sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] === 'asc' ? 'ASC' : 'DESC';
5102
5103 // Build WHERE clause
5104 $where = '1=1';
5105 $params = array();
5106
5107 if ($search) {
5108 $search_like = '%' . $wpdb->esc_like($search) . '%';
5109 $where .= ' AND (intent_label LIKE %s OR phrases LIKE %s)';
5110 $params[] = $search_like;
5111 $params[] = $search_like;
5112 }
5113
5114 if ($callback_filter) {
5115 $where .= ' AND callback_function = %s';
5116 $params[] = $callback_filter;
5117 }
5118
5119 // Get total count
5120 $count_query = "SELECT COUNT(*) FROM $table_name WHERE $where";
5121 if (!empty($params)) {
5122 $count_query = $wpdb->prepare($count_query, $params);
5123 }
5124 $total_actions = $wpdb->get_var($count_query);
5125
5126 // Get actions
5127 $query = "SELECT * FROM $table_name WHERE $where ORDER BY id $sort_order LIMIT %d OFFSET %d";
5128 $all_params = array_merge($params, array($per_page, $offset));
5129 $actions = $wpdb->get_results($wpdb->prepare($query, $all_params));
5130
5131 // Get available callbacks for labels/icons
5132 $available_callbacks = $this->mxchat_get_available_callbacks();
5133
5134 // Prefetch individual phrase counts for all fetched actions
5135 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5136 $phrase_counts = array();
5137 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5138 $action_ids = wp_list_pluck($actions, 'id');
5139 if (!empty($action_ids)) {
5140 $id_placeholders = implode(',', array_fill(0, count($action_ids), '%d'));
5141 $count_results = $wpdb->get_results($wpdb->prepare(
5142 "SELECT intent_id, COUNT(*) as cnt FROM $phrases_table WHERE intent_id IN ($id_placeholders) GROUP BY intent_id",
5143 $action_ids
5144 ));
5145 foreach ($count_results as $row) {
5146 $phrase_counts[$row->intent_id] = intval($row->cnt);
5147 }
5148 }
5149 }
5150
5151 // Format actions for response
5152 $formatted_actions = array();
5153 foreach ($actions as $action) {
5154 $callback_data = isset($available_callbacks[$action->callback_function])
5155 ? $available_callbacks[$action->callback_function]
5156 : array('label' => $action->callback_function, 'icon' => 'admin-generic');
5157
5158 $enabled_bots = json_decode($action->enabled_bots, true);
5159 if (!is_array($enabled_bots)) {
5160 $enabled_bots = array('default');
5161 }
5162
5163 $formatted_actions[] = array(
5164 'id' => intval($action->id),
5165 'label' => $action->intent_label,
5166 'phrases' => $action->phrases,
5167 'callback_function' => $action->callback_function,
5168 'callback_label' => $callback_data['label'],
5169 'icon' => isset($callback_data['icon']) ? $callback_data['icon'] : 'admin-generic',
5170 'threshold' => round($action->similarity_threshold * 100),
5171 'enabled' => (bool) $action->enabled,
5172 'enabled_bots' => $enabled_bots,
5173 'has_legacy_vector' => !empty($action->embedding_vector),
5174 'individual_phrase_count' => isset($phrase_counts[$action->id]) ? $phrase_counts[$action->id] : 0,
5175 );
5176 }
5177
5178 $total_pages = ceil($total_actions / $per_page);
5179 $showing_start = $total_actions > 0 ? $offset + 1 : 0;
5180 $showing_end = min($offset + $per_page, $total_actions);
5181
5182 wp_send_json_success(array(
5183 'actions' => $formatted_actions,
5184 'page' => $page,
5185 'per_page' => $per_page,
5186 'total_actions' => intval($total_actions),
5187 'total_pages' => $total_pages,
5188 'showing_start' => $showing_start,
5189 'showing_end' => $showing_end,
5190 ));
5191 }
5192
5193 /**
5194 * AJAX handler to toggle action enabled status
5195 */
5196 public function mxchat_toggle_action_status() {
5197 check_ajax_referer('mxchat_actions_nonce', 'security');
5198
5199 if (!current_user_can('manage_options')) {
5200 wp_send_json_error(__('Unauthorized', 'mxchat'));
5201 }
5202
5203 $action_id = isset($_POST['action_id']) ? intval($_POST['action_id']) : 0;
5204 $enabled = isset($_POST['enabled']) ? intval($_POST['enabled']) : 0;
5205
5206 if (!$action_id) {
5207 wp_send_json_error(__('Invalid action ID', 'mxchat'));
5208 }
5209
5210 global $wpdb;
5211 $table_name = $wpdb->prefix . 'mxchat_intents';
5212
5213 $result = $wpdb->update(
5214 $table_name,
5215 array('enabled' => $enabled ? 1 : 0),
5216 array('id' => $action_id),
5217 array('%d'),
5218 array('%d')
5219 );
5220
5221 if ($result === false) {
5222 wp_send_json_error(__('Failed to update action status', 'mxchat'));
5223 }
5224
5225 wp_send_json_success(array('enabled' => (bool) $enabled));
5226 }
5227
5228 /**
5229 * AJAX handler to bulk delete actions
5230 */
5231 public function mxchat_bulk_delete_actions() {
5232 check_ajax_referer('mxchat_delete_intent_nonce', 'security');
5233
5234 if (!current_user_can('manage_options')) {
5235 wp_send_json_error(__('Unauthorized', 'mxchat'));
5236 }
5237
5238 $action_ids = isset($_POST['action_ids']) ? array_map('intval', (array) $_POST['action_ids']) : array();
5239
5240 if (empty($action_ids)) {
5241 wp_send_json_error(__('No actions selected', 'mxchat'));
5242 }
5243
5244 global $wpdb;
5245 $table_name = $wpdb->prefix . 'mxchat_intents';
5246
5247 $placeholders = implode(',', array_fill(0, count($action_ids), '%d'));
5248 $query = $wpdb->prepare("DELETE FROM $table_name WHERE id IN ($placeholders)", $action_ids);
5249 $result = $wpdb->query($query);
5250
5251 if ($result === false) {
5252 wp_send_json_error(__('Failed to delete actions', 'mxchat'));
5253 }
5254
5255 // Also delete individual phrases for these intents
5256 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5257 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5258 $wpdb->query($wpdb->prepare("DELETE FROM $phrases_table WHERE intent_id IN ($placeholders)", $action_ids));
5259 }
5260
5261 wp_send_json_success(array('deleted' => $result));
5262 }
5263
5264 /**
5265 * AJAX handler to add a new intent/action
5266 */
5267 public function mxchat_add_intent_ajax() {
5268 check_ajax_referer('mxchat_add_intent_nonce', 'security');
5269
5270 if (!current_user_can('manage_options')) {
5271 wp_send_json_error(__('Unauthorized', 'mxchat'));
5272 }
5273
5274 global $wpdb;
5275 $table_name = $wpdb->prefix . 'mxchat_intents';
5276
5277 // Sanitize input
5278 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
5279 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
5280 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
5281 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
5282 $enabled_bots = isset($_POST['enabled_bots']) ? array_map('sanitize_text_field', (array) $_POST['enabled_bots']) : array('default');
5283
5284 // Validate
5285 // Check if using new individual phrases mode or legacy mode
5286 $individual_phrases = isset($_POST['individual_phrases']) ? array_filter(array_map('sanitize_text_field', (array) $_POST['individual_phrases'])) : array();
5287 $use_individual = !empty($individual_phrases);
5288
5289 // Validate required fields (phrases not required when using individual mode)
5290 if (empty($intent_label) || empty($callback_function)) {
5291 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
5292 }
5293 if (!$use_individual && empty($phrases_input)) {
5294 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
5295 }
5296
5297 // Ensure default bot is included
5298 if (!in_array('default', $enabled_bots)) {
5299 $enabled_bots[] = 'default';
5300 }
5301 $enabled_bots_json = json_encode($enabled_bots);
5302
5303 if ($use_individual) {
5304 // New mode: individual phrases each get their own vector
5305 // Insert the intent row with empty legacy fields
5306 $result = $wpdb->insert(
5307 $table_name,
5308 array(
5309 'intent_label' => $intent_label,
5310 'phrases' => '',
5311 'embedding_vector' => '',
5312 'similarity_threshold' => $similarity_threshold,
5313 'callback_function' => $callback_function,
5314 'enabled' => 1,
5315 'enabled_bots' => $enabled_bots_json,
5316 )
5317 );
5318
5319 if ($result === false) {
5320 wp_send_json_error(__('Failed to add action to database.', 'mxchat'));
5321 }
5322
5323 $intent_id = $wpdb->insert_id;
5324
5325 // Insert each phrase individually with its own embedding
5326 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5327 $failed_phrases = array();
5328 foreach ($individual_phrases as $phrase) {
5329 $phrase = trim($phrase);
5330 if (empty($phrase)) continue;
5331
5332 $embedding_vector = $this->mxchat_generate_embedding($phrase);
5333 if (is_wp_error($embedding_vector)) {
5334 $failed_phrases[] = $phrase;
5335 continue;
5336 }
5337
5338 $wpdb->insert(
5339 $phrases_table,
5340 array(
5341 'intent_id' => $intent_id,
5342 'phrase' => $phrase,
5343 'embedding_vector' => maybe_serialize($embedding_vector),
5344 )
5345 );
5346 }
5347
5348 $response = array('id' => $intent_id);
5349 if (!empty($failed_phrases)) {
5350 $response['failed_phrases'] = $failed_phrases;
5351 }
5352 wp_send_json_success($response);
5353
5354 } else {
5355 // Legacy mode: combine all phrases into one embedding (backwards compatible)
5356 $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
5357 if (empty($phrases_array)) {
5358 wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
5359 }
5360
5361 // Generate embedding (combine phrases into single string for embedding)
5362 $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
5363 if (is_wp_error($embedding_vector)) {
5364 // Fallback: store without embedding
5365 $serialized_vector = null;
5366 } else {
5367 $serialized_vector = maybe_serialize($embedding_vector);
5368 }
5369
5370 // Insert
5371 $result = $wpdb->insert(
5372 $table_name,
5373 array(
5374 'intent_label' => $intent_label,
5375 'phrases' => implode(', ', $phrases_array),
5376 'embedding_vector' => $serialized_vector,
5377 'similarity_threshold' => $similarity_threshold,
5378 'callback_function' => $callback_function,
5379 'enabled' => 1,
5380 'enabled_bots' => $enabled_bots_json,
5381 )
5382 );
5383
5384 if ($result === false) {
5385 wp_send_json_error(__('Failed to add action to database.', 'mxchat'));
5386 }
5387
5388 wp_send_json_success(array('id' => $wpdb->insert_id));
5389 }
5390 }
5391
5392 /**
5393 * AJAX handler to edit an existing intent/action
5394 */
5395 public function mxchat_edit_intent_ajax() {
5396 check_ajax_referer('mxchat_edit_intent', 'security');
5397
5398 if (!current_user_can('manage_options')) {
5399 wp_send_json_error(__('Unauthorized', 'mxchat'));
5400 }
5401
5402 global $wpdb;
5403 $table_name = $wpdb->prefix . 'mxchat_intents';
5404
5405 // Sanitize input
5406 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5407 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
5408 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
5409 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
5410 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
5411 $enabled_bots = isset($_POST['enabled_bots']) ? array_map('sanitize_text_field', (array) $_POST['enabled_bots']) : array('default');
5412
5413 // Validate
5414 if (!$intent_id || empty($intent_label)) {
5415 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
5416 }
5417
5418 // Check if phrases are managed individually (empty phrases_input means individual mode)
5419 $uses_individual_phrases = empty($phrases_input);
5420
5421 if ($uses_individual_phrases) {
5422 // Individual phrase mode: only update non-phrase fields, skip embedding regeneration
5423 $update_data = array(
5424 'intent_label' => $intent_label,
5425 'similarity_threshold' => $similarity_threshold,
5426 'callback_function' => $callback_function,
5427 'enabled_bots' => json_encode($enabled_bots),
5428 );
5429 } else {
5430 // Legacy mode: process phrases and regenerate embedding (backwards compatible for add-ons)
5431 $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
5432 if (empty($phrases_array)) {
5433 wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
5434 }
5435
5436 // Generate new embedding (combine phrases into single string for embedding)
5437 $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
5438 if (is_wp_error($embedding_vector)) {
5439 // Keep existing embedding
5440 $update_data = array(
5441 'intent_label' => $intent_label,
5442 'phrases' => implode(', ', $phrases_array),
5443 'similarity_threshold' => $similarity_threshold,
5444 'callback_function' => $callback_function,
5445 'enabled_bots' => json_encode($enabled_bots),
5446 );
5447 } else {
5448 $serialized_vector = maybe_serialize($embedding_vector);
5449 $update_data = array(
5450 'intent_label' => $intent_label,
5451 'phrases' => implode(', ', $phrases_array),
5452 'embedding_vector' => $serialized_vector,
5453 'similarity_threshold' => $similarity_threshold,
5454 'callback_function' => $callback_function,
5455 'enabled_bots' => json_encode($enabled_bots),
5456 );
5457 }
5458 }
5459
5460 // Ensure default bot is included
5461 if (!in_array('default', $enabled_bots)) {
5462 $enabled_bots[] = 'default';
5463 }
5464 $update_data['enabled_bots'] = json_encode($enabled_bots);
5465
5466 $result = $wpdb->update(
5467 $table_name,
5468 $update_data,
5469 array('id' => $intent_id),
5470 null,
5471 array('%d')
5472 );
5473
5474 if ($result === false) {
5475 wp_send_json_error(__('Failed to update action.', 'mxchat'));
5476 }
5477
5478 wp_send_json_success(array('updated' => true));
5479 }
5480
5481 /**
5482 * AJAX handler to delete a single intent/action
5483 */
5484 public function mxchat_delete_intent_ajax() {
5485 check_ajax_referer('mxchat_delete_intent_nonce', 'security');
5486
5487 if (!current_user_can('manage_options')) {
5488 wp_send_json_error(__('Unauthorized', 'mxchat'));
5489 }
5490
5491 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5492
5493 if (!$intent_id) {
5494 wp_send_json_error(__('Invalid action ID', 'mxchat'));
5495 }
5496
5497 global $wpdb;
5498 $table_name = $wpdb->prefix . 'mxchat_intents';
5499
5500 $result = $wpdb->delete($table_name, array('id' => $intent_id), array('%d'));
5501
5502 if ($result === false) {
5503 wp_send_json_error(__('Failed to delete action', 'mxchat'));
5504 }
5505
5506 // Also delete individual phrases for this intent
5507 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5508 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5509 $wpdb->delete($phrases_table, array('intent_id' => $intent_id), array('%d'));
5510 }
5511
5512 wp_send_json_success(array('deleted' => true));
5513 }
5514
5515 /**
5516 * AJAX handler to add a single phrase with its own embedding to an intent
5517 */
5518 public function mxchat_add_phrase_ajax() {
5519 check_ajax_referer('mxchat_add_phrase_nonce', 'security');
5520
5521 if (!current_user_can('manage_options')) {
5522 wp_send_json_error(__('Unauthorized', 'mxchat'));
5523 }
5524
5525 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5526 $phrase = isset($_POST['phrase']) ? sanitize_text_field($_POST['phrase']) : '';
5527
5528 if (!$intent_id || empty($phrase)) {
5529 wp_send_json_error(__('Please provide an intent ID and phrase.', 'mxchat'));
5530 }
5531
5532 // Verify the intent exists
5533 global $wpdb;
5534 $intents_table = $wpdb->prefix . 'mxchat_intents';
5535 $intent = $wpdb->get_row($wpdb->prepare("SELECT id FROM $intents_table WHERE id = %d", $intent_id));
5536 if (!$intent) {
5537 wp_send_json_error(__('Action not found.', 'mxchat'));
5538 }
5539
5540 // Generate embedding for this single phrase
5541 $embedding_vector = $this->mxchat_generate_embedding($phrase);
5542 if (is_wp_error($embedding_vector)) {
5543 wp_send_json_error(__('Failed to generate embedding: ', 'mxchat') . $embedding_vector->get_error_message());
5544 }
5545
5546 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5547 $result = $wpdb->insert(
5548 $phrases_table,
5549 array(
5550 'intent_id' => $intent_id,
5551 'phrase' => $phrase,
5552 'embedding_vector' => maybe_serialize($embedding_vector),
5553 )
5554 );
5555
5556 if ($result === false) {
5557 wp_send_json_error(__('Failed to add phrase.', 'mxchat'));
5558 }
5559
5560 wp_send_json_success(array('id' => $wpdb->insert_id, 'phrase' => $phrase));
5561 }
5562
5563 /**
5564 * AJAX handler to delete a single phrase from wp_mxchat_intent_phrases
5565 */
5566 public function mxchat_delete_phrase_ajax() {
5567 check_ajax_referer('mxchat_delete_phrase_nonce', 'security');
5568
5569 if (!current_user_can('manage_options')) {
5570 wp_send_json_error(__('Unauthorized', 'mxchat'));
5571 }
5572
5573 $phrase_id = isset($_POST['phrase_id']) ? intval($_POST['phrase_id']) : 0;
5574 if (!$phrase_id) {
5575 wp_send_json_error(__('Invalid phrase ID.', 'mxchat'));
5576 }
5577
5578 global $wpdb;
5579 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5580 $result = $wpdb->delete($phrases_table, array('id' => $phrase_id), array('%d'));
5581
5582 if ($result === false) {
5583 wp_send_json_error(__('Failed to delete phrase.', 'mxchat'));
5584 }
5585
5586 wp_send_json_success(array('deleted' => true));
5587 }
5588
5589 /**
5590 * AJAX handler to fetch individual phrases for an intent
5591 */
5592 public function mxchat_get_phrases_ajax() {
5593 check_ajax_referer('mxchat_get_phrases_nonce', 'security');
5594
5595 if (!current_user_can('manage_options')) {
5596 wp_send_json_error(__('Unauthorized', 'mxchat'));
5597 }
5598
5599 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5600 if (!$intent_id) {
5601 wp_send_json_error(__('Invalid intent ID.', 'mxchat'));
5602 }
5603
5604 global $wpdb;
5605 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5606
5607 $phrases = array();
5608 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5609 $phrases = $wpdb->get_results($wpdb->prepare(
5610 "SELECT id, phrase, created_at FROM $phrases_table WHERE intent_id = %d ORDER BY created_at ASC",
5611 $intent_id
5612 ));
5613 }
5614
5615 wp_send_json_success(array('phrases' => $phrases));
5616 }
5617
5618 /**
5619 * AJAX handler to clear legacy phrases and embedding from the main intents table
5620 */
5621 public function mxchat_delete_legacy_phrases_ajax() {
5622 check_ajax_referer('mxchat_delete_legacy_nonce', 'security');
5623
5624 if (!current_user_can('manage_options')) {
5625 wp_send_json_error(__('Unauthorized', 'mxchat'));
5626 }
5627
5628 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5629 if (!$intent_id) {
5630 wp_send_json_error(__('Invalid intent ID.', 'mxchat'));
5631 }
5632
5633 global $wpdb;
5634 $table_name = $wpdb->prefix . 'mxchat_intents';
5635
5636 $result = $wpdb->update(
5637 $table_name,
5638 array('phrases' => '', 'embedding_vector' => ''),
5639 array('id' => $intent_id),
5640 array('%s', '%s'),
5641 array('%d')
5642 );
5643
5644 if ($result === false) {
5645 wp_send_json_error(__('Failed to clear legacy phrases.', 'mxchat'));
5646 }
5647
5648 wp_send_json_success(array('cleared' => true));
5649 }
5650
5651 /**
5652 * Enhanced get_available_callbacks function with form action exclusion
5653 *
5654 * @param bool $grouped Whether to return callbacks grouped by category
5655 * @param bool $include_all Whether to include all potential actions (even if add-on not installed)
5656 * @return array Callbacks data with icons, descriptions and availability status
5657 */
5658 private function mxchat_get_available_callbacks($grouped = false, $include_all = true) {
5659 // Load WordPress plugin functions if needed
5660 if (!function_exists('get_plugins')) {
5661 require_once ABSPATH . 'wp-admin/includes/plugin.php';
5662 }
5663
5664 // Get active plugins
5665 $active_plugins = get_option('active_plugins', array());
5666
5667 // Functions to exclude from the action selector only if Pro is activated
5668 // If user doesn't have Pro, show these so they can see what they're missing
5669 $excluded_when_pro_active_functions = array(
5670 'mxchat_handle_form_collection' // Forms add-on action
5671 );
5672
5673 // Always excluded functions (regardless of Pro status)
5674 $always_excluded_functions = array();
5675
5676 // Combine exclusion lists based on Pro activation status
5677 $excluded_functions = $always_excluded_functions;
5678 if ($this->is_activated) {
5679 // Only exclude add-on managed functions if Pro is active
5680 $excluded_functions = array_merge($excluded_functions, $excluded_when_pro_active_functions);
5681 }
5682
5683 // Define add-on plugin files and their corresponding action functions
5684 $addon_plugins = array(
5685 'mxchat-woo/mxchat-woo.php' => array(
5686 'functions' => array(
5687 'mxchat_handle_product_recommendations',
5688 'mxchat_handle_order_history',
5689 'mxchat_show_product_card',
5690 'mxchat_add_to_cart',
5691 'mxchat_checkout_redirect',
5692 'mxchat_handle_featured_products'
5693 ),
5694 'name' => __('WooCommerce Add-on', 'mxchat'),
5695 'pro_required' => true
5696 ),
5697 'mxchat-perplexity/mxchat-perplexity.php' => array(
5698 'functions' => array('mxchat_perplexity_research'),
5699 'name' => __('Perplexity Add-on', 'mxchat'),
5700 'pro_required' => true
5701 ),
5702 'mxchat-forms/mxchat-forms.php' => array(
5703 'functions' => array('mxchat_handle_form_collection'),
5704 'name' => __('Forms Add-on', 'mxchat'),
5705 'pro_required' => true
5706 ),
5707 // Add other add-ons and their functions here
5708 );
5709
5710 // Get the functions that are provided by active add-ons
5711 $addon_provided_functions = array();
5712 $addon_function_mapping = array(); // Maps functions to their add-on info
5713
5714 // Check which add-ons are active
5715 foreach ($addon_plugins as $plugin_file => $addon_info) {
5716 $is_active = in_array($plugin_file, $active_plugins);
5717
5718 // For each function in this addon
5719 foreach ($addon_info['functions'] as $function) {
5720 // Consider a function installed only if:
5721 // 1. The add-on is active AND
5722 // 2. Either it doesn't require Pro OR Pro is activated
5723 $is_installed = $is_active && (!$addon_info['pro_required'] || $this->is_activated);
5724
5725 // If the add-on is installed, mark this function as provided by an add-on
5726 if ($is_installed) {
5727 $addon_provided_functions[] = $function;
5728 }
5729
5730 // Store addon info for this function regardless of installation status
5731 $addon_function_mapping[$function] = array(
5732 'addon' => basename(dirname($plugin_file)),
5733 'addon_name' => $addon_info['name'],
5734 'pro_required' => $addon_info['pro_required'],
5735 'is_active' => $is_active,
5736 'is_installed' => $is_installed
5737 );
5738 }
5739 }
5740
5741 // Core callbacks - always available in the base plugin
5742 $core_callbacks = array(
5743 'mxchat_handle_email_capture' => array(
5744 'label' => __('Loops Email Capture', 'mxchat'),
5745 'pro_only' => false,
5746 'group' => __('Customer Engagement', 'mxchat'),
5747 'icon' => 'email-alt',
5748 'description' => __('Collect visitor emails for your mailing list in Loops', 'mxchat'),
5749 'addon' => false, // Not from an add-on
5750 'installed' => true // Always installed with base plugin
5751 ),
5752 'mxchat_handle_search_request' => array(
5753 'label' => __('Brave Web Search', 'mxchat'),
5754 'pro_only' => false,
5755 'group' => __('Search Features', 'mxchat'),
5756 'icon' => 'search',
5757 'description' => __('Let users search the web directly from the chat (requires a Brave Search API key)', 'mxchat'),
5758 'addon' => false,
5759 'installed' => true
5760 ),
5761 'mxchat_handle_image_search_request' => array(
5762 'label' => __('Brave Image Search', 'mxchat'),
5763 'pro_only' => false,
5764 'group' => __('Search Features', 'mxchat'),
5765 'icon' => 'format-image',
5766 'description' => __('Search and display images in the chat conversation (requires a Brave Search API key)', 'mxchat'),
5767 'addon' => false,
5768 'installed' => true
5769 ),
5770 // Pro core features - check is_activated property
5771 'mxchat_generate_image' => array(
5772 'label' => __('Generate Image (OpenAI)', 'mxchat'),
5773 'pro_only' => false,
5774 'group' => __('Other Features', 'mxchat'),
5775 'icon' => 'art',
5776 'description' => __('Create images with GPT Image from OpenAI (requires OpenAI API key)', 'mxchat'),
5777 'addon' => false,
5778 'installed' => true
5779 ),
5780 'mxchat_generate_gemini_image' => array(
5781 'label' => __('Generate Image (Gemini)', 'mxchat'),
5782 'pro_only' => false,
5783 'group' => __('Other Features', 'mxchat'),
5784 'icon' => 'art',
5785 'description' => __('Create images with Imagen from Google (requires Gemini API key)', 'mxchat'),
5786 'addon' => false,
5787 'installed' => true
5788 ),
5789 'mxchat_handle_pdf_discussion' => array(
5790 'label' => __('Chat with PDF', 'mxchat'),
5791 'pro_only' => false,
5792 'group' => __('Other Features', 'mxchat'),
5793 'icon' => 'media-document',
5794 'description' => __('Answer questions about uploaded PDF documents', 'mxchat'),
5795 'addon' => false,
5796 'installed' => true
5797 ),
5798 'mxchat_live_agent_handover' => array(
5799 'label' => __('Slack Live Agent', 'mxchat'),
5800 'pro_only' => false,
5801 'group' => __('Customer Engagement', 'mxchat'),
5802 'icon' => 'admin-users',
5803 'description' => __('Transfer conversation to a human support agent on Slack', 'mxchat'),
5804 'addon' => false,
5805 'installed' => true
5806 ),
5807 'mxchat_telegram_live_agent_handover' => array(
5808 'label' => __('Telegram Live Agent', 'mxchat'),
5809 'pro_only' => false,
5810 'group' => __('Customer Engagement', 'mxchat'),
5811 'icon' => 'format-chat',
5812 'description' => __('Transfer conversation to a human support agent on Telegram', 'mxchat'),
5813 'addon' => false,
5814 'installed' => true
5815 ),
5816 'mxchat_handle_switch_to_chatbot_intent' => array(
5817 'label' => __('Back to Chatbot', 'mxchat'),
5818 'pro_only' => false,
5819 'group' => __('Customer Engagement', 'mxchat'),
5820 'icon' => 'backup',
5821 'description' => __('Return from live agent mode to AI chatbot', 'mxchat'),
5822 'addon' => false,
5823 'installed' => true
5824 ),
5825 );
5826
5827 // Add-on callbacks with placeholders - only include if the add-on is NOT active
5828 // These are promotional/informational only — not selectable as real actions
5829 $addon_callbacks = array(
5830 // WooCommerce Add-on
5831 'mxchat_handle_product_recommendations' => array(
5832 'label' => __('Product Recommendations', 'mxchat'),
5833 'pro_only' => false,
5834 'addon_promo' => true,
5835 'group' => __('WooCommerce Features', 'mxchat'),
5836 'icon' => 'cart',
5837 'description' => __('Suggest products based on customer preferences', 'mxchat'),
5838 ),
5839 'mxchat_handle_order_history' => array(
5840 'label' => __('Order History', 'mxchat'),
5841 'pro_only' => false,
5842 'addon_promo' => true,
5843 'group' => __('WooCommerce Features', 'mxchat'),
5844 'icon' => 'clipboard',
5845 'description' => __('Allow customers to check their order status', 'mxchat'),
5846 ),
5847 'mxchat_show_product_card' => array(
5848 'label' => __('Show Product Card', 'mxchat'),
5849 'pro_only' => false,
5850 'addon_promo' => true,
5851 'group' => __('WooCommerce Features', 'mxchat'),
5852 'icon' => 'products',
5853 'description' => __('Display product information in the chat', 'mxchat'),
5854 ),
5855 'mxchat_add_to_cart' => array(
5856 'label' => __('Add to Cart', 'mxchat'),
5857 'pro_only' => false,
5858 'addon_promo' => true,
5859 'group' => __('WooCommerce Features', 'mxchat'),
5860 'icon' => 'plus-alt',
5861 'description' => __('Add products to cart directly from chat', 'mxchat'),
5862 ),
5863 'mxchat_checkout_redirect' => array(
5864 'label' => __('Proceed to Checkout', 'mxchat'),
5865 'pro_only' => false,
5866 'addon_promo' => true,
5867 'group' => __('WooCommerce Features', 'mxchat'),
5868 'icon' => 'arrow-right-alt',
5869 'description' => __('Redirect customer to checkout page', 'mxchat'),
5870 ),
5871 'mxchat_handle_featured_products' => array(
5872 'label' => __('Featured Products Showcase', 'mxchat'),
5873 'pro_only' => false,
5874 'addon_promo' => true,
5875 'group' => __('WooCommerce Features', 'mxchat'),
5876 'icon' => 'star-filled',
5877 'description' => __('Display a curated selection of products with an AI-generated message', 'mxchat'),
5878 ),
5879
5880 // Perplexity Add-on
5881 'mxchat_perplexity_research' => array(
5882 'label' => __('Perplexity Research', 'mxchat'),
5883 'pro_only' => false,
5884 'addon_promo' => true,
5885 'group' => __('Search Features', 'mxchat'),
5886 'icon' => 'book-alt',
5887 'description' => __('Allows the chatbot to search the web for accurate, up-to-date answers', 'mxchat'),
5888 ),
5889
5890 // Forms Add-on
5891 'mxchat_handle_form_collection' => array(
5892 'label' => __('Form Collection', 'mxchat'),
5893 'pro_only' => false,
5894 'addon_promo' => true,
5895 'group' => __('Form Features', 'mxchat'),
5896 'icon' => 'feedback',
5897 'description' => __('Collect user information through custom forms in chat', 'mxchat'),
5898 ),
5899 );
5900
5901 // Enhance add-on callbacks with installation status and addon info
5902 foreach ($addon_callbacks as $function => $data) {
5903 if (isset($addon_function_mapping[$function])) {
5904 $addon_info = $addon_function_mapping[$function];
5905
5906 $addon_callbacks[$function]['addon'] = $addon_info['addon'];
5907 $addon_callbacks[$function]['addon_name'] = $addon_info['addon_name'];
5908 $addon_callbacks[$function]['installed'] = $addon_info['is_installed'];
5909
5910 // Set pro_only based on add-on configuration
5911 $addon_callbacks[$function]['pro_only'] = $addon_info['pro_required'];
5912 } else {
5913 $addon_callbacks[$function]['addon'] = 'unknown';
5914 $addon_callbacks[$function]['addon_name'] = __('Unknown Add-on', 'mxchat');
5915 $addon_callbacks[$function]['installed'] = false;
5916 }
5917 }
5918
5919 // Initialize callbacks with core features
5920 $callbacks = $core_callbacks;
5921
5922 // Get callbacks from active add-ons
5923 $active_addon_callbacks = apply_filters('mxchat_available_callbacks', array());
5924
5925 // Add placeholder callbacks only for add-ons that aren't active
5926 if ($include_all) {
5927 foreach ($addon_callbacks as $function => $data) {
5928 // Skip placeholders for functions provided by active add-ons
5929 if (in_array($function, $addon_provided_functions)) {
5930 continue;
5931 }
5932
5933 // Skip excluded functions
5934 if (in_array($function, $excluded_functions)) {
5935 continue;
5936 }
5937
5938 // Add the placeholder
5939 $callbacks[$function] = $data;
5940 }
5941 }
5942
5943 // Add callbacks from active add-ons (will override placeholders)
5944 foreach ($active_addon_callbacks as $function => $data) {
5945 // Skip excluded functions
5946 if (in_array($function, $excluded_functions)) {
5947 continue;
5948 }
5949
5950 // Always include callbacks from add-ons
5951 $callbacks[$function] = $data;
5952
5953 // Ensure they have the proper add-on info
5954 if (isset($addon_function_mapping[$function])) {
5955 $addon_info = $addon_function_mapping[$function];
5956 $callbacks[$function]['addon'] = $addon_info['addon'];
5957 $callbacks[$function]['addon_name'] = $addon_info['addon_name'];
5958 $callbacks[$function]['installed'] = $addon_info['is_installed'];
5959 $callbacks[$function]['pro_only'] = $addon_info['pro_required'];
5960 }
5961 }
5962
5963 // Just before returning callbacks, sort them to prioritize free features
5964 if (!$grouped) {
5965 // Create temporary arrays for sorting
5966 $free_callbacks = array();
5967 $pro_callbacks = array();
5968
5969 // Split callbacks into free and pro
5970 foreach ($callbacks as $key => $data) {
5971 if (isset($data['pro_only']) && $data['pro_only']) {
5972 $pro_callbacks[$key] = $data;
5973 } else {
5974 $free_callbacks[$key] = $data;
5975 }
5976 }
5977
5978 // Merge with free callbacks first
5979 $callbacks = array_merge($free_callbacks, $pro_callbacks);
5980 }
5981
5982 // Return grouped structure if requested
5983 if ($grouped) {
5984 $grouped_callbacks = array();
5985 foreach ($callbacks as $key => $data) {
5986 $group_label = isset($data['group']) ? $data['group'] : __('Other Features', 'mxchat');
5987
5988 // Ensure we carry forward all the new fields in grouped mode
5989 $callback_data = array(
5990 'label' => $data['label'],
5991 'pro_only' => isset($data['pro_only']) ? $data['pro_only'] : false,
5992 'icon' => isset($data['icon']) ? $data['icon'] : 'admin-generic',
5993 'description' => isset($data['description']) ? $data['description'] : __('Custom action for your chatbot', 'mxchat'),
5994 'addon' => isset($data['addon']) ? $data['addon'] : false,
5995 'addon_name' => isset($data['addon_name']) ? $data['addon_name'] : '',
5996 'installed' => isset($data['installed']) ? $data['installed'] : true
5997 );
5998
5999 $grouped_callbacks[$group_label][$key] = $callback_data;
6000 }
6001
6002 // Sort within each group to prioritize free features
6003 foreach ($grouped_callbacks as $group => $items) {
6004 $free_items = array();
6005 $pro_items = array();
6006
6007 foreach ($items as $key => $data) {
6008 if (isset($data['pro_only']) && $data['pro_only']) {
6009 $pro_items[$key] = $data;
6010 } else {
6011 $free_items[$key] = $data;
6012 }
6013 }
6014
6015 $grouped_callbacks[$group] = array_merge($free_items, $pro_items);
6016 }
6017
6018 return $grouped_callbacks;
6019 }
6020
6021 return $callbacks;
6022 }
6023
6024 public function mxchat_page_init() {
6025 register_setting(
6026 'mxchat_option_group',
6027 'mxchat_options',
6028 array($this, 'mxchat_sanitize')
6029 );
6030
6031 register_setting(
6032 'mxchat_option_group',
6033 'mxchat_similarity_threshold',
6034 array(
6035 'type' => 'number',
6036 'sanitize_callback' => function($value) {
6037 $value = absint($value);
6038 return min(max($value, 20), 95);
6039 },
6040 'default' => 80,
6041 )
6042 );
6043
6044 // Chatbot Settings Section
6045 add_settings_section(
6046 'mxchat_chatbot_section',
6047 esc_html__('Chatbot Settings', 'mxchat'),
6048 null,
6049 'mxchat-chatbot'
6050 );
6051
6052 // API Keys Settings Section
6053 add_settings_section(
6054 'mxchat_api_keys_section',
6055 esc_html__('API Keys', 'mxchat'),
6056 array($this, 'mxchat_api_keys_section_callback'),
6057 'mxchat-api-keys'
6058 );
6059
6060 // OpenAI API Key
6061 add_settings_field(
6062 'api_key',
6063 esc_html__('OpenAI API Key', 'mxchat'),
6064 array($this, 'api_key_callback'),
6065 'mxchat-api-keys',
6066 'mxchat_api_keys_section'
6067 );
6068
6069 // X.AI API Key
6070 add_settings_field(
6071 'xai_api_key',
6072 esc_html__('X.AI API Key', 'mxchat'),
6073 array($this, 'xai_api_key_callback'),
6074 'mxchat-api-keys',
6075 'mxchat_api_keys_section'
6076 );
6077
6078 // Claude API Key
6079 add_settings_field(
6080 'claude_api_key',
6081 esc_html__('Claude API Key', 'mxchat'),
6082 array($this, 'claude_api_key_callback'),
6083 'mxchat-api-keys',
6084 'mxchat_api_keys_section'
6085 );
6086
6087 // DeepSeek API Key
6088 add_settings_field(
6089 'deepseek_api_key',
6090 esc_html__('DeepSeek API Key', 'mxchat'),
6091 array($this, 'deepseek_api_key_callback'),
6092 'mxchat-api-keys',
6093 'mxchat_api_keys_section'
6094 );
6095
6096 // Google Gemini API Key
6097 add_settings_field(
6098 'gemini_api_key',
6099 esc_html__('Google Gemini API Key', 'mxchat'),
6100 array($this, 'gemini_api_key_callback'),
6101 'mxchat-api-keys',
6102 'mxchat_api_keys_section'
6103 );
6104
6105 // Voyage AI API Key
6106 add_settings_field(
6107 'voyage_api_key',
6108 esc_html__('Voyage AI API Key', 'mxchat'),
6109 array($this, 'voyage_api_key_callback'),
6110 'mxchat-api-keys',
6111 'mxchat_api_keys_section'
6112 );
6113
6114 // OpenRouter API Key
6115 add_settings_field(
6116 'openrouter_api_key',
6117 esc_html__('OpenRouter API Key', 'mxchat'),
6118 array($this, 'openrouter_api_key_callback'),
6119 'mxchat-api-keys',
6120 'mxchat_api_keys_section'
6121 );
6122
6123 // Custom (OpenAI-compatible) Provider — for Ollama, LM Studio, vLLM, llama.cpp, Azure OpenAI, etc.
6124 add_settings_field(
6125 'custom_provider',
6126 esc_html__('Custom Provider (OpenAI-compatible)', 'mxchat'),
6127 array($this, 'custom_provider_callback'),
6128 'mxchat-api-keys',
6129 'mxchat_api_keys_section'
6130 );
6131
6132 // Loops API Key
6133 add_settings_field(
6134 'loops_api_key',
6135 esc_html__('Loops API Key', 'mxchat'),
6136 array($this, 'mxchat_loops_api_key_callback'),
6137 'mxchat-api-keys',
6138 'mxchat_api_keys_section'
6139 );
6140
6141 // Brave Search API Key
6142 add_settings_field(
6143 'brave_api_key',
6144 __('Brave API Key', 'mxchat'),
6145 array($this, 'mxchat_brave_api_key_callback'),
6146 'mxchat-api-keys',
6147 'mxchat_api_keys_section'
6148 );
6149
6150 // Similarity Threshold Slider
6151 add_settings_field(
6152 'similarity_threshold', // Field ID
6153 esc_html__('Similarity Threshold', 'mxchat'), // Field title
6154 array($this, 'mxchat_similarity_threshold_callback'), // Callback function
6155 'mxchat-chatbot', // Page
6156 'mxchat_chatbot_section' // Section
6157 );
6158
6159 // RAG Sources Limit Slider
6160 add_settings_field(
6161 'rag_sources_limit', // Field ID
6162 esc_html__('RAG Sources Limit', 'mxchat'), // Field title
6163 array($this, 'mxchat_rag_sources_limit_callback'), // Callback function
6164 'mxchat-chatbot', // Page
6165 'mxchat_chatbot_section' // Section
6166 );
6167
6168 // RAG Chunks Limit Slider
6169 add_settings_field(
6170 'rag_chunks_limit', // Field ID
6171 esc_html__('RAG Chunks Limit', 'mxchat'), // Field title
6172 array($this, 'mxchat_rag_chunks_limit_callback'), // Callback function
6173 'mxchat-chatbot', // Page
6174 'mxchat_chatbot_section' // Section
6175 );
6176
6177 add_settings_field(
6178 'append_to_body',
6179 esc_html__('Auto-Display Chatbot', 'mxchat'),
6180 array($this, 'mxchat_append_to_body_callback'),
6181 'mxchat-chatbot',
6182 'mxchat_chatbot_section'
6183 );
6184
6185 add_settings_field(
6186 'contextual_awareness_toggle',
6187 esc_html__('Contextual Awareness', 'mxchat'),
6188 array($this, 'mxchat_contextual_awareness_callback'),
6189 'mxchat-chatbot',
6190 'mxchat_chatbot_section'
6191 );
6192
6193 add_settings_field(
6194 'citation_links_toggle',
6195 esc_html__('Citation Links', 'mxchat'),
6196 array($this, 'mxchat_citation_links_toggle_callback'),
6197 'mxchat-chatbot',
6198 'mxchat_chatbot_section'
6199 );
6200
6201 // Satisfaction rating toggle (plan-a5b006).
6202 add_settings_field(
6203 'satisfaction_rating_enabled',
6204 esc_html__('Satisfaction Rating Prompt', 'mxchat'),
6205 array($this, 'mxchat_satisfaction_rating_toggle_callback'),
6206 'mxchat-chatbot',
6207 'mxchat_chatbot_section'
6208 );
6209
6210 // Satisfaction rating customization (plan-141a12, plan-29caac):
6211 // the 5 customization fields are now inline-rendered inside
6212 // mxchat_satisfaction_rating_toggle_callback's sub-options wrapper.
6213
6214 add_settings_field(
6215 'enable_streaming_toggle',
6216 esc_html__('Enable Streaming', 'mxchat'),
6217 array($this, 'enable_streaming_toggle_callback'),
6218 'mxchat-chatbot',
6219 'mxchat_chatbot_section', // Same section as your working toggle
6220 array(
6221 'class' => 'mxchat-setting-row streaming-setting',
6222 'style' => 'display: none;' // Hidden by default, shown when OpenAI/Claude selected
6223 )
6224 );
6225
6226
6227 add_settings_field(
6228 'model',
6229 esc_html__('Chat Model', 'mxchat'),
6230 array($this, 'mxchat_model_callback'),
6231 'mxchat-chatbot',
6232 'mxchat_chatbot_section'
6233 );
6234
6235 add_settings_field(
6236 'embedding_model',
6237 esc_html__('Embedding Model', 'mxchat'),
6238 array($this, 'embedding_model_callback'),
6239 'mxchat-chatbot',
6240 'mxchat_chatbot_section'
6241 );
6242
6243 add_settings_field(
6244 'system_prompt_instructions',
6245 esc_html__('AI Instructions (Behavior)', 'mxchat'),
6246 array($this, 'system_prompt_instructions_callback'),
6247 'mxchat-chatbot',
6248 'mxchat_chatbot_section'
6249 );
6250
6251
6252 add_settings_field(
6253 'top_bar_title',
6254 esc_html__('Top Bar Title', 'mxchat'),
6255 array($this, 'mxchat_top_bar_title_callback'),
6256 'mxchat-chatbot',
6257 'mxchat_chatbot_section'
6258 );
6259
6260 add_settings_field(
6261 'ai_agent_text',
6262 esc_html__('AI Agent Text', 'mxchat'),
6263 array($this, 'mxchat_ai_agent_text_callback'),
6264 'mxchat-chatbot',
6265 'mxchat_chatbot_section'
6266 );
6267
6268 add_settings_field(
6269 'enable_email_block',
6270 esc_html__('Require Email To Chat', 'mxchat'),
6271 array($this, 'enable_email_block_callback'),
6272 'mxchat-chatbot',
6273 'mxchat_chatbot_section'
6274 );
6275
6276 add_settings_field(
6277 'email_blocker_header_content',
6278 esc_html__('Require Email Chat Content', 'mxchat'),
6279 array($this, 'email_blocker_header_content_callback'),
6280 'mxchat-chatbot',
6281 'mxchat_chatbot_section'
6282 );
6283
6284 add_settings_field(
6285 'email_blocker_button_text',
6286 esc_html__('Require Email Chat Button Text', 'mxchat'),
6287 [$this, 'email_blocker_button_text_callback'],
6288 'mxchat-chatbot',
6289 'mxchat_chatbot_section'
6290 );
6291
6292 add_settings_field(
6293 'enable_name_field',
6294 esc_html__('Require Name Field', 'mxchat'),
6295 array($this, 'enable_name_field_callback'),
6296 'mxchat-chatbot',
6297 'mxchat_chatbot_section'
6298 );
6299
6300 add_settings_field(
6301 'name_field_placeholder',
6302 esc_html__('Name Field Placeholder', 'mxchat'),
6303 array($this, 'name_field_placeholder_callback'),
6304 'mxchat-chatbot',
6305 'mxchat_chatbot_section'
6306 );
6307
6308 add_settings_field(
6309 'intro_message',
6310 esc_html__('Introductory Message', 'mxchat'),
6311 array($this, 'mxchat_intro_message_callback'),
6312 'mxchat-chatbot',
6313 'mxchat_chatbot_section'
6314 );
6315
6316 add_settings_field(
6317 'input_copy',
6318 esc_html__('Input Copy', 'mxchat'),
6319 array($this, 'mxchat_input_copy_callback'),
6320 'mxchat-chatbot',
6321 'mxchat_chatbot_section'
6322 );
6323
6324 add_settings_field(
6325 'pre_chat_message',
6326 esc_html__('Chat Teaser Pop-up', 'mxchat'),
6327 array($this, 'mxchat_pre_chat_message_callback'),
6328 'mxchat-chatbot',
6329 'mxchat_chatbot_section'
6330 );
6331
6332 add_settings_field(
6333 'privacy_toggle',
6334 esc_html__('Toggle Privacy Notice', 'mxchat'),
6335 array($this, 'mxchat_privacy_toggle_callback'),
6336 'mxchat-chatbot',
6337 'mxchat_chatbot_section'
6338 );
6339
6340 add_settings_field(
6341 'complianz_toggle',
6342 esc_html__('Enable Complianz', 'mxchat'),
6343 array($this, 'mxchat_complianz_toggle_callback'),
6344 'mxchat-chatbot',
6345 'mxchat_chatbot_section'
6346 );
6347
6348 add_settings_field(
6349 'link_target_toggle',
6350 esc_html__('Open Links in a New Tab', 'mxchat'),
6351 array($this, 'mxchat_link_target_toggle_callback'),
6352 'mxchat-chatbot',
6353 'mxchat_chatbot_section'
6354 );
6355
6356 add_settings_field(
6357 'chat_persistence_toggle',
6358 esc_html__('Enable Chat Persistence', 'mxchat'),
6359 array($this, 'mxchat_chat_persistence_toggle_callback'),
6360 'mxchat-chatbot',
6361 'mxchat_chatbot_section'
6362 );
6363
6364 add_settings_field(
6365 'print_button_enabled',
6366 esc_html__('Show Download Transcript Button', 'mxchat'),
6367 array($this, 'mxchat_print_button_toggle_callback'),
6368 'mxchat-chatbot',
6369 'mxchat_chatbot_section'
6370 );
6371
6372 add_settings_field(
6373 'reset_chat_enabled',
6374 esc_html__('Show Start-New-Chat Button', 'mxchat'),
6375 array($this, 'mxchat_reset_chat_toggle_callback'),
6376 'mxchat-chatbot',
6377 'mxchat_chatbot_section'
6378 );
6379
6380 add_settings_field(
6381 'reset_chat_label',
6382 esc_html__('Start-New-Chat Button Label', 'mxchat'),
6383 array($this, 'mxchat_reset_chat_label_callback'),
6384 'mxchat-chatbot',
6385 'mxchat_chatbot_section'
6386 );
6387
6388 add_settings_field(
6389 'popular_question_1',
6390 esc_html__('Quick Question 1', 'mxchat'),
6391 array($this, 'mxchat_popular_question_1_callback'),
6392 'mxchat-chatbot',
6393 'mxchat_chatbot_section'
6394 );
6395
6396 add_settings_field(
6397 'popular_question_2',
6398 esc_html__('Quick Question 2', 'mxchat'),
6399 array($this, 'mxchat_popular_question_2_callback'),
6400 'mxchat-chatbot',
6401 'mxchat_chatbot_section'
6402 );
6403
6404 add_settings_field(
6405 'popular_question_3',
6406 esc_html__('Quick Question 3', 'mxchat'),
6407 array($this, 'mxchat_popular_question_3_callback'),
6408 'mxchat-chatbot',
6409 'mxchat_chatbot_section'
6410 );
6411
6412 add_settings_field(
6413 'additional_popular_questions',
6414 esc_html__('Additional Quick Questions', 'mxchat'),
6415 array($this, 'mxchat_additional_popular_questions_callback'),
6416 'mxchat-chatbot',
6417 'mxchat_chatbot_section'
6418 );
6419
6420
6421 add_settings_field(
6422 'rate_limits',
6423 __('Rate Limits Settings', 'mxchat'),
6424 array($this, 'mxchat_rate_limits_callback'),
6425 'mxchat-chatbot',
6426 'mxchat_chatbot_section'
6427 );
6428
6429 // Loops Settings Section
6430 add_settings_section(
6431 'mxchat_loops_section',
6432 esc_html__('Loops Settings', 'mxchat'),
6433 null,
6434 'mxchat-embed'
6435 );
6436
6437 // Loops Settings Fields (API Key moved to API Keys tab)
6438 add_settings_field(
6439 'loops_mailing_list',
6440 esc_html__('Loops Mailing List', 'mxchat'),
6441 array($this, 'mxchat_loops_mailing_list_callback'),
6442 'mxchat-embed',
6443 'mxchat_loops_section'
6444 );
6445
6446 add_settings_field(
6447 'triggered_phrase_response',
6448 esc_html__('Triggered Phrase Response', 'mxchat'),
6449 array($this, 'mxchat_triggered_phrase_response_callback'),
6450 'mxchat-embed',
6451 'mxchat_loops_section'
6452 );
6453
6454 add_settings_field(
6455 'email_capture_response',
6456 esc_html__('Email Capture Response', 'mxchat'),
6457 array($this, 'mxchat_email_capture_response_callback'),
6458 'mxchat-embed',
6459 'mxchat_loops_section'
6460 );
6461
6462 // Brave Search Settings Fields
6463 add_settings_section(
6464 'mxchat_brave_section',
6465 __('Brave Search Settings', 'mxchat'),
6466 array($this, 'mxchat_brave_section_callback'),
6467 'mxchat-embed'
6468 );
6469
6470 // Brave API Key moved to API Keys tab
6471 add_settings_field(
6472 'brave_image_count',
6473 __('Number of Images to Return', 'mxchat'),
6474 array($this, 'mxchat_brave_image_count_callback'),
6475 'mxchat-embed',
6476 'mxchat_brave_section'
6477 );
6478
6479 add_settings_field(
6480 'brave_safe_search',
6481 __('Safe Search', 'mxchat'),
6482 array($this, 'mxchat_brave_safe_search_callback'),
6483 'mxchat-embed',
6484 'mxchat_brave_section'
6485 );
6486
6487 add_settings_field(
6488 'brave_news_count',
6489 __('Number of News Articles', 'mxchat'),
6490 array($this, 'mxchat_brave_news_count_callback'),
6491 'mxchat-embed',
6492 'mxchat_brave_section'
6493 );
6494
6495 add_settings_field(
6496 'brave_country',
6497 __('Country', 'mxchat'),
6498 array($this, 'mxchat_brave_country_callback'),
6499 'mxchat-embed',
6500 'mxchat_brave_section'
6501 );
6502
6503 add_settings_field(
6504 'brave_language',
6505 __('Language', 'mxchat'),
6506 array($this, 'mxchat_brave_language_callback'),
6507 'mxchat-embed',
6508 'mxchat_brave_section'
6509 );
6510
6511 // Chat with PDF Intent Settings Fields
6512 add_settings_section(
6513 'mxchat_pdf_intent_section',
6514 __('Toolbar Settings & Intents', 'mxchat'),
6515 array($this, 'mxchat_pdf_intent_section_callback'),
6516 'mxchat-embed'
6517 );
6518
6519 add_settings_field(
6520 'chat_toolbar_toggle',
6521 __('Show Chat Toolbar', 'mxchat'),
6522 array($this, 'mxchat_chat_toolbar_toggle_callback'),
6523 'mxchat-embed',
6524 'mxchat_pdf_intent_section'
6525 );
6526
6527 // PDF Upload Button Toggle
6528 add_settings_field(
6529 'show_pdf_upload_button',
6530 __('Show PDF Upload Button', 'mxchat'),
6531 array($this, 'mxchat_show_pdf_upload_button_callback'),
6532 'mxchat-embed',
6533 'mxchat_pdf_intent_section'
6534 );
6535
6536 // Word Upload Button Toggle
6537 add_settings_field(
6538 'show_word_upload_button',
6539 __('Show Word Upload Button', 'mxchat'),
6540 array($this, 'mxchat_show_word_upload_button_callback'),
6541 'mxchat-embed',
6542 'mxchat_pdf_intent_section'
6543 );
6544
6545 add_settings_field(
6546 'pdf_intent_trigger_text',
6547 __('Intent Trigger Text', 'mxchat'),
6548 array($this, 'mxchat_pdf_intent_trigger_text_callback'),
6549 'mxchat-embed',
6550 'mxchat_pdf_intent_section'
6551 );
6552
6553 add_settings_field(
6554 'pdf_intent_success_text',
6555 __('Success Text', 'mxchat'),
6556 array($this, 'mxchat_pdf_intent_success_text_callback'),
6557 'mxchat-embed',
6558 'mxchat_pdf_intent_section'
6559 );
6560
6561 add_settings_field(
6562 'pdf_intent_error_text',
6563 __('Error Text', 'mxchat'),
6564 array($this, 'mxchat_pdf_intent_error_text_callback'),
6565 'mxchat-embed',
6566 'mxchat_pdf_intent_section'
6567 );
6568
6569 // Add PDF Maximum Pages Field
6570 add_settings_field(
6571 'pdf_max_pages',
6572 __('Maximum Document Pages', 'mxchat'),
6573 array($this, 'mxchat_pdf_max_pages_callback'),
6574 'mxchat-embed',
6575 'mxchat_pdf_intent_section'
6576 );
6577
6578 // Live Agent Settings Fields
6579 add_settings_section(
6580 'mxchat_live_agent_section',
6581 __('Live Agent Settings', 'mxchat'),
6582 array($this, 'mxchat_live_agent_section_callback'),
6583 'mxchat-embed'
6584 );
6585
6586 // Live Agent Status Fields (add at top of live agent settings)
6587 add_settings_field(
6588 'live_agent_status',
6589 __('Live Agent Status', 'mxchat'),
6590 array($this, 'mxchat_live_agent_status_callback'),
6591 'mxchat-embed',
6592 'mxchat_live_agent_section'
6593 );
6594
6595 add_settings_field(
6596 'live_agent_notification_message',
6597 __('Notification Message', 'mxchat'),
6598 array($this, 'mxchat_live_agent_notification_message_callback'),
6599 'mxchat-embed',
6600 'mxchat_live_agent_section'
6601 );
6602
6603 add_settings_field(
6604 'live_agent_away_message',
6605 __('Away Message', 'mxchat'),
6606 array($this, 'mxchat_live_agent_away_message_callback'),
6607 'mxchat-embed',
6608 'mxchat_live_agent_section'
6609 );
6610
6611 add_settings_field(
6612 'live_agent_user_ids',
6613 __('Slack Agent User IDs', 'mxchat'),
6614 array($this, 'mxchat_live_agent_user_ids_callback'),
6615 'mxchat-embed',
6616 'mxchat_live_agent_section'
6617 );
6618
6619 add_settings_field(
6620 'live_agent_webhook_url',
6621 __('Slack Webhook URL', 'mxchat'),
6622 array($this, 'mxchat_live_agent_webhook_url_callback'),
6623 'mxchat-embed',
6624 'mxchat_live_agent_section'
6625 );
6626
6627 add_settings_field(
6628 'live_agent_secret_key',
6629 __('Slack Secret Key', 'mxchat'),
6630 array($this, 'mxchat_live_agent_secret_key_callback'),
6631 'mxchat-embed',
6632 'mxchat_live_agent_section'
6633 );
6634
6635 // Live Agent Integration Fields
6636 add_settings_field(
6637 'live_agent_bot_token',
6638 __('Slack Bot OAuth Token', 'mxchat'),
6639 array($this, 'mxchat_live_agent_bot_token_callback'),
6640 'mxchat-embed',
6641 'mxchat_live_agent_section'
6642 );
6643
6644 // Telegram Integration Section
6645 add_settings_section(
6646 'mxchat_telegram_section',
6647 __('Telegram Settings', 'mxchat'),
6648 array($this, 'mxchat_telegram_section_callback'),
6649 'mxchat-embed'
6650 );
6651
6652 add_settings_field(
6653 'telegram_status',
6654 __('Live Agent Status', 'mxchat'),
6655 array($this, 'mxchat_telegram_status_callback'),
6656 'mxchat-embed',
6657 'mxchat_telegram_section'
6658 );
6659
6660 add_settings_field(
6661 'telegram_notification_message',
6662 __('Notification Message', 'mxchat'),
6663 array($this, 'mxchat_telegram_notification_message_callback'),
6664 'mxchat-embed',
6665 'mxchat_telegram_section'
6666 );
6667
6668 add_settings_field(
6669 'telegram_away_message',
6670 __('Away Message', 'mxchat'),
6671 array($this, 'mxchat_telegram_away_message_callback'),
6672 'mxchat-embed',
6673 'mxchat_telegram_section'
6674 );
6675
6676 add_settings_field(
6677 'telegram_bot_token',
6678 __('Telegram Bot Token', 'mxchat'),
6679 array($this, 'mxchat_telegram_bot_token_callback'),
6680 'mxchat-embed',
6681 'mxchat_telegram_section'
6682 );
6683
6684 add_settings_field(
6685 'telegram_group_id',
6686 __('Telegram Group ID', 'mxchat'),
6687 array($this, 'mxchat_telegram_group_id_callback'),
6688 'mxchat-embed',
6689 'mxchat_telegram_section'
6690 );
6691
6692 add_settings_field(
6693 'telegram_webhook_secret',
6694 __('Webhook Secret Token', 'mxchat'),
6695 array($this, 'mxchat_telegram_webhook_secret_callback'),
6696 'mxchat-embed',
6697 'mxchat_telegram_section'
6698 );
6699
6700 // General Settings Section
6701 add_settings_section(
6702 'mxchat_general_section',
6703 esc_html__('YouTube Tutorials', 'mxchat'),
6704 null,
6705 'mxchat-general'
6706 );
6707 }
6708
6709 public function mxchat_prompts_page_init() {
6710 register_setting(
6711 'mxchat_prompts_options',
6712 'mxchat_prompts_options',
6713 array(
6714 'type' => 'array',
6715 'description' => __('MXChat Knowledge Base Settings', 'mxchat'),
6716 'default' => array(
6717 'mxchat_auto_sync_posts' => 0,
6718 'mxchat_auto_sync_pages' => 0,
6719 'mxchat_use_pinecone' => 0,
6720 'mxchat_pinecone_api_key' => '',
6721 'mxchat_pinecone_environment' => '',
6722 'mxchat_pinecone_index' => '',
6723 'mxchat_pinecone_host' => '',
6724 ),
6725 'sanitize_callback' => array($this, 'sanitize_prompts_options'),
6726 )
6727 );
6728
6729 add_action('admin_notices', array($this, 'sync_settings_notice'));
6730 }
6731
6732 public function mxchat_transcripts_page_init() {
6733 register_setting(
6734 'mxchat_transcripts_options',
6735 'mxchat_transcripts_options',
6736 array(
6737 'type' => 'array',
6738 'description' => __('MXChat Transcripts Notification Settings', 'mxchat'),
6739 'default' => array(
6740 'mxchat_enable_notifications' => 0,
6741 'mxchat_notification_email' => get_option('admin_email'),
6742 'mxchat_auto_delete_transcripts' => 'never',
6743 ),
6744 'sanitize_callback' => array($this, 'sanitize_transcripts_options'),
6745 )
6746 );
6747
6748 add_settings_section(
6749 'mxchat_transcripts_notification_section',
6750 esc_html__('Chat Notification Settings', 'mxchat'),
6751 array($this, 'mxchat_transcripts_notification_section_callback'),
6752 'mxchat-transcripts'
6753 );
6754
6755 add_settings_field(
6756 'mxchat_enable_notifications',
6757 esc_html__('Enable Chat Notifications', 'mxchat'),
6758 array($this, 'mxchat_enable_notifications_callback'),
6759 'mxchat-transcripts',
6760 'mxchat_transcripts_notification_section'
6761 );
6762
6763 add_settings_field(
6764 'mxchat_notification_email',
6765 esc_html__('Notification Email Address', 'mxchat'),
6766 array($this, 'mxchat_notification_email_callback'),
6767 'mxchat-transcripts',
6768 'mxchat_transcripts_notification_section'
6769 );
6770
6771 add_settings_field(
6772 'mxchat_auto_delete_transcripts',
6773 esc_html__('Auto-Delete Old Transcripts', 'mxchat'),
6774 array($this, 'mxchat_auto_delete_transcripts_callback'),
6775 'mxchat-transcripts',
6776 'mxchat_transcripts_notification_section'
6777 );
6778
6779 add_settings_field(
6780 'mxchat_retention_days',
6781 esc_html__('Custom Retention (Days)', 'mxchat'),
6782 array($this, 'mxchat_retention_days_callback'),
6783 'mxchat-transcripts',
6784 'mxchat_transcripts_notification_section'
6785 );
6786
6787 add_settings_field(
6788 'mxchat_auto_email_transcript',
6789 esc_html__('Auto-Email Full Transcript', 'mxchat'),
6790 array($this, 'mxchat_auto_email_transcript_callback'),
6791 'mxchat-transcripts',
6792 'mxchat_transcripts_notification_section'
6793 );
6794 }
6795
6796
6797 /**
6798 * Sanitize all prompts options
6799 *
6800 * @param array $input The unsanitized options array
6801 * @return array The sanitized options array
6802 */
6803 public function sanitize_prompts_options($input) {
6804 // Log the incoming input.
6805 //error_log('Sanitizing inputs: ' . print_r($input, true));
6806
6807 $sanitized = array();
6808
6809 // Boolean options
6810 $sanitized['mxchat_auto_sync_posts'] = isset($input['mxchat_auto_sync_posts']) ? 1 : 0;
6811 $sanitized['mxchat_auto_sync_pages'] = isset($input['mxchat_auto_sync_pages']) ? 1 : 0;
6812 $sanitized['mxchat_use_pinecone'] = !empty($input['mxchat_use_pinecone']) ? 1 : 0;
6813
6814 // API Key: if less than 32 characters, flag as invalid.
6815 $api_key = sanitize_text_field($input['mxchat_pinecone_api_key'] ?? '');
6816 if (!empty($api_key) && strlen($api_key) < 32) {
6817 add_settings_error(
6818 'mxchat_prompts_options',
6819 'invalid_api_key',
6820 __('The Pinecone API key appears to be invalid. Please check your API key.', 'mxchat')
6821 );
6822 $existing_options = get_option('mxchat_prompts_options', array());
6823 $sanitized['mxchat_pinecone_api_key'] = $existing_options['mxchat_pinecone_api_key'] ?? '';
6824 } else {
6825 $sanitized['mxchat_pinecone_api_key'] = $api_key;
6826 }
6827
6828 // Environment and Index Name
6829 $sanitized['mxchat_pinecone_environment'] = sanitize_text_field($input['mxchat_pinecone_environment'] ?? '');
6830 $sanitized['mxchat_pinecone_index'] = sanitize_text_field($input['mxchat_pinecone_index'] ?? '');
6831
6832 // Host: Remove protocol and validate format.
6833 $host = sanitize_text_field($input['mxchat_pinecone_host'] ?? '');
6834 $host = preg_replace('#^https?://#', '', $host);
6835 //error_log('Host after removing protocol: ' . $host);
6836 if (!empty($host)) {
6837 if (!preg_match('/^[\w-]+\.svc\.[\w-]+\.pinecone\.io$/', $host)) {
6838 add_settings_error(
6839 'mxchat_prompts_options',
6840 'invalid_host',
6841 __('The Pinecone host appears to be invalid. It should look like "mxchat-vectors-zrmsquq.svc.aped-4627-b74a.pinecone.io"', 'mxchat')
6842 );
6843 $existing_options = get_option('mxchat_prompts_options', array());
6844 $sanitized['mxchat_pinecone_host'] = $existing_options['mxchat_pinecone_host'] ?? '';
6845 } else {
6846 $sanitized['mxchat_pinecone_host'] = $host;
6847 }
6848 } else {
6849 $sanitized['mxchat_pinecone_host'] = '';
6850 }
6851
6852 //error_log('Final sanitized array: ' . print_r($sanitized, true));
6853
6854 return $sanitized;
6855 }
6856
6857 public function sync_settings_notice() {
6858 // Only show notice on our plugin page
6859 if (!isset($_GET['page']) || $_GET['page'] !== 'mxchat-prompts') {
6860 return;
6861 }
6862
6863 // Check if settings were updated
6864 if (isset($_GET['settings-updated'])) {
6865
6866 ?>
6867 <div class="notice notice-success is-dismissible">
6868 <p><?php esc_html_e('Sync settings updated successfully.', 'mxchat'); ?></p>
6869 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php esc_html_e('Dismiss this notice.', 'mxchat'); ?></span></button>
6870 </div>
6871 <?php
6872
6873 }
6874 }
6875 // Add this sanitization function to your class
6876 public function sanitize_sync_setting($input) {
6877 return (bool)$input ? __('1', 'mxchat') : __('', 'mxchat');
6878 }
6879
6880 public function mxchat_rate_limits_callback() {
6881 $all_options = get_option('mxchat_options', []);
6882
6883 // Define available rate limits
6884 $rate_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited');
6885
6886 // Define available timeframes
6887 $timeframes = array(
6888 'hourly' => __('Per Hour', 'mxchat'),
6889 'daily' => __('Per Day', 'mxchat'),
6890 'weekly' => __('Per Week', 'mxchat'),
6891 'monthly' => __('Per Month', 'mxchat')
6892 );
6893
6894 // Get all roles plus a "logged_out" pseudo-role
6895 $roles = wp_roles()->get_names();
6896 $roles['logged_out'] = __('Logged Out Users', 'mxchat');
6897
6898 // Start the wrapper
6899 echo '<div class="pro-feature-wrapper active">';
6900 echo '<div class="mxchat-rate-limits-container">';
6901
6902 echo '<p class="description" style="margin-bottom: 20px;">' .
6903 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') .
6904 '</p>';
6905
6906 // Add markdown link documentation
6907 echo '<div class="notice notice-info inline" style="margin-bottom: 20px; padding: 10px;">';
6908 echo '<p><strong>' . esc_html__('Markdown Links Supported:', 'mxchat') . '</strong></p>';
6909 echo '<p>' . esc_html__('You can include clickable links in your custom messages using markdown syntax:', 'mxchat') . '</p>';
6910 echo '<ul style="margin-left: 20px;">';
6911 echo '<li><code>[Link text](https://example.com)</code> - Creates a clickable link</li>';
6912 echo '<li><code>[Visit our pricing](https://example.com/pricing)</code> - Link with custom text</li>';
6913 echo '<li><code>Plain URLs like https://example.com will also become clickable</code></li>';
6914 echo '</ul>';
6915 echo '</div>';
6916
6917 // Output the controls for each role
6918 foreach ($roles as $role_id => $role_name) {
6919 // Get saved options or defaults
6920 $default_limit = ($role_id === 'logged_out') ? '10' : '100';
6921 $default_timeframe = 'daily';
6922 $default_message = __('Rate limit exceeded. Please try again later.', 'mxchat');
6923
6924 $selected_limit = isset($all_options['rate_limits'][$role_id]['limit'])
6925 ? $all_options['rate_limits'][$role_id]['limit']
6926 : $default_limit;
6927
6928 $selected_timeframe = isset($all_options['rate_limits'][$role_id]['timeframe'])
6929 ? $all_options['rate_limits'][$role_id]['timeframe']
6930 : $default_timeframe;
6931
6932 $custom_message = isset($all_options['rate_limits'][$role_id]['message'])
6933 ? $all_options['rate_limits'][$role_id]['message']
6934 : $default_message;
6935
6936 // Output the row
6937 echo '<div class="mxchat-rate-limit-row mxchat-autosave-section">';
6938
6939 // Role label
6940 echo '<div class="mxchat-rate-limit-role">' . esc_html($role_name) . '</div>';
6941
6942 // Controls section
6943 echo '<div class="mxchat-rate-limit-controls-wrapper">';
6944
6945 // Rate limit and timeframe controls
6946 echo '<div class="mxchat-rate-limit-controls">';
6947
6948 // Limit dropdown
6949 echo '<div>';
6950 echo '<label for="rate_limits_' . esc_attr($role_id) . '_limit">' . esc_html__('Limit:', 'mxchat') . '</label>';
6951 echo '<select
6952 id="rate_limits_' . esc_attr($role_id) . '_limit"
6953 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][limit]"
6954 class="mxchat-autosave-field">';
6955 foreach ($rate_limits as $limit) {
6956 echo '<option value="' . esc_attr($limit) . '" ' . selected($selected_limit, $limit, false) . '>' . esc_html($limit) . '</option>';
6957 }
6958 echo '</select>';
6959 echo '</div>';
6960
6961 // Timeframe dropdown
6962 echo '<div>';
6963 echo '<label for="rate_limits_' . esc_attr($role_id) . '_timeframe">' . esc_html__('Timeframe:', 'mxchat') . '</label>';
6964 echo '<select
6965 id="rate_limits_' . esc_attr($role_id) . '_timeframe"
6966 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][timeframe]"
6967 class="mxchat-autosave-field">';
6968 foreach ($timeframes as $value => $label) {
6969 echo '<option value="' . esc_attr($value) . '" ' . selected($selected_timeframe, $value, false) . '>' . esc_html($label) . '</option>';
6970 }
6971 echo '</select>';
6972 echo '</div>';
6973
6974 echo '</div>'; // End controls
6975
6976 // Custom message textarea
6977 echo '<div class="mxchat-rate-limit-message">';
6978 echo '<label for="rate_limits_' . esc_attr($role_id) . '_message">' . esc_html__('Custom Message:', 'mxchat') . '</label>';
6979 echo '<textarea
6980 id="rate_limits_' . esc_attr($role_id) . '_message"
6981 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][message]"
6982 class="mxchat-autosave-field"
6983 placeholder="' . esc_attr__('Enter custom message when rate limit is exceeded', 'mxchat') . '">' .
6984 esc_textarea($custom_message) .
6985 '</textarea>';
6986 echo '<p class="description">' .
6987 esc_html__('Example: Rate limit reached! [Visit our pricing page](https://example.com/pricing) to upgrade.', 'mxchat') .
6988 '</p>';
6989 echo '</div>'; // End message
6990
6991 echo '</div>'; // End controls wrapper
6992
6993 echo '</div>'; // End row
6994 }
6995
6996 echo '</div>'; // End container
6997
6998 echo '</div>'; // End pro-feature-wrapper
6999 }
7000
7001 private function mxchat_add_option_field($id, $title, $callback = '') {
7002 add_settings_field(
7003 $id,
7004 __($title, 'mxchat'),
7005 $callback ? array($this, $callback) : array($this, $id . '_callback'),
7006 'mxchat-max',
7007 'mxchat_setting_section_id',
7008 $id === 'model' ? ['label_for' => 'model'] : []
7009 );
7010 }
7011
7012 // API Keys Section Callback
7013 public function mxchat_api_keys_section_callback() {
7014 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>';
7015 }
7016
7017 // OpenAI API Key
7018 public function api_key_callback() {
7019 $apiKey = isset($this->options['api_key']) ? esc_attr($this->options['api_key']) : '';
7020 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7021
7022 echo '<div class="api-key-wrapper">';
7023 echo '<input type="text" id="api_key" name="api_key" value="' . $apiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7024 echo '<button type="button" id="toggleApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7025 echo '<p class="description">' . esc_html__('Required for OpenAI GPT models and OpenAI embeddings. Get your API key from OpenAI Platform.', 'mxchat') . '</p>';
7026 echo $this->mxchat_provider_key_test_button('openai', 'api_key');
7027 echo '</div>';
7028 }
7029
7030 // X.AI API Key
7031 public function xai_api_key_callback() {
7032 $xaiApiKey = isset($this->options['xai_api_key']) ? esc_attr($this->options['xai_api_key']) : '';
7033 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7034
7035 echo '<div class="api-key-wrapper">';
7036 echo '<input type="text" id="xai_api_key" name="xai_api_key" value="' . $xaiApiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7037 echo '<button type="button" id="toggleXaiApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7038 echo '<p class="description">' . esc_html__('Required for X.AI Grok models. Get your API key from X.AI Console.', 'mxchat') . '</p>';
7039 echo $this->mxchat_provider_key_test_button('xai', 'xai_api_key');
7040 echo '</div>';
7041 }
7042 // Claude API Key
7043 public function claude_api_key_callback() {
7044 $claudeApiKey = isset($this->options['claude_api_key']) ? esc_attr($this->options['claude_api_key']) : '';
7045 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7046
7047 echo '<div class="api-key-wrapper">';
7048 echo '<input type="text" id="claude_api_key" name="claude_api_key" value="' . $claudeApiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7049 echo '<button type="button" id="toggleClaudeApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7050 echo '<p class="description">' . esc_html__('Required for Anthropic Claude models. Get your API key from Anthropic Console.', 'mxchat') . '</p>';
7051 echo $this->mxchat_provider_key_test_button('claude', 'claude_api_key');
7052 echo '</div>';
7053 }
7054
7055 // DeepSeek API Key
7056 public function deepseek_api_key_callback() {
7057 $apiKey = isset($this->options['deepseek_api_key']) ? esc_attr($this->options['deepseek_api_key']) : '';
7058 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7059
7060 echo '<div class="api-key-wrapper">';
7061 echo '<input type="text" id="deepseek_api_key" name="deepseek_api_key" value="' . $apiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7062 echo '<button type="button" id="toggleDeepSeekApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7063 echo '<p class="description">' . esc_html__('Required for DeepSeek models. Get your API key from DeepSeek Platform.', 'mxchat') . '</p>';
7064 echo $this->mxchat_provider_key_test_button('deepseek', 'deepseek_api_key');
7065 echo '</div>';
7066 }
7067
7068 // Gemini API Key
7069 public function gemini_api_key_callback() {
7070 $geminiApiKey = isset($this->options['gemini_api_key']) ? esc_attr($this->options['gemini_api_key']) : '';
7071 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7072
7073 echo '<div class="api-key-wrapper">';
7074 echo '<input type="text" id="gemini_api_key" name="gemini_api_key" value="' . $geminiApiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7075 echo '<button type="button" id="toggleGeminiApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7076 echo '<p class="description">' . esc_html__('Required for Google Gemini models and embeddings. Get your API key from Google AI Studio.', 'mxchat') . '</p>';
7077 echo $this->mxchat_provider_key_test_button('gemini', 'gemini_api_key');
7078 echo '</div>';
7079 }
7080
7081
7082 // OpenRouter API Key
7083 public function openrouter_api_key_callback() {
7084 $openrouterApiKey = isset($this->options['openrouter_api_key']) ? esc_attr($this->options['openrouter_api_key']) : '';
7085 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7086
7087 echo '<div class="api-key-wrapper">';
7088 echo '<input type="text" id="openrouter_api_key" name="openrouter_api_key" value="' . $openrouterApiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7089 echo '<button type="button" id="toggleOpenRouterApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7090 echo '<p class="description">' . esc_html__('Required for OpenRouter models. Get your API key from OpenRouter.ai', 'mxchat') . '</p>';
7091 echo $this->mxchat_provider_key_test_button('openrouter', 'openrouter_api_key');
7092 echo '</div>';
7093 }
7094
7095 /**
7096 * Renders a "Test key" button + result target next to a built-in provider key
7097 * field, and emits the shared delegated click handler ONCE (static guard). The
7098 * button carries data-target = the key field id so the owner can test the value
7099 * they just typed (test-before-save); the AJAX handler falls back to the saved
7100 * key when the field is empty. Styled with the WP `button` class to match the
7101 * adjacent Custom-provider "Test Connection" button on this same page.
7102 * plan-mxchat-20260623-c41f74.
7103 */
7104 public function mxchat_provider_key_test_button($provider, $target_id) {
7105 static $script_emitted = false;
7106 $nonce = wp_create_nonce('mxchat_test_provider_key');
7107 $html = '<div class="mxchat-key-test" style="margin-top:8px;">'
7108 . '<button type="button" class="button mxchat-test-provider-key" data-provider="' . esc_attr($provider) . '" data-target="' . esc_attr($target_id) . '" data-nonce="' . esc_attr($nonce) . '">' . esc_html__('Test key', 'mxchat') . '</button>'
7109 . '<span class="mxchat-test-provider-key-result" style="margin-left:10px;font-size:13px;vertical-align:middle;"></span>'
7110 . '</div>';
7111 if (!$script_emitted) {
7112 $script_emitted = true;
7113 $html .= $this->mxchat_provider_key_test_script();
7114 }
7115 return $html;
7116 }
7117
7118 /**
7119 * One-time delegated click handler shared by every .mxchat-test-provider-key
7120 * button. Posts the typed key value + provider to mxchat_test_provider_key and
7121 * renders the success/error message inline. Mirrors the Custom-provider test.
7122 */
7123 private function mxchat_provider_key_test_script() {
7124 $t_testing = esc_js(__('Testing...', 'mxchat'));
7125 $t_valid = esc_js(__('Key is valid.', 'mxchat'));
7126 $t_failed = esc_js(__('Failed', 'mxchat'));
7127 $t_req = esc_js(__('Request failed', 'mxchat'));
7128 return '<script>(function(){'
7129 . 'if (window.__mxchatProviderKeyTestWired) { return; }'
7130 . 'window.__mxchatProviderKeyTestWired = true;'
7131 . 'document.addEventListener("click", function(e){'
7132 . 'var btn = e.target && e.target.closest ? e.target.closest(".mxchat-test-provider-key") : null;'
7133 . 'if (!btn) { return; }'
7134 . 'e.preventDefault();'
7135 . 'var field = btn.getAttribute("data-target") ? document.getElementById(btn.getAttribute("data-target")) : null;'
7136 . 'var out = btn.parentNode ? btn.parentNode.querySelector(".mxchat-test-provider-key-result") : null;'
7137 . 'if (out) { out.textContent = "' . $t_testing . '"; out.style.color = "#646970"; }'
7138 . 'btn.disabled = true;'
7139 . 'var fd = new FormData();'
7140 . 'fd.append("action", "mxchat_test_provider_key");'
7141 . 'fd.append("_wpnonce", btn.getAttribute("data-nonce"));'
7142 . 'fd.append("provider", btn.getAttribute("data-provider") || "");'
7143 . 'fd.append("key", field ? field.value : "");'
7144 . 'fetch(ajaxurl, { method:"POST", credentials:"same-origin", body: fd })'
7145 . '.then(function(r){ return r.json(); })'
7146 . '.then(function(j){'
7147 . 'if (out) {'
7148 . 'if (j && j.success) { out.textContent = "✓ " + ((j.data && j.data.message) ? j.data.message : "' . $t_valid . '"); out.style.color = "#00a32a"; }'
7149 . 'else { out.textContent = "⚠ " + ((j && j.data && j.data.message) ? j.data.message : "' . $t_failed . '"); out.style.color = "#d63638"; }'
7150 . '}'
7151 . 'btn.disabled = false;'
7152 . '})'
7153 . '.catch(function(){ if (out) { out.textContent = "⚠ ' . $t_req . '"; out.style.color = "#d63638"; } btn.disabled = false; });'
7154 . '});'
7155 . '})();</script>';
7156 }
7157
7158 // Custom (OpenAI-compatible) Provider — Ollama, LM Studio, vLLM, llama.cpp, Azure OpenAI, etc.
7159 public function custom_provider_callback() {
7160 $base_url = isset($this->options['custom_provider_base_url']) ? esc_attr($this->options['custom_provider_base_url']) : '';
7161 $api_key = isset($this->options['custom_provider_api_key']) ? esc_attr($this->options['custom_provider_api_key']) : '';
7162 $model_name = isset($this->options['custom_provider_model']) ? esc_attr($this->options['custom_provider_model']) : '';
7163 $auth_scheme = isset($this->options['custom_provider_auth_scheme']) ? esc_attr($this->options['custom_provider_auth_scheme']) : 'bearer';
7164 $api_version = isset($this->options['custom_provider_api_version']) ? esc_attr($this->options['custom_provider_api_version']) : '';
7165 $use_embed = !empty($this->options['custom_provider_for_embeddings']) && $this->options['custom_provider_for_embeddings'] === 'on';
7166 $use_images = !empty($this->options['custom_provider_for_images']) && $this->options['custom_provider_for_images'] === 'on';
7167 $embed_model = isset($this->options['custom_provider_embedding_model']) ? esc_attr($this->options['custom_provider_embedding_model']) : '';
7168 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7169 $test_nonce = wp_create_nonce('mxchat_test_custom_provider');
7170
7171 echo '<style>
7172 .mxchat-cp { max-width: 680px; }
7173 .mxchat-cp .mxchat-cp-intro { margin: 0 0 16px; color: #50575e; font-size: 13px; line-height: 1.5; }
7174 .mxchat-cp .mxchat-cp-row { display: block; margin: 0 0 18px; }
7175 .mxchat-cp .mxchat-cp-row > label { display: block; font-weight: 600; margin: 0 0 6px; color: #1d2327; font-size: 13px; }
7176 .mxchat-cp .mxchat-cp-row > input[type="text"],
7177 .mxchat-cp .mxchat-cp-row > input[type="password"],
7178 .mxchat-cp .mxchat-cp-row > select { display: block; width: 100%; max-width: 480px; margin: 0; }
7179 .mxchat-cp .mxchat-cp-row > .description { display: block; margin: 6px 0 0; color: #646970; font-size: 12px; line-height: 1.5; max-width: 480px; }
7180 .mxchat-cp .mxchat-cp-test { margin-top: 4px; padding-top: 14px; border-top: 1px solid #e5e7eb; }
7181 .mxchat-cp .mxchat-cp-test .mxchat-cp-test-status { display: inline-block; margin-left: 10px; vertical-align: middle; font-size: 13px; }
7182 .mxchat-cp .mxchat-cp-azure { margin: 0 0 18px; padding: 12px 14px; background: #f6f7ff; border: 1px solid #dfe1f5; border-left: 3px solid #7873f5; border-radius: 6px; max-width: 480px; }
7183 .mxchat-cp .mxchat-cp-azure > .mxchat-cp-azure-title { display: block; font-weight: 600; color: #1d2327; font-size: 12px; text-transform: uppercase; letter-spacing: 0.4px; margin: 0 0 8px; }
7184 .mxchat-cp .mxchat-cp-azure ol { margin: 0; padding: 0 0 0 18px; color: #50575e; font-size: 12px; line-height: 1.7; }
7185 .mxchat-cp .mxchat-cp-azure code { background: #eceefb; padding: 1px 5px; border-radius: 3px; font-size: 11px; }
7186 </style>';
7187
7188 echo '<div class="api-key-wrapper mxchat-cp">';
7189 echo '<p class="mxchat-cp-intro">' . esc_html__('Point MxChat at any OpenAI-compatible /v1/chat/completions endpoint: Ollama, LM Studio, vLLM, llama.cpp, LocalAI, Azure OpenAI, etc. Then select "Custom (OpenAI-compatible)" in the model picker.', 'mxchat') . '</p>';
7190
7191 // Azure OpenAI quick start — consolidates the 4-field Azure recipe in one scannable callout
7192 // so admins can configure Azure without piecing it together from each field's hint.
7193 echo '<div class="mxchat-cp-azure">';
7194 echo '<span class="mxchat-cp-azure-title">' . esc_html__('Azure OpenAI quick start', 'mxchat') . '</span>';
7195 echo '<ol>';
7196 echo '<li>' . wp_kses(__('<strong>Base URL</strong> → <code>https://&lt;resource&gt;.openai.azure.com/openai/deployments/&lt;deployment&gt;</code>', 'mxchat'), array('strong' => array(), 'code' => array())) . '</li>';
7197 echo '<li>' . wp_kses(__('<strong>API Key</strong> → your Azure OpenAI key (required)', 'mxchat'), array('strong' => array(), 'code' => array())) . '</li>';
7198 echo '<li>' . wp_kses(__('<strong>Auth Scheme</strong> → <code>api-key header (Azure OpenAI)</code>', 'mxchat'), array('strong' => array(), 'code' => array())) . '</li>';
7199 echo '<li>' . wp_kses(__('<strong>API Version</strong> → required for Azure, e.g. <code>2024-08-01-preview</code>', 'mxchat'), array('strong' => array(), 'code' => array())) . '</li>';
7200 echo '</ol>';
7201 echo '</div>';
7202
7203 echo '<div class="mxchat-cp-row">';
7204 echo '<label for="custom_provider_base_url">' . esc_html__('Base URL', 'mxchat') . '</label>';
7205 echo '<input type="text" id="custom_provider_base_url" name="custom_provider_base_url" value="' . $base_url . '" class="regular-text mxchat-autosave-field" placeholder="http://localhost:11434/v1" autocomplete="off" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7206 echo '<p class="description">' . esc_html__('Examples: Ollama http://localhost:11434/v1 · LM Studio http://localhost:1234/v1 · vLLM http://gpu:8000/v1 · Azure https://<resource>.openai.azure.com/openai/deployments/<deployment>', 'mxchat') . '</p>';
7207 echo '</div>';
7208
7209 echo '<div class="mxchat-cp-row">';
7210 echo '<label for="custom_provider_api_key">' . esc_html__('API Key (optional)', 'mxchat') . '</label>';
7211 echo '<input type="password" id="custom_provider_api_key" name="custom_provider_api_key" value="' . $api_key . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7212 echo '<p class="description">' . esc_html__('Leave empty for unauthenticated local servers. Required for Azure / vLLM / hosted endpoints.', 'mxchat') . '</p>';
7213 echo '</div>';
7214
7215 echo '<div class="mxchat-cp-row">';
7216 echo '<label for="custom_provider_model">' . esc_html__('Model Name', 'mxchat') . '</label>';
7217 echo '<input type="text" id="custom_provider_model" name="custom_provider_model" value="' . $model_name . '" class="regular-text mxchat-autosave-field" placeholder="llama3.2" autocomplete="off" data-lpignore="true" data-nonce="' . $nonce . '" />';
7218 echo '<p class="description">' . esc_html__('The model identifier the upstream server expects (e.g. llama3.2, mistral, gpt-oss). For Azure this is the deployment ID — leave empty if the Base URL already includes /deployments/<deployment>.', 'mxchat') . '</p>';
7219 echo '</div>';
7220
7221 echo '<div class="mxchat-cp-row">';
7222 echo '<label for="custom_provider_auth_scheme">' . esc_html__('Auth Scheme', 'mxchat') . '</label>';
7223 echo '<select id="custom_provider_auth_scheme" name="custom_provider_auth_scheme" class="mxchat-autosave-field" data-nonce="' . $nonce . '">';
7224 echo '<option value="bearer"' . selected($auth_scheme, 'bearer', false) . '>' . esc_html__('Authorization: Bearer (OpenAI / Ollama / vLLM / LM Studio)', 'mxchat') . '</option>';
7225 echo '<option value="api-key"' . selected($auth_scheme, 'api-key', false) . '>' . esc_html__('api-key header (Azure OpenAI)', 'mxchat') . '</option>';
7226 echo '</select>';
7227 echo '<p class="description">' . esc_html__('Most OpenAI-compatible servers use Bearer. Azure OpenAI uses the api-key header.', 'mxchat') . '</p>';
7228 echo '</div>';
7229
7230 echo '<div class="mxchat-cp-row">';
7231 echo '<label for="custom_provider_api_version">' . esc_html__('API Version (Azure only)', 'mxchat') . '</label>';
7232 echo '<input type="text" id="custom_provider_api_version" name="custom_provider_api_version" value="' . $api_version . '" class="regular-text mxchat-autosave-field" placeholder="2024-08-01-preview" autocomplete="off" data-lpignore="true" data-nonce="' . $nonce . '" />';
7233 echo '<p class="description">' . esc_html__('Appended as ?api-version=... on the request URL. Required for Azure OpenAI; leave empty for non-Azure providers.', 'mxchat') . '</p>';
7234 echo '</div>';
7235
7236 // Extended-use checkboxes — opt-in routing of other dispatcher paths through the custom provider.
7237 echo '<div class="mxchat-cp-row">';
7238 echo '<label style="font-weight:600; display:block; margin:0 0 6px; color:#1d2327; font-size:13px;">' . esc_html__('Extended routing (opt-in)', 'mxchat') . '</label>';
7239 echo '<label style="display:block; margin:0 0 6px; font-weight:400;"><input type="checkbox" id="custom_provider_for_embeddings" name="custom_provider_for_embeddings" value="on"' . checked($use_embed, true, false) . ' class="mxchat-autosave-field" data-nonce="' . $nonce . '" /> ' . esc_html__('Use custom provider for embeddings', 'mxchat') . '</label>';
7240 echo '<label style="display:block; margin:0 0 0; font-weight:400;"><input type="checkbox" id="custom_provider_for_images" name="custom_provider_for_images" value="on"' . checked($use_images, true, false) . ' class="mxchat-autosave-field" data-nonce="' . $nonce . '" /> ' . esc_html__('Use custom provider for image generation', 'mxchat') . '</label>';
7241 echo '<p class="description">' . esc_html__('When off, embeddings and image generation continue to use OpenAI (current behavior). Turn on only if your endpoint exposes OpenAI-compatible /embeddings or /images/generations routes (e.g. Ollama, vLLM, LocalAI).', 'mxchat') . '</p>';
7242 echo '</div>';
7243
7244 echo '<div class="mxchat-cp-row">';
7245 echo '<label for="custom_provider_embedding_model">' . esc_html__('Custom Embedding Model', 'mxchat') . '</label>';
7246 echo '<input type="text" id="custom_provider_embedding_model" name="custom_provider_embedding_model" value="' . $embed_model . '" class="regular-text mxchat-autosave-field" placeholder="nomic-embed-text" autocomplete="off" data-lpignore="true" data-nonce="' . $nonce . '" />';
7247 echo '<p class="description">' . esc_html__('Only used when "Use custom provider for embeddings" is on. The embedding model name is separate from the chat model name above (e.g. Ollama embedding models: nomic-embed-text, mxbai-embed-large). Leave blank to fall back to the chat model name.', 'mxchat') . '</p>';
7248 echo '</div>';
7249
7250 echo '<div class="mxchat-cp-test">';
7251 echo '<button type="button" class="button" id="mxchat-test-custom-provider" data-nonce="' . $test_nonce . '">' . esc_html__('Test Connection', 'mxchat') . '</button>';
7252 echo '<span id="mxchat-test-custom-provider-result" class="mxchat-cp-test-status"></span>';
7253 echo '</div>';
7254
7255 echo '<script>(function(){
7256 var btn = document.getElementById("mxchat-test-custom-provider");
7257 if (!btn || btn._wired) { return; } btn._wired = true;
7258 btn.addEventListener("click", function(){
7259 var out = document.getElementById("mxchat-test-custom-provider-result");
7260 out.textContent = "' . esc_js(__('Testing...', 'mxchat')) . '";
7261 out.style.color = "#646970";
7262 var fd = new FormData();
7263 fd.append("action", "mxchat_test_custom_provider");
7264 fd.append("_wpnonce", btn.getAttribute("data-nonce"));
7265 fetch(ajaxurl, { method:"POST", credentials:"same-origin", body: fd })
7266 .then(function(r){ return r.json(); })
7267 .then(function(j){
7268 if (j && j.success) {
7269 out.textContent = "✓ " + (j.data && j.data.message ? j.data.message : "' . esc_js(__('OK', 'mxchat')) . '");
7270 out.style.color = "#00a32a";
7271 } else {
7272 out.textContent = "⚠ " + (j && j.data && j.data.message ? j.data.message : "' . esc_js(__('Failed', 'mxchat')) . '");
7273 out.style.color = "#d63638";
7274 }
7275 })
7276 .catch(function(){
7277 out.textContent = "⚠ ' . esc_js(__('Request failed', 'mxchat')) . '";
7278 out.style.color = "#d63638";
7279 });
7280 });
7281 })();</script>';
7282
7283 echo '</div>';
7284 }
7285
7286 // Voyage API Key
7287 public function voyage_api_key_callback() {
7288 $apiKey = isset($this->options['voyage_api_key']) ? esc_attr($this->options['voyage_api_key']) : '';
7289 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7290
7291 echo '<div class="api-key-wrapper">';
7292 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 . '" />';
7293 echo '<button type="button" id="toggleVoyageAPIKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7294 echo '<p class="description">' . esc_html__('Required for Voyage AI embedding models. Get your API key from Voyage AI.', 'mxchat') . '</p>';
7295 echo '</div>';
7296 }
7297
7298 public function mxchat_loops_api_key_callback() {
7299 $loops_api_key = isset($this->options['loops_api_key']) ? esc_attr($this->options['loops_api_key']) : '';
7300 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7301
7302 echo '<div class="api-key-wrapper">';
7303 echo sprintf(
7304 '<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" />',
7305 $loops_api_key,
7306 $nonce
7307 );
7308 echo '<button type="button" id="toggleLoopsApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7309 echo '</div>';
7310 echo '<p class="description">' . esc_html__('Required for Loops email integration. Get your API key from Loops.so', 'mxchat') . '</p>';
7311 }
7312 public function mxchat_loops_mailing_list_callback() {
7313 // Add error handling and type checking
7314 $loops_api_key = '';
7315 $selected_list = '';
7316 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7317
7318 // Safely get the API key
7319 if (isset($this->options['loops_api_key']) && is_string($this->options['loops_api_key'])) {
7320 $loops_api_key = $this->options['loops_api_key'];
7321 }
7322
7323 // Safely get the selected list
7324 if (isset($this->options['loops_mailing_list']) && is_string($this->options['loops_mailing_list'])) {
7325 $selected_list = $this->options['loops_mailing_list'];
7326 }
7327
7328 if (!empty($loops_api_key)) {
7329 $lists = $this->mxchat_fetch_loops_mailing_lists($loops_api_key);
7330 if (is_array($lists) && !empty($lists)) {
7331 echo '<div class="mxchat-field-wrapper">';
7332 echo '<select id="loops_mailing_list" name="loops_mailing_list" class="mxchat-autosave-field" data-nonce="' . $nonce . '">';
7333
7334 // Add a default "Select a list" option
7335 echo '<option value="" ' . selected($selected_list, '', false) . '>' . esc_html__('Select a list', 'mxchat') . '</option>';
7336
7337 foreach ($lists as $list) {
7338 if (is_array($list) && isset($list['id']) && isset($list['name'])) {
7339 echo sprintf(
7340 '<option value="%s" %s>%s</option>',
7341 esc_attr($list['id']),
7342 selected($selected_list, $list['id'], false),
7343 esc_html($list['name'])
7344 );
7345 }
7346 }
7347 echo '</select>';
7348 echo '</div>';
7349 echo '<p class="description">' . esc_html__('Please select a mailing list to use with Loops.', 'mxchat') . '</p>';
7350 } else {
7351 echo '<p class="description">' . esc_html__('No lists found. Please verify your API Key.', 'mxchat') . '</p>';
7352 }
7353 } else {
7354 echo '<p class="description">' . esc_html__('Enter a valid Loops API Key to load mailing lists.', 'mxchat') . '</p>';
7355 }
7356 }
7357 public function mxchat_triggered_phrase_response_callback() {
7358 $default_response = __('Would you like to join our mailing list? Please provide your email below.', 'mxchat');
7359 $triggered_response = isset($this->options['triggered_phrase_response'])
7360 ? $this->options['triggered_phrase_response']
7361 : $default_response;
7362 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7363
7364 echo '<div class="mxchat-field-wrapper">';
7365 echo sprintf(
7366 '<textarea id="triggered_phrase_response" name="triggered_phrase_response" rows="3" cols="50" class="mxchat-autosave-field" data-nonce="%s">%s</textarea>',
7367 $nonce,
7368 esc_textarea($triggered_response)
7369 );
7370 echo '</div>';
7371 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>';
7372 }
7373
7374 public function mxchat_email_capture_response_callback() {
7375 $default_response = __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
7376 $email_capture_response = isset($this->options['email_capture_response'])
7377 ? $this->options['email_capture_response']
7378 : $default_response;
7379 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7380
7381 echo '<div class="mxchat-field-wrapper">';
7382 echo sprintf(
7383 '<textarea id="email_capture_response" name="email_capture_response" rows="3" cols="50" class="mxchat-autosave-field" data-nonce="%s">%s</textarea>',
7384 $nonce,
7385 esc_textarea($email_capture_response)
7386 );
7387 echo '</div>';
7388 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>';
7389 }
7390 public function mxchat_pre_chat_message_callback() {
7391 // Load the entire 'mxchat_options' array
7392 $all_options = get_option('mxchat_options', []);
7393
7394 // Retrieve the saved message or use the default value
7395 $default_message = __('Hey there! Ask me anything!', 'mxchat');
7396 $pre_chat_message = isset($all_options['pre_chat_message']) ? $all_options['pre_chat_message'] : $default_message;
7397
7398 // Output the textarea
7399 printf(
7400 '<textarea id="pre_chat_message" name="pre_chat_message" rows="5" cols="50">%s</textarea>',
7401 esc_textarea($pre_chat_message)
7402 );
7403 }
7404
7405 // Callback for AI Instructions textarea
7406 public function system_prompt_instructions_callback() {
7407 // Retrieve the current value of the system prompt instructions
7408 $instructions = isset($this->options['system_prompt_instructions']) ? esc_textarea($this->options['system_prompt_instructions']) : '';
7409 // Render the textarea field
7410 printf(
7411 '<textarea id="system_prompt_instructions" name="system_prompt_instructions" rows="5" cols="50">%s</textarea>',
7412 $instructions
7413 );
7414 // Personalization hint
7415 echo '<p class="description" style="margin-top: 8px;">';
7416 echo esc_html__('Use {visitor_name} to personalize AI responses when lead capture is enabled.', 'mxchat') . '<br>';
7417 echo '<code style="font-size: 12px;">' . esc_html__('Example: The visitor\'s name is {visitor_name}. Address them by name.', 'mxchat') . '</code>';
7418 echo '</p>';
7419 // Sample instructions button
7420 echo '<div class="mxchat-instructions-container">';
7421 echo '<button type="button" class="mxchat-instructions-btn" id="mxchatViewSampleBtn">';
7422 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">';
7423 echo '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>';
7424 echo '<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>';
7425 echo '</svg>';
7426 echo esc_html__('View Sample Instructions', 'mxchat');
7427 echo '</button>';
7428 echo '</div>';
7429
7430 // Add modal to WordPress admin footer instead of inline
7431 add_action('admin_footer', array($this, 'render_sample_instructions_modal'));
7432 }
7433
7434 // New method to render modal in admin footer
7435 public function render_sample_instructions_modal() {
7436 static $modal_rendered = false;
7437 if ($modal_rendered) return; // Prevent duplicate modals
7438 $modal_rendered = true;
7439
7440 echo '<div class="mxchat-instructions-modal-overlay" id="mxchatSampleModal">';
7441 echo '<div class="mxchat-instructions-modal-content">';
7442 echo '<div class="mxchat-instructions-modal-header">';
7443 echo '<h3 class="mxchat-instructions-modal-title">';
7444 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">';
7445 echo '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>';
7446 echo '<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>';
7447 echo '</svg>';
7448 echo esc_html__('Sample AI Instructions', 'mxchat');
7449 echo '</h3>';
7450 echo '<button type="button" class="mxchat-instructions-modal-close" id="mxchatModalClose">';
7451 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">';
7452 echo '<line x1="18" y1="6" x2="6" y2="18"/>';
7453 echo '<line x1="6" y1="6" x2="18" y2="18"/>';
7454 echo '</svg>';
7455 echo '</button>';
7456 echo '</div>';
7457 echo '<div class="mxchat-instructions-modal-body">';
7458 echo '<div class="mxchat-instructions-content">';
7459 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:
7460
7461 # Response Style - CRITICALLY IMPORTANT
7462 - MAXIMUM LENGTH: 1-3 short sentences per response
7463 - Ultra-concise: Get straight to the answer with no filler
7464 - No introductions like "Sure!" or "I\'d be happy to help"
7465 - No phrases like "based on my knowledge" or "according to information"
7466 - No explanatory text before giving the answer
7467 - No summaries or repetition
7468 - Hyperlink all URLs
7469 - Respond in user\'s language
7470 - Minor chit chat or conversation is okay, but try to keep it focused on [insert topic]
7471
7472 # Knowledge Base Requirements - PREVENT HALLUCINATIONS
7473 - ONLY answer using information explicitly provided in OFFICIAL KNOWLEDGE DATABASE CONTENT sections marked with ===== delimiters
7474 - If required information is NOT in the knowledge database: "I don\'t have enough information in my knowledge base to answer that question accurately."
7475 - NEVER invent or hallucinate URLs, links, product specs, procedures, dates, statistics, names, contacts, or company information
7476 - When knowledge base information is unclear or contradictory, acknowledge the limitation rather than guessing
7477 - Better to admit insufficient information than provide inaccurate answers');
7478 echo '</div>';
7479 echo '<button type="button" class="mxchat-instructions-copy-btn" id="mxchatCopyBtn">';
7480 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">';
7481 echo '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>';
7482 echo '<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>';
7483 echo '</svg>';
7484 echo esc_html__('Copy Instructions', 'mxchat');
7485 echo '</button>';
7486 echo '</div>';
7487 echo '<div class="mxchat-instructions-modal-footer">';
7488 echo '<button type="button" class="mxchat-instructions-btn-secondary" id="mxchatCloseBtn">' . esc_html__('Close', 'mxchat') . '</button>';
7489 echo '</div>';
7490 echo '</div>';
7491 echo '</div>';
7492 }
7493
7494
7495 public function mxchat_model_callback() {
7496 // Catalog refactor (plan-d14e89): single source of truth lives in
7497 // includes/class-mxchat-model-catalog.php. Dropdown groups are the
7498 // provider labels; each group maps model_id => "Label" strings.
7499 if (!class_exists('MxChat_Model_Catalog')) {
7500 require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
7501 }
7502 $models = MxChat_Model_Catalog::settings_dropdown_groups();
7503
7504 // Retrieve the currently selected model from saved options
7505 $selected_model = isset($this->options['model']) ? esc_attr($this->options['model']) : 'gpt-5.1-chat-latest';
7506
7507 // Begin the select dropdown
7508 echo '<select id="model" name="model">';
7509
7510 // Iterate over groups of models
7511 foreach ($models as $group_label => $group_models) {
7512 echo '<optgroup label="' . esc_attr($group_label) . '">';
7513
7514 foreach ($group_models as $model_value => $model_label) {
7515 echo '<option value="' . esc_attr($model_value) . '" ' . selected($selected_model, $model_value, false) . '>' . esc_html($model_label) . '</option>';
7516 }
7517
7518 echo '</optgroup>';
7519 }
7520
7521 echo '</select>';
7522
7523 // Add a note for OpenRouter
7524 echo '<p class="description" id="openrouter-model-note" style="display:none; color: #d63638; font-weight: 500;">';
7525 echo '<span class="dashicons dashicons-info" style="font-size: 16px; vertical-align: middle;"></span> ';
7526 echo esc_html__('After entering your OpenRouter API key above, click the button below to load available models.', 'mxchat');
7527 echo '</p>';
7528
7529 // API Key Status Messages (hidden by default, shown by JS based on selected model)
7530 $has_openai_key = !empty($this->options['api_key']);
7531 $has_claude_key = !empty($this->options['claude_api_key']);
7532 $has_xai_key = !empty($this->options['xai_api_key']);
7533 $has_deepseek_key = !empty($this->options['deepseek_api_key']);
7534 $has_gemini_key = !empty($this->options['gemini_api_key']);
7535 $has_openrouter_key = !empty($this->options['openrouter_api_key']);
7536
7537 // OpenAI/GPT models
7538 echo '<p class="mxchat-api-status" data-provider="openai" style="display:none;">';
7539 if ($has_openai_key) {
7540 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenAI detected', 'mxchat') . '</span>';
7541 } else {
7542 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenAI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7543 }
7544 echo '</p>';
7545
7546 // Claude models
7547 echo '<p class="mxchat-api-status" data-provider="claude" style="display:none;">';
7548 if ($has_claude_key) {
7549 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Anthropic (Claude) detected', 'mxchat') . '</span>';
7550 } else {
7551 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Anthropic (Claude) detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7552 }
7553 echo '</p>';
7554
7555 // X.AI models
7556 echo '<p class="mxchat-api-status" data-provider="xai" style="display:none;">';
7557 if ($has_xai_key) {
7558 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for X.AI (Grok) detected', 'mxchat') . '</span>';
7559 } else {
7560 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>';
7561 }
7562 echo '</p>';
7563
7564 // DeepSeek models
7565 echo '<p class="mxchat-api-status" data-provider="deepseek" style="display:none;">';
7566 if ($has_deepseek_key) {
7567 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for DeepSeek detected', 'mxchat') . '</span>';
7568 } else {
7569 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for DeepSeek detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7570 }
7571 echo '</p>';
7572
7573 // Gemini models
7574 echo '<p class="mxchat-api-status" data-provider="gemini" style="display:none;">';
7575 if ($has_gemini_key) {
7576 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Google Gemini detected', 'mxchat') . '</span>';
7577 } else {
7578 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Google Gemini detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7579 }
7580 echo '</p>';
7581
7582 // OpenRouter models
7583 echo '<p class="mxchat-api-status" data-provider="openrouter" style="display:none;">';
7584 if ($has_openrouter_key) {
7585 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenRouter detected', 'mxchat') . '</span>';
7586 } else {
7587 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenRouter detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7588 }
7589 echo '</p>';
7590
7591 // ADD THESE HIDDEN FIELDS RIGHT HERE:
7592 $openrouter_model = isset($this->options['openrouter_selected_model']) ? esc_attr($this->options['openrouter_selected_model']) : '';
7593 $openrouter_model_name = isset($this->options['openrouter_selected_model_name']) ? esc_attr($this->options['openrouter_selected_model_name']) : '';
7594
7595 echo '<input type="hidden" id="openrouter_selected_model" name="openrouter_selected_model" value="' . $openrouter_model . '" />';
7596 echo '<input type="hidden" id="openrouter_selected_model_name" name="openrouter_selected_model_name" value="' . $openrouter_model_name . '" />';
7597 }
7598
7599 // Update your existing callback method
7600 public function enable_streaming_toggle_callback() {
7601 // Get value from options array, default to 'on'
7602 $enabled = isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on';
7603 $checked = ($enabled === 'on') ? 'checked' : '';
7604
7605 echo '<label class="toggle-switch">';
7606 echo sprintf(
7607 '<input type="checkbox" id="enable_streaming_toggle" name="enable_streaming_toggle" value="on" %s />',
7608 esc_attr($checked)
7609 );
7610 echo '<span class="slider"></span>';
7611 echo '</label>';
7612
7613 // Test button — branded .mxch-btn with inline SVG (IDs preserved for AJAX binding)
7614 echo '<div class="mxch-streaming-test-row">';
7615 echo '<button type="button" id="mxchat-test-streaming-btn" class="mxch-btn mxch-btn-secondary">';
7616 echo '<svg class="mxch-streaming-test-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>';
7617 echo esc_html__('Test Streaming Compatibility', 'mxchat');
7618 echo '</button>';
7619 echo '<p id="mxchat-test-streaming-result" class="mxch-streaming-test-result"></p>';
7620 echo '</div>';
7621 }
7622
7623 // Web Search toggle callback
7624 public function enable_web_search_toggle_callback() {
7625 // Get value from options array, default to 'off'
7626 $enabled = isset($this->options['enable_web_search']) ? $this->options['enable_web_search'] : 'off';
7627 $checked = ($enabled === 'on') ? 'checked' : '';
7628
7629 // Get current model to determine if we should show/enable the toggle
7630 $current_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest';
7631
7632 // Models that DON'T support web search — OpenAI-docs-driven exception list.
7633 // Keep hardcoded; the catalog can't infer "supports web search" per-model, so any
7634 // future OpenAI model that lacks Responses-API web_search support is added here.
7635 $unsupported_models = array('gpt-4.1-nano');
7636
7637 // Web-search-capable chat-model allowlists, derived from the central model catalog
7638 // (class-mxchat-model-catalog.php). When a new OpenAI/Gemini chat model is added there,
7639 // the Web Search toggle picks it up automatically — no edit here.
7640 // OpenAI grounds via the Responses-API web_search tool; Gemini grounds natively via the
7641 // Google Search tool (plan 46b9ea wired the Gemini dispatch — every shipped Gemini chat
7642 // model is 2.x/3.x and grounds, matching that path's empty opt-out list, so all catalog
7643 // Gemini models are supported here).
7644 if (!class_exists('MxChat_Model_Catalog')) {
7645 require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
7646 }
7647 $chat_catalog = MxChat_Model_Catalog::chat_models();
7648 $openai_models = (isset($chat_catalog['openai']['models']) && is_array($chat_catalog['openai']['models']))
7649 ? array_keys($chat_catalog['openai']['models'])
7650 : array();
7651 $gemini_models = (isset($chat_catalog['gemini']['models']) && is_array($chat_catalog['gemini']['models']))
7652 ? array_keys($chat_catalog['gemini']['models'])
7653 : array();
7654
7655 $is_capable = in_array($current_model, $openai_models) || in_array($current_model, $gemini_models);
7656 $is_supported = $is_capable && !in_array($current_model, $unsupported_models);
7657
7658 // Wrapper div with data attributes for JS to show/hide. data-openai-models is kept for
7659 // back-compat; data-gemini-models is the added second provider the JS now also honors.
7660 echo '<div id="web-search-toggle-wrapper" data-openai-models="' . esc_attr(implode(',', $openai_models)) . '" data-gemini-models="' . esc_attr(implode(',', $gemini_models)) . '" data-unsupported-models="' . esc_attr(implode(',', $unsupported_models)) . '"' . (!$is_supported ? ' style="display:none;"' : '') . '>';
7661
7662 echo '<label class="toggle-switch">';
7663 echo sprintf(
7664 '<input type="checkbox" id="enable_web_search" name="enable_web_search" value="on" %s />',
7665 esc_attr($checked)
7666 );
7667 echo '<span class="slider"></span>';
7668 echo '</label>';
7669
7670 echo '</div>';
7671
7672 // Message shown when a model that can't ground (Claude, Grok, DeepSeek, OpenRouter, etc.) is selected
7673 echo '<p id="web-search-unavailable-message" class="description" style="color: #666;' . ($is_supported ? ' display:none;' : '') . '">';
7674 echo '<span class="dashicons dashicons-info" style="font-size: 16px; vertical-align: middle; margin-right: 4px;"></span>';
7675 echo esc_html__('Web search is only available for OpenAI and Gemini models.', 'mxchat');
7676 echo '</p>';
7677 }
7678
7679 // AJAX handler to fetch OpenRouter models
7680 public function fetch_openrouter_models() {
7681 check_ajax_referer('mxchat_fetch_openrouter_models', 'nonce');
7682
7683 $api_key = isset($_POST['api_key']) ? sanitize_text_field($_POST['api_key']) : '';
7684
7685 if (empty($api_key)) {
7686 wp_send_json_error(array('message' => 'API key is required'));
7687 }
7688
7689 $response = wp_remote_get('https://openrouter.ai/api/v1/models', array(
7690 'headers' => array(
7691 'Authorization' => 'Bearer ' . $api_key,
7692 'Content-Type' => 'application/json',
7693 ),
7694 'timeout' => 15,
7695 ));
7696
7697 if (is_wp_error($response)) {
7698 wp_send_json_error(array('message' => $response->get_error_message()));
7699 }
7700
7701 $body = wp_remote_retrieve_body($response);
7702 $data = json_decode($body, true);
7703
7704 if (isset($data['data']) && is_array($data['data'])) {
7705 // Format the models for the frontend
7706 $models = array_map(function($model) {
7707 return array(
7708 'id' => $model['id'],
7709 'name' => $model['name'] ?? $model['id'],
7710 'description' => $model['description'] ?? '',
7711 'context_length' => $model['context_length'] ?? 0,
7712 'pricing' => array(
7713 'prompt' => $model['pricing']['prompt'] ?? 0,
7714 'completion' => $model['pricing']['completion'] ?? 0,
7715 ),
7716 );
7717 }, $data['data']);
7718
7719 wp_send_json_success(array('models' => $models));
7720 } else {
7721 wp_send_json_error(array('message' => 'Invalid response from OpenRouter'));
7722 }
7723 }
7724
7725 // Callback function for embedding model selection
7726 public function embedding_model_callback() {
7727 $models = array(
7728 esc_html__('OpenAI Embeddings', 'mxchat') => array(
7729 'text-embedding-3-small' => esc_html__('TE3 Small (1536, Efficient)', 'mxchat'),
7730 'text-embedding-ada-002' => esc_html__('Ada 2 (1536, Recommended)', 'mxchat'),
7731 'text-embedding-3-large' => esc_html__('TE3 Large (3072, Powerful)', 'mxchat'),
7732 ),
7733 esc_html__('Voyage AI Embeddings', 'mxchat') => array(
7734 'voyage-3-large' => esc_html__('Voyage-3 Large (2048, Most Capable)', 'mxchat'),
7735 ),
7736 esc_html__('Google Gemini Embeddings', 'mxchat') => array(
7737 'gemini-embedding-001' => esc_html__('Gemini Embedding (1536, Stable)', 'mxchat'),
7738 )
7739 );
7740 $selected_model = isset($this->options['embedding_model']) ? esc_attr($this->options['embedding_model']) : 'text-embedding-ada-002';
7741 echo '<select id="embedding_model" name="embedding_model">';
7742 foreach ($models as $group_label => $group_models) {
7743 echo '<optgroup label="' . esc_attr($group_label) . '">';
7744 foreach ($group_models as $model_value => $model_label) {
7745 echo '<option value="' . esc_attr($model_value) . '" ' . selected($selected_model, $model_value, false) . '>' . esc_html($model_label) . '</option>';
7746 }
7747 echo '</optgroup>';
7748 }
7749 echo '</select>';
7750
7751 // API Key Status Messages for Embedding Models
7752 $has_openai_key = !empty($this->options['api_key']);
7753 $has_voyage_key = !empty($this->options['voyage_api_key']);
7754 $has_gemini_key = !empty($this->options['gemini_api_key']);
7755
7756 // OpenAI Embeddings
7757 echo '<p class="mxchat-embedding-api-status" data-provider="openai" style="display:none;">';
7758 if ($has_openai_key) {
7759 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenAI detected', 'mxchat') . '</span>';
7760 } else {
7761 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenAI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7762 }
7763 echo '</p>';
7764
7765 // Voyage AI Embeddings
7766 echo '<p class="mxchat-embedding-api-status" data-provider="voyage" style="display:none;">';
7767 if ($has_voyage_key) {
7768 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Voyage AI detected', 'mxchat') . '</span>';
7769 } else {
7770 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Voyage AI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7771 }
7772 echo '</p>';
7773
7774 // Gemini Embeddings
7775 echo '<p class="mxchat-embedding-api-status" data-provider="gemini" style="display:none;">';
7776 if ($has_gemini_key) {
7777 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Google Gemini detected', 'mxchat') . '</span>';
7778 } else {
7779 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Google Gemini detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7780 }
7781 echo '</p>';
7782
7783 }
7784
7785
7786 public function mxchat_top_bar_title_callback() {
7787 // Retrieve the current value of the top bar title from saved options
7788 $top_bar_title = isset($this->options['top_bar_title']) ? esc_attr($this->options['top_bar_title']) : '';
7789
7790 // Render the input field
7791 echo '<input type="text" id="top_bar_title" name="top_bar_title" value="' . $top_bar_title . '" />';
7792 }
7793 public function mxchat_ai_agent_text_callback() {
7794 // Retrieve the current value of the AI agent text from saved options
7795 $ai_agent_text = isset($this->options['ai_agent_text']) ? esc_attr($this->options['ai_agent_text']) : '';
7796 // Render the input field
7797 echo '<input type="text" id="ai_agent_text" name="ai_agent_text" value="' . $ai_agent_text . '" />';
7798 }
7799
7800
7801 public function enable_email_block_callback() {
7802 // Load full plugin options array
7803 $all_options = get_option('mxchat_options', []);
7804
7805 // Get the value, default to 'off'
7806 $enable_email_block = isset($all_options['enable_email_block']) ? $all_options['enable_email_block'] : 'off';
7807
7808 // Check if it's 'on'
7809 $checked = ($enable_email_block === 'on') ? 'checked' : '';
7810
7811 echo '<label class="toggle-switch">';
7812 echo sprintf(
7813 '<input type="checkbox" id="enable_email_block" name="enable_email_block" value="on" %s />',
7814 esc_attr($checked)
7815 );
7816 echo '<span class="slider"></span>';
7817 echo '</label>';
7818 }
7819
7820 public function email_blocker_header_content_callback() {
7821 // Load the entire 'mxchat_options' array
7822 $all_options = get_option('mxchat_options', []);
7823
7824 // Retrieve the saved content or default to empty
7825 $content = isset($all_options['email_blocker_header_content'])
7826 ? $all_options['email_blocker_header_content']
7827 : '';
7828
7829 // Render the textarea - IMPORTANT: name should be just "email_blocker_header_content"
7830 echo '<textarea
7831 id="email_blocker_header_content"
7832 name="email_blocker_header_content"
7833 rows="5"
7834 cols="70"
7835 data-setting="email_blocker_header_content"
7836 >' . esc_textarea($content) . '</textarea>';
7837 }
7838
7839 public function email_blocker_button_text_callback() {
7840 // Load the entire 'mxchat_options' array
7841 $all_options = get_option('mxchat_options', []);
7842
7843 // Retrieve the saved button text or default to empty
7844 $button_text = isset($all_options['email_blocker_button_text'])
7845 ? $all_options['email_blocker_button_text']
7846 : '';
7847
7848 // Use esc_attr to safely render the existing text
7849 echo '<input type="text" id="email_blocker_button_text" name="email_blocker_button_text" value="' . esc_attr($button_text) . '" style="width: 300px;" />';
7850 }
7851
7852 //Enable name field callback
7853 public function enable_name_field_callback() {
7854 // Load full plugin options array
7855 $all_options = get_option('mxchat_options', []);
7856 // Get the value, default to 'off'
7857 $enable_name_field = isset($all_options['enable_name_field']) ? $all_options['enable_name_field'] : 'off';
7858 // Check if it's 'on'
7859 $checked = ($enable_name_field === 'on') ? 'checked' : '';
7860 echo '<label class="toggle-switch">';
7861 echo sprintf(
7862 '<input type="checkbox" id="enable_name_field" name="enable_name_field" value="on" %s />',
7863 esc_attr($checked)
7864 );
7865 echo '<span class="slider"></span>';
7866 echo '</label>';
7867 }
7868 //Name field placeholder callback
7869 public function name_field_placeholder_callback() {
7870 $all_options = get_option('mxchat_options', []);
7871 $placeholder = isset($all_options['name_field_placeholder'])
7872 ? $all_options['name_field_placeholder']
7873 : esc_html__('Enter your name', 'mxchat');
7874
7875 echo '<input type="text" id="name_field_placeholder" name="name_field_placeholder" value="' . esc_attr($placeholder) . '" style="width: 300px;" />';
7876 }
7877
7878
7879
7880 public function mxchat_intro_message_callback() {
7881 // Load the entire 'mxchat_options' array
7882 $all_options = get_option('mxchat_options', []);
7883 // Retrieve the saved intro message or use the default
7884 $default_message = __('Hello! How can I assist you today?', 'mxchat');
7885 $saved_message = isset($all_options['intro_message']) ? $all_options['intro_message'] : $default_message;
7886 // Escape on output (esc_textarea) — neutralizes any payload already stored before the
7887 // Wordfence Stored-XSS fix (CWE-79, plan-3f8158) and prevents </textarea> context-breakout.
7888 ?>
7889 <textarea id="intro_message" name="intro_message" rows="5" cols="50"><?php echo esc_textarea( $saved_message ); ?></textarea>
7890 <p class="description" style="margin-top: 8px;">
7891 <?php esc_html_e('Use {visitor_name} to personalize greetings when lead capture is enabled.', 'mxchat'); ?><br>
7892 <code style="font-size: 12px;"><?php esc_html_e('Example: Hello {visitor_name}! How can I help you today?', 'mxchat'); ?></code>
7893 </p>
7894 <?php
7895 }
7896
7897 public function mxchat_input_copy_callback() {
7898 // Load the entire 'mxchat_options' array
7899 $all_options = get_option('mxchat_options', []);
7900
7901 // Retrieve the saved input copy or use the default value
7902 $default_copy = __('How can I assist?', 'mxchat');
7903 $input_copy = isset($all_options['input_copy']) ? $all_options['input_copy'] : $default_copy;
7904
7905 // Output the input field with the saved value
7906 printf(
7907 '<input type="text" id="input_copy" name="input_copy" value="%s" placeholder="%s" />',
7908 esc_attr($input_copy),
7909 esc_attr__('How can I assist?', 'mxchat')
7910 );
7911 }
7912
7913
7914 public function mxchat_append_to_body_callback() {
7915 // Fetch fresh options to ensure we have the latest saved values
7916 $options = get_option('mxchat_options', array());
7917
7918 // Get value from options array, default to 'off'
7919 $append_to_body = isset($options['append_to_body']) ? $options['append_to_body'] : 'off';
7920 $checked = ($append_to_body === 'on') ? 'checked' : '';
7921
7922 // Get post type visibility settings
7923 $visibility_mode = isset($options['post_type_visibility_mode']) ? $options['post_type_visibility_mode'] : 'all';
7924 $visibility_list = isset($options['post_type_visibility_list']) ? $options['post_type_visibility_list'] : array();
7925 if (!is_array($visibility_list)) {
7926 $visibility_list = array();
7927 }
7928
7929 echo '<div class="mxchat-autosave-section">';
7930
7931 // Main toggle
7932 echo '<label class="toggle-switch">';
7933 echo sprintf(
7934 '<input type="checkbox" id="append_to_body" name="append_to_body" value="on" %s />',
7935 esc_attr($checked)
7936 );
7937 echo '<span class="slider"></span>';
7938 echo '</label>';
7939
7940 // Post Type Visibility Options (only visible when auto-display is ON)
7941 $display_style = ($append_to_body === 'on') ? '' : 'display: none;';
7942 echo '<div id="post-type-visibility-options" class="mxchat-sub-options" style="' . esc_attr($display_style) . '">';
7943
7944 echo '<div class="mxchat-post-type-visibility-header">';
7945 echo '<h4>' . esc_html__('Post Type Visibility', 'mxchat') . '</h4>';
7946 echo '</div>';
7947
7948 // Mode selector (radio buttons)
7949 echo '<div class="mxchat-visibility-mode">';
7950
7951 echo '<label class="mxchat-radio-label">';
7952 echo '<input type="radio" name="post_type_visibility_mode" value="all" ' . checked($visibility_mode, 'all', false) . ' />';
7953 echo '<span>' . esc_html__('Show on all post types', 'mxchat') . '</span>';
7954 echo '</label>';
7955
7956 echo '<label class="mxchat-radio-label">';
7957 echo '<input type="radio" name="post_type_visibility_mode" value="include" ' . checked($visibility_mode, 'include', false) . ' />';
7958 echo '<span>' . esc_html__('Only show on selected post types', 'mxchat') . '</span>';
7959 echo '</label>';
7960
7961 echo '<label class="mxchat-radio-label">';
7962 echo '<input type="radio" name="post_type_visibility_mode" value="exclude" ' . checked($visibility_mode, 'exclude', false) . ' />';
7963 echo '<span>' . esc_html__('Hide on selected post types', 'mxchat') . '</span>';
7964 echo '</label>';
7965
7966 echo '</div>';
7967
7968 // Post type checkboxes (only visible when mode is include or exclude)
7969 $list_display = ($visibility_mode !== 'all') ? '' : 'display: none;';
7970 echo '<div id="post-type-list" class="mxchat-post-type-list" style="' . esc_attr($list_display) . '">';
7971
7972 // Get all public post types
7973 $post_types = get_post_types(array('public' => true), 'objects');
7974
7975 foreach ($post_types as $post_type) {
7976 // Skip attachments
7977 if ($post_type->name === 'attachment') {
7978 continue;
7979 }
7980
7981 $is_checked = in_array($post_type->name, $visibility_list) ? 'checked' : '';
7982
7983 echo '<label class="mxchat-checkbox-label">';
7984 echo '<input type="checkbox" name="post_type_visibility_list[]" value="' . esc_attr($post_type->name) . '" ' . $is_checked . ' />';
7985 echo '<span>' . esc_html($post_type->label) . '</span>';
7986 echo '</label>';
7987 }
7988
7989 echo '</div>'; // End post-type-list
7990 echo '</div>'; // End post-type-visibility-options
7991 echo '</div>'; // End mxchat-autosave-section
7992 }
7993
7994 public function mxchat_contextual_awareness_callback() {
7995 // Get value from options array, default to 'off'
7996 $contextual_awareness = isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off';
7997 $checked = ($contextual_awareness === 'on') ? 'checked' : '';
7998 echo '<label class="toggle-switch">';
7999 echo sprintf(
8000 '<input type="checkbox" id="contextual_awareness_toggle" name="contextual_awareness_toggle" value="on" %s />',
8001 esc_attr($checked)
8002 );
8003 echo '<span class="slider"></span>';
8004 echo '</label>';
8005 }
8006
8007 public function mxchat_citation_links_toggle_callback() {
8008 // Get value from options array, default to 'on' (enabled by default)
8009 $citation_links = isset($this->options['citation_links_toggle']) ? $this->options['citation_links_toggle'] : 'on';
8010 $checked = ($citation_links === 'on') ? 'checked' : '';
8011 echo '<label class="toggle-switch">';
8012 echo sprintf(
8013 '<input type="checkbox" id="citation_links_toggle" name="citation_links_toggle" value="on" %s />',
8014 esc_attr($checked)
8015 );
8016 echo '<span class="slider"></span>';
8017 echo '</label>';
8018 }
8019
8020 /**
8021 * Toggle for the end-of-session satisfaction rating prompt (plan-a5b006).
8022 * Default ON. The widget reads this through the localized object; the
8023 * mxchat_satisfaction_rating_enabled filter still lets developers force
8024 * the value site-wide.
8025 *
8026 * The 5 customization fields (idle/question/thanks/placeholder/saved) are
8027 * rendered inline here inside a single wrapper div whose initial display
8028 * is set server-side from the toggle value (plan-29caac). Mirrors the
8029 * auto-display chatbot pattern at mxchat_append_to_body_callback — no
8030 * DOMContentLoaded race because rows exist as direct children of this
8031 * callback's output, and the wrapper's display: none is inline at render
8032 * time so refresh shows the correct state with no flash.
8033 */
8034 public function mxchat_satisfaction_rating_toggle_callback() {
8035 $options = $this->options;
8036 $value = isset($options['satisfaction_rating_enabled']) ? $options['satisfaction_rating_enabled'] : 'off';
8037 $checked = ($value === 'on') ? 'checked' : '';
8038
8039 $idle = isset($options['satisfaction_rating_idle_seconds']) ? intval($options['satisfaction_rating_idle_seconds']) : 60;
8040 $idle = max(5, min(600, $idle));
8041 $question = isset($options['satisfaction_rating_question']) ? $options['satisfaction_rating_question'] : '';
8042 $thanks = isset($options['satisfaction_rating_thanks']) ? $options['satisfaction_rating_thanks'] : '';
8043 $placeholder = isset($options['satisfaction_rating_placeholder']) ? $options['satisfaction_rating_placeholder'] : '';
8044 $saved = isset($options['satisfaction_rating_saved']) ? $options['satisfaction_rating_saved'] : '';
8045
8046 echo '<div class="mxchat-autosave-section">';
8047
8048 echo '<label class="toggle-switch">';
8049 echo sprintf(
8050 '<input type="checkbox" id="satisfaction_rating_enabled" name="satisfaction_rating_enabled" value="on" %s />',
8051 esc_attr($checked)
8052 );
8053 echo '<span class="slider"></span>';
8054 echo '</label>';
8055
8056 ?>
8057 <style>
8058 .mxchat-sub-options-field { margin: 12px 0; }
8059 .mxchat-sub-options-field label { display: inline-block; margin-bottom: 4px; }
8060 #satisfaction-rating-sub-options h4 { margin: 16px 0 8px; }
8061 </style>
8062 <?php
8063
8064 $display_style = ($value === 'on') ? '' : 'display: none;';
8065 echo '<div id="satisfaction-rating-sub-options" class="mxchat-sub-options" style="' . esc_attr($display_style) . '">';
8066
8067 echo '<h4>' . esc_html__('Customize the prompt (optional)', 'mxchat') . '</h4>';
8068
8069 echo '<div class="mxchat-sub-options-field">';
8070 echo '<label for="satisfaction_rating_idle_seconds"><strong>' . esc_html__('Idle Timeout', 'mxchat') . '</strong></label><br />';
8071 printf(
8072 '<input type="number" id="satisfaction_rating_idle_seconds" name="satisfaction_rating_idle_seconds" value="%d" min="5" max="600" step="1" class="small-text" /> <span class="description">%s</span>',
8073 (int) $idle,
8074 esc_html__('seconds of user inactivity before the prompt appears (5-600)', 'mxchat')
8075 );
8076 echo '</div>';
8077
8078 echo '<div class="mxchat-sub-options-field">';
8079 echo '<label for="satisfaction_rating_question"><strong>' . esc_html__('Prompt Question', 'mxchat') . '</strong></label><br />';
8080 printf(
8081 '<input type="text" id="satisfaction_rating_question" name="satisfaction_rating_question" value="%s" maxlength="200" class="regular-text" placeholder="%s" />',
8082 esc_attr($question),
8083 esc_attr__('Was this helpful?', 'mxchat')
8084 );
8085 echo '<p class="description">' . esc_html__('Leave blank for the default. Shown above the thumbs up/down.', 'mxchat') . '</p>';
8086 echo '</div>';
8087
8088 echo '<div class="mxchat-sub-options-field">';
8089 echo '<label for="satisfaction_rating_thanks"><strong>' . esc_html__('Thank-You Message', 'mxchat') . '</strong></label><br />';
8090 printf(
8091 '<input type="text" id="satisfaction_rating_thanks" name="satisfaction_rating_thanks" value="%s" maxlength="300" class="regular-text" placeholder="%s" />',
8092 esc_attr($thanks),
8093 esc_attr__('Thanks! Anything we should improve? (optional)', 'mxchat')
8094 );
8095 echo '<p class="description">' . esc_html__('Leave blank for the default. Shown after the user clicks a thumb.', 'mxchat') . '</p>';
8096 echo '</div>';
8097
8098 echo '<div class="mxchat-sub-options-field">';
8099 echo '<label for="satisfaction_rating_placeholder"><strong>' . esc_html__('Feedback Placeholder', 'mxchat') . '</strong></label><br />';
8100 printf(
8101 '<input type="text" id="satisfaction_rating_placeholder" name="satisfaction_rating_placeholder" value="%s" maxlength="200" class="regular-text" placeholder="%s" />',
8102 esc_attr($placeholder),
8103 esc_attr__('Tell us what could be better…', 'mxchat')
8104 );
8105 echo '<p class="description">' . esc_html__('Leave blank for the default. Placeholder text inside the feedback textarea.', 'mxchat') . '</p>';
8106 echo '</div>';
8107
8108 echo '<div class="mxchat-sub-options-field">';
8109 echo '<label for="satisfaction_rating_saved"><strong>' . esc_html__('Saved Confirmation', 'mxchat') . '</strong></label><br />';
8110 printf(
8111 '<input type="text" id="satisfaction_rating_saved" name="satisfaction_rating_saved" value="%s" maxlength="200" class="regular-text" placeholder="%s" />',
8112 esc_attr($saved),
8113 esc_attr__('Thanks for the feedback.', 'mxchat')
8114 );
8115 echo '<p class="description">' . esc_html__('Leave blank for the default. Shown after the feedback is sent.', 'mxchat') . '</p>';
8116 echo '</div>';
8117
8118 echo '</div>'; // #satisfaction-rating-sub-options
8119 echo '</div>'; // .mxchat-autosave-section
8120
8121 ?>
8122 <script>
8123 (function() {
8124 document.addEventListener('DOMContentLoaded', function() {
8125 var toggle = document.getElementById('satisfaction_rating_enabled');
8126 var subOptions = document.getElementById('satisfaction-rating-sub-options');
8127 if (!toggle || !subOptions) return;
8128 toggle.addEventListener('change', function() {
8129 subOptions.style.display = toggle.checked ? '' : 'none';
8130 });
8131 });
8132 })();
8133 </script>
8134 <?php
8135 }
8136
8137 public function mxchat_privacy_toggle_callback() {
8138 // Load from mxchat_options array
8139 $options = get_option('mxchat_options', []);
8140
8141 // Get privacy toggle value with fallback
8142 $privacy_toggle = isset($options['privacy_toggle']) ? $options['privacy_toggle'] : 'off';
8143 $checked = ($privacy_toggle === 'on') ? 'checked' : '';
8144
8145 // Get privacy text with fallback
8146 $privacy_text = isset($options['privacy_text'])
8147 ? $options['privacy_text']
8148 : __('By chatting, you agree to our <a href="https://example.com/privacy-policy" target="_blank">privacy policy</a>.', 'mxchat');
8149
8150 // Output the toggle switch
8151 echo '<label class="toggle-switch">';
8152 echo sprintf(
8153 '<input type="checkbox" id="privacy_toggle" name="privacy_toggle" value="on" %s />',
8154 esc_attr($checked)
8155 );
8156 echo '<span class="slider"></span>';
8157 echo '</label>';
8158
8159 // Output the custom text input field
8160 echo sprintf(
8161 '<textarea id="privacy_text" name="privacy_text" rows="5" cols="50" class="regular-text">%s</textarea>',
8162 esc_textarea($privacy_text)
8163 );
8164 }
8165
8166
8167 public function mxchat_complianz_toggle_callback() {
8168 // Load from mxchat_options array
8169 $options = get_option('mxchat_options', []);
8170
8171 // Get complianz toggle value with fallback
8172 $complianz_toggle = isset($options['complianz_toggle']) ? $options['complianz_toggle'] : 'off';
8173 $checked = ($complianz_toggle === 'on') ? 'checked' : '';
8174
8175 // Output the toggle switch
8176 echo '<label class="toggle-switch">';
8177 echo sprintf(
8178 '<input type="checkbox" id="complianz_toggle" name="complianz_toggle" value="on" %s />',
8179 esc_attr($checked)
8180 );
8181 echo '<span class="slider"></span>';
8182 echo '</label>';
8183 }
8184
8185 public function mxchat_link_target_toggle_callback() {
8186 // Load from mxchat_options array
8187 $options = get_option('mxchat_options', []);
8188
8189 // Get link target toggle value with fallback
8190 $link_target_toggle = isset($options['link_target_toggle']) ? $options['link_target_toggle'] : 'off';
8191 $checked = ($link_target_toggle === 'on') ? 'checked' : '';
8192
8193 // Output the toggle switch
8194 echo '<label class="toggle-switch">';
8195 echo sprintf(
8196 '<input type="checkbox" id="link_target_toggle" name="link_target_toggle" value="on" %s />',
8197 esc_attr($checked)
8198 );
8199 echo '<span class="slider"></span>';
8200 echo '</label>';
8201 }
8202
8203 public function mxchat_chat_persistence_toggle_callback() {
8204 // Load from mxchat_options array
8205 $options = get_option('mxchat_options', []);
8206
8207 // Get chat persistence toggle value with fallback
8208 $chat_persistence_toggle = isset($options['chat_persistence_toggle']) ? $options['chat_persistence_toggle'] : 'off';
8209 $checked = ($chat_persistence_toggle === 'on') ? 'checked' : '';
8210
8211 // Output the toggle switch
8212 echo '<label class="toggle-switch">';
8213 echo sprintf(
8214 '<input type="checkbox" id="chat_persistence_toggle" name="chat_persistence_toggle" value="on" %s />',
8215 esc_attr($checked)
8216 );
8217 echo '<span class="slider"></span>';
8218 echo '</label>';
8219 }
8220
8221 public function mxchat_print_button_toggle_callback() {
8222 // Load from mxchat_options array
8223 $options = get_option('mxchat_options', []);
8224
8225 // Default ON — the option was previously unexposed and the button always showed.
8226 $print_button_enabled = isset($options['print_button_enabled']) ? $options['print_button_enabled'] : 'on';
8227 $checked = ($print_button_enabled === 'on') ? 'checked' : '';
8228
8229 // Output the toggle switch
8230 echo '<label class="toggle-switch">';
8231 echo sprintf(
8232 '<input type="checkbox" id="print_button_enabled" name="print_button_enabled" value="on" %s />',
8233 esc_attr($checked)
8234 );
8235 echo '<span class="slider"></span>';
8236 echo '</label>';
8237 }
8238
8239 public function mxchat_reset_chat_toggle_callback() {
8240 // Load from mxchat_options array. plan ac2e81 — default OFF (new, opt-in).
8241 $options = get_option('mxchat_options', []);
8242 $reset_chat_enabled = isset($options['reset_chat_enabled']) ? $options['reset_chat_enabled'] : 'off';
8243 $checked = ($reset_chat_enabled === 'on') ? 'checked' : '';
8244
8245 echo '<label class="toggle-switch">';
8246 echo sprintf(
8247 '<input type="checkbox" id="reset_chat_enabled" name="reset_chat_enabled" value="on" %s />',
8248 esc_attr($checked)
8249 );
8250 echo '<span class="slider"></span>';
8251 echo '</label>';
8252 }
8253
8254 public function mxchat_reset_chat_label_callback() {
8255 // Editable label for the "Start new chat" menu item. plan ac2e81.
8256 $options = get_option('mxchat_options', []);
8257 $reset_chat_label = isset($options['reset_chat_label']) ? $options['reset_chat_label'] : '';
8258
8259 printf(
8260 '<input type="text" id="reset_chat_label" name="reset_chat_label" value="%s" placeholder="%s" class="regular-text" />',
8261 esc_attr($reset_chat_label),
8262 esc_attr__('Start new chat', 'mxchat')
8263 );
8264 }
8265
8266 public function mxchat_popular_question_1_callback() {
8267 // Load the full plugin options array
8268 $all_options = get_option('mxchat_options', []);
8269
8270 // Retrieve the specific option for popular_question_1
8271 $popular_question_1 = isset($all_options['popular_question_1']) ? $all_options['popular_question_1'] : '';
8272
8273 // Render the input field
8274 printf(
8275 '<input type="text" id="popular_question_1" name="popular_question_1" value="%s" placeholder="%s" class="regular-text" />',
8276 esc_attr($popular_question_1),
8277 esc_attr__('Enter Quick Question 1', 'mxchat')
8278 );
8279 }
8280
8281
8282 public function mxchat_popular_question_2_callback() {
8283 // Load the full plugin options array
8284 $all_options = get_option('mxchat_options', []);
8285
8286 // Retrieve the specific option for popular_question_2
8287 $popular_question_2 = isset($all_options['popular_question_2']) ? $all_options['popular_question_2'] : '';
8288
8289 // Render the input field
8290 printf(
8291 '<input type="text" id="popular_question_2" name="popular_question_2" value="%s" placeholder="%s" class="regular-text" />',
8292 esc_attr($popular_question_2),
8293 esc_attr__('Enter Quick Question 2', 'mxchat')
8294 );
8295 }
8296
8297
8298 public function mxchat_popular_question_3_callback() {
8299 // Load the full plugin options array
8300 $all_options = get_option('mxchat_options', []);
8301
8302 // Retrieve the specific option for popular_question_3
8303 $popular_question_3 = isset($all_options['popular_question_3']) ? $all_options['popular_question_3'] : '';
8304
8305 // Render the input field
8306 printf(
8307 '<input type="text" id="popular_question_3" name="popular_question_3" value="%s" placeholder="%s" class="regular-text" />',
8308 esc_attr($popular_question_3),
8309 esc_attr(__('Enter Quick Question 3', 'mxchat'))
8310 );
8311 }
8312
8313 public function mxchat_additional_popular_questions_callback() {
8314 $options = get_option('mxchat_options', []);
8315 $additional_questions = isset($options['additional_popular_questions'])
8316 ? $options['additional_popular_questions']
8317 : get_option('additional_popular_questions', array());
8318
8319 echo '<div id="mxchat-additional-questions-container">';
8320 if (!empty($additional_questions)) {
8321 foreach ($additional_questions as $index => $question) {
8322 printf(
8323 '<div class="mxchat-question-row">
8324 <input type="text" name="additional_popular_questions[]"
8325 value="%s"
8326 placeholder="%s"
8327 class="regular-text mxchat-question-input"
8328 data-question-index="%d" />
8329 <button type="button" class="button mxchat-remove-question"
8330 aria-label="%s">%s</button>
8331 </div>',
8332 esc_attr($question),
8333 esc_attr(sprintf(__('Enter Additional Quick Question %d', 'mxchat'), $index + 4)),
8334 $index,
8335 esc_attr(__('Remove question', 'mxchat')),
8336 esc_html__('Remove', 'mxchat')
8337 );
8338 }
8339 } else {
8340 printf(
8341 '<div class="mxchat-question-row">
8342 <input type="text" name="additional_popular_questions[]"
8343 value=""
8344 placeholder="%s"
8345 class="regular-text mxchat-question-input"
8346 data-question-index="0" />
8347 <button type="button" class="button mxchat-remove-question"
8348 aria-label="%s">%s</button>
8349 </div>',
8350 esc_attr(__('Enter Additional Quick Question 4', 'mxchat')),
8351 esc_attr(__('Remove question', 'mxchat')),
8352 esc_html__('Remove', 'mxchat')
8353 );
8354 }
8355 echo '</div>';
8356 printf(
8357 '<button type="button" class="button mxchat-add-question" aria-label="%s">%s</button>',
8358 esc_attr(__('Add question', 'mxchat')),
8359 esc_html__('Add Question', 'mxchat')
8360 );
8361 }
8362
8363 public function mxchat_brave_api_key_callback() {
8364 $brave_api_key = isset($this->options['brave_api_key']) ? esc_attr($this->options['brave_api_key']) : '';
8365 $nonce = wp_create_nonce('mxchat_autosave_nonce');
8366
8367 echo '<div class="api-key-wrapper">';
8368 echo sprintf(
8369 '<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" />',
8370 $brave_api_key,
8371 $nonce
8372 );
8373 echo '<button type="button" id="toggleBraveApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
8374 echo '</div>';
8375 echo '<p class="description">' . __('Required for Brave Search integration. Get your API key from Brave Search API.', 'mxchat') . '</p>';
8376 }
8377
8378 public function mxchat_brave_image_count_callback() {
8379 $brave_image_count = isset($this->options['brave_image_count'])
8380 ? intval($this->options['brave_image_count'])
8381 : 4;
8382 $nonce = wp_create_nonce('mxchat_autosave_nonce');
8383
8384 echo '<div class="mxchat-field-wrapper">';
8385 echo sprintf(
8386 '<input type="number" id="brave_image_count" name="brave_image_count"
8387 value="%d" min="1" max="6" class="small-text mxchat-autosave-field" data-nonce="%s" />',
8388 $brave_image_count,
8389 $nonce
8390 );
8391 echo '</div>';
8392 echo '<p class="description">' . __('Select the number of images to return (1-6).', 'mxchat') . '</p>';
8393 }
8394
8395 public function mxchat_brave_safe_search_callback() {
8396 $brave_safe_search = isset($this->options['brave_safe_search'])
8397 ? esc_attr($this->options['brave_safe_search'])
8398 : 'strict';
8399 $nonce = wp_create_nonce('mxchat_autosave_nonce');
8400
8401 echo '<div class="mxchat-field-wrapper">';
8402 echo '<select id="brave_safe_search" name="brave_safe_search" class="mxchat-autosave-field" data-nonce="' . $nonce . '">';
8403 echo sprintf(
8404 '<option value="strict" %s>%s</option>',
8405 selected($brave_safe_search, 'strict', false),
8406 __('Strict', 'mxchat')
8407 );
8408 echo sprintf(
8409 '<option value="off" %s>%s</option>',
8410 selected($brave_safe_search, 'off', false),
8411 __('Off', 'mxchat')
8412 );
8413 echo '</select>';
8414 echo '</div>';
8415 echo '<p class="description">' .
8416 esc_html__('Set the Safe Search level for image searches. Brave Search only supports "Strict" and "Off" options.', 'mxchat') .
8417 '</p>';
8418 }
8419
8420 public function mxchat_brave_news_count_callback() {
8421 $brave_news_count = isset($this->options['brave_news_count'])
8422 ? intval($this->options['brave_news_count'])
8423 : 3;
8424 $nonce = wp_create_nonce('mxchat_autosave_nonce');
8425
8426 echo '<div class="mxchat-field-wrapper">';
8427 echo sprintf(
8428 '<input type="number" id="brave_news_count" name="brave_news_count"
8429 value="%d" min="1" max="10" class="small-text mxchat-autosave-field" data-nonce="%s" />',
8430 $brave_news_count,
8431 $nonce
8432 );
8433 echo '</div>';
8434 echo '<p class="description">' . esc_html__('Select the number of news articles to retrieve (1-10).', 'mxchat') . '</p>';
8435 }
8436
8437 public function mxchat_brave_country_callback() {
8438 $brave_country = isset($this->options['brave_country'])
8439 ? esc_attr($this->options['brave_country'])
8440 : 'us';
8441 $nonce = wp_create_nonce('mxchat_autosave_nonce');
8442
8443 echo '<div class="mxchat-field-wrapper">';
8444 echo sprintf(
8445 '<input type="text" id="brave_country" name="brave_country"
8446 value="%s" maxlength="2" class="small-text mxchat-autosave-field" data-nonce="%s" />',
8447 $brave_country,
8448 $nonce
8449 );
8450 echo '</div>';
8451 echo '<p class="description">' . esc_html__('Enter the country code (e.g., "us" for United States).', 'mxchat') . '</p>';
8452 }
8453
8454 public function mxchat_brave_language_callback() {
8455 $brave_language = isset($this->options['brave_language'])
8456 ? esc_attr($this->options['brave_language'])
8457 : 'en';
8458 $nonce = wp_create_nonce('mxchat_autosave_nonce');
8459
8460 echo '<div class="mxchat-field-wrapper">';
8461 echo sprintf(
8462 '<input type="text" id="brave_language" name="brave_language"
8463 value="%s" maxlength="2" class="small-text mxchat-autosave-field" data-nonce="%s" />',
8464 $brave_language,
8465 $nonce
8466 );
8467 echo '</div>';
8468 echo '<p class="description">' . esc_html__('Enter the language code (e.g., "en" for English).', 'mxchat') . '</p>';
8469 }
8470
8471
8472
8473
8474
8475 // Section Callback
8476 public function mxchat_pdf_intent_section_callback() {
8477 echo '<p>' . esc_html__('Configure the intent settings for the Chat with PDF feature.', 'mxchat') . '</p>';
8478 }
8479
8480 public function mxchat_chat_toolbar_toggle_callback() {
8481 // Get chat toolbar toggle value with fallback
8482 $chat_toolbar_toggle = isset($this->options['chat_toolbar_toggle']) ? $this->options['chat_toolbar_toggle'] : 'off';
8483 $checked = ($chat_toolbar_toggle === 'on') ? 'checked' : '';
8484
8485 // Output the toggle switch
8486 echo '<label class="toggle-switch">';
8487 echo sprintf(
8488 '<input type="checkbox" id="chat_toolbar_toggle" name="chat_toolbar_toggle" value="on" %s />',
8489 esc_attr($checked)
8490 );
8491 echo '<span class="slider"></span>';
8492 echo '</label>';
8493
8494 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>';
8495 }
8496
8497 /**
8498 * Callback for PDF upload button toggle setting
8499 */
8500 public function mxchat_show_pdf_upload_button_callback() {
8501 // Get toggle value with fallback
8502 $show_pdf_button = isset($this->options['show_pdf_upload_button']) ? $this->options['show_pdf_upload_button'] : 'on';
8503 $checked = ($show_pdf_button === 'on') ? 'checked' : '';
8504
8505 // Output the toggle switch
8506 echo '<label class="toggle-switch">';
8507 echo sprintf(
8508 '<input type="checkbox" id="show_pdf_upload_button" name="show_pdf_upload_button" value="on" %s />',
8509 esc_attr($checked)
8510 );
8511 echo '<span class="slider"></span>';
8512 echo '</label>';
8513
8514 echo '<p class="description">' . esc_html__('Enable to show the PDF upload button in the chatbot toolbar. Disable to hide it.', 'mxchat') . '</p>';
8515 }
8516
8517 /**
8518 * Callback for Word upload button toggle setting
8519 */
8520 public function mxchat_show_word_upload_button_callback() {
8521 // Get toggle value with fallback
8522 $show_word_button = isset($this->options['show_word_upload_button']) ? $this->options['show_word_upload_button'] : 'on';
8523 $checked = ($show_word_button === 'on') ? 'checked' : '';
8524
8525 // Output the toggle switch
8526 echo '<label class="toggle-switch">';
8527 echo sprintf(
8528 '<input type="checkbox" id="show_word_upload_button" name="show_word_upload_button" value="on" %s />',
8529 esc_attr($checked)
8530 );
8531 echo '<span class="slider"></span>';
8532 echo '</label>';
8533
8534 echo '<p class="description">' . esc_html__('Enable to show the Word document upload button in the chatbot toolbar. Disable to hide it.', 'mxchat') . '</p>';
8535 }
8536
8537 public function mxchat_pdf_intent_trigger_text_callback() {
8538 $default_text = __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat');
8539
8540 echo sprintf(
8541 '<textarea id="pdf_intent_trigger_text"
8542 name="pdf_intent_trigger_text"
8543 rows="3"
8544 cols="50"
8545 placeholder="%s">%s</textarea>',
8546 esc_attr__('Enter trigger text', 'mxchat'),
8547 isset($this->options['pdf_intent_trigger_text'])
8548 ? esc_textarea($this->options['pdf_intent_trigger_text'])
8549 : esc_textarea($default_text)
8550 );
8551 echo '<p class="description">' . esc_html__('Text displayed when the intent is triggered.', 'mxchat') . '</p>';
8552 }
8553
8554 public function mxchat_pdf_intent_success_text_callback() {
8555 $default_text = __("I've processed the PDF. What questions do you have about it?", 'mxchat');
8556
8557 echo sprintf(
8558 '<textarea id="pdf_intent_success_text"
8559 name="pdf_intent_success_text"
8560 rows="3"
8561 cols="50"
8562 placeholder="%s">%s</textarea>',
8563 esc_attr__('Enter success text', 'mxchat'),
8564 isset($this->options['pdf_intent_success_text'])
8565 ? esc_textarea($this->options['pdf_intent_success_text'])
8566 : esc_textarea($default_text)
8567 );
8568 echo '<p class="description">' . esc_html__('Text displayed when the intent is successful.', 'mxchat') . '</p>';
8569 }
8570
8571 public function mxchat_pdf_intent_error_text_callback() {
8572 $default_text = __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
8573
8574 echo sprintf(
8575 '<textarea id="pdf_intent_error_text"
8576 name="pdf_intent_error_text"
8577 rows="3"
8578 cols="50"
8579 placeholder="%s">%s</textarea>',
8580 esc_attr__('Enter error text', 'mxchat'),
8581 isset($this->options['pdf_intent_error_text'])
8582 ? esc_textarea($this->options['pdf_intent_error_text'])
8583 : esc_textarea($default_text)
8584 );
8585 echo '<p class="description">' . esc_html__('Text displayed when an error occurs during the intent.', 'mxchat') . '</p>';
8586 }
8587
8588 public function mxchat_pdf_max_pages_callback() {
8589 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
8590
8591 echo sprintf(
8592 '<input type="range"
8593 id="pdf_max_pages"
8594 name="pdf_max_pages"
8595 min="1"
8596 max="69"
8597 value="%d"
8598 class="range-slider" />',
8599 esc_attr($max_pages)
8600 );
8601 echo '<span id="pdf_max_pages_output">' . esc_html($max_pages) . '</span>';
8602 echo '<p class="description">' . esc_html__('Set the maximum number of document pages users can upload for processing. (1-69 pages)', 'mxchat') . '</p>';
8603 }
8604
8605 public function mxchat_live_agent_status_callback() {
8606 // Always get fresh options instead of using cached $this->options
8607 $fresh_options = get_option('mxchat_options');
8608 $status = isset($fresh_options['live_agent_status']) ? $fresh_options['live_agent_status'] : 'off';
8609
8610 echo '<label class="toggle-switch">';
8611 echo sprintf(
8612 '<input type="checkbox" id="live_agent_status" name="live_agent_status" value="on" %s />',
8613 checked($status, 'on', false)
8614 );
8615 echo '<span class="slider"></span>';
8616 echo '</label>';
8617 echo '<label for="live_agent_status" class="mxchat-status-label">';
8618 echo '<span class="status-text">' . ($status === 'on' ? esc_html__('Online', 'mxchat') : esc_html__('Offline', 'mxchat')) . '</span>';
8619 echo '</label>';
8620 }
8621
8622 public function mxchat_live_agent_away_message_callback() {
8623 $message = isset($this->options['live_agent_away_message'])
8624 ? $this->options['live_agent_away_message']
8625 : __('Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.', 'mxchat');
8626
8627 printf(
8628 '<textarea id="live_agent_away_message" name="live_agent_away_message" rows="3" cols="50">%s</textarea>',
8629 esc_textarea($message)
8630 );
8631 echo '<p class="description">' . esc_html__('Message shown when live agents are offline.', 'mxchat') . '</p>';
8632 }
8633
8634 public function mxchat_live_agent_notification_message_callback() {
8635 $message = isset($this->options['live_agent_notification_message'])
8636 ? $this->options['live_agent_notification_message']
8637 : __('Live agent has been notified.', 'mxchat');
8638
8639 printf(
8640 '<textarea id="live_agent_notification_message" name="live_agent_notification_message" rows="3" cols="50">%s</textarea>',
8641 esc_textarea($message)
8642 );
8643 echo '<p class="description">' . esc_html__('Message shown when live transfer activated.', 'mxchat') . '</p>';
8644 }
8645
8646 public function mxchat_live_agent_webhook_url_callback() {
8647 $webhook_url = isset($this->options['live_agent_webhook_url'])
8648 ? esc_url($this->options['live_agent_webhook_url'])
8649 : esc_url(get_option('live_agent_webhook_url', ''));
8650
8651 printf(
8652 '<input type="password" id="live_agent_webhook_url" name="live_agent_webhook_url" value="%s" class="regular-text" />',
8653 $webhook_url
8654 );
8655 echo '<button type="button" id="toggleWebhookUrlVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
8656 echo '<p class="description">' . esc_html__('Enter your Slack webhook URL for live agent notifications.', 'mxchat') . '</p>';
8657 }
8658
8659 public function mxchat_live_agent_secret_key_callback() {
8660 printf(
8661 '<input type="password" id="live_agent_secret_key" name="live_agent_secret_key" value="%s" class="regular-text" />',
8662 isset($this->options['live_agent_secret_key']) ? esc_attr($this->options['live_agent_secret_key']) : ''
8663 );
8664 echo '<button type="button" id="toggleSecretKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
8665 echo '<p class="description">' . esc_html__('Secret key for validating Slack requests. Keep this secure.', 'mxchat') . '</p>';
8666 }
8667
8668 public function mxchat_live_agent_bot_token_callback() {
8669 printf(
8670 '<input type="password" id="live_agent_bot_token" name="live_agent_bot_token" value="%s" class="regular-text" />',
8671 isset($this->options['live_agent_bot_token']) ? esc_attr($this->options['live_agent_bot_token']) : ''
8672 );
8673 echo '<button type="button" id="toggleBotTokenVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
8674 echo '<p class="description">' . esc_html__('Your Slack Bot OAuth Token (starts with xoxb-). Keep this secure.', 'mxchat') . '</p>';
8675 }
8676
8677 public function mxchat_live_agent_user_ids_callback() {
8678 $user_ids = isset($this->options['live_agent_user_ids'])
8679 ? esc_textarea($this->options['live_agent_user_ids'])
8680 : '';
8681
8682 printf(
8683 '<textarea id="live_agent_user_ids" name="live_agent_user_ids" rows="4" class="large-text">%s</textarea>',
8684 $user_ids
8685 );
8686 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>';
8687 }
8688
8689 /**
8690 * Telegram Integration Callbacks
8691 */
8692 public function mxchat_telegram_section_callback() {
8693 echo '<p>' . esc_html__('Configure Telegram integration for live agent support.', 'mxchat') . '</p>';
8694 }
8695
8696 public function mxchat_telegram_status_callback() {
8697 $fresh_options = get_option('mxchat_options');
8698 $status = isset($fresh_options['telegram_status']) ? $fresh_options['telegram_status'] : 'off';
8699
8700 echo '<label class="toggle-switch">';
8701 echo sprintf(
8702 '<input type="checkbox" id="telegram_status" name="telegram_status" value="on" %s />',
8703 checked($status, 'on', false)
8704 );
8705 echo '<span class="slider"></span>';
8706 echo '</label>';
8707 echo '<label for="telegram_status" class="mxchat-status-label">';
8708 echo '<span class="status-text">' . ($status === 'on' ? esc_html__('Online', 'mxchat') : esc_html__('Offline', 'mxchat')) . '</span>';
8709 echo '</label>';
8710 }
8711
8712 public function mxchat_telegram_notification_message_callback() {
8713 $message = isset($this->options['telegram_notification_message'])
8714 ? $this->options['telegram_notification_message']
8715 : __("I've notified a support agent. Please allow a moment for them to respond.", 'mxchat');
8716
8717 printf(
8718 '<textarea id="telegram_notification_message" name="telegram_notification_message" rows="3" cols="50">%s</textarea>',
8719 esc_textarea($message)
8720 );
8721 echo '<p class="description">' . esc_html__('Message shown when live transfer activated.', 'mxchat') . '</p>';
8722 }
8723
8724 public function mxchat_telegram_away_message_callback() {
8725 $message = isset($this->options['telegram_away_message'])
8726 ? $this->options['telegram_away_message']
8727 : __('Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.', 'mxchat');
8728
8729 printf(
8730 '<textarea id="telegram_away_message" name="telegram_away_message" rows="3" cols="50">%s</textarea>',
8731 esc_textarea($message)
8732 );
8733 echo '<p class="description">' . esc_html__('Message shown when live agents are offline.', 'mxchat') . '</p>';
8734 }
8735
8736 public function mxchat_telegram_bot_token_callback() {
8737 printf(
8738 '<input type="password" id="telegram_bot_token" name="telegram_bot_token" value="%s" class="regular-text" />',
8739 isset($this->options['telegram_bot_token']) ? esc_attr($this->options['telegram_bot_token']) : ''
8740 );
8741 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>';
8742 echo '<p class="description">' . esc_html__('Your Telegram Bot Token from @BotFather (e.g., 123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ).', 'mxchat') . '</p>';
8743 }
8744
8745 public function mxchat_telegram_group_id_callback() {
8746 printf(
8747 '<input type="text" id="telegram_group_id" name="telegram_group_id" value="%s" class="regular-text" />',
8748 isset($this->options['telegram_group_id']) ? esc_attr($this->options['telegram_group_id']) : ''
8749 );
8750 echo '<p class="description">' . esc_html__('Your Telegram supergroup ID (starts with -100). The group must have forum topics enabled.', 'mxchat') . '</p>';
8751 }
8752
8753 public function mxchat_telegram_webhook_secret_callback() {
8754 $secret = isset($this->options['telegram_webhook_secret']) ? $this->options['telegram_webhook_secret'] : '';
8755
8756 // Auto-generate secret if empty
8757 if (empty($secret)) {
8758 $secret = wp_generate_password(64, false, false);
8759 $options = get_option('mxchat_options', []);
8760 $options['telegram_webhook_secret'] = $secret;
8761 update_option('mxchat_options', $options);
8762 $this->options['telegram_webhook_secret'] = $secret;
8763 }
8764
8765 printf(
8766 '<input type="password" id="telegram_webhook_secret" name="telegram_webhook_secret" value="%s" class="regular-text" />',
8767 esc_attr($secret)
8768 );
8769 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>';
8770 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>';
8771 }
8772
8773 public function mxchat_similarity_threshold_callback() {
8774 // Load from mxchat_options array
8775 $options = get_option('mxchat_options', []);
8776
8777 // Get value from options array with default of 35
8778 $threshold = isset($options['similarity_threshold']) ? $options['similarity_threshold'] : 35;
8779
8780 echo '<div class="slider-container">';
8781 echo sprintf(
8782 '<input type="range"
8783 id="similarity_threshold"
8784 name="similarity_threshold"
8785 min="20"
8786 max="85"
8787 step="1"
8788 value="%s"
8789 class="range-slider" />',
8790 esc_attr($threshold)
8791 );
8792 echo sprintf(
8793 '<span id="threshold_value" class="range-value">%s</span>',
8794 esc_html($threshold)
8795 );
8796 echo '</div>';
8797 }
8798
8799 public function mxchat_max_input_length_callback() {
8800 // Load from mxchat_options array (plan a3fae2 part C).
8801 $options = get_option('mxchat_options', []);
8802
8803 // 0 = unlimited (default — preserves current behavior, no cap).
8804 $max_input_length = isset($options['max_input_length']) ? intval($options['max_input_length']) : 0;
8805
8806 echo sprintf(
8807 '<input type="number"
8808 id="max_input_length"
8809 name="max_input_length"
8810 min="0"
8811 max="100000"
8812 step="1"
8813 value="%s"
8814 placeholder="0"
8815 class="mxch-input mxch-input-sm" />',
8816 esc_attr($max_input_length)
8817 );
8818 }
8819
8820 public function mxchat_rag_sources_limit_callback() {
8821 // Load from mxchat_options array
8822 $options = get_option('mxchat_options', []);
8823
8824 // Get value from options array with default of 3
8825 $rag_sources_limit = isset($options['rag_sources_limit']) ? intval($options['rag_sources_limit']) : 3;
8826
8827 echo '<div class="slider-container">';
8828 echo sprintf(
8829 '<input type="range"
8830 id="rag_sources_limit"
8831 name="rag_sources_limit"
8832 min="3"
8833 max="10"
8834 step="1"
8835 value="%s"
8836 class="range-slider" />',
8837 esc_attr($rag_sources_limit)
8838 );
8839 echo sprintf(
8840 '<span id="rag_sources_limit_value" class="range-value">%s</span>',
8841 esc_html($rag_sources_limit)
8842 );
8843 echo '</div>';
8844 }
8845
8846 public function mxchat_rag_chunks_limit_callback() {
8847 // Load from mxchat_options array
8848 $options = get_option('mxchat_options', []);
8849
8850 // Get value from options array with default of 15
8851 $rag_chunks_limit = isset($options['rag_chunks_limit']) ? intval($options['rag_chunks_limit']) : 15;
8852
8853 echo '<div class="slider-container">';
8854 echo sprintf(
8855 '<input type="range"
8856 id="rag_chunks_limit"
8857 name="rag_chunks_limit"
8858 min="8"
8859 max="20"
8860 step="1"
8861 value="%s"
8862 class="range-slider" />',
8863 esc_attr($rag_chunks_limit)
8864 );
8865 echo sprintf(
8866 '<span id="rag_chunks_limit_value" class="range-value">%s</span>',
8867 esc_html($rag_chunks_limit)
8868 );
8869 echo '</div>';
8870 }
8871
8872 /**
8873 * Add body class on the Onboarding wizard page so CSS-only chrome surgery
8874 * (collapsing the WP admin sidebar) only applies on this page.
8875 * Plan: plan-mxchat-20260527-905439.
8876 */
8877 public function mxchat_add_onboarding_body_class($classes) {
8878 if (isset($_GET['page']) && $_GET['page'] === 'mxchat-onboarding') {
8879 $classes .= ' mxchat-onboarding-focused';
8880 }
8881 return $classes;
8882 }
8883
8884 public function mxchat_enqueue_admin_assets() {
8885 // Get plugin version
8886 $version = MXCHAT_VERSION;
8887
8888 // Use file modification time for development (remove in production)
8889 if (defined('WP_DEBUG') && WP_DEBUG) {
8890 $version = filemtime(plugin_dir_path(__FILE__) . '../mxchat-basic.php');
8891 }
8892
8893 $current_page = isset($_GET['page']) ? $_GET['page'] : '';
8894 $plugin_url = plugin_dir_url(__FILE__) . '../';
8895
8896 // Only load on MxChat pages
8897 if (strpos($current_page, 'mxchat') === false) {
8898 return;
8899 }
8900
8901 // Always load these on all MxChat pages
8902 $this->enqueue_core_admin_assets($plugin_url, $version);
8903 $this->enqueue_page_specific_assets($current_page, $plugin_url, $version);
8904 $this->localize_admin_scripts($current_page);
8905 }
8906 private function enqueue_core_admin_assets($plugin_url, $version) {
8907 // Core admin styles
8908 wp_enqueue_style('mxchat-admin-css', $plugin_url . 'css/admin-style.css', array(), $version);
8909 wp_enqueue_style('mxchat-knowledge-css', $plugin_url . 'css/knowledge-style.css', array(), $version);
8910
8911 // New sidebar navigation styles (for main settings page)
8912 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8913
8914 // Core admin scripts
8915 wp_enqueue_script('mxchat-admin-js', $plugin_url . 'js/mxchat-admin.js', array('jquery'), $version, true);
8916 }
8917 private function enqueue_page_specific_assets($current_page, $plugin_url, $version) {
8918 switch ($current_page) {
8919 case 'mxchat-prompts':
8920 // Knowledge processing page assets
8921 wp_enqueue_style('mxchat-content-selector-css', $plugin_url . 'css/content-selector.css', array(), $version);
8922 wp_enqueue_script('mxchat-content-selector-js', $plugin_url . 'js/content-selector.js', array('jquery'), $version, true);
8923 // Add the knowledge processing script (common script needed for WordPress dismiss functionality)
8924 wp_enqueue_script('mxchat-knowledge-processing', $plugin_url . 'js/knowledge-processing.js', array('jquery', 'common'), $version, true);
8925 // Per-entry "View indexed content" inspector (plan-d8cb4b) reuses the
8926 // Testing tab's match-card / chunk-detail components, which are scoped
8927 // under .mxch-testing-results. Load that stylesheet here so the inspector
8928 // modal renders identically to the Testing tab.
8929 wp_enqueue_style('mxchat-admin-testing-css', $plugin_url . 'css/admin-testing-tab.css', array('mxchat-admin-sidebar-css'), $version);
8930 break;
8931
8932 case 'mxchat-transcripts':
8933 // Load admin sidebar CSS first (shared styles for sidebar navigation)
8934 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8935 // Load transcripts-specific styles
8936 wp_enqueue_style('mxchat-chat-transcripts-css', $plugin_url . 'css/chat-transcripts.css', array('mxchat-admin-sidebar-css'), $version);
8937 wp_enqueue_script('mxchat-transcripts-js', $plugin_url . 'js/mxchat_transcripts.js', array('jquery'), $version, true);
8938 break;
8939
8940 case 'mxchat-actions':
8941 // Load admin sidebar CSS first (shared styles for sidebar navigation)
8942 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8943 // Load actions-specific styles
8944 wp_enqueue_style('mxchat-actions-css', $plugin_url . 'css/actions.css', array('mxchat-admin-sidebar-css'), $version);
8945 wp_enqueue_script('mxchat-actions-js', $plugin_url . 'js/mxchat_actions.js', array('jquery'), $version, true);
8946
8947 // Localize script data for actions page
8948 $is_activated = get_option('mxchat_pro_license_status') === 'active';
8949 wp_localize_script('mxchat-actions-js', 'mxchActionsData', array(
8950 'ajaxUrl' => admin_url('admin-ajax.php'),
8951 'nonce' => wp_create_nonce('mxchat_actions_nonce'),
8952 'addNonce' => wp_create_nonce('mxchat_add_intent_nonce'),
8953 'editNonce' => wp_create_nonce('mxchat_edit_intent'),
8954 'deleteNonce' => wp_create_nonce('mxchat_delete_intent_nonce'),
8955 'toggleNonce' => wp_create_nonce('mxchat_actions_nonce'),
8956 'addPhraseNonce' => wp_create_nonce('mxchat_add_phrase_nonce'),
8957 'deletePhraseNonce' => wp_create_nonce('mxchat_delete_phrase_nonce'),
8958 'getPhrasesNonce' => wp_create_nonce('mxchat_get_phrases_nonce'),
8959 'deleteLegacyNonce' => wp_create_nonce('mxchat_delete_legacy_nonce'),
8960 'isActivated' => $is_activated,
8961 'i18n' => array(
8962 'confirmDelete' => __('Are you sure you want to delete this trigger phrase?', 'mxchat'),
8963 'confirmBulkDelete' => __('Are you sure you want to delete the selected trigger phrases?', 'mxchat'),
8964 'saving' => __('Saving...', 'mxchat'),
8965 'saved' => __('Saved successfully!', 'mxchat'),
8966 'error' => __('An error occurred. Please try again.', 'mxchat'),
8967 'proRequired' => __('This feature requires MxChat Pro.', 'mxchat'),
8968 'addonRequired' => __('This feature requires an add-on.', 'mxchat')
8969 )
8970 ));
8971 break;
8972
8973 case 'mxchat-activation':
8974 // Load admin sidebar CSS first (shared styles for sidebar navigation)
8975 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8976 // Load pro page-specific styles
8977 wp_enqueue_style('mxchat-pro-css', $plugin_url . 'css/admin-pro.css', array('mxchat-admin-sidebar-css'), $version);
8978 // Load pro page JavaScript
8979 wp_enqueue_script('mxchat-pro-js', $plugin_url . 'js/mxchat_pro.js', array('jquery'), $version, true);
8980 // Load activation script for license activation/deactivation
8981 wp_enqueue_script('mxchat-activation-js', $plugin_url . 'js/activation-script.js', array('jquery'), $version, true);
8982 break;
8983
8984 case 'mxchat-content':
8985 // Load admin sidebar CSS (shared styles for sidebar navigation)
8986 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8987 // Load content page-specific styles
8988 wp_enqueue_style('mxchat-content-css', $plugin_url . 'css/admin-content.css', array('mxchat-admin-sidebar-css'), $version);
8989 // Load content page JavaScript
8990 wp_enqueue_script('mxchat-content-js', $plugin_url . 'js/mxchat-content.js', array('jquery'), $version, true);
8991 break;
8992
8993 case 'mxchat-api-access':
8994 // Shared sidebar shell (CSS + JS for tab switching / mobile menu / copy buttons).
8995 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8996 wp_enqueue_script('mxchat-admin-sidebar-js', $plugin_url . 'js/admin-sidebar.js', array(), $version, true);
8997 wp_localize_script('mxchat-admin-sidebar-js', 'MxChatAdminSidebarI18n', array(
8998 'copied' => __('Copied', 'mxchat'),
8999 ));
9000 break;
9001
9002 case 'mxchat-max':
9003 case 'mxchat-onboarding':
9004 // Onboarding page — uses the shared admin shell PLUS the wizard
9005 // overlay (plan-905439). admin-onboarding-wizard.css scopes the
9006 // WP-chrome surgery to body.mxchat-onboarding-focused so it only
9007 // applies on THIS page. The body class is added below via the
9008 // admin_body_class filter.
9009 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
9010 wp_enqueue_script('mxchat-admin-sidebar-js', $plugin_url . 'js/admin-sidebar.js', array(), $version, true);
9011 wp_localize_script('mxchat-admin-sidebar-js', 'MxChatAdminSidebarI18n', array(
9012 'copied' => __('Copied', 'mxchat'),
9013 ));
9014 // admin-style.css provides the .mxchat-instructions-modal-* classes
9015 // the new Behavior step's "View Sample Instructions" modal needs.
9016 // Enqueued AFTER admin-sidebar.css but BEFORE the wizard overlay
9017 // so the wizard's own rules win where they collide. plan-a2e4d6.
9018 wp_enqueue_style('mxchat-admin-style-css', $plugin_url . 'css/admin-style.css', array('mxchat-admin-sidebar-css'), $version);
9019 wp_enqueue_style('mxchat-admin-onboarding-wizard-css', $plugin_url . 'css/admin-onboarding-wizard.css', array('mxchat-admin-sidebar-css', 'mxchat-admin-style-css'), $version);
9020 wp_enqueue_script('mxchat-admin-onboarding-wizard-js', $plugin_url . 'js/admin-onboarding-wizard.js', array(), $version, true);
9021 break;
9022 default:
9023 wp_enqueue_script(
9024 'mxchat-test-streaming-js',
9025 $plugin_url . 'js/mxchat-test-streaming.js',
9026 ['jquery'],
9027 $version,
9028 true
9029 );
9030
9031 wp_localize_script('mxchat-test-streaming-js', 'mxchatTestStreamingAjax', [
9032 'ajax_url' => admin_url('admin-ajax.php'),
9033 'nonce' => wp_create_nonce('mxchat_test_streaming_nonce'),
9034 'settings_nonce' => wp_create_nonce('mxchat_save_setting_nonce')
9035 ]);
9036
9037 // Testing Tab: Load chatbot assets for the embedded testing chatbot
9038 wp_enqueue_style('mxchat-chat-css', $plugin_url . 'css/chat-style.css', array(), $version);
9039 wp_enqueue_style('mxchat-admin-testing-css', $plugin_url . 'css/admin-testing-tab.css', array('mxchat-admin-sidebar-css', 'mxchat-chat-css'), $version);
9040
9041 wp_enqueue_script('mxchat-chat-js', $plugin_url . 'js/chat-script.js', array('jquery'), $version, true);
9042 wp_enqueue_script('mxchat-admin-testing-js', $plugin_url . 'js/admin-testing-tab.js', array('jquery', 'mxchat-chat-js'), $version, true);
9043
9044 // Allow add-ons to enqueue their public CSS/JS for the testing chatbot
9045 do_action('mxchat_enqueue_testing_tab_assets');
9046
9047 // Localize mxchatChat for the chatbot JS (same settings as frontend)
9048 $options = get_option('mxchat_options', array());
9049 $prompts_options = get_option('mxchat_prompts_options', array());
9050 $theme_options = get_option('mxchat_theme_options', array());
9051 $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
9052 $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
9053 $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
9054
9055 wp_localize_script('mxchat-chat-js', 'mxchatChat', array(
9056 'ajax_url' => admin_url('admin-ajax.php'),
9057 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
9058 'model' => isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest',
9059 'enable_streaming_toggle' => isset($options['enable_streaming_toggle']) ? $options['enable_streaming_toggle'] : 'on',
9060 'contextual_awareness_toggle' => isset($options['contextual_awareness_toggle']) ? $options['contextual_awareness_toggle'] : 'off',
9061 'link_target_toggle' => $options['link_target_toggle'] ?? 'off',
9062 'rate_limit_message' => $options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
9063 'complianz_toggle' => isset($options['complianz_toggle']) && $options['complianz_toggle'] === 'on',
9064 'user_message_bg_color' => $options['user_message_bg_color'] ?? '#fff',
9065 'user_message_font_color' => $options['user_message_font_color'] ?? '#212121',
9066 'bot_message_bg_color' => $options['bot_message_bg_color'] ?? '#212121',
9067 'bot_message_font_color' => $options['bot_message_font_color'] ?? '#fff',
9068 'top_bar_bg_color' => $options['top_bar_bg_color'] ?? '#212121',
9069 'send_button_font_color' => $options['send_button_font_color'] ?? '#212121',
9070 'close_button_color' => $options['close_button_color'] ?? '#fff',
9071 'chatbot_background_color' => $options['chatbot_background_color'] ?? '#212121',
9072 'chatbot_bg_color' => $options['chatbot_bg_color'] ?? '#fff',
9073 'icon_color' => $options['icon_color'] ?? '#fff',
9074 'chat_input_font_color' => $options['chat_input_font_color'] ?? '#212121',
9075 'chat_persistence_toggle' => 'off', // Always off for testing chatbot
9076 'appendWidgetToBody' => 'off',
9077 'live_agent_message_bg_color' => $options['live_agent_message_bg_color'] ?? '#ffffff',
9078 'live_agent_message_font_color' => $options['live_agent_message_font_color'] ?? '#333333',
9079 'chat_toolbar_toggle' => $options['chat_toolbar_toggle'] ?? 'off',
9080 'mode_indicator_bg_color' => $options['mode_indicator_bg_color'] ?? '#767676',
9081 'mode_indicator_font_color' => $options['mode_indicator_font_color'] ?? '#ffffff',
9082 'toolbar_icon_color' => $options['toolbar_icon_color'] ?? '#212121',
9083 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
9084 'email_collection_enabled' => 'off', // No email collection in testing
9085 'initial_email_state' => null,
9086 'skip_email_check' => true,
9087 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
9088 'skip_inline_colors' => $skip_inline_colors,
9089 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array()
9090 ));
9091
9092 // Localize testing tab script data
9093 wp_localize_script('mxchat-admin-testing-js', 'mxchatAdminTestData', array(
9094 'ajaxUrl' => admin_url('admin-ajax.php'),
9095 'nonce' => wp_create_nonce('mxchat_test_nonce'),
9096 'isAdmin' => true,
9097 'testingEnabled' => true
9098 ));
9099
9100 // Add testing enabled flag for the chatbot to return debug data
9101 add_action('admin_footer', function() {
9102 echo '<script>window.mxchatTestingEnabled = true;</script>';
9103 });
9104
9105 break;
9106 }
9107 }
9108 private function localize_admin_scripts($current_page) {
9109 // Base localization data for main admin script
9110 $base_data = array(
9111 'ajax_url' => admin_url('admin-ajax.php'),
9112 'nonce' => wp_create_nonce('mxchat_admin_nonce'),
9113 'admin_url' => admin_url(),
9114 'license_nonce' => wp_create_nonce('mxchat_activate_license_nonce'),
9115 'inline_edit_nonce' => wp_create_nonce('mxchat_save_inline_nonce'),
9116 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
9117 'export_nonce' => wp_create_nonce('mxchat_export_transcripts'),
9118 'actions_nonce' => wp_create_nonce('mxchat_actions_nonce'),
9119 'add_intent_nonce' => wp_create_nonce('mxchat_add_intent_nonce'),
9120 'edit_intent_nonce' => wp_create_nonce('mxchat_edit_intent'),
9121 'add_phrase_nonce' => wp_create_nonce('mxchat_add_phrase_nonce'),
9122 'delete_phrase_nonce' => wp_create_nonce('mxchat_delete_phrase_nonce'),
9123 'get_phrases_nonce' => wp_create_nonce('mxchat_get_phrases_nonce'),
9124 'delete_legacy_nonce' => wp_create_nonce('mxchat_delete_legacy_nonce'),
9125 'toggle_action_nonce' => wp_create_nonce('mxchat_actions_nonce'),
9126 'fetch_openrouter_models_nonce' => wp_create_nonce('mxchat_fetch_openrouter_models'),
9127 'is_activated' => $this->is_activated ? '1' : '0',
9128 'status_refresh_interval' => 5000,
9129 'prompts_setting_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
9130 'ajaxurl' => admin_url('admin-ajax.php')
9131 );
9132
9133 // Localize main admin script with base data
9134 wp_localize_script('mxchat-admin-js', 'mxchatAdmin', $base_data);
9135
9136 // Canonical chat-model catalog for the modal picker grid
9137 // (plan-d14e89). Adding a model in class-mxchat-model-catalog.php
9138 // automatically appears here.
9139 if (!class_exists('MxChat_Model_Catalog')) {
9140 require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
9141 }
9142 wp_localize_script('mxchat-admin-js', 'mxchatChatModelCatalog', MxChat_Model_Catalog::js_picker_shape());
9143
9144 // Page-specific localizations
9145 $this->localize_page_specific_scripts($current_page);
9146 }
9147 private function localize_page_specific_scripts($current_page) {
9148 switch ($current_page) {
9149 case 'mxchat-prompts':
9150 // Status updater localization
9151 wp_localize_script('mxchat-status-updater', 'mxchat_status_data', array(
9152 'ajax_url' => admin_url('admin-ajax.php'),
9153 'nonce' => wp_create_nonce('mxchat_status_nonce')
9154 ));
9155
9156 // Content selector localization
9157 $mxchat_options_for_selector = get_option('mxchat_options', array());
9158 $acf_pdf_extract_default = !empty($mxchat_options_for_selector['acf_pdf_extract_default']);
9159 wp_localize_script('mxchat-content-selector-js', 'mxchatSelector', array(
9160 'ajaxurl' => admin_url('admin-ajax.php'),
9161 'nonce' => wp_create_nonce('mxchat_content_selector_nonce'),
9162 'acfPdfExtractDefault' => $acf_pdf_extract_default ? 1 : 0,
9163 'i18n' => array(
9164 'searchPlaceholder' => __('Search posts and pages...', 'mxchat'),
9165 'selectAll' => __('Select All', 'mxchat'),
9166 'process' => __('Process Selected', 'mxchat'),
9167 'cancel' => __('Cancel', 'mxchat'),
9168 'noResults' => __('No content found.', 'mxchat'),
9169 'extractingPdfs' => __('extracting %d PDF(s)...', 'mxchat'),
9170 'pdfExtractedSuffix' => __(' (%d PDF(s) extracted)', 'mxchat')
9171 )
9172 ));
9173
9174 wp_localize_script('mxchat-knowledge-processing', 'mxchatAdmin', array(
9175 'ajax_url' => admin_url('admin-ajax.php'),
9176 'status_nonce' => wp_create_nonce('mxchat_status_nonce'),
9177 'queue_nonce' => wp_create_nonce('mxchat_queue_nonce'),
9178 'stop_nonce' => wp_create_nonce('mxchat_stop_processing_action'),
9179 'settings_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
9180 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
9181 'entries_nonce' => wp_create_nonce('mxchat_entries_nonce'),
9182 'admin_url' => admin_url(),
9183 'ajaxurl' => admin_url('admin-ajax.php'),
9184 'status_refresh_interval' => 2000,
9185 'bot_id' => isset($_GET['bot_id']) ? sanitize_text_field($_GET['bot_id']) : 'default'
9186 ));
9187 break;
9188
9189 case 'mxchat-activation':
9190 wp_localize_script('mxchat-activation-js', 'mxchatAdmin', array(
9191 'ajax_url' => admin_url('admin-ajax.php'),
9192 'license_nonce' => wp_create_nonce('mxchat_activate_license_nonce')
9193 ));
9194 break;
9195
9196 case 'mxchat-content':
9197 wp_localize_script('mxchat-content-js', 'mxchatContent', array(
9198 'ajaxUrl' => admin_url('admin-ajax.php'),
9199 'nonce' => wp_create_nonce('mxchat_content_nonce'),
9200 'settingNonce' => wp_create_nonce('mxchat_save_setting_nonce'),
9201 'previewUrl' => home_url('/?p='),
9202 'isActivated' => $this->is_activated(),
9203 'hasAdvancedContent' => apply_filters('mxchat_content_pro_feature', false, 'seo_readability'),
9204 'hasGSC' => apply_filters('mxchat_content_pro_feature', false, 'gsc_integration'),
9205 'seoOptimize' => array(
9206 'meta_description' => ($options['seo_optimize_meta_desc'] ?? 'on') === 'on',
9207 'seo_title' => ($options['seo_optimize_seo_title'] ?? 'on') === 'on',
9208 'slug' => ($options['seo_optimize_slug'] ?? 'on') === 'on',
9209 'readability' => ($options['seo_optimize_readability'] ?? 'on') === 'on',
9210 'internal_links' => ($options['seo_optimize_internal_links'] ?? 'on') === 'on',
9211 'img_alt' => ($options['seo_optimize_img_alt'] ?? 'on') === 'on',
9212 'featured_img' => ($options['seo_optimize_featured_img'] ?? 'on') === 'on',
9213 ),
9214 'i18n' => array(
9215 'generating' => __('Generating...', 'mxchat'),
9216 'planning' => __('Planning content structure...', 'mxchat'),
9217 'images' => __('Generating images...', 'mxchat'),
9218 'writing' => __('Writing full content...', 'mxchat'),
9219 'creating' => __('Creating WordPress post...', 'mxchat'),
9220 'done' => __('Content generated successfully!', 'mxchat'),
9221 'error' => __('An error occurred. Please try again.', 'mxchat'),
9222 'editSuccess' => __('Content updated successfully.', 'mxchat'),
9223 'promptEmpty' => __('Please enter a prompt.', 'mxchat'),
9224 )
9225 ));
9226 break;
9227
9228 case 'mxchat-transcripts':
9229 // Get chart data for localization
9230 $chart_data = $this->get_transcripts_chart_data();
9231
9232 wp_localize_script('mxchat-transcripts-js', 'mxchatAdmin', array(
9233 'ajax_url' => admin_url('admin-ajax.php'),
9234 'export_nonce' => wp_create_nonce('mxchat_export_transcripts'),
9235 'delete_nonce' => wp_create_nonce('mxchat_delete_chat_history'),
9236 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
9237 'translate_nonce' => wp_create_nonce('mxchat_translate_messages')
9238 ));
9239
9240 // Localize chart data separately - use array_values to ensure proper JSON array encoding
9241 wp_localize_script('mxchat-transcripts-js', 'mxchatChartData', array(
9242 'labels' => array_values($chart_data['labels']),
9243 'chats' => array_values($chart_data['chats']),
9244 'messages' => array_values($chart_data['messages'])
9245 ));
9246 break;
9247
9248 case 'mxchat-settings':
9249 default:
9250 // Color picker and settings localization
9251 wp_localize_script('mxchat-color-picker', 'mxchatStyleSettings', array(
9252 'ajax_url' => admin_url('admin-ajax.php'),
9253 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
9254 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
9255 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
9256 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
9257 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
9258 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
9259 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
9260 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
9261 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
9262 'icon_color' => $this->options['icon_color'] ?? '#fff',
9263 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
9264 'pre_chat_message' => $this->options['pre_chat_message'] ?? esc_html__('Hey there! Ask me anything!', 'mxchat'),
9265 'rate_limit_message' => $this->options['rate_limit_message'] ?? esc_html__('Rate limit exceeded. Please try again later.', 'mxchat'),
9266 'loops_api_key' => $this->options['loops_api_key'] ?? '',
9267 'loops_mailing_list' => $this->options['loops_mailing_list'] ?? '',
9268 'triggered_phrase_response' => $this->options['triggered_phrase_response'] ?? esc_html__('Would you like to join our mailing list? Please provide your email below.', 'mxchat'),
9269 'email_capture_response' => $this->options['email_capture_response'] ?? esc_html__('Thank you for providing your email! You\'ve been added to our list.', 'mxchat'),
9270 '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'),
9271 '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'),
9272 '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'),
9273 'pdf_max_pages' => $this->options['pdf_max_pages'] ?? 69,
9274 'live_agent_webhook_url' => $this->options['live_agent_webhook_url'] ?? '',
9275 'live_agent_secret_key' => $this->options['live_agent_secret_key'] ?? '',
9276 'live_agent_bot_token' => $this->options['live_agent_bot_token'] ?? '',
9277 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
9278 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
9279 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
9280 'show_pdf_upload_button' => $this->options['show_pdf_upload_button'] ?? 'on',
9281 'show_word_upload_button' => $this->options['show_word_upload_button'] ?? 'on',
9282 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
9283 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
9284 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
9285 ));
9286 break;
9287 }
9288
9289 // Additional localization that was in the original code
9290 wp_localize_script('mxchat-admin-js', 'mxchatPromptsAdmin', array(
9291 'ajax_url' => admin_url('admin-ajax.php'),
9292 'prompts_setting_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
9293 ));
9294 }
9295
9296 public function mxchat_sanitize($input) {
9297 $new_input = array();
9298
9299 if (isset($input['api_key'])) {
9300 $new_input['api_key'] = sanitize_text_field($input['api_key']);
9301 }
9302
9303 if (isset($input['similarity_threshold'])) {
9304 $new_input['similarity_threshold'] = absint($input['similarity_threshold']); // Ensure it's an integer
9305 $new_input['similarity_threshold'] = min(max($new_input['similarity_threshold'], 20), 85); // Enforce range
9306 }
9307
9308 if (isset($input['rag_sources_limit'])) {
9309 $new_input['rag_sources_limit'] = absint($input['rag_sources_limit']); // Ensure it's an integer
9310 $new_input['rag_sources_limit'] = min(max($new_input['rag_sources_limit'], 3), 10); // Enforce range 3-10
9311 }
9312
9313 if (isset($input['rag_chunks_limit'])) {
9314 $new_input['rag_chunks_limit'] = absint($input['rag_chunks_limit']); // Ensure it's an integer
9315 $new_input['rag_chunks_limit'] = min(max($new_input['rag_chunks_limit'], 8), 20); // Enforce range 8-20
9316 }
9317
9318 if (isset($input['xai_api_key'])) {
9319 $new_input['xai_api_key'] = sanitize_text_field($input['xai_api_key']);
9320 }
9321
9322 if (isset($input['claude_api_key'])) {
9323 $new_input['claude_api_key'] = sanitize_text_field($input['claude_api_key']);
9324 }
9325
9326 if (isset($input['enable_streaming_toggle'])) {
9327 $new_input['enable_streaming_toggle'] = ($input['enable_streaming_toggle'] === 'on') ? 'on' : 'off';
9328 } else {
9329 // If checkbox not checked, it won't be in $input, so set to 'off'
9330 $new_input['enable_streaming_toggle'] = 'off';
9331 }
9332
9333 if (isset($input['enable_web_search'])) {
9334 $new_input['enable_web_search'] = ($input['enable_web_search'] === 'on') ? 'on' : 'off';
9335 } else {
9336 // If checkbox not checked, it won't be in $input, so set to 'off'
9337 $new_input['enable_web_search'] = 'off';
9338 }
9339
9340 if (isset($input['deepseek_api_key'])) {
9341 $new_input['deepseek_api_key'] = sanitize_text_field($input['deepseek_api_key']);
9342 }
9343
9344 if (isset($input['gemini_api_key'])) {
9345 $new_input['gemini_api_key'] = sanitize_text_field($input['gemini_api_key']);
9346 }
9347
9348 if (isset($input['enable_woocommerce_integration'])) {
9349 $new_input['enable_woocommerce_integration'] = $input['enable_woocommerce_integration'] === 'on' ? 'on' : 'off';
9350 }
9351
9352 if (isset($input['privacy_toggle'])) {
9353 $new_input['privacy_toggle'] = $input['privacy_toggle'];
9354 }
9355
9356 if (isset($input['complianz_toggle'])) {
9357 $new_input['complianz_toggle'] = $input['complianz_toggle'];
9358 }
9359
9360 // Handle custom privacy text input
9361 if (isset($input['privacy_text'])) {
9362 // Allow basic HTML for links
9363 $new_input['privacy_text'] = wp_kses_post($input['privacy_text']);
9364 }
9365
9366 if (isset($input['system_prompt_instructions'])) {
9367 $new_input['system_prompt_instructions'] = sanitize_textarea_field($input['system_prompt_instructions']);
9368 }
9369
9370 if (isset($input['mxchat_pro_email'])) {
9371 $new_input['mxchat_pro_email'] = sanitize_email($input['mxchat_pro_email']);
9372 }
9373
9374 if (isset($input['mxchat_activation_key'])) {
9375 $new_input['mxchat_activation_key'] = sanitize_text_field($input['mxchat_activation_key']);
9376 }
9377
9378 if (isset($input['append_to_body'])) {
9379 $new_input['append_to_body'] = $input['append_to_body'] === 'on' ? 'on' : 'off';
9380 }
9381
9382 // Post type visibility settings
9383 if (isset($input['post_type_visibility_mode'])) {
9384 $allowed_modes = array('all', 'include', 'exclude');
9385 $new_input['post_type_visibility_mode'] = in_array($input['post_type_visibility_mode'], $allowed_modes)
9386 ? $input['post_type_visibility_mode']
9387 : 'all';
9388 }
9389
9390 if (isset($input['post_type_visibility_list'])) {
9391 if (is_array($input['post_type_visibility_list'])) {
9392 $new_input['post_type_visibility_list'] = array_map('sanitize_key', $input['post_type_visibility_list']);
9393 } else {
9394 $new_input['post_type_visibility_list'] = array();
9395 }
9396 }
9397
9398 if (isset($input['contextual_awareness_toggle'])) {
9399 $new_input['contextual_awareness_toggle'] = $input['contextual_awareness_toggle'] === 'on' ? 'on' : 'off';
9400 }
9401
9402 if (isset($input['citation_links_toggle'])) {
9403 $new_input['citation_links_toggle'] = $input['citation_links_toggle'] === 'on' ? 'on' : 'off';
9404 }
9405
9406 // Satisfaction rating prompt — defaults ON when unchecked (so first save
9407 // doesn't accidentally disable it). The form posts 'on' when checked;
9408 // unchecked checkboxes don't post a value at all, so we infer 'off' only
9409 // when the autosave/PHP request explicitly clears it via empty string.
9410 if (array_key_exists('satisfaction_rating_enabled', $input)) {
9411 $new_input['satisfaction_rating_enabled'] = $input['satisfaction_rating_enabled'] === 'on' ? 'on' : 'off';
9412 }
9413
9414 // Satisfaction rating customization (plan-141a12). Idle is clamped 5-600;
9415 // the 4 text strings are sanitized + capped server-side. Blank values are
9416 // preserved so the integrator falls back to translated defaults.
9417 if (array_key_exists('satisfaction_rating_idle_seconds', $input)) {
9418 $new_input['satisfaction_rating_idle_seconds'] = max(5, min(600, intval($input['satisfaction_rating_idle_seconds'])));
9419 }
9420
9421 // Max input length in characters (plan a3fae2 part C). 0 = unlimited (default,
9422 // preserves current behavior). Clamp to a sane ceiling so a typo can't lock out
9423 // all input. REQUIRED here — mxchat_sanitize() rebuilds the array from a whitelist,
9424 // so without this isset-handler the key is stripped on the next save of ANY field
9425 // (the 2c02ea global-rate-limit footgun).
9426 if (array_key_exists('max_input_length', $input)) {
9427 $new_input['max_input_length'] = max(0, min(100000, intval($input['max_input_length'])));
9428 }
9429 if (array_key_exists('satisfaction_rating_question', $input)) {
9430 $new_input['satisfaction_rating_question'] = mb_substr(sanitize_text_field($input['satisfaction_rating_question']), 0, 200);
9431 }
9432 if (array_key_exists('satisfaction_rating_thanks', $input)) {
9433 $new_input['satisfaction_rating_thanks'] = mb_substr(sanitize_text_field($input['satisfaction_rating_thanks']), 0, 300);
9434 }
9435 if (array_key_exists('satisfaction_rating_placeholder', $input)) {
9436 $new_input['satisfaction_rating_placeholder'] = mb_substr(sanitize_text_field($input['satisfaction_rating_placeholder']), 0, 200);
9437 }
9438 if (array_key_exists('satisfaction_rating_saved', $input)) {
9439 $new_input['satisfaction_rating_saved'] = mb_substr(sanitize_text_field($input['satisfaction_rating_saved']), 0, 200);
9440 }
9441
9442 if (isset($input['top_bar_title'])) {
9443 $new_input['top_bar_title'] = sanitize_text_field($input['top_bar_title']);
9444 }
9445
9446 if (isset($input['ai_agent_text'])) {
9447 $new_input['ai_agent_text'] = sanitize_text_field($input['ai_agent_text']);
9448 }
9449
9450 if (isset($input['enable_email_block'])) {
9451 $new_input['enable_email_block'] = sanitize_text_field($input['enable_email_block']);
9452 }
9453
9454 if (isset($input['email_blocker_header_content'])) {
9455 // wp_kses_post() allows standard HTML tags permitted by WordPress
9456 $new_input['email_blocker_header_content'] = wp_kses_post($input['email_blocker_header_content']);
9457 }
9458 if (isset($input['email_blocker_button_text'])) {
9459 $new_input['email_blocker_button_text'] = sanitize_text_field($input['email_blocker_button_text']);
9460 }
9461 // Sanitize name field toggle
9462 if (isset($input['enable_name_field'])) {
9463 $new_input['enable_name_field'] = ($input['enable_name_field'] === 'on') ? 'on' : 'off';
9464 } else {
9465 $new_input['enable_name_field'] = 'off';
9466 }
9467 // Sanitize name field placeholder
9468 if (isset($input['name_field_placeholder'])) {
9469 $new_input['name_field_placeholder'] = sanitize_text_field($input['name_field_placeholder']);
9470 }
9471 if (isset($input['intro_message'])) {
9472 $new_input['intro_message'] = wp_kses_post($input['intro_message']); // Use wp_kses_post instead
9473 }
9474
9475 if (isset($input['input_copy'])) {
9476 $new_input['input_copy'] = sanitize_text_field($input['input_copy']);
9477 }
9478
9479 if (isset($input['rate_limit_message'])) {
9480 $new_input['rate_limit_message'] = sanitize_text_field($input['rate_limit_message']);
9481 }
9482
9483 // Handle the new rate limits format
9484 if (isset($input['rate_limits']) && is_array($input['rate_limits'])) {
9485 $new_input['rate_limits'] = array();
9486 $allowed_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited');
9487 $allowed_timeframes = array('hourly', 'daily', 'weekly', 'monthly');
9488
9489 foreach ($input['rate_limits'] as $role_id => $settings) {
9490 $new_input['rate_limits'][$role_id] = array();
9491
9492 // Sanitize limit
9493 if (isset($settings['limit'])) {
9494 $limit = sanitize_text_field($settings['limit']);
9495 // Accept presets, 'unlimited', the '__custom__' sentinel, OR any positive
9496 // integer (custom value) — mirrors the global branch (plan-2c02ea). Without
9497 // the custom path the per-role custom input was dropped and reset to the role
9498 // default on every save (plan-7e23e7).
9499 if (in_array($limit, $allowed_limits, true) || $limit === '__custom__' || (ctype_digit($limit) && (int) $limit >= 1)) {
9500 $new_input['rate_limits'][$role_id]['limit'] = $limit;
9501 } else {
9502 $new_input['rate_limits'][$role_id]['limit'] = ($role_id === 'logged_out') ? '10' : '100'; // Default
9503 }
9504 }
9505
9506 // Preserve the per-role custom value (mirrors the global branch's limit_custom).
9507 if (isset($settings['limit_custom'])) {
9508 $new_input['rate_limits'][$role_id]['limit_custom'] = preg_replace('/[^0-9]/', '', (string) $settings['limit_custom']);
9509 }
9510
9511 // Sanitize timeframe
9512 if (isset($settings['timeframe'])) {
9513 $timeframe = sanitize_text_field($settings['timeframe']);
9514 if (in_array($timeframe, $allowed_timeframes, true)) {
9515 $new_input['rate_limits'][$role_id]['timeframe'] = $timeframe;
9516 } else {
9517 $new_input['rate_limits'][$role_id]['timeframe'] = 'daily'; // Default
9518 }
9519 }
9520
9521 // Sanitize message
9522 if (isset($settings['message'])) {
9523 $new_input['rate_limits'][$role_id]['message'] = sanitize_textarea_field($settings['message']);
9524 }
9525 }
9526 }
9527
9528 // Handle the whole-chatbot global rate limit (plan-mxchat-20260603-2c02ea).
9529 // Mirrors the per-role block above, adapted to the single global shape. Without
9530 // this branch the whitelist-rebuild dropped rate_limits_global entirely on every
9531 // save, so the global cap silently fell back to its 'unlimited' default.
9532 // MUST accept arbitrary positive integers so the custom-value path (d55f65) is not regressed.
9533 if (isset($input['rate_limits_global']) && is_array($input['rate_limits_global'])) {
9534 $g = $input['rate_limits_global'];
9535 $allowed_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited');
9536 $allowed_timeframes = array('hourly', 'daily', 'weekly', 'monthly');
9537 $global_out = array();
9538
9539 if (isset($g['limit'])) {
9540 $limit = sanitize_text_field($g['limit']);
9541 // Accept presets, 'unlimited', the '__custom__' sentinel (resolved by the
9542 // autosave handler / renderer), OR any positive integer (custom value).
9543 if (in_array($limit, $allowed_limits, true) || $limit === '__custom__' || (ctype_digit($limit) && (int) $limit >= 1)) {
9544 $global_out['limit'] = $limit;
9545 } else {
9546 $global_out['limit'] = 'unlimited';
9547 }
9548 }
9549
9550 if (isset($g['limit_custom'])) {
9551 $global_out['limit_custom'] = preg_replace('/[^0-9]/', '', (string) $g['limit_custom']);
9552 }
9553
9554 if (isset($g['timeframe'])) {
9555 $timeframe = sanitize_text_field($g['timeframe']);
9556 $global_out['timeframe'] = in_array($timeframe, $allowed_timeframes, true) ? $timeframe : 'daily';
9557 }
9558
9559 if (isset($g['message'])) {
9560 $global_out['message'] = sanitize_textarea_field($g['message']);
9561 }
9562
9563 $new_input['rate_limits_global'] = $global_out;
9564 }
9565
9566 if (isset($input['pre_chat_message'])) {
9567 $new_input['pre_chat_message'] = sanitize_textarea_field($input['pre_chat_message']);
9568 }
9569
9570 if (isset($input['voyage_api_key'])) {
9571 $new_input['voyage_api_key'] = sanitize_text_field($input['voyage_api_key']);
9572 }
9573
9574 // Add to your sanitize function
9575 if (isset($input['embedding_model'])) {
9576 // Catalog refactor (plan-d14e89): allowlist derived from the canonical
9577 // catalog in includes/class-mxchat-model-catalog.php.
9578 if (!class_exists('MxChat_Model_Catalog')) {
9579 require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
9580 }
9581 $allowed_models = MxChat_Model_Catalog::embedding_model_ids();
9582 if (in_array($input['embedding_model'], $allowed_models)) {
9583 $new_input['embedding_model'] = sanitize_text_field($input['embedding_model']);
9584 }
9585 }
9586
9587 if (isset($input['model'])) {
9588 if ($input['model'] === 'openrouter') {
9589 $new_input['model'] = 'openrouter';
9590 } else {
9591 if (!class_exists('MxChat_Model_Catalog')) {
9592 require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
9593 }
9594 $allowed_models = MxChat_Model_Catalog::chat_model_ids();
9595
9596 if (in_array($input['model'], $allowed_models)) {
9597 $new_input['model'] = sanitize_text_field($input['model']);
9598 } else {
9599 // Fallback for any deprecated model
9600 $new_input['model'] = 'gpt-5.1-chat-latest';
9601 }
9602 }
9603 }
9604
9605 if (isset($input['openrouter_selected_model'])) {
9606 // Just sanitize it, don't validate against a whitelist
9607 $new_input['openrouter_selected_model'] = sanitize_text_field($input['openrouter_selected_model']);
9608 }
9609
9610 // ADD THIS:
9611 if (isset($input['openrouter_selected_model_name'])) {
9612 $new_input['openrouter_selected_model_name'] = sanitize_text_field($input['openrouter_selected_model_name']);
9613 }
9614
9615 if (isset($input['openrouter_api_key'])) {
9616 $new_input['openrouter_api_key'] = sanitize_text_field($input['openrouter_api_key']);
9617 }
9618
9619 // Custom (OpenAI-compatible) Provider — Ollama, LM Studio, vLLM, Azure OpenAI, etc.
9620 if (isset($input['custom_provider_base_url'])) {
9621 $new_input['custom_provider_base_url'] = esc_url_raw(rtrim(trim((string) $input['custom_provider_base_url']), '/'));
9622 }
9623 if (isset($input['custom_provider_api_key'])) {
9624 $new_input['custom_provider_api_key'] = sanitize_text_field($input['custom_provider_api_key']);
9625 }
9626 if (isset($input['custom_provider_model'])) {
9627 $new_input['custom_provider_model'] = sanitize_text_field($input['custom_provider_model']);
9628 }
9629 if (isset($input['custom_provider_auth_scheme'])) {
9630 $scheme = sanitize_text_field($input['custom_provider_auth_scheme']);
9631 $new_input['custom_provider_auth_scheme'] = in_array($scheme, array('bearer', 'api-key'), true) ? $scheme : 'bearer';
9632 }
9633 if (isset($input['custom_provider_for_embeddings'])) {
9634 $new_input['custom_provider_for_embeddings'] = ($input['custom_provider_for_embeddings'] === 'on') ? 'on' : 'off';
9635 }
9636 if (isset($input['custom_provider_for_images'])) {
9637 $new_input['custom_provider_for_images'] = ($input['custom_provider_for_images'] === 'on') ? 'on' : 'off';
9638 }
9639 if (isset($input['custom_provider_embedding_model'])) {
9640 $new_input['custom_provider_embedding_model'] = sanitize_text_field($input['custom_provider_embedding_model']);
9641 }
9642 if (isset($input['custom_provider_api_version'])) {
9643 $new_input['custom_provider_api_version'] = sanitize_text_field($input['custom_provider_api_version']);
9644 }
9645
9646
9647
9648 if (isset($input['woocommerce_consumer_key'])) {
9649 $new_input['woocommerce_consumer_key'] = sanitize_text_field($input['woocommerce_consumer_key']);
9650 }
9651
9652 if (isset($input['woocommerce_consumer_secret'])) {
9653 $new_input['woocommerce_consumer_secret'] = sanitize_text_field($input['woocommerce_consumer_secret']);
9654 }
9655
9656
9657 // Sanitize link_target_toggle
9658 if (isset($input['link_target_toggle'])) {
9659 $new_input['link_target_toggle'] = $input['link_target_toggle'] === 'on' ? 'on' : 'off';
9660 }
9661
9662 // Sanitize Loops API Key
9663 if (isset($input['loops_api_key'])) {
9664 $new_input['loops_api_key'] = sanitize_text_field($input['loops_api_key']);
9665 }
9666
9667 if (isset($input['chat_persistence_toggle'])) {
9668 $new_input['chat_persistence_toggle'] = $input['chat_persistence_toggle'] === 'on' ? 'on' : 'off';
9669 }
9670
9671 // No else clause: an absent key stays absent, so the front-end default ('on') applies
9672 // and the rebuild never strips a saved 'off' (autosave passes the full options array back through here).
9673 if (isset($input['print_button_enabled'])) {
9674 $new_input['print_button_enabled'] = $input['print_button_enabled'] === 'on' ? 'on' : 'off';
9675 }
9676
9677 // plan ac2e81 — "Start new chat" toggle (default OFF) + editable label.
9678 // No else on the toggle: an absent key stays absent so the front-end '?? off'
9679 // default applies, and the full-array autosave rebuild preserves a saved value.
9680 if (isset($input['reset_chat_enabled'])) {
9681 $new_input['reset_chat_enabled'] = $input['reset_chat_enabled'] === 'on' ? 'on' : 'off';
9682 }
9683
9684 if (isset($input['reset_chat_label'])) {
9685 $new_input['reset_chat_label'] = sanitize_text_field($input['reset_chat_label']);
9686 }
9687
9688 if (isset($input['popular_question_1'])) {
9689 $new_input['popular_question_1'] = sanitize_text_field($input['popular_question_1']);
9690 }
9691
9692 if (isset($input['popular_question_2'])) {
9693 $new_input['popular_question_2'] = sanitize_text_field($input['popular_question_2']);
9694 }
9695
9696 if (isset($input['popular_question_3'])) {
9697 $new_input['popular_question_3'] = sanitize_text_field($input['popular_question_3']);
9698 }
9699
9700 if (isset($input['additional_popular_questions']) && is_array($input['additional_popular_questions'])) {
9701 $new_input['additional_popular_questions'] = array_map('sanitize_text_field', $input['additional_popular_questions']);
9702 }
9703
9704 // Sanitize Loops Mailing List
9705 if (isset($input['loops_mailing_list'])) {
9706 $new_input['loops_mailing_list'] = sanitize_text_field($input['loops_mailing_list']);
9707 }
9708
9709 // Sanitize Triggered Phrase Response
9710 if (isset($input['triggered_phrase_response'])) {
9711 $new_input['triggered_phrase_response'] = wp_kses_post($input['triggered_phrase_response']);
9712 }
9713 if (isset($input['email_capture_response'])) {
9714 $new_input['email_capture_response'] = wp_kses_post($input['email_capture_response']);
9715 }
9716
9717 // Sanitize Brave Search Settings
9718 if (isset($input['brave_api_key'])) {
9719 $new_input['brave_api_key'] = sanitize_text_field($input['brave_api_key']);
9720 }
9721
9722 if (isset($input['brave_image_count'])) {
9723 $image_count = intval($input['brave_image_count']);
9724 $new_input['brave_image_count'] = ($image_count >=1 && $image_count <=6) ? $image_count : 4;
9725 }
9726
9727 if (isset($input['brave_safe_search'])) {
9728 $allowed = array('strict', 'off');
9729 $new_input['brave_safe_search'] = in_array($input['brave_safe_search'], $allowed, true) ? $input['brave_safe_search'] : 'strict';
9730 }
9731
9732 if (isset($input['brave_news_count'])) {
9733 $news_count = intval($input['brave_news_count']);
9734 $new_input['brave_news_count'] = ($news_count >=1 && $news_count <=10) ? $news_count : 3;
9735 }
9736
9737 if (isset($input['brave_country'])) {
9738 $new_input['brave_country'] = sanitize_text_field($input['brave_country']);
9739 }
9740
9741 if (isset($input['brave_language'])) {
9742 $new_input['brave_language'] = sanitize_text_field($input['brave_language']);
9743 }
9744
9745 if (isset($input['chat_toolbar_toggle'])) {
9746 $new_input['chat_toolbar_toggle'] = $input['chat_toolbar_toggle'] === 'on' ? 'on' : 'off';
9747 }
9748
9749 // Sanitize PDF upload button toggle
9750 if (isset($input['show_pdf_upload_button'])) {
9751 $new_input['show_pdf_upload_button'] = $input['show_pdf_upload_button'] === 'on' ? 'on' : 'off';
9752 } else {
9753 $new_input['show_pdf_upload_button'] = 'off'; // If checkbox is unchecked
9754 }
9755
9756 // Sanitize Word upload button toggle
9757 if (isset($input['show_word_upload_button'])) {
9758 $new_input['show_word_upload_button'] = $input['show_word_upload_button'] === 'on' ? 'on' : 'off';
9759 } else {
9760 $new_input['show_word_upload_button'] = 'off'; // If checkbox is unchecked
9761 }
9762
9763 if (isset($input['pdf_intent_trigger_text'])) {
9764 $new_input['pdf_intent_trigger_text'] = sanitize_text_field($input['pdf_intent_trigger_text']);
9765 }
9766
9767 if (isset($input['pdf_intent_success_text'])) {
9768 $new_input['pdf_intent_success_text'] = sanitize_text_field($input['pdf_intent_success_text']);
9769 }
9770
9771 if (isset($input['pdf_intent_error_text'])) {
9772 $new_input['pdf_intent_error_text'] = sanitize_text_field($input['pdf_intent_error_text']);
9773 }
9774
9775 if (isset($input['pdf_max_pages'])) {
9776 $new_input['pdf_max_pages'] = intval($input['pdf_max_pages']);
9777 if ($new_input['pdf_max_pages'] < 1 || $new_input['pdf_max_pages'] > 69) {
9778 $new_input['pdf_max_pages'] = 69; // Default to 69 if out of range
9779 }
9780 }
9781
9782 if (isset($input['live_agent_webhook_url'])) {
9783 $new_input['live_agent_webhook_url'] = esc_url_raw($input['live_agent_webhook_url']);
9784 }
9785 if (isset($input['live_agent_secret_key'])) {
9786 $new_input['live_agent_secret_key'] = sanitize_text_field($input['live_agent_secret_key']);
9787 }
9788
9789 // Live Agent Integration
9790 if (isset($input['live_agent_bot_token'])) {
9791 $new_input['live_agent_bot_token'] = sanitize_text_field($input['live_agent_bot_token']);
9792 }
9793
9794 if (isset($input['live_agent_user_ids'])) {
9795 $new_input['live_agent_user_ids'] = sanitize_textarea_field($input['live_agent_user_ids']);
9796 }
9797
9798 if (isset($input['live_agent_status'])) {
9799 $new_input['live_agent_status'] = ($input['live_agent_status'] === 'on') ? 'on' : 'off';
9800 }
9801 if (isset($input['live_agent_away_message'])) {
9802 $new_input['live_agent_away_message'] = sanitize_textarea_field($input['live_agent_away_message']);
9803 }
9804 if (isset($input['live_agent_notification_message'])) {
9805 $new_input['live_agent_notification_message'] = sanitize_textarea_field($input['live_agent_notification_message']);
9806 }
9807
9808 // Telegram Integration
9809 if (isset($input['telegram_status'])) {
9810 $new_input['telegram_status'] = ($input['telegram_status'] === 'on') ? 'on' : 'off';
9811 }
9812 if (isset($input['telegram_bot_token'])) {
9813 $new_input['telegram_bot_token'] = sanitize_text_field($input['telegram_bot_token']);
9814 }
9815 if (isset($input['telegram_group_id'])) {
9816 $new_input['telegram_group_id'] = sanitize_text_field($input['telegram_group_id']);
9817 }
9818 if (isset($input['telegram_webhook_secret'])) {
9819 $new_input['telegram_webhook_secret'] = sanitize_text_field($input['telegram_webhook_secret']);
9820 }
9821 if (isset($input['telegram_notification_message'])) {
9822 $new_input['telegram_notification_message'] = sanitize_textarea_field($input['telegram_notification_message']);
9823 }
9824 if (isset($input['telegram_away_message'])) {
9825 $new_input['telegram_away_message'] = sanitize_textarea_field($input['telegram_away_message']);
9826 }
9827
9828 // Sanitize script loading strategy
9829 if (isset($input['script_loading_strategy'])) {
9830 $allowed_strategies = array('default', 'defer', 'delay_1s', 'delay_3s', 'delay_5s', 'on_interaction');
9831 $new_input['script_loading_strategy'] = in_array($input['script_loading_strategy'], $allowed_strategies)
9832 ? $input['script_loading_strategy']
9833 : 'default';
9834 }
9835
9836 // Sanitize debug mode
9837 if (isset($input['debug_mode'])) {
9838 $new_input['debug_mode'] = ($input['debug_mode'] === 'on') ? 'on' : 'off';
9839 }
9840
9841 // Content Generator Settings
9842 if (isset($input['content_model'])) {
9843 $new_input['content_model'] = sanitize_text_field($input['content_model']);
9844 }
9845 if (isset($input['content_image_model'])) {
9846 $new_input['content_image_model'] = sanitize_text_field($input['content_image_model']);
9847 }
9848 if (isset($input['content_image_quality'])) {
9849 $q = sanitize_text_field($input['content_image_quality']);
9850 $new_input['content_image_quality'] = in_array($q, array('auto', 'low', 'medium', 'high'), true) ? $q : 'auto';
9851 }
9852 if (isset($input['content_enable_images'])) {
9853 $new_input['content_enable_images'] = ($input['content_enable_images'] === 'on') ? 'on' : 'off';
9854 }
9855 if (isset($input['content_use_placeholders'])) {
9856 $new_input['content_use_placeholders'] = ($input['content_use_placeholders'] === 'on') ? 'on' : 'off';
9857 }
9858 if (isset($input['content_internal_linking'])) {
9859 $new_input['content_internal_linking'] = ($input['content_internal_linking'] === 'on') ? 'on' : 'off';
9860 }
9861 if (isset($input['content_tool_use'])) {
9862 $new_input['content_tool_use'] = ($input['content_tool_use'] === 'on') ? 'on' : 'off';
9863 }
9864 if (isset($input['content_image_count'])) {
9865 $new_input['content_image_count'] = (string) max(1, min(5, (int) $input['content_image_count']));
9866 }
9867
9868 // SEO Optimize toggle fields
9869 foreach (array('seo_optimize_meta_desc', 'seo_optimize_seo_title', 'seo_optimize_slug', 'seo_optimize_readability', 'seo_optimize_internal_links', 'seo_optimize_img_alt', 'seo_optimize_featured_img') as $seo_key) {
9870 if (isset($input[$seo_key])) {
9871 $new_input[$seo_key] = ($input[$seo_key] === 'on') ? 'on' : 'off';
9872 }
9873 }
9874
9875 // Preserve content generator settings when saving from main settings page
9876 // (where content fields are not in the form submission)
9877 $existing = get_option('mxchat_options', array());
9878 foreach (array('content_model', 'content_image_model', 'content_image_quality', 'content_image_count', 'content_enable_images', 'content_use_placeholders', 'content_internal_linking', 'content_tool_use', 'seo_optimize_meta_desc', 'seo_optimize_seo_title', 'seo_optimize_slug', 'seo_optimize_readability', 'seo_optimize_internal_links', 'seo_optimize_img_alt', 'seo_optimize_featured_img') as $key) {
9879 if (!isset($new_input[$key]) && isset($existing[$key])) {
9880 $new_input[$key] = $existing[$key];
9881 }
9882 }
9883
9884 return $new_input;
9885 }
9886
9887 /**
9888 * Log a debug message if debug mode is enabled
9889 *
9890 * @param string $type The type of log entry (settings_save, api_error, activation, etc.)
9891 * @param string $message The log message
9892 * @param array $data Optional additional data to log
9893 * @return bool Whether the message was logged
9894 */
9895 public static function mxchat_log_debug( $type, $message, $data = array() ) {
9896 $options = get_option( 'mxchat_options', array() );
9897
9898 // Check if debug mode is enabled
9899 if ( ! isset( $options['debug_mode'] ) || $options['debug_mode'] !== 'on' ) {
9900 return false;
9901 }
9902
9903 // Get current log
9904 $log = get_option( 'mxchat_debug_log', array() );
9905 if ( ! is_array( $log ) ) {
9906 $log = array();
9907 }
9908
9909 // Add new entry
9910 $entry = array(
9911 'time' => current_time( 'Y-m-d H:i:s' ),
9912 'type' => sanitize_key( $type ),
9913 'message' => sanitize_text_field( $message ),
9914 );
9915
9916 if ( ! empty( $data ) ) {
9917 $entry['data'] = $data;
9918 }
9919
9920 // Add to beginning of array (newest first)
9921 array_unshift( $log, $entry );
9922
9923 // Keep only last 100 entries
9924 if ( count( $log ) > 100 ) {
9925 $log = array_slice( $log, 0, 100 );
9926 }
9927
9928 // Save log
9929 update_option( 'mxchat_debug_log', $log, false );
9930
9931 return true;
9932 }
9933
9934 /**
9935 * Get the debug log entries
9936 *
9937 * @return array The debug log entries
9938 */
9939 public static function mxchat_get_debug_log() {
9940 $log = get_option( 'mxchat_debug_log', array() );
9941 return is_array( $log ) ? $log : array();
9942 }
9943
9944 /**
9945 * Clear the debug log
9946 *
9947 * @return bool Whether the log was cleared
9948 */
9949 public static function mxchat_clear_debug_log() {
9950 return delete_option( 'mxchat_debug_log' );
9951 }
9952
9953 /**
9954 * Export settings as JSON with masked API keys
9955 *
9956 * @return array The sanitized settings array
9957 */
9958 public static function mxchat_export_settings() {
9959 $options = get_option( 'mxchat_options', array() );
9960
9961 if ( ! is_array( $options ) ) {
9962 return array();
9963 }
9964
9965 // List of API key fields to mask
9966 $api_key_fields = array(
9967 'api_key',
9968 'xai_api_key',
9969 'claude_api_key',
9970 'deepseek_api_key',
9971 'voyage_api_key',
9972 'gemini_api_key',
9973 'openrouter_api_key',
9974 'loops_api_key',
9975 'brave_api_key',
9976 'live_agent_secret_key',
9977 'live_agent_bot_token',
9978 'telegram_bot_token',
9979 'telegram_webhook_secret',
9980 'woocommerce_consumer_key',
9981 'woocommerce_consumer_secret',
9982 );
9983
9984 // Mask API keys (show only last 4 characters)
9985 foreach ( $api_key_fields as $field ) {
9986 if ( isset( $options[ $field ] ) && ! empty( $options[ $field ] ) ) {
9987 $value = $options[ $field ];
9988 if ( strlen( $value ) > 4 ) {
9989 $options[ $field ] = str_repeat( '*', strlen( $value ) - 4 ) . substr( $value, -4 );
9990 } else {
9991 $options[ $field ] = '****';
9992 }
9993 }
9994 }
9995
9996 // Add metadata
9997 $export = array(
9998 'plugin_version' => defined( 'MXCHAT_VERSION' ) ? MXCHAT_VERSION : 'unknown',
9999 'export_date' => current_time( 'Y-m-d H:i:s' ),
10000 'wordpress_version' => get_bloginfo( 'version' ),
10001 'php_version' => phpversion(),
10002 'settings' => $options,
10003 );
10004
10005 return $export;
10006 }
10007
10008 /**
10009 * Reset all settings to defaults
10010 *
10011 * @return bool Whether the reset was successful
10012 */
10013 public static function mxchat_reset_all_settings() {
10014 // Delete main options
10015 $deleted = delete_option( 'mxchat_options' );
10016
10017 // Also clear the debug log
10018 delete_option( 'mxchat_debug_log' );
10019
10020 // Log the reset (will create new log since we just cleared it)
10021 // We need to temporarily enable debug mode to log this
10022 $temp_options = array( 'debug_mode' => 'on' );
10023 update_option( 'mxchat_options', $temp_options );
10024
10025 self::mxchat_log_debug( 'reset', 'All settings have been reset to defaults' );
10026
10027 // Now delete again to trigger re-initialization
10028 delete_option( 'mxchat_options' );
10029
10030 return $deleted;
10031 }
10032
10033 // Method to append the chatbot to the body
10034 public function mxchat_append_chatbot_to_body() {
10035 $options = get_option('mxchat_options');
10036 if (isset($options['append_to_body']) && $options['append_to_body'] === 'on') {
10037 echo do_shortcode('[mxchat_chatbot floating="yes"]');
10038 }
10039 }
10040
10041
10042
10043
10044
10045 private function mxchat_fetch_loops_mailing_lists($api_key) {
10046 $url = 'https://app.loops.so/api/v1/lists';
10047 $response = wp_remote_get($url, array(
10048 'headers' => array(
10049 'Authorization' => 'Bearer ' . $api_key,
10050 'Content-Type' => 'application/json'
10051 )
10052 ));
10053
10054 if (is_wp_error($response)) {
10055 return array();
10056 }
10057
10058 $body = wp_remote_retrieve_body($response);
10059 $lists = json_decode($body, true);
10060
10061 return isset($lists) && is_array($lists) ? $lists : array();
10062 }
10063
10064 function mxchat_calculate_cosine_similarity($vec1, $vec2) {
10065 if (empty($vec1) || empty($vec2)) {
10066 return 0.0;
10067 }
10068
10069 $dot_product = 0.0;
10070 $norm_a = 0.0;
10071 $norm_b = 0.0;
10072
10073 for ($i = 0; $i < count($vec1); $i++) {
10074 $dot_product += $vec1[$i] * $vec2[$i];
10075 $norm_a += pow($vec1[$i], 2);
10076 $norm_b += pow($vec2[$i], 2);
10077 }
10078
10079 if ($norm_a == 0.0 || $norm_b == 0.0) {
10080 return 0.0;
10081 } else {
10082 return $dot_product / (sqrt($norm_a) * sqrt($norm_b));
10083 }
10084 }
10085
10086 /**
10087 * Validates nonce and deletes all chat prompts
10088 */
10089 public function mxchat_handle_delete_all_prompts() {
10090 //error_log('=== DELETE ALL DEBUG START ===');
10091
10092 // Verify nonce
10093 if (!isset($_POST['mxchat_delete_all_prompts_nonce']) || !wp_verify_nonce($_POST['mxchat_delete_all_prompts_nonce'], 'mxchat_delete_all_prompts_action')) {
10094 //error_log('DEBUG: Nonce verification failed');
10095 wp_die(__('Nonce verification failed.', 'mxchat'));
10096 }
10097
10098 // Check permissions
10099 if (!current_user_can('manage_options')) {
10100 //error_log('DEBUG: Permission check failed');
10101 wp_die(__('You do not have sufficient permissions to delete all prompts.', 'mxchat'));
10102 }
10103
10104 // Get bot_id and content type filter from POST data
10105 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
10106 $content_type_filter = isset($_POST['content_type_filter']) ? sanitize_text_field($_POST['content_type_filter']) : '';
10107
10108 $success = true;
10109 $error_messages = array();
10110
10111 // Get bot-specific Pinecone configuration
10112 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
10113 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
10114
10115 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
10116
10117 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
10118 // Delete from Pinecone (with optional content type filter)
10119 $result = $pinecone_manager->mxchat_delete_all_from_pinecone($pinecone_options, $content_type_filter);
10120
10121 if (!$result['success']) {
10122 $success = false;
10123 $error_messages[] = $result['message'];
10124 }
10125
10126 } else {
10127 // Delete from WordPress database
10128 global $wpdb;
10129 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
10130
10131 // Build WHERE conditions
10132 $where_clauses = array();
10133 $where_values = array();
10134
10135 // Bot filter
10136 if ($bot_id !== 'default' && class_exists('MxChat_Multi_Bot_Manager')) {
10137 $where_clauses[] = 'bot_id = %s';
10138 $where_values[] = $bot_id;
10139 }
10140
10141 // Content type filter
10142 if (!empty($content_type_filter)) {
10143 $where_clauses[] = 'content_type = %s';
10144 $where_values[] = $content_type_filter;
10145 }
10146
10147 if (!empty($where_clauses)) {
10148 $where_sql = implode(' AND ', $where_clauses);
10149 $result = $wpdb->query($wpdb->prepare("DELETE FROM {$table_name} WHERE {$where_sql}", $where_values));
10150 } else {
10151 // No filters — delete all
10152 $result = $wpdb->query("DELETE FROM {$table_name}");
10153 }
10154
10155 if ($result === false) {
10156 $success = false;
10157 $error_messages[] = 'Failed to delete from WordPress database';
10158 }
10159 }
10160
10161 // Redirect back with a success message and bot_id
10162 $redirect_url = add_query_arg(array(
10163 'page' => 'mxchat-prompts',
10164 'bot_id' => $bot_id,
10165 'all_deleted' => $success ? 'true' : 'false'
10166 ), admin_url('admin.php'));
10167
10168 wp_safe_redirect($redirect_url);
10169 exit;
10170 }
10171
10172
10173
10174 /**
10175 * Handles deletion prompt with nonce validation
10176 */
10177 public function mxchat_handle_delete_prompt() {
10178 // Sanitize and validate nonce
10179 $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : '';
10180 if (empty($nonce) || !wp_verify_nonce($nonce, 'mxchat_delete_prompt_nonce')) {
10181 wp_die(esc_html__('Nonce verification failed.', 'mxchat'));
10182 }
10183
10184 // Check permissions
10185 if (!current_user_can('manage_options')) {
10186 wp_die(esc_html__('You do not have sufficient permissions to delete prompts.', 'mxchat'));
10187 }
10188
10189 // Get ID and source parameters
10190 $id = isset($_GET['id']) ? sanitize_text_field($_GET['id']) : '';
10191 $source = isset($_GET['source']) ? sanitize_text_field($_GET['source']) : '';
10192
10193 if (empty($id)) {
10194 wp_die(esc_html__('Invalid prompt ID.', 'mxchat'));
10195 }
10196
10197 $success = false;
10198 $error_message = '';
10199
10200 // Check if Pinecone is enabled and determine source automatically if not specified
10201 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
10202 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
10203
10204 // If source is not specified, determine based on Pinecone configuration
10205 if (empty($source)) {
10206 $source = $use_pinecone ? 'pinecone' : 'wordpress';
10207 }
10208
10209 if ($source === 'pinecone' || $use_pinecone) {
10210 //error_log('[MXCHAT-DELETE] Deleting from Pinecone, ID: ' . $id);
10211
10212 // Handle Pinecone deletion
10213 if (empty($pinecone_options['mxchat_pinecone_host']) ||
10214 empty($pinecone_options['mxchat_pinecone_api_key'])) {
10215 wp_die(esc_html__('Pinecone configuration is missing.', 'mxchat'));
10216 }
10217
10218 // Delete from Pinecone using the vector ID directly
10219 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
10220 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
10221 $id,
10222 $pinecone_options['mxchat_pinecone_api_key'],
10223 $pinecone_options['mxchat_pinecone_host']
10224 );
10225 if ($result['success']) {
10226 $success = true;
10227
10228 // Remove from vector cache
10229 $pinecone_manager->mxchat_remove_from_pinecone_vector_cache($id);
10230 $pinecone_manager->mxchat_remove_from_processed_content_caches($id);
10231
10232 set_transient('mxchat_admin_notice_success',
10233 esc_html__('Vector deleted successfully from Pinecone.', 'mxchat'), 30);
10234 } else {
10235 $error_message = $result['message'];
10236 set_transient('mxchat_admin_notice_error',
10237 esc_html__('Failed to delete from Pinecone: ', 'mxchat') . esc_html($error_message), 30);
10238 }
10239 } else {
10240 //error_log('[MXCHAT-DELETE] Deleting from WordPress database, ID: ' . $id);
10241
10242 // Handle WordPress database deletion
10243 global $wpdb;
10244 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
10245
10246 // Clear cache and delete prompt
10247 wp_cache_delete('prompt_' . $id, 'mxchat_prompts');
10248
10249 $result = $wpdb->delete(
10250 $table_name,
10251 array('id' => intval($id)),
10252 array('%d')
10253 );
10254
10255 if ($result !== false) {
10256 $success = true;
10257 set_transient('mxchat_admin_notice_success',
10258 esc_html__('Entry deleted successfully.', 'mxchat'), 30);
10259 } else {
10260 set_transient('mxchat_admin_notice_error',
10261 esc_html__('Failed to delete entry from database.', 'mxchat'), 30);
10262 }
10263 }
10264
10265 // Redirect back to the prompts page
10266 wp_safe_redirect(add_query_arg(
10267 array(
10268 'page' => 'mxchat-prompts',
10269 'deleted' => $success ? 'true' : 'false'
10270 ),
10271 admin_url('admin.php')
10272 ));
10273 exit;
10274 }
10275
10276
10277
10278
10279 /**
10280 * Averages multiple vectors into a single vector
10281 */
10282 private function mxchat_average_vectors($vectors) {
10283 $vector_length = count($vectors[0]);
10284 $sum_vector = array_fill(0, $vector_length, 0);
10285
10286 foreach ($vectors as $vector) {
10287 for ($i = 0; $i < $vector_length; $i++) {
10288 $sum_vector[$i] += $vector[$i];
10289 }
10290 }
10291
10292 // Divide each component by the number of vectors to get the average
10293 $num_vectors = count($vectors);
10294 for ($i = 0; $i < $vector_length; $i++) {
10295 $sum_vector[$i] /= $num_vectors;
10296 }
10297
10298 return $sum_vector;
10299 }
10300
10301
10302
10303
10304 /**
10305 * Generates embeddings from input text for MXChat
10306 */
10307 public function mxchat_generate_embedding($text) {
10308 // Enable detailed logging for debugging
10309 //error_log('[MXCHAT-EMBED] Starting embedding generation. Text length: ' . strlen($text) . ' bytes');
10310 //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
10311
10312 $options = get_option('mxchat_options');
10313 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
10314 //error_log('[MXCHAT-EMBED] Selected embedding model: ' . $selected_model);
10315
10316 // Determine provider and endpoint
10317 if (strpos($selected_model, 'voyage') === 0) {
10318 $api_key = $options['voyage_api_key'] ?? '';
10319 $endpoint = 'https://api.voyageai.com/v1/embeddings';
10320 $provider_name = 'Voyage AI';
10321 //error_log('[MXCHAT-EMBED] Using Voyage AI API');
10322 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
10323 $api_key = $options['gemini_api_key'] ?? '';
10324 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
10325 $provider_name = 'Google Gemini';
10326 //error_log('[MXCHAT-EMBED] Using Google Gemini API');
10327 } else {
10328 $api_key = $options['api_key'] ?? '';
10329 $endpoint = 'https://api.openai.com/v1/embeddings';
10330 $provider_name = 'OpenAI';
10331 //error_log('[MXCHAT-EMBED] Using OpenAI API');
10332 }
10333
10334 //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
10335
10336 if (empty($api_key)) {
10337 $error_message = sprintf('Missing %s API key. Please configure your API key in the MxChat settings.', $provider_name);
10338 //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
10339 return $error_message;
10340 }
10341
10342 // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
10343 $estimated_tokens = ceil(str_word_count($text) / 0.75);
10344 //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
10345
10346 if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
10347 //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
10348 // Consider truncating text here
10349 }
10350
10351 // Prepare request body based on provider
10352 if (strpos($selected_model, 'gemini-embedding') === 0) {
10353 // Gemini API format
10354 $request_body = array(
10355 'model' => 'models/' . $selected_model,
10356 'content' => array(
10357 'parts' => array(
10358 array('text' => $text)
10359 )
10360 )
10361 );
10362
10363 // Set output dimensionality to 1536 for consistency with other models
10364 $request_body['outputDimensionality'] = 1536;
10365 } else {
10366 // OpenAI/Voyage API format
10367 $request_body = array(
10368 'model' => $selected_model,
10369 'input' => $text
10370 );
10371
10372 // Add output_dimension for voyage-3-large model
10373 if ($selected_model === 'voyage-3-large') {
10374 $request_body['output_dimension'] = 2048;
10375 }
10376 }
10377
10378 //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
10379
10380 // Prepare headers based on provider
10381 if (strpos($selected_model, 'gemini-embedding') === 0) {
10382 // Gemini uses API key as query parameter
10383 $endpoint .= '?key=' . $api_key;
10384 $headers = array(
10385 'Content-Type' => 'application/json'
10386 );
10387 } else {
10388 // OpenAI/Voyage use Bearer token
10389 $headers = array(
10390 'Authorization' => 'Bearer ' . $api_key,
10391 'Content-Type' => 'application/json'
10392 );
10393 }
10394
10395 // Make API request
10396 //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
10397 $response = wp_remote_post($endpoint, array(
10398 'body' => wp_json_encode($request_body),
10399 'headers' => $headers,
10400 'timeout' => 60 // Increased timeout for large inputs
10401 ));
10402
10403 // Handle wp_remote_post errors
10404 if (is_wp_error($response)) {
10405 $error_message = $response->get_error_message();
10406 //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
10407 return 'Connection error: ' . $error_message;
10408 }
10409
10410 // Get and check HTTP response code
10411 $http_code = wp_remote_retrieve_response_code($response);
10412 //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
10413
10414 if ($http_code !== 200) {
10415 $error_body = wp_remote_retrieve_body($response);
10416 //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
10417
10418 // Try to parse error for more details
10419 $error_json = json_decode($error_body, true);
10420 if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
10421 $error_type = $error_json['error']['type'] ?? 'unknown';
10422 $error_message = $error_json['error']['message'] ?? 'No message';
10423 //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
10424 //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
10425
10426 // Customize error message for common API errors
10427 if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
10428 $error_message = sprintf('Invalid %s API key. Please check your API key in the MxChat settings.', $provider_name);
10429 } elseif ($error_type === 'authentication_error') {
10430 $error_message = sprintf('%s authentication failed. Please verify your API key in the MxChat settings.', $provider_name);
10431 }
10432
10433 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
10434 return $error_message;
10435 }
10436
10437 $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding", $http_code);
10438 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
10439 return $error_message;
10440 }
10441
10442 // Parse response body
10443 $response_body = wp_remote_retrieve_body($response);
10444 //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
10445
10446 $response_data = json_decode($response_body, true);
10447
10448 if (json_last_error() !== JSON_ERROR_NONE) {
10449 $error = json_last_error_msg();
10450 //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
10451 //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
10452 return "Failed to parse API response: $error";
10453 }
10454
10455 // Handle different response formats based on provider
10456 if (strpos($selected_model, 'gemini-embedding') === 0) {
10457 // Gemini API response format
10458 if (isset($response_data['embedding']['values'])) {
10459 $embedding_dimensions = count($response_data['embedding']['values']);
10460 //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
10461
10462 // Check if embedding dimensions are as expected (should be 1536)
10463 if ($embedding_dimensions !== 1536) {
10464 //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
10465 }
10466
10467 MxChat_Utils::stamp_active_embedding_model($selected_model);
10468 return $response_data['embedding']['values'];
10469 } else {
10470 //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
10471 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
10472
10473 if (isset($response_data['error'])) {
10474 $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
10475 //error_log('[MXCHAT-EMBED] ' . $error_message);
10476 return $error_message;
10477 }
10478
10479 $error_message = "Invalid Gemini API response format: No embedding found";
10480 //error_log('[MXCHAT-EMBED] ' . $error_message);
10481 return $error_message;
10482 }
10483 } else {
10484 // OpenAI/Voyage API response format
10485 if (isset($response_data['data'][0]['embedding'])) {
10486 $embedding_dimensions = count($response_data['data'][0]['embedding']);
10487 //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
10488
10489 // Check if embedding dimensions are as expected
10490 if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
10491 ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
10492 //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
10493 }
10494
10495 MxChat_Utils::stamp_active_embedding_model($selected_model);
10496 return $response_data['data'][0]['embedding'];
10497 } else {
10498 //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
10499 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
10500
10501 if (isset($response_data['error'])) {
10502 $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
10503 //error_log('[MXCHAT-EMBED] ' . $error_message);
10504 return $error_message;
10505 }
10506
10507 $error_message = "Invalid API response format: No embedding found";
10508 //error_log('[MXCHAT-EMBED] ' . $error_message);
10509 return $error_message;
10510 }
10511 }
10512 }
10513
10514
10515
10516 }
10517 ?>
10518