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

9,718 lines 398.0 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 add_action('admin_init', array($this, 'mxchat_page_init'));
46 add_action('admin_init', array($this, 'mxchat_prompts_page_init'));
47 add_action('admin_enqueue_scripts', array($this, 'mxchat_enqueue_admin_assets'));
48 add_action('wp_ajax_mxchat_delete_chat_history', array($this, 'mxchat_delete_chat_history'));
49 add_action('admin_post_mxchat_delete_prompt', array($this, 'mxchat_handle_delete_prompt'));
50 add_action('wp_ajax_mxchat_fetch_chat_history', array($this, 'mxchat_fetch_chat_history'));
51 add_action('wp_ajax_nopriv_mxchat_fetch_chat_history', array($this, 'mxchat_fetch_chat_history'));
52 add_action('wp_ajax_mxchat_fetch_conversation', array($this, 'mxchat_fetch_conversation'));
53 add_action('wp_footer', array($this, 'mxchat_append_chatbot_to_body'));
54 add_action('admin_head-mxchat-prompts', array($this, 'mxchat_enqueue_admin_assets'));
55 add_action('admin_head-toplevel_page_mxchat-max', array($this, 'mxchat_enqueue_admin_assets'));
56 add_action('admin_notices', array($this, 'mxchat_display_admin_notice'));
57 add_action('admin_post_mxchat_delete_all_prompts', array($this, 'mxchat_handle_delete_all_prompts'));
58 add_action('admin_post_mxchat_add_intent', array($this, 'mxchat_handle_add_intent'));
59 add_action('admin_post_mxchat_delete_intent', array($this, 'mxchat_handle_delete_intent'));
60 add_action('admin_post_mxchat_edit_intent', array($this, 'mxchat_handle_edit_intent'));
61 add_action('wp_ajax_mxchat_export_transcripts', array($this, 'export_chat_transcripts'));
62
63 // Leads tab (inside Transcripts)
64 add_action('wp_ajax_mxchat_fetch_leads', array($this, 'mxchat_fetch_leads'));
65 add_action('wp_ajax_mxchat_delete_leads', array($this, 'mxchat_delete_leads'));
66 add_action('wp_ajax_mxchat_export_leads', array($this, 'mxchat_export_leads'));
67
68 add_action('admin_init', array($this, 'mxchat_transcripts_page_init'));
69 add_action('wp_ajax_dismiss_live_agent_notice', array($this, 'dismiss_live_agent_notice'));
70 add_action('wp_ajax_dismiss_theme_migration_notice', array($this, 'dismiss_theme_migration_notice'));
71 add_action('mxchat_cleanup_old_transcripts', array($this, 'cleanup_old_transcripts'));
72
73 add_action('admin_init', array($this, 'register_pinecone_settings'));
74 add_action('admin_init', array($this, 'register_openai_vectorstore_settings'));
75
76 add_action('admin_notices', array($this, 'display_admin_notices'));
77
78 add_action('wp_ajax_mxchat_test_streaming_actual', [$this, 'mxchat_handle_test_streaming_actual']);
79 add_action('wp_ajax_mxchat_test_streaming', [$this, 'mxchat_handle_test_streaming']); // Keep existing as fallback
80 add_action('wp_ajax_mxchat_test_vectorstore_connection', array($this, 'mxchat_test_vectorstore_connection'));
81
82 add_action('wp_ajax_mxchat_save_selected_bot', array($this, 'mxchat_save_selected_bot'));
83 add_action('wp_ajax_mxchat_fetch_openrouter_models', array($this, 'fetch_openrouter_models'));
84 add_action('wp_ajax_mxchat_get_rag_context', array($this, 'mxchat_get_rag_context'));
85
86 // Actions page AJAX handlers
87 add_action('wp_ajax_mxchat_fetch_actions_list', array($this, 'mxchat_fetch_actions_list'));
88 add_action('wp_ajax_mxchat_toggle_action_status', array($this, 'mxchat_toggle_action_status'));
89 add_action('wp_ajax_mxchat_bulk_delete_actions', array($this, 'mxchat_bulk_delete_actions'));
90 add_action('wp_ajax_mxchat_add_intent_ajax', array($this, 'mxchat_add_intent_ajax'));
91 add_action('wp_ajax_mxchat_edit_intent_ajax', array($this, 'mxchat_edit_intent_ajax'));
92 add_action('wp_ajax_mxchat_delete_intent_ajax', array($this, 'mxchat_delete_intent_ajax'));
93 add_action('wp_ajax_mxchat_add_phrase', array($this, 'mxchat_add_phrase_ajax'));
94 add_action('wp_ajax_mxchat_delete_phrase', array($this, 'mxchat_delete_phrase_ajax'));
95 add_action('wp_ajax_mxchat_get_phrases', array($this, 'mxchat_get_phrases_ajax'));
96 add_action('wp_ajax_mxchat_delete_legacy_phrases', array($this, 'mxchat_delete_legacy_phrases_ajax'));
97
98 // Slack test connection
99 add_action('wp_ajax_mxchat_test_slack_connection', array($this, 'mxchat_test_slack_connection'));
100
101 // Translation handlers
102 add_action('wp_ajax_mxchat_translate_messages', array($this, 'mxchat_translate_messages'));
103 add_action('wp_ajax_mxchat_get_transcript_translation', array($this, 'mxchat_get_transcript_translation'));
104
105 // 3.2.3: Embedding model switch protection
106 add_action('wp_ajax_mxchat_check_embedding_switch', array($this, 'mxchat_check_embedding_switch_ajax'));
107 add_action('wp_ajax_mxchat_dismiss_embedding_mismatch', array($this, 'mxchat_dismiss_embedding_mismatch_ajax'));
108 add_action('admin_notices', array($this, 'mxchat_embedding_mismatch_notice'));
109 }
110
111 /**
112 * 3.2.3: Preflight check before allowing the embedding model dropdown to
113 * switch. Pure option comparison — no counts, no DB queries beyond the
114 * cached options. The dialog is shown whenever the user has previously
115 * embedded with a different model than the one they're switching to.
116 */
117 public function mxchat_check_embedding_switch_ajax() {
118 check_ajax_referer('mxchat_admin_nonce', 'security');
119 if (!current_user_can('manage_options')) {
120 wp_send_json_error(__('Unauthorized', 'mxchat'));
121 }
122
123 $new_model = isset($_POST['new_model']) ? sanitize_text_field(wp_unslash($_POST['new_model'])) : '';
124 $active_model = MxChat_Utils::get_active_embedding_model();
125
126 $is_mismatch = !empty($active_model) && !empty($new_model) && $active_model !== $new_model;
127
128 $active_dims = MxChat_Utils::embedding_model_dimensions($active_model);
129 $new_dims = MxChat_Utils::embedding_model_dimensions($new_model);
130
131 wp_send_json_success(array(
132 'is_mismatch' => $is_mismatch,
133 'active_model' => $active_model,
134 'active_label' => MxChat_Utils::embedding_model_label($active_model),
135 'new_model' => $new_model,
136 'new_label' => MxChat_Utils::embedding_model_label($new_model),
137 'dims_differ' => ($active_dims > 0 && $new_dims > 0 && $active_dims !== $new_dims),
138 'active_dims' => $active_dims,
139 'new_dims' => $new_dims,
140 ));
141 }
142
143 /**
144 * 3.2.3: Dismiss the persistent mismatch banner. Tied to the active+selected
145 * pair so the banner reappears on the next switch event.
146 */
147 public function mxchat_dismiss_embedding_mismatch_ajax() {
148 check_ajax_referer('mxchat_admin_nonce', 'security');
149 if (!current_user_can('manage_options')) {
150 wp_send_json_error(__('Unauthorized', 'mxchat'));
151 }
152
153 $options = get_option('mxchat_options', array());
154 $selected = $options['embedding_model'] ?? '';
155 $active = MxChat_Utils::get_active_embedding_model();
156 update_option('mxchat_dismissed_embedding_mismatch', $active . '|' . $selected, false);
157 wp_send_json_success();
158 }
159
160 /**
161 * 3.2.3: Persistent admin banner shown whenever the active embedding model
162 * (last used to actually embed something) differs from the currently
163 * selected model. Pure option comparison — no DB queries on every page
164 * load. The banner auto-clears once both match again, i.e. after a delete
165 * + re-embed cycle.
166 */
167 public function mxchat_embedding_mismatch_notice() {
168 if (!current_user_can('manage_options')) {
169 return;
170 }
171
172 $options = get_option('mxchat_options', array());
173 $selected = $options['embedding_model'] ?? '';
174 $active = MxChat_Utils::get_active_embedding_model();
175
176 if (empty($active) || empty($selected) || $active === $selected) {
177 return;
178 }
179
180 $dismissed = get_option('mxchat_dismissed_embedding_mismatch', '');
181 if ($dismissed === $active . '|' . $selected) {
182 return;
183 }
184
185 $active_label = MxChat_Utils::embedding_model_label($active);
186 $selected_label = MxChat_Utils::embedding_model_label($selected);
187 $active_dims = MxChat_Utils::embedding_model_dimensions($active);
188 $selected_dims = MxChat_Utils::embedding_model_dimensions($selected);
189 $dims_differ = ($active_dims > 0 && $selected_dims > 0 && $active_dims !== $selected_dims);
190
191 $kb_url = admin_url('admin.php?page=mxchat-prompts');
192 $actions_url = admin_url('admin.php?page=mxchat-actions');
193
194 ?>
195 <div class="notice notice-error is-dismissible mxchat-embedding-mismatch-notice"
196 data-active="<?php echo esc_attr($active); ?>"
197 data-selected="<?php echo esc_attr($selected); ?>">
198 <p><strong><?php esc_html_e('MxChat: Embedding model mismatch detected', 'mxchat'); ?></strong></p>
199 <p>
200 <?php
201 printf(
202 /* translators: 1: previously-used model name, 2: currently-selected model name */
203 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'),
204 '<code>' . esc_html($active_label) . '</code>',
205 '<code>' . esc_html($selected_label) . '</code>'
206 );
207 ?>
208 </p>
209 <?php if ($dims_differ) : ?>
210 <p>
211 <strong><?php esc_html_e('Dimension mismatch:', 'mxchat'); ?></strong>
212 <?php
213 printf(
214 /* translators: 1: old dim count, 2: new dim count */
215 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'),
216 (int) $active_dims,
217 (int) $selected_dims
218 );
219 ?>
220 </p>
221 <?php endif; ?>
222 <p>
223 <?php esc_html_e('To fix this:', 'mxchat'); ?>
224 <a href="<?php echo esc_url($kb_url); ?>"><?php esc_html_e('Delete all knowledge base entries', 'mxchat'); ?></a> ·
225 <a href="<?php echo esc_url($actions_url); ?>"><?php esc_html_e('Delete all actions', 'mxchat'); ?></a> ·
226 <?php esc_html_e('then re-import / re-add them with the new model selected.', 'mxchat'); ?>
227 </p>
228 </div>
229 <script>
230 (function($){
231 $(document).on('click', '.mxchat-embedding-mismatch-notice .notice-dismiss', function(){
232 $.post(ajaxurl, {
233 action: 'mxchat_dismiss_embedding_mismatch',
234 security: '<?php echo esc_js(wp_create_nonce('mxchat_admin_nonce')); ?>'
235 });
236 });
237 })(jQuery);
238 </script>
239 <?php
240 }
241
242 private function is_license_active() {
243 $license_status = get_option('mxchat_license_status', 'inactive');
244 return ($license_status === 'active');
245 }
246
247 private function initialize_default_options() {
248 $default_options = array(
249 'api_key' => '',
250 'xai_api_key' => '',
251 'claude_api_key' => '',
252 'deepseek_api_key' => '',
253 'voyage_api_key' => '',
254 'gemini_api_key' => '',
255 'enable_streaming_toggle' => 'off',
256 'enable_web_search' => 'off',
257 'embedding_model' => 'text-embedding-ada-002',
258 '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:
259
260 # Response Style - CRITICALLY IMPORTANT
261 - MAXIMUM LENGTH: 1-3 short sentences per response
262 - Ultra-concise: Get straight to the answer with no filler
263 - No introductions like "Sure!" or "I\'d be happy to help"
264 - No phrases like "based on my knowledge" or "according to information"
265 - No explanatory text before giving the answer
266 - No summaries or repetition
267 - Hyperlink all URLs
268 - Respond in user\'s language
269 - Minor chit chat or conversation is okay, but try to keep it focused on [insert topic]
270
271 # Knowledge Base Requirements - PREVENT HALLUCINATIONS
272 - ONLY answer questions using information explicitly provided in OFFICIAL KNOWLEDGE DATABASE CONTENT sections marked with ===== delimiters
273 - If required information is NOT in the knowledge database: "I don\'t have enough information in my knowledge base to answer that question accurately."
274 - NEVER invent or hallucinate URLs, links, product specs, procedures, dates, statistics, names, contacts, or company information
275 - When knowledge base information is unclear or contradictory, acknowledge the limitation rather than guessing
276 - Better to admit insufficient information than provide inaccurate answers',
277 'model' => esc_html__('gpt-5.1-chat-latest', 'mxchat'),
278 'rate_limit_logged_out' => esc_html__('100', 'mxchat'),
279 'role_rate_limits' => array(),
280 'rate_limit_message' => esc_html__('Rate limit exceeded. Please try again later.', 'mxchat'),
281 'enable_email_block' => '',
282 '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'),
283 'email_blocker_button_text' => esc_html__('Start Chat', 'mxchat'),
284 'enable_name_field' => 'off', // NEW
285 'name_field_placeholder' => esc_html__('Enter your name', 'mxchat'), // NEW
286 'top_bar_title' => esc_html__('MxChat', 'mxchat'),
287 'intro_message' => __('Hello! How can I assist you today?', 'mxchat'),
288 'ai_agent_text' => esc_html__('AI Agent', 'mxchat'),
289 'input_copy' => esc_html__('How can I assist?', 'mxchat'),
290 'append_to_body' => esc_html__('off', 'mxchat'),
291 'post_type_visibility_mode' => 'all', // 'all', 'include', 'exclude'
292 'post_type_visibility_list' => array(), // Array of post type slugs
293 'contextual_awareness_toggle' => 'off',
294 'citation_links_toggle' => 'on',
295 'close_button_color' => esc_html__('#fff', 'mxchat'),
296 'chatbot_bg_color' => esc_html__('#fff', 'mxchat'),
297 'user_message_bg_color' => esc_html__('#fff', 'mxchat'),
298 'user_message_font_color' => esc_html__('#212121', 'mxchat'),
299 'bot_message_bg_color' => esc_html__('#212121', 'mxchat'),
300 'bot_message_font_color' => esc_html__('#fff', 'mxchat'),
301 'top_bar_bg_color' => esc_html__('#212121', 'mxchat'),
302 'send_button_font_color' => esc_html__('#212121', 'mxchat'),
303 'chat_input_font_color' => esc_html__('#212121', 'mxchat'),
304 'chatbot_background_color' => esc_html__('#212121', 'mxchat'),
305 'icon_color' => esc_html__('#fff', 'mxchat'),
306 'enable_woocommerce_integration' => esc_html__('0', 'mxchat'),
307 'link_target_toggle' => esc_html__('off', 'mxchat'),
308 'pre_chat_message' => esc_html__('Hey there! Ask me anything!', 'mxchat'),
309
310 // New fields for Loops Integration
311 'loops_api_key' => '',
312 'loops_mailing_list' => '',
313 'triggered_phrase_response' => __('Would you like to join our mailing list? Please provide your email below.', 'mxchat'),
314 'email_capture_response' => __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat'),
315 'popular_question_1' => '',
316 'popular_question_2' => '',
317 'popular_question_3' => '',
318 'pdf_intent_trigger_text' => __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat'),
319 'pdf_intent_success_text' => __("I've processed the PDF. What questions do you have about it?", 'mxchat'),
320 'pdf_intent_error_text' => __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat'),
321 'pdf_max_pages' => 69,
322 'show_pdf_upload_button' => 'on',
323 'show_word_upload_button' => 'on',
324
325 // Live Agent Integration (Slack)
326 'live_agent_webhook_url' => '',
327 'live_agent_secret_key' => '',
328 'live_agent_bot_token' => '',
329 'live_agent_message_bg_color' => esc_html__('#ffffff', 'mxchat'),
330 'live_agent_message_font_color' => esc_html__('#333333', 'mxchat'),
331
332 // Telegram Integration
333 'telegram_status' => 'off',
334 'telegram_bot_token' => '',
335 'telegram_group_id' => '',
336 'telegram_webhook_secret' => '',
337 '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'),
338 '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'),
339
340 'chat_toolbar_toggle' => esc_html__('off', 'mxchat'),
341 'mode_indicator_bg_color' => esc_html__('#767676', 'mxchat'),
342 'mode_indicator_font_color' => esc_html__('#ffffff', 'mxchat'),
343 'toolbar_icon_color' => esc_html__('#212121', 'mxchat'),
344
345 // Optimization settings
346 'script_loading_strategy' => 'default',
347
348 // Debug settings
349 'debug_mode' => 'off',
350 );
351
352
353 // Merge existing options with defaults
354 $existing_options = get_option('mxchat_options', array());
355 $merged_options = wp_parse_args($existing_options, $default_options);
356
357 // Update the options if they have changed
358 if ($existing_options !== $merged_options) {
359 update_option('mxchat_options', $merged_options);
360 }
361
362 // Add default limits for each role
363 $roles = wp_roles()->get_names();
364 foreach ($roles as $role_id => $role_name) {
365 $default_options['role_rate_limits'][$role_id] = esc_html__('100', 'mxchat');
366 }
367
368 return $default_options;
369
370 // Update the $this->options property
371 $this->options = $merged_options;
372 }
373
374 public function mxchat_add_plugin_page() {
375 // Main menu page
376 add_menu_page(
377 esc_html__('MxChat Settings', 'mxchat'),
378 esc_html__('MxChat', 'mxchat'),
379 'manage_options',
380 'mxchat-max',
381 array($this, 'mxchat_create_admin_page'),
382 'dashicons-testimonial',
383 6
384 );
385
386 // Rename the first submenu item from "MxChat" to "Settings"
387 add_submenu_page(
388 'mxchat-max',
389 esc_html__('MxChat Settings', 'mxchat'),
390 esc_html__('Settings', 'mxchat'),
391 'manage_options',
392 'mxchat-max',
393 array($this, 'mxchat_create_admin_page')
394 );
395
396 // Submenu page for Knowledge
397 add_submenu_page(
398 'mxchat-max',
399 esc_html__('Prompts', 'mxchat'),
400 esc_html__('Knowledge', 'mxchat'),
401 'manage_options',
402 'mxchat-prompts',
403 array($this, 'mxchat_create_prompts_page')
404 );
405
406 add_submenu_page(
407 'mxchat-max',
408 esc_html__('Chat Transcripts', 'mxchat'),
409 esc_html__('Transcripts', 'mxchat'),
410 'manage_options',
411 'mxchat-transcripts',
412 array($this, 'mxchat_create_transcripts_page')
413 );
414
415 add_submenu_page(
416 'mxchat-max',
417 esc_html__('MxChat Actions', 'mxchat'),
418 esc_html__('Actions', 'mxchat'),
419 'manage_options',
420 'mxchat-actions',
421 array($this, 'mxchat_actions_page_html')
422 );
423
424 // Content Generator page
425 add_submenu_page(
426 'mxchat-max',
427 esc_html__('Content', 'mxchat'),
428 esc_html__('Content', 'mxchat'),
429 'manage_options',
430 'mxchat-content',
431 array($this, 'mxchat_create_content_page')
432 );
433
434 }
435
436 /**
437 * Register the Pro & Extensions submenu on a later admin_menu priority so it
438 * always renders as the bottom-most item in the MxChat sidebar — below
439 * configuration pages like API Access (priority 20).
440 */
441 public function mxchat_add_pro_extensions_page() {
442 add_submenu_page(
443 'mxchat-max',
444 esc_html__('Pro & Extensions', 'mxchat'),
445 esc_html__('Pro & Extensions', 'mxchat'),
446 'manage_options',
447 'mxchat-activation',
448 array($this, 'mxchat_create_activation_page')
449 );
450 }
451
452 public function mxchat_create_addons_page() {
453 require_once plugin_dir_path(__FILE__) . 'class-mxchat-addons.php';
454 $addons_page = new MxChat_Addons();
455 $addons_page->render_page();
456 }
457
458 /**
459 * Render the Content Generator admin page
460 */
461 public function mxchat_create_content_page() {
462 require_once plugin_dir_path(__FILE__) . 'admin-content-page.php';
463 mxchat_render_content_page($this);
464 }
465
466 /**
467 * Test actual streaming functionality in the WordPress environment
468 */
469 public function mxchat_handle_test_streaming_actual() {
470 check_ajax_referer('mxchat_test_streaming_nonce', 'nonce');
471
472 // Check if headers have already been sent
473 if (headers_sent()) {
474 wp_send_json_error(['message' => 'Headers already sent - streaming not possible']);
475 return;
476 }
477
478 // Check for required functions
479 if (!function_exists('curl_init')) {
480 wp_send_json_error(['message' => 'cURL not available - streaming requires cURL']);
481 return;
482 }
483
484 // Get user's selected model and API key
485 $options = get_option('mxchat_options', []);
486 $selected_model = $options['model'] ?? 'gpt-5.1-chat-latest';
487
488 // Get the provider from the model
489 $model_parts = explode('-', $selected_model);
490 $provider = strtolower($model_parts[0]);
491
492 // Get the appropriate API key
493 $api_key = '';
494 switch ($provider) {
495 case 'gpt':
496 case 'o1':
497 $api_key = $options['api_key'] ?? '';
498 break;
499 case 'claude':
500 $api_key = $options['claude_api_key'] ?? '';
501 break;
502 case 'grok':
503 $api_key = $options['xai_api_key'] ?? '';
504 break;
505 case 'deepseek':
506 $api_key = $options['deepseek_api_key'] ?? '';
507 break;
508 case 'gemini':
509 $api_key = $options['gemini_api_key'] ?? '';
510 break;
511 default:
512 // Default to OpenAI for unknown models
513 $api_key = $options['api_key'] ?? '';
514 $provider = 'gpt';
515 break;
516 }
517
518 if (empty($api_key)) {
519 wp_send_json_error(['message' => "API key not configured for {$provider} provider"]);
520 return;
521 }
522
523 // Test streaming with the selected model and provider
524 try {
525 $this->perform_streaming_test($provider, $selected_model, $api_key);
526 } catch (Exception $e) {
527 wp_send_json_error(['message' => 'Streaming test exception: ' . $e->getMessage()]);
528 }
529 }
530
531 /**
532 * Perform the actual streaming test
533 */
534 private function perform_streaming_test($provider, $model, $api_key) {
535 // Set streaming headers
536 header('Content-Type: text/event-stream');
537 header('Cache-Control: no-cache');
538 header('X-Accel-Buffering: no'); // Disable nginx buffering
539
540 // Prepare test message
541 $test_message = "Please respond with exactly: 'Streaming test successful!' - send this as a short response for testing.";
542
543 // Configure API request based on provider
544 $url = '';
545 $headers = [];
546 $body = [];
547
548 switch ($provider) {
549 case 'gpt':
550 case 'o1':
551 $url = 'https://api.openai.com/v1/chat/completions';
552 $headers = [
553 'Content-Type: application/json',
554 'Authorization: Bearer ' . $api_key
555 ];
556 $body = [
557 'model' => $model,
558 'messages' => [['role' => 'user', 'content' => $test_message]],
559 'max_tokens' => 50,
560 'temperature' => 0.3,
561 'stream' => true
562 ];
563 break;
564
565 case 'claude':
566 $url = 'https://api.anthropic.com/v1/messages';
567 $headers = [
568 'Content-Type: application/json',
569 'x-api-key: ' . $api_key,
570 'anthropic-version: 2023-06-01'
571 ];
572 $body = [
573 'model' => $model,
574 'messages' => [['role' => 'user', 'content' => $test_message]],
575 'max_tokens' => 50,
576 'temperature' => 0.3,
577 'stream' => true
578 ];
579 break;
580
581 case 'grok':
582 $url = 'https://api.x.ai/v1/chat/completions';
583 $headers = [
584 'Content-Type: application/json',
585 'Authorization: Bearer ' . $api_key
586 ];
587 $body = [
588 'model' => $model,
589 'messages' => [['role' => 'user', 'content' => $test_message]],
590 'max_tokens' => 50,
591 'temperature' => 0.3,
592 'stream' => true
593 ];
594 break;
595
596 case 'deepseek':
597 $url = 'https://api.deepseek.com/v1/chat/completions';
598 $headers = [
599 'Content-Type: application/json',
600 'Authorization: Bearer ' . $api_key
601 ];
602 $body = [
603 'model' => $model,
604 'messages' => [['role' => 'user', 'content' => $test_message]],
605 'max_tokens' => 50,
606 'temperature' => 0.3,
607 'stream' => true
608 ];
609 break;
610
611 default:
612 echo "data: " . json_encode(['error' => 'Unsupported provider for streaming test: ' . $provider]) . "\n\n";
613 flush();
614 return;
615 }
616
617 // Initialize cURL
618 $ch = curl_init();
619 curl_setopt($ch, CURLOPT_URL, $url);
620 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
621 curl_setopt($ch, CURLOPT_POST, true);
622 curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
623 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
624 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
625 curl_setopt($ch, CURLOPT_TIMEOUT, 30);
626 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use ($provider) {
627 return $this->process_streaming_test_data($data, $provider);
628 });
629
630 // Send initial test message
631 echo "data: " . json_encode(['content' => '[Starting streaming test...]']) . "\n\n";
632 flush();
633
634 $result = curl_exec($ch);
635 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
636 $curl_error = curl_error($ch);
637 curl_close($ch);
638
639 if ($curl_error) {
640 echo "data: " . json_encode(['error' => 'cURL Error: ' . $curl_error]) . "\n\n";
641 flush();
642 return;
643 }
644
645 if ($http_code !== 200) {
646 echo "data: " . json_encode(['error' => 'API returned HTTP ' . $http_code]) . "\n\n";
647 flush();
648 return;
649 }
650
651 // Send completion signal
652 echo "data: [DONE]\n\n";
653 flush();
654 }
655
656 /**
657 * Process streaming data for the test
658 */
659 private function process_streaming_test_data($data, $provider) {
660 static $chunk_count = 0;
661
662 $lines = explode("\n", $data);
663
664 foreach ($lines as $line) {
665 if (trim($line) === '') {
666 continue;
667 }
668
669 // Handle different provider formats
670 if ($provider === 'claude') {
671 // Claude uses event: and data: format
672 if (strpos($line, 'data: ') === 0) {
673 $json_str = substr($line, 6);
674 $json = json_decode($json_str, true);
675
676 if (isset($json['type']) && $json['type'] === 'content_block_delta') {
677 if (isset($json['delta']['text'])) {
678 $chunk_count++;
679 echo "data: " . json_encode([
680 'content' => $json['delta']['text'],
681 'test_chunk' => $chunk_count
682 ]) . "\n\n";
683 flush();
684 }
685 }
686 }
687 } else {
688 // OpenAI, X.AI, DeepSeek format
689 if (strpos($line, 'data: ') === 0) {
690 $json_str = substr($line, 6);
691
692 if ($json_str === '[DONE]') {
693 // Don't echo [DONE] here, let the main function handle it
694 continue;
695 }
696
697 $json = json_decode($json_str, true);
698 if (isset($json['choices'][0]['delta']['content'])) {
699 $chunk_count++;
700 echo "data: " . json_encode([
701 'content' => $json['choices'][0]['delta']['content'],
702 'test_chunk' => $chunk_count
703 ]) . "\n\n";
704 flush();
705 }
706 }
707 }
708 }
709
710 return strlen($data);
711 }
712
713 /**
714 * Updated version of your existing test method (keep this as a fallback)
715 */
716 public function mxchat_handle_test_streaming() {
717 check_ajax_referer('mxchat_test_streaming_nonce', 'nonce');
718
719 // Use the actual streaming test instead
720 $this->mxchat_handle_test_streaming_actual();
721 }
722
723
724 public function register_pinecone_settings() {
725 register_setting(
726 'mxchat_pinecone_addon_options',
727 'mxchat_pinecone_addon_options',
728 array(
729 'type' => 'array',
730 'sanitize_callback' => array($this, 'sanitize_pinecone_settings'),
731 'default' => array(
732 'mxchat_use_pinecone' => '0',
733 'mxchat_pinecone_api_key' => '',
734 'mxchat_pinecone_host' => '',
735 'mxchat_pinecone_index' => '',
736 'mxchat_pinecone_environment' => ''
737 )
738 )
739 );
740 }
741
742 public function sanitize_pinecone_settings($input) {
743 $sanitized = array();
744
745 $sanitized['mxchat_use_pinecone'] = isset($input['mxchat_use_pinecone']) ? '1' : '0';
746 $sanitized['mxchat_pinecone_api_key'] = sanitize_text_field($input['mxchat_pinecone_api_key'] ?? '');
747 $sanitized['mxchat_pinecone_host'] = sanitize_text_field($input['mxchat_pinecone_host'] ?? '');
748 $sanitized['mxchat_pinecone_index'] = sanitize_text_field($input['mxchat_pinecone_index'] ?? '');
749 $sanitized['mxchat_pinecone_environment'] = sanitize_text_field($input['mxchat_pinecone_environment'] ?? '');
750
751 // Remove https:// from host if present
752 $sanitized['mxchat_pinecone_host'] = str_replace(['https://', 'http://'], '', $sanitized['mxchat_pinecone_host']);
753
754 return $sanitized;
755 }
756
757 public function register_openai_vectorstore_settings() {
758 register_setting(
759 'mxchat_openai_vectorstore_options',
760 'mxchat_openai_vectorstore_options',
761 array(
762 'type' => 'array',
763 'sanitize_callback' => array($this, 'sanitize_openai_vectorstore_settings'),
764 'default' => array(
765 'mxchat_use_openai_vectorstore' => '0',
766 'mxchat_vectorstore_ids' => '',
767 'mxchat_vectorstore_max_results' => 5
768 )
769 )
770 );
771 }
772
773 public function sanitize_openai_vectorstore_settings($input) {
774 $sanitized = array();
775
776 $sanitized['mxchat_use_openai_vectorstore'] = isset($input['mxchat_use_openai_vectorstore']) ? '1' : '0';
777 $sanitized['mxchat_vectorstore_ids'] = sanitize_text_field($input['mxchat_vectorstore_ids'] ?? '');
778 $sanitized['mxchat_vectorstore_max_results'] = absint($input['mxchat_vectorstore_max_results'] ?? 5);
779
780 // Ensure max results is within reasonable range
781 if ($sanitized['mxchat_vectorstore_max_results'] < 1) {
782 $sanitized['mxchat_vectorstore_max_results'] = 1;
783 }
784 if ($sanitized['mxchat_vectorstore_max_results'] > 20) {
785 $sanitized['mxchat_vectorstore_max_results'] = 20;
786 }
787
788 return $sanitized;
789 }
790
791 /**
792 * AJAX handler to test OpenAI Vector Store connection
793 */
794 public function mxchat_test_vectorstore_connection() {
795 check_ajax_referer('mxchat_admin_nonce', 'nonce');
796
797 if (!current_user_can('manage_options')) {
798 wp_send_json_error(array('message' => __('Permission denied.', 'mxchat')));
799 return;
800 }
801
802 $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
803 $vectorstore_ids = $vectorstore_options['mxchat_vectorstore_ids'] ?? '';
804 $mxchat_options = get_option('mxchat_options', array());
805 $api_key = $mxchat_options['api_key'] ?? '';
806
807 if (empty($api_key)) {
808 wp_send_json_error(array('message' => __('OpenAI API key is not configured.', 'mxchat')));
809 return;
810 }
811
812 if (empty($vectorstore_ids)) {
813 wp_send_json_error(array('message' => __('No Vector Store ID configured.', 'mxchat')));
814 return;
815 }
816
817 // Get the first Vector Store ID for testing
818 $ids_array = array_map('trim', explode(',', $vectorstore_ids));
819 $test_id = $ids_array[0];
820
821 // Test by retrieving the Vector Store info
822 $response = wp_remote_get(
823 'https://api.openai.com/v1/vector_stores/' . $test_id,
824 array(
825 'headers' => array(
826 'Authorization' => 'Bearer ' . $api_key,
827 'Content-Type' => 'application/json',
828 'OpenAI-Beta' => 'assistants=v2'
829 ),
830 'timeout' => 30
831 )
832 );
833
834 if (is_wp_error($response)) {
835 wp_send_json_error(array('message' => __('Connection failed: ', 'mxchat') . $response->get_error_message()));
836 return;
837 }
838
839 $status_code = wp_remote_retrieve_response_code($response);
840 $body = json_decode(wp_remote_retrieve_body($response), true);
841
842 if ($status_code === 200 && isset($body['id'])) {
843 $file_count = $body['file_counts']['completed'] ?? 0;
844 $name = $body['name'] ?? $test_id;
845 wp_send_json_success(array(
846 'message' => sprintf(
847 __('Connected successfully! Vector Store: %s (%d files)', 'mxchat'),
848 esc_html($name),
849 $file_count
850 )
851 ));
852 } elseif ($status_code === 404) {
853 wp_send_json_error(array('message' => __('Vector Store not found. Please check the ID.', 'mxchat')));
854 } elseif ($status_code === 401) {
855 wp_send_json_error(array('message' => __('Invalid API key.', 'mxchat')));
856 } else {
857 $error_message = $body['error']['message'] ?? __('Unknown error occurred.', 'mxchat');
858 wp_send_json_error(array('message' => $error_message));
859 }
860 }
861
862 /**
863 * AJAX handler to test Slack connection and validate scopes
864 */
865 public function mxchat_test_slack_connection() {
866 check_ajax_referer('mxchat_admin_nonce', 'nonce');
867
868 if (!current_user_can('manage_options')) {
869 wp_send_json_error(array('message' => __('Permission denied.', 'mxchat')));
870 return;
871 }
872
873 $bot_token = $this->options['live_agent_bot_token'] ?? '';
874
875 if (empty($bot_token)) {
876 wp_send_json_error(array('message' => __('Slack Bot Token is not configured. Please enter your token and save settings first.', 'mxchat')));
877 return;
878 }
879
880 // Test 1: Validate bot token with auth.test
881 $auth_response = wp_remote_post('https://slack.com/api/auth.test', array(
882 'headers' => array(
883 'Authorization' => 'Bearer ' . $bot_token,
884 'Content-Type' => 'application/json'
885 ),
886 'timeout' => 15
887 ));
888
889 if (is_wp_error($auth_response)) {
890 wp_send_json_error(array('message' => __('Connection failed: ', 'mxchat') . $auth_response->get_error_message()));
891 return;
892 }
893
894 $auth_body = json_decode(wp_remote_retrieve_body($auth_response), true);
895
896 if (!isset($auth_body['ok']) || !$auth_body['ok']) {
897 $error = $auth_body['error'] ?? 'unknown_error';
898 $error_messages = array(
899 'invalid_auth' => __('Invalid bot token. Please check your token starts with xoxb-', 'mxchat'),
900 'not_authed' => __('No authentication token provided.', 'mxchat'),
901 'account_inactive' => __('The Slack workspace has been deactivated.', 'mxchat'),
902 'token_revoked' => __('The bot token has been revoked. Please generate a new one.', 'mxchat'),
903 );
904 $message = $error_messages[$error] ?? sprintf(__('Authentication failed: %s', 'mxchat'), $error);
905 wp_send_json_error(array('message' => $message));
906 return;
907 }
908
909 $team_name = $auth_body['team'] ?? 'Unknown Workspace';
910 $bot_name = $auth_body['user'] ?? 'Unknown Bot';
911
912 // Test 2: Check if we can list channels (tests channels:read scope)
913 $channels_response = wp_remote_post('https://slack.com/api/conversations.list', array(
914 'headers' => array(
915 'Authorization' => 'Bearer ' . $bot_token,
916 'Content-Type' => 'application/json'
917 ),
918 'body' => json_encode(array('limit' => 1)),
919 'timeout' => 15
920 ));
921
922 $channels_body = json_decode(wp_remote_retrieve_body($channels_response), true);
923 $can_read_channels = isset($channels_body['ok']) && $channels_body['ok'];
924
925 // Test 3: Check if we can create channels (tests channels:manage scope)
926 // We'll just check the error message without actually creating
927 $create_response = wp_remote_post('https://slack.com/api/conversations.create', array(
928 'headers' => array(
929 'Authorization' => 'Bearer ' . $bot_token,
930 'Content-Type' => 'application/json'
931 ),
932 'body' => json_encode(array('name' => 'mxchat-test-' . time(), 'is_private' => false)),
933 'timeout' => 15
934 ));
935
936 $create_body = json_decode(wp_remote_retrieve_body($create_response), true);
937
938 // If channel was created, delete it immediately
939 if (isset($create_body['ok']) && $create_body['ok'] && isset($create_body['channel']['id'])) {
940 wp_remote_post('https://slack.com/api/conversations.archive', array(
941 'headers' => array(
942 'Authorization' => 'Bearer ' . $bot_token,
943 'Content-Type' => 'application/json'
944 ),
945 'body' => json_encode(array('channel' => $create_body['channel']['id'])),
946 'timeout' => 15
947 ));
948 $can_create_channels = true;
949 } else {
950 $create_error = $create_body['error'] ?? '';
951 // name_taken means we have permission but channel exists
952 $can_create_channels = ($create_error === 'name_taken' || (isset($create_body['ok']) && $create_body['ok']));
953
954 // Check for missing scope errors
955 if ($create_error === 'missing_scope') {
956 $can_create_channels = false;
957 }
958 }
959
960 // Build result message
961 $results = array();
962 $results[] = sprintf(__('Workspace: %s', 'mxchat'), esc_html($team_name));
963 $results[] = sprintf(__('Bot: %s', 'mxchat'), esc_html($bot_name));
964 $results[] = '';
965 $results[] = ($can_read_channels ? '✓' : '✗') . ' ' . __('channels:read - List channels', 'mxchat');
966 $results[] = ($can_create_channels ? '✓' : '✗') . ' ' . __('channels:manage - Create channels', 'mxchat');
967
968 $missing_scopes = array();
969 if (!$can_read_channels) $missing_scopes[] = 'channels:read';
970 if (!$can_create_channels) $missing_scopes[] = 'channels:manage';
971
972 if (!empty($missing_scopes)) {
973 wp_send_json_error(array(
974 'message' => implode("\n", $results),
975 'missing_scopes' => $missing_scopes,
976 'partial' => true
977 ));
978 } else {
979 wp_send_json_success(array(
980 'message' => implode("\n", $results)
981 ));
982 }
983 }
984
985 public function mxchat_display_admin_notice() {
986 // Success notice
987 if ($message = get_transient('mxchat_admin_notice_success')) {
988 ?>
989 <div class="notice notice-success is-dismissible">
990 <p><?php echo esc_html($message); ?></p>
991 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php echo esc_html__('Dismiss this notice.', 'mxchat'); ?></span></button>
992 </div>
993 <?php
994 delete_transient('mxchat_admin_notice_success'); // Clear the transient after displaying
995 }
996
997 // Error notice
998 if ($message = get_transient('mxchat_admin_notice_error')) {
999 ?>
1000 <div class="notice notice-error is-dismissible">
1001 <p><?php echo esc_html($message); ?></p>
1002 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php echo esc_html__('Dismiss this notice.', 'mxchat'); ?></span></button>
1003 </div>
1004 <?php
1005 delete_transient('mxchat_admin_notice_error'); // Clear the transient after displaying
1006 }
1007 }
1008
1009 public function show_live_agent_disabled_banner() {
1010 $show_disabled_notice = get_option('mxchat_show_live_agent_disabled_notice', false);
1011
1012 if ($show_disabled_notice) {
1013 ?>
1014 <div class="mxchat-live-agent-disabled-notice" id="mxchat-disabled-notice">
1015 <div class="mxchat-pro-notification">
1016 <button type="button" class="mxchat-dismiss-btn" onclick="dismissLiveAgentNotice()" aria-label="Dismiss notification">
1017 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1018 <line x1="18" y1="6" x2="6" y2="18"></line>
1019 <line x1="6" y1="6" x2="18" y2="18"></line>
1020 </svg>
1021 </button>
1022 <div class="mxchat-live-agent-content">
1023 <h3>🔧 Live Agent Integration Updated!</h3>
1024 <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>
1025 </div>
1026 </div>
1027 </div>
1028 <?php
1029 }
1030 }
1031 public function mxchat_create_admin_page() {
1032 $this->add_live_agent_nonce();
1033 $this->add_theme_migration_nonce();
1034
1035 // Include and render the new sidebar-based settings page
1036 require_once plugin_dir_path(__FILE__) . 'admin-settings-page.php';
1037 mxchat_render_settings_page($this);
1038 }
1039
1040 public function dismiss_live_agent_notice() {
1041 // Add debugging
1042 //error_log('dismiss_live_agent_notice called');
1043 //error_log('POST data: ' . print_r($_POST, true));
1044
1045 // Verify nonce
1046 if (!wp_verify_nonce($_POST['nonce'], 'dismiss_live_agent_notice')) {
1047 //error_log('Nonce verification failed');
1048 wp_die('Security check failed');
1049 }
1050
1051 // Remove the notice flag
1052 $deleted = delete_option('mxchat_show_live_agent_disabled_notice');
1053 //error_log('Option deleted: ' . ($deleted ? 'yes' : 'no'));
1054
1055 wp_send_json_success();
1056 }
1057 public function add_live_agent_nonce() {
1058 if (get_option('mxchat_show_live_agent_disabled_notice', false)) {
1059 // Make sure your admin script is enqueued and localize the data
1060 wp_localize_script('mxchat-admin-js', 'mxchatLiveAgent', array(
1061 'nonce' => wp_create_nonce('dismiss_live_agent_notice'),
1062 'ajaxurl' => admin_url('admin-ajax.php')
1063 ));
1064 }
1065 }
1066
1067 /**
1068 * Show theme migration notice for Pro users with AI-generated themes
1069 * Only shown once - dismissible and stored in options
1070 */
1071 public function show_theme_migration_banner() {
1072 // Only show if Pro is activated
1073 if (!$this->is_activated) {
1074 return;
1075 }
1076
1077 // Check if notice should be shown
1078 $show_notice = get_option('mxchat_show_theme_migration_notice', false);
1079
1080 if ($show_notice) {
1081 ?>
1082 <div class="mxchat-theme-migration-notice" id="mxchat-theme-migration-notice">
1083 <div class="mxchat-pro-notification">
1084 <button type="button" class="mxchat-dismiss-btn" onclick="dismissThemeMigrationNotice()" aria-label="Dismiss notification">
1085 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1086 <line x1="18" y1="6" x2="6" y2="18"></line>
1087 <line x1="6" y1="6" x2="18" y2="18"></line>
1088 </svg>
1089 </button>
1090 <div class="mxchat-theme-migration-content">
1091 <h3>🎨 AI Theme Migration Required</h3>
1092 <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>
1093 </div>
1094 </div>
1095 </div>
1096 <?php
1097 }
1098 }
1099
1100 /**
1101 * Dismiss theme migration notice via AJAX
1102 */
1103 public function dismiss_theme_migration_notice() {
1104 // Verify nonce
1105 if (!wp_verify_nonce($_POST['nonce'], 'dismiss_theme_migration_notice')) {
1106 wp_die('Security check failed');
1107 }
1108
1109 // Remove the notice flag
1110 delete_option('mxchat_show_theme_migration_notice');
1111
1112 wp_send_json_success();
1113 }
1114
1115 /**
1116 * Add nonce for theme migration notice dismiss
1117 */
1118 public function add_theme_migration_nonce() {
1119 if (get_option('mxchat_show_theme_migration_notice', false) && $this->is_activated) {
1120 wp_localize_script('mxchat-admin-js', 'mxchatThemeMigration', array(
1121 'nonce' => wp_create_nonce('dismiss_theme_migration_notice'),
1122 'ajaxurl' => admin_url('admin-ajax.php')
1123 ));
1124 }
1125 }
1126
1127 public function mxchat_create_transcripts_page() {
1128 global $wpdb;
1129 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1130
1131 // Get basic stats
1132 $total_chats = $wpdb->get_var("SELECT COUNT(DISTINCT session_id) FROM $table_name") ?: 0;
1133 $total_messages = $wpdb->get_var("SELECT COUNT(*) FROM $table_name") ?: 0;
1134
1135 // Count unique users with detailed breakdown
1136 $total_users = $wpdb->get_var("
1137 SELECT COUNT(DISTINCT
1138 CASE
1139 WHEN user_email != '' AND user_email IS NOT NULL THEN user_email
1140 WHEN user_id != 0 THEN CONCAT('user_', user_id)
1141 WHEN user_identifier NOT LIKE 'Tech-Savvy User'
1142 AND user_identifier NOT LIKE 'Detail-Oriented User'
1143 AND user_identifier NOT LIKE 'Language Learner'
1144 AND user_identifier NOT LIKE 'Casual Browser'
1145 AND user_identifier NOT LIKE 'Policy Enforcer'
1146 AND user_identifier NOT LIKE 'Researcher'
1147 AND user_identifier NOT LIKE 'Loyalty Member'
1148 AND user_identifier NOT LIKE 'Gift Buyer'
1149 AND user_identifier NOT LIKE 'Parent or Caregiver'
1150 THEN user_identifier
1151 ELSE session_id
1152 END
1153 )
1154 FROM $table_name
1155 WHERE role != 'assistant'
1156 ");
1157
1158 // Get user type breakdown
1159 $registered_users = $wpdb->get_var("
1160 SELECT COUNT(DISTINCT user_email)
1161 FROM $table_name
1162 WHERE user_email != '' AND user_email IS NOT NULL
1163 ");
1164
1165 $guest_users = $wpdb->get_var("
1166 SELECT COUNT(DISTINCT user_identifier)
1167 FROM $table_name
1168 WHERE (user_email = '' OR user_email IS NULL)
1169 AND role != 'assistant'
1170 AND user_identifier NOT LIKE 'Tech-Savvy User'
1171 AND user_identifier NOT LIKE 'Detail-Oriented User'
1172 AND user_identifier NOT LIKE 'Language Learner'
1173 AND user_identifier NOT LIKE 'Casual Browser'
1174 AND user_identifier NOT LIKE 'Policy Enforcer'
1175 AND user_identifier NOT LIKE 'Researcher'
1176 AND user_identifier NOT LIKE 'Loyalty Member'
1177 AND user_identifier NOT LIKE 'Gift Buyer'
1178 AND user_identifier NOT LIKE 'Parent or Caregiver'
1179 ");
1180
1181 // Get agent test messages count
1182 $agent_tests = $wpdb->get_var("
1183 SELECT COUNT(DISTINCT session_id)
1184 FROM $table_name
1185 WHERE user_identifier IN (
1186 'Tech-Savvy User',
1187 'Detail-Oriented User',
1188 'Language Learner',
1189 'Casual Browser',
1190 'Policy Enforcer',
1191 'Researcher',
1192 'Loyalty Member',
1193 'Gift Buyer',
1194 'Parent or Caregiver'
1195 )
1196 ");
1197 // Get activity metrics
1198 $today_chats = $wpdb->get_var("
1199 SELECT COUNT(DISTINCT session_id)
1200 FROM $table_name
1201 WHERE DATE(timestamp) = CURDATE()
1202 ");
1203
1204 $week_chats = $wpdb->get_var("
1205 SELECT COUNT(DISTINCT session_id)
1206 FROM $table_name
1207 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)
1208 ");
1209
1210 $month_chats = $wpdb->get_var("
1211 SELECT COUNT(DISTINCT session_id)
1212 FROM $table_name
1213 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 30 DAY)
1214 ");
1215
1216 // Get daily chat data for last 7 days
1217 $daily_stats = $wpdb->get_results("
1218 SELECT
1219 DATE(timestamp) as date,
1220 COUNT(DISTINCT session_id) as chats,
1221 COUNT(*) as messages
1222 FROM $table_name
1223 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)
1224 GROUP BY DATE(timestamp)
1225 ORDER BY date ASC
1226 ");
1227
1228 // Get average messages per chat
1229 $avg_messages = $wpdb->get_var("
1230 SELECT AVG(message_count)
1231 FROM (
1232 SELECT session_id, COUNT(*) as message_count
1233 FROM $table_name
1234 GROUP BY session_id
1235 ) as chat_counts
1236 ");
1237 $avg_messages = $avg_messages ? round($avg_messages, 1) : 0;
1238
1239 // Get busiest hour
1240 $busiest_hour = $wpdb->get_row("
1241 SELECT HOUR(timestamp) as hour, COUNT(DISTINCT session_id) as chat_count
1242 FROM $table_name
1243 GROUP BY HOUR(timestamp)
1244 ORDER BY chat_count DESC
1245 LIMIT 1
1246 ");
1247
1248 // Prepare chart data
1249 $chart_labels = array();
1250 $chart_chats = array();
1251 $chart_messages = array();
1252
1253 // Fill last 7 days with data
1254 for ($i = 6; $i >= 0; $i--) {
1255 $date = date('Y-m-d', strtotime("-$i days"));
1256 $day_name = date('D', strtotime("-$i days"));
1257 $chart_labels[] = $day_name;
1258
1259 $found = false;
1260 foreach ($daily_stats as $stat) {
1261 if ($stat->date === $date) {
1262 $chart_chats[] = (int)$stat->chats;
1263 $chart_messages[] = (int)$stat->messages;
1264 $found = true;
1265 break;
1266 }
1267 }
1268 if (!$found) {
1269 $chart_chats[] = 0;
1270 $chart_messages[] = 0;
1271 }
1272 }
1273
1274 // Prepare page data for the template
1275 $page_data = array(
1276 'total_chats' => $total_chats,
1277 'total_messages' => $total_messages,
1278 'total_users' => $total_users,
1279 'registered_users' => $registered_users,
1280 'guest_users' => $guest_users,
1281 'agent_tests' => $agent_tests,
1282 'today_chats' => $today_chats,
1283 'week_chats' => $week_chats,
1284 'month_chats' => $month_chats,
1285 'avg_messages' => $avg_messages,
1286 'busiest_hour' => $busiest_hour,
1287 'chart_labels' => $chart_labels,
1288 'chart_chats' => $chart_chats,
1289 'chart_messages' => $chart_messages,
1290 );
1291
1292 // Include and render the new template
1293 require_once plugin_dir_path(__FILE__) . 'admin-transcripts-page.php';
1294 mxchat_render_transcripts_page($this, $page_data);
1295 }
1296
1297 /**
1298 * Get chart data for transcripts page
1299 * Used by both page render and script localization
1300 *
1301 * @return array Chart data with labels, chats, and messages arrays
1302 */
1303 private function get_transcripts_chart_data() {
1304 global $wpdb;
1305 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1306
1307 // Get daily chat data for last 7 days - same query as mxchat_transcripts_page()
1308 $daily_stats = $wpdb->get_results("
1309 SELECT
1310 DATE(timestamp) as date,
1311 COUNT(DISTINCT session_id) as chats,
1312 COUNT(*) as messages
1313 FROM $table_name
1314 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)
1315 GROUP BY DATE(timestamp)
1316 ORDER BY date ASC
1317 ");
1318
1319 // Prepare chart data
1320 $chart_labels = array();
1321 $chart_chats = array();
1322 $chart_messages = array();
1323
1324 // Fill last 7 days with data - same logic as mxchat_transcripts_page()
1325 for ($i = 6; $i >= 0; $i--) {
1326 $date = date('Y-m-d', strtotime("-$i days"));
1327 $day_name = date('D', strtotime("-$i days"));
1328 $chart_labels[] = $day_name;
1329
1330 $found = false;
1331 if ($daily_stats) {
1332 foreach ($daily_stats as $stat) {
1333 if ($stat->date === $date) {
1334 $chart_chats[] = (int)$stat->chats;
1335 $chart_messages[] = (int)$stat->messages;
1336 $found = true;
1337 break;
1338 }
1339 }
1340 }
1341 if (!$found) {
1342 $chart_chats[] = 0;
1343 $chart_messages[] = 0;
1344 }
1345 }
1346
1347 return array(
1348 'labels' => $chart_labels,
1349 'chats' => $chart_chats,
1350 'messages' => $chart_messages
1351 );
1352 }
1353
1354 public function mxchat_transcripts_notification_section_callback() {
1355 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>';
1356 }
1357 public function mxchat_enable_notifications_callback() {
1358 $options = get_option('mxchat_transcripts_options', array());
1359 $enabled = isset($options['mxchat_enable_notifications']) ? $options['mxchat_enable_notifications'] : 0;
1360 ?>
1361 <label for="mxchat_enable_notifications">
1362 <input type="checkbox" id="mxchat_enable_notifications"
1363 name="mxchat_transcripts_options[mxchat_enable_notifications]"
1364 value="1" <?php checked(1, $enabled); ?>>
1365 <?php esc_html_e('Send email notification when a new chat session starts', 'mxchat'); ?>
1366 </label>
1367 <p class="description">
1368 <?php esc_html_e('Enable this option to receive email notifications for new chat sessions.', 'mxchat'); ?>
1369 </p>
1370 <?php
1371 }
1372 public function mxchat_notification_email_callback() {
1373 $options = get_option('mxchat_transcripts_options', array());
1374 $email = isset($options['mxchat_notification_email']) ? $options['mxchat_notification_email'] : get_option('admin_email');
1375 ?>
1376 <input type="email" id="mxchat_notification_email"
1377 name="mxchat_transcripts_options[mxchat_notification_email]"
1378 value="<?php echo esc_attr($email); ?>"
1379 class="regular-text">
1380 <p class="description">
1381 <?php esc_html_e('Enter the email address where notifications should be sent. Defaults to the admin email address.', 'mxchat'); ?>
1382 </p>
1383 <?php
1384 }
1385
1386 public function mxchat_auto_delete_transcripts_callback() {
1387 $options = get_option('mxchat_transcripts_options', array());
1388 $interval = isset($options['mxchat_auto_delete_transcripts']) ? $options['mxchat_auto_delete_transcripts'] : 'never';
1389 ?>
1390 <select id="mxchat_auto_delete_transcripts"
1391 name="mxchat_transcripts_options[mxchat_auto_delete_transcripts]">
1392 <option value="never" <?php selected($interval, 'never'); ?>>
1393 <?php esc_html_e('Never (Keep All Transcripts)', 'mxchat'); ?>
1394 </option>
1395 <option value="1week" <?php selected($interval, '1week'); ?>>
1396 <?php esc_html_e('After 1 Week', 'mxchat'); ?>
1397 </option>
1398 <option value="2weeks" <?php selected($interval, '2weeks'); ?>>
1399 <?php esc_html_e('After 2 Weeks', 'mxchat'); ?>
1400 </option>
1401 <option value="1month" <?php selected($interval, '1month'); ?>>
1402 <?php esc_html_e('After 1 Month', 'mxchat'); ?>
1403 </option>
1404 </select>
1405 <p class="description">
1406 <?php esc_html_e('Automatically delete old chat transcripts after the selected time period. This helps manage database size and privacy.', 'mxchat'); ?>
1407 </p>
1408 <?php
1409 }
1410
1411 /**
1412 * Custom retention-days input. When > 0 it overrides the bucket dropdown above
1413 * and deletes transcripts older than the given number of days. Set to 0 to fall
1414 * back to the dropdown (or "Never" if the dropdown is also Never).
1415 *
1416 * Devs can override the final day count via the `mxchat_transcript_retention_days`
1417 * filter — runs in `cleanup_old_transcripts()` after this option is read.
1418 *
1419 * (plan-mxchat-20260509-9b80b1)
1420 */
1421 public function mxchat_retention_days_callback() {
1422 $options = get_option('mxchat_transcripts_options', array());
1423 $days = isset($options['mxchat_retention_days']) ? (int) $options['mxchat_retention_days'] : 0;
1424 ?>
1425 <input type="number"
1426 id="mxchat_retention_days"
1427 name="mxchat_transcripts_options[mxchat_retention_days]"
1428 value="<?php echo esc_attr($days); ?>"
1429 min="0"
1430 max="3650"
1431 step="1"
1432 style="width: 90px;" />
1433 <p class="description">
1434 <?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'); ?>
1435 <br>
1436 <code>apply_filters( 'mxchat_transcript_retention_days', $days )</code>
1437 <?php esc_html_e('lets developers override the final day count programmatically.', 'mxchat'); ?>
1438 </p>
1439 <?php
1440 }
1441
1442 public function mxchat_auto_email_transcript_callback() {
1443 $options = get_option('mxchat_transcripts_options', array());
1444 $enabled = isset($options['mxchat_auto_email_transcript_enabled']) ? $options['mxchat_auto_email_transcript_enabled'] : 0;
1445 $delay = isset($options['mxchat_auto_email_transcript_delay']) ? $options['mxchat_auto_email_transcript_delay'] : '30';
1446 $require_contact = isset($options['mxchat_auto_email_transcript_require_contact']) ? $options['mxchat_auto_email_transcript_require_contact'] : 0;
1447 ?>
1448 <label>
1449 <input type="checkbox"
1450 id="mxchat_auto_email_transcript_enabled"
1451 name="mxchat_transcripts_options[mxchat_auto_email_transcript_enabled]"
1452 value="1"
1453 <?php checked($enabled, 1); ?>>
1454 <?php esc_html_e('Enable Auto-Email of Full Transcript', 'mxchat'); ?>
1455 </label>
1456 <br><br>
1457 <label for="mxchat_auto_email_transcript_delay">
1458 <?php esc_html_e('Send transcript after:', 'mxchat'); ?>
1459 </label>
1460 <select id="mxchat_auto_email_transcript_delay"
1461 name="mxchat_transcripts_options[mxchat_auto_email_transcript_delay]">
1462 <option value="15" <?php selected($delay, '15'); ?>>
1463 <?php esc_html_e('15 minutes', 'mxchat'); ?>
1464 </option>
1465 <option value="30" <?php selected($delay, '30'); ?>>
1466 <?php esc_html_e('30 minutes', 'mxchat'); ?>
1467 </option>
1468 <option value="60" <?php selected($delay, '60'); ?>>
1469 <?php esc_html_e('1 hour', 'mxchat'); ?>
1470 </option>
1471 </select>
1472 <br><br>
1473 <label>
1474 <input type="checkbox"
1475 id="mxchat_auto_email_transcript_require_contact"
1476 name="mxchat_transcripts_options[mxchat_auto_email_transcript_require_contact]"
1477 value="1"
1478 <?php checked($require_contact, 1); ?>>
1479 <?php esc_html_e('Only send if visitor provided contact info', 'mxchat'); ?>
1480 </label>
1481 <p class="description">
1482 <?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'); ?>
1483 <br>
1484 <?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'); ?>
1485 </p>
1486 <?php
1487 }
1488
1489
1490
1491 public function sanitize_transcripts_options($input) {
1492 $sanitized = array();
1493
1494 $sanitized['mxchat_enable_notifications'] = isset($input['mxchat_enable_notifications']) ? 1 : 0;
1495
1496 if (isset($input['mxchat_notification_email'])) {
1497 $sanitized['mxchat_notification_email'] = sanitize_email($input['mxchat_notification_email']);
1498 if (!is_email($sanitized['mxchat_notification_email'])) {
1499 add_settings_error(
1500 'mxchat_transcripts_options',
1501 'invalid_email',
1502 __('Please enter a valid email address for notifications.', 'mxchat'),
1503 'error'
1504 );
1505 $sanitized['mxchat_notification_email'] = get_option('admin_email');
1506 }
1507 }
1508
1509 // Sanitize auto-delete setting
1510 $valid_intervals = array('never', '1week', '2weeks', '1month');
1511 if (isset($input['mxchat_auto_delete_transcripts'])) {
1512 $sanitized['mxchat_auto_delete_transcripts'] = in_array($input['mxchat_auto_delete_transcripts'], $valid_intervals)
1513 ? $input['mxchat_auto_delete_transcripts']
1514 : 'never';
1515 } else {
1516 $sanitized['mxchat_auto_delete_transcripts'] = 'never';
1517 }
1518
1519 // Sanitize custom retention-days override (plan-9b80b1).
1520 if (isset($input['mxchat_retention_days'])) {
1521 $days = (int) $input['mxchat_retention_days'];
1522 $sanitized['mxchat_retention_days'] = max(0, min(3650, $days));
1523 } else {
1524 $sanitized['mxchat_retention_days'] = 0;
1525 }
1526
1527 // Get old values to check if auto-delete or retention-days changed.
1528 $old_options = get_option('mxchat_transcripts_options');
1529 $old_interval = isset($old_options['mxchat_auto_delete_transcripts']) ? $old_options['mxchat_auto_delete_transcripts'] : 'never';
1530 $old_retention = isset($old_options['mxchat_retention_days']) ? (int) $old_options['mxchat_retention_days'] : 0;
1531
1532 // If either setting changed, reschedule the cron job. The schedule_transcript_cleanup
1533 // helper now treats "any active retention" (dropdown != never OR custom days > 0) as a
1534 // reason to keep the daily cron registered.
1535 $interval_changed = ($old_interval !== $sanitized['mxchat_auto_delete_transcripts']);
1536 $retention_changed = ($old_retention !== $sanitized['mxchat_retention_days']);
1537 if ($interval_changed || $retention_changed) {
1538 $any_active = ($sanitized['mxchat_auto_delete_transcripts'] !== 'never') || ($sanitized['mxchat_retention_days'] > 0);
1539 $this->schedule_transcript_cleanup($any_active ? 'active' : 'never');
1540 }
1541
1542 // Sanitize auto-email transcript settings
1543 $sanitized['mxchat_auto_email_transcript_enabled'] = isset($input['mxchat_auto_email_transcript_enabled']) ? 1 : 0;
1544
1545 $valid_delays = array('15', '30', '60');
1546 if (isset($input['mxchat_auto_email_transcript_delay'])) {
1547 $sanitized['mxchat_auto_email_transcript_delay'] = in_array($input['mxchat_auto_email_transcript_delay'], $valid_delays)
1548 ? $input['mxchat_auto_email_transcript_delay']
1549 : '30';
1550 } else {
1551 $sanitized['mxchat_auto_email_transcript_delay'] = '30';
1552 }
1553
1554 // Sanitize require contact info setting
1555 $sanitized['mxchat_auto_email_transcript_require_contact'] = isset($input['mxchat_auto_email_transcript_require_contact']) ? 1 : 0;
1556
1557 return $sanitized;
1558 }
1559
1560 /**
1561 * Schedule or unschedule the transcript cleanup cron job
1562 */
1563 public function schedule_transcript_cleanup($interval) {
1564 // Clear any existing scheduled event
1565 $timestamp = wp_next_scheduled('mxchat_cleanup_old_transcripts');
1566 if ($timestamp) {
1567 wp_unschedule_event($timestamp, 'mxchat_cleanup_old_transcripts');
1568 }
1569
1570 // Schedule new event whenever retention is active. "active" is the canonical
1571 // value passed by sanitize_transcripts_options when either the dropdown != never
1572 // OR the custom retention-days > 0; "never" turns the cron off. Any other value
1573 // (the legacy "1week" / "2weeks" / "1month" strings) is also treated as active.
1574 if ($interval !== 'never') {
1575 // Schedule to run daily at 3 AM
1576 $next_run = strtotime('tomorrow 3:00 AM');
1577 wp_schedule_event($next_run, 'daily', 'mxchat_cleanup_old_transcripts');
1578 }
1579 }
1580
1581 /**
1582 * Delete old transcripts based on the configured interval
1583 */
1584 public function cleanup_old_transcripts() {
1585 $options = get_option('mxchat_transcripts_options', array());
1586 $interval = isset($options['mxchat_auto_delete_transcripts']) ? $options['mxchat_auto_delete_transcripts'] : 'never';
1587 $custom_days = isset($options['mxchat_retention_days']) ? (int) $options['mxchat_retention_days'] : 0;
1588
1589 // Custom retention-days (plan-9b80b1) takes precedence over the bucket dropdown.
1590 $days = 0;
1591 if ($custom_days > 0) {
1592 $days = $custom_days;
1593 } else {
1594 switch ($interval) {
1595 case '1week': $days = 7; break;
1596 case '2weeks': $days = 14; break;
1597 case '1month': $days = 30; break;
1598 case 'never':
1599 default:
1600 $days = 0;
1601 }
1602 }
1603
1604 // Devs can override the final day count programmatically.
1605 $days = (int) apply_filters('mxchat_transcript_retention_days', $days);
1606
1607 if ($days <= 0) {
1608 return; // Retention disabled — bail.
1609 }
1610
1611 global $wpdb;
1612 $transcripts_table = $wpdb->prefix . 'mxchat_chat_transcripts';
1613 $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
1614 $url_clicks_table = $wpdb->prefix . 'mxchat_url_clicks';
1615
1616 // Defensive: if the main table doesn't exist (fresh-ish install), bail.
1617 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $transcripts_table)) !== $transcripts_table) {
1618 return;
1619 }
1620
1621 $cutoff_date = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
1622
1623 // Cap at 5000 session_ids per run so a large unattended site doesn't OOM —
1624 // the cron will pick up where it left off on the next tick (within hours).
1625 $batch_cap = (int) apply_filters('mxchat_transcript_retention_batch_cap', 5000);
1626
1627 $sessions_to_delete = $wpdb->get_col(
1628 $wpdb->prepare(
1629 "SELECT DISTINCT session_id FROM {$transcripts_table} WHERE timestamp IS NOT NULL AND timestamp < %s LIMIT %d",
1630 $cutoff_date,
1631 $batch_cap
1632 )
1633 );
1634
1635 if (empty($sessions_to_delete)) {
1636 return;
1637 }
1638
1639 $placeholders = implode(',', array_fill(0, count($sessions_to_delete), '%s'));
1640
1641 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1642 // $placeholders is a server-built list of literal "%s" tokens.
1643 $deleted_transcripts = (int) $wpdb->query(
1644 $wpdb->prepare(
1645 "DELETE FROM {$transcripts_table} WHERE session_id IN ($placeholders)",
1646 $sessions_to_delete
1647 )
1648 );
1649
1650 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $translations_table)) === $translations_table) {
1651 $wpdb->query(
1652 $wpdb->prepare(
1653 "DELETE FROM {$translations_table} WHERE session_id IN ($placeholders)",
1654 $sessions_to_delete
1655 )
1656 );
1657 }
1658
1659 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $url_clicks_table)) === $url_clicks_table) {
1660 $wpdb->query(
1661 $wpdb->prepare(
1662 "DELETE FROM {$url_clicks_table} WHERE session_id IN ($placeholders)",
1663 $sessions_to_delete
1664 )
1665 );
1666 }
1667 // phpcs:enable
1668
1669 update_option('mxchat_retention_last_swept_at', time(), false);
1670 update_option('mxchat_retention_rows_last_deleted', $deleted_transcripts, false);
1671 }
1672
1673 public function export_chat_transcripts() {
1674 if (!current_user_can('manage_options')) {
1675 wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'mxchat'));
1676 }
1677
1678 check_ajax_referer('mxchat_export_transcripts', 'security');
1679
1680 global $wpdb;
1681 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1682
1683 // Get all transcripts ordered by session and timestamp
1684 $results = $wpdb->get_results(
1685 "SELECT session_id, user_email, user_identifier, role, message, timestamp
1686 FROM {$table_name}
1687 ORDER BY session_id, timestamp ASC"
1688 );
1689
1690 if (empty($results)) {
1691 wp_send_json_error(array('message' => 'No transcripts found.'));
1692 wp_die();
1693 }
1694
1695 // Set headers for CSV download
1696 header('Content-Type: text/csv');
1697 header('Content-Disposition: attachment; filename="chat-transcripts-' . date('Y-m-d') . '.csv"');
1698 header('Pragma: no-cache');
1699 header('Expires: 0');
1700
1701 // Create output stream
1702 $output = fopen('php://output', 'w');
1703
1704 // Add UTF-8 BOM for proper Excel encoding
1705 fputs($output, "\xEF\xBB\xBF");
1706
1707 // Add CSV headers
1708 fputcsv($output, array(
1709 'Session ID',
1710 'Email',
1711 'User Identifier',
1712 'Role',
1713 'Message',
1714 'Timestamp'
1715 ));
1716
1717 // Add data rows
1718 foreach ($results as $row) {
1719 fputcsv($output, array(
1720 $row->session_id,
1721 $row->user_email,
1722 $row->user_identifier,
1723 $row->role,
1724 $row->message,
1725 $row->timestamp
1726 ));
1727 }
1728
1729 fclose($output);
1730 wp_die();
1731 }
1732
1733 // ============================================================================
1734 // Leads tab (inside Transcripts)
1735 //
1736 // Leads are derived from existing data — no dedicated table. Primary source:
1737 // wp_mxchat_chat_transcripts rows where user_email is populated. Secondary
1738 // source: wp_options entries `mxchat_email_{session_id}` / `mxchat_name_{sid}`
1739 // for "orphan" leads who submitted the pre-chat form but never chatted.
1740 // ============================================================================
1741
1742 /**
1743 * Fetch leads: dedup-by-email rows, stats strip, and top pages in one call.
1744 */
1745 public function mxchat_fetch_leads() {
1746 if (!current_user_can('manage_options')) {
1747 wp_send_json_error(['message' => 'Insufficient permissions']);
1748 wp_die();
1749 }
1750
1751 global $wpdb;
1752 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
1753
1754 $page = isset($_POST['page']) ? max(1, absint($_POST['page'])) : 1;
1755 $per_page = isset($_POST['per_page']) ? min(100, max(10, absint($_POST['per_page']))) : 25;
1756 $offset = ($page - 1) * $per_page;
1757 $search = isset($_POST['search']) ? sanitize_text_field(wp_unslash($_POST['search'])) : '';
1758 $date_range = isset($_POST['date_range']) ? sanitize_key($_POST['date_range']) : 'all';
1759 $status = isset($_POST['status']) ? sanitize_key($_POST['status']) : 'all';
1760 $page_filter = isset($_POST['page_url']) ? esc_url_raw(wp_unslash($_POST['page_url'])) : '';
1761 $sort = isset($_POST['sort']) ? sanitize_key($_POST['sort']) : 'last_seen';
1762 $sort_dir = (isset($_POST['sort_dir']) && $_POST['sort_dir'] === 'asc') ? 'ASC' : 'DESC';
1763
1764 $date_cutoff = self::mxchat_leads_date_cutoff($date_range);
1765 $has_page_url_column = !empty($wpdb->get_results("SHOW COLUMNS FROM $table LIKE 'originating_page_url'"));
1766
1767 // Base WHERE for transcripts leads.
1768 $where_clauses = ["user_email IS NOT NULL", "user_email != ''"];
1769 $where_params = [];
1770
1771 if ($date_cutoff) {
1772 $where_clauses[] = 'timestamp >= %s';
1773 $where_params[] = $date_cutoff;
1774 }
1775 if ($page_filter && $has_page_url_column) {
1776 $where_clauses[] = 'originating_page_url = %s';
1777 $where_params[] = $page_filter;
1778 }
1779 if ($search !== '') {
1780 $like = '%' . $wpdb->esc_like($search) . '%';
1781 $where_clauses[] = '(user_email LIKE %s OR user_name LIKE %s)';
1782 $where_params[] = $like;
1783 $where_params[] = $like;
1784 }
1785 $where_sql = 'WHERE ' . implode(' AND ', $where_clauses);
1786
1787 // Aggregate query grouped by email.
1788 $select_sql = $has_page_url_column
1789 ? "SELECT user_email, MAX(timestamp) AS last_seen, MIN(timestamp) AS first_seen,
1790 COUNT(DISTINCT session_id) AS conversation_count"
1791 : "SELECT user_email, MAX(timestamp) AS last_seen, MIN(timestamp) AS first_seen,
1792 COUNT(DISTINCT session_id) AS conversation_count";
1793
1794 $order_column = in_array($sort, ['last_seen', 'conversation_count', 'first_seen'], true) ? $sort : 'last_seen';
1795 $group_order_limit = " GROUP BY user_email ORDER BY {$order_column} {$sort_dir} LIMIT %d OFFSET %d";
1796
1797 $transcripts_sql = $wpdb->prepare(
1798 "{$select_sql} FROM {$table} {$where_sql}{$group_order_limit}",
1799 array_merge($where_params, [$per_page, $offset])
1800 );
1801 $transcript_rows = $wpdb->get_results($transcripts_sql);
1802
1803 // Count of unique transcript-based leads under the same filters.
1804 $count_sql = $wpdb->prepare(
1805 "SELECT COUNT(DISTINCT user_email) FROM {$table} {$where_sql}",
1806 $where_params
1807 );
1808 $transcripts_lead_count = (int) $wpdb->get_var($count_sql);
1809
1810 // Hydrate each row: name, latest_session_id, top page.
1811 $leads = [];
1812 foreach ($transcript_rows as $row) {
1813 $detail = $has_page_url_column
1814 ? $wpdb->get_row($wpdb->prepare(
1815 "SELECT session_id, user_name, originating_page_url, originating_page_title
1816 FROM {$table} WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
1817 $row->user_email
1818 ))
1819 : $wpdb->get_row($wpdb->prepare(
1820 "SELECT session_id, user_name FROM {$table}
1821 WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
1822 $row->user_email
1823 ));
1824
1825 $leads[] = [
1826 'email' => $row->user_email,
1827 'name' => isset($detail->user_name) ? (string) $detail->user_name : '',
1828 'conversation_count' => (int) $row->conversation_count,
1829 'last_seen' => $row->last_seen,
1830 'last_seen_display' => self::mxchat_leads_format_relative($row->last_seen),
1831 'first_seen' => $row->first_seen,
1832 'latest_session_id' => isset($detail->session_id) ? $detail->session_id : '',
1833 'top_page_url' => isset($detail->originating_page_url) ? $detail->originating_page_url : '',
1834 'top_page_title' => isset($detail->originating_page_title) ? $detail->originating_page_title : '',
1835 'is_orphan' => false,
1836 'status' => 'active',
1837 ];
1838 }
1839
1840 // Non-transcript lead sources. Built once here, then filtered/merged based on the
1841 // status filter below. Deduplication priority when the same email appears in multiple
1842 // sources: transcripts > chat_deleted > orphan.
1843 $transcripts_emails_seen = array_flip(array_map(
1844 function ($r) { return strtolower($r['email']); },
1845 $leads
1846 ));
1847
1848 // Chat-deleted leads: had a conversation that an admin removed. Preserved via
1849 // mxchat_lead_del_* options, with timestamps so they respect date filters.
1850 $chat_deleted_leads_all = [];
1851 if ($status === 'all' || $status === 'chat_deleted') {
1852 $chat_deleted_leads_all = self::mxchat_collect_chat_deleted_leads($search, $date_cutoff);
1853 // Dedup: drop any chat_deleted row whose email is already in the transcripts set.
1854 $chat_deleted_leads_all = array_values(array_filter(
1855 $chat_deleted_leads_all,
1856 function ($row) use ($transcripts_emails_seen) {
1857 return !isset($transcripts_emails_seen[strtolower($row['email'])]);
1858 }
1859 ));
1860 foreach ($chat_deleted_leads_all as $row) {
1861 $transcripts_emails_seen[strtolower($row['email'])] = true;
1862 }
1863 }
1864
1865 // Orphans: pre-chat form captures with no conversation. No timestamp, so skipped
1866 // when a date filter is active.
1867 $orphan_leads_all = [];
1868 if (($status === 'all' || $status === 'orphan') && !$date_cutoff && !$page_filter) {
1869 $orphan_leads_all = self::mxchat_collect_orphan_leads($search);
1870 $orphan_leads_all = array_values(array_filter(
1871 $orphan_leads_all,
1872 function ($row) use ($transcripts_emails_seen) {
1873 return !isset($transcripts_emails_seen[strtolower($row['email'])]);
1874 }
1875 ));
1876 }
1877
1878 // Apply status filter to the transcripts-derived list.
1879 if ($status === 'orphan' || $status === 'chat_deleted') {
1880 $leads = [];
1881 $transcripts_lead_count = 0;
1882 }
1883
1884 // Stitch the current page from the three buckets in priority order.
1885 $total_count = $transcripts_lead_count + count($chat_deleted_leads_all) + count($orphan_leads_all);
1886 $remaining_slots = $per_page - count($leads);
1887
1888 if ($remaining_slots > 0 && !empty($chat_deleted_leads_all)) {
1889 $start = max(0, ($page - 1) * $per_page - $transcripts_lead_count);
1890 if ($start < count($chat_deleted_leads_all)) {
1891 $leads = array_merge($leads, array_slice($chat_deleted_leads_all, $start, $remaining_slots));
1892 $remaining_slots = $per_page - count($leads);
1893 }
1894 }
1895
1896 if ($remaining_slots > 0 && !empty($orphan_leads_all)) {
1897 $before = $transcripts_lead_count + count($chat_deleted_leads_all);
1898 $start = max(0, ($page - 1) * $per_page - $before);
1899 if ($start < count($orphan_leads_all)) {
1900 $leads = array_merge($leads, array_slice($orphan_leads_all, $start, $remaining_slots));
1901 }
1902 }
1903
1904 $total_pages = $per_page > 0 ? (int) ceil($total_count / $per_page) : 1;
1905
1906 // Stats strip: always computed over full dataset, unaffected by filters.
1907 $stats = self::mxchat_leads_stats($table, $has_page_url_column);
1908
1909 // Top pages: top 5 by distinct emails captured.
1910 $top_pages = [];
1911 if ($has_page_url_column) {
1912 $top_pages_rows = $wpdb->get_results(
1913 "SELECT originating_page_url AS url,
1914 MAX(originating_page_title) AS title,
1915 COUNT(DISTINCT user_email) AS lead_count
1916 FROM {$table}
1917 WHERE user_email IS NOT NULL AND user_email != ''
1918 AND originating_page_url IS NOT NULL AND originating_page_url != ''
1919 GROUP BY originating_page_url
1920 ORDER BY lead_count DESC, url ASC
1921 LIMIT 5"
1922 );
1923 foreach ($top_pages_rows as $p) {
1924 $top_pages[] = [
1925 'url' => $p->url,
1926 'title' => $p->title ?: $p->url,
1927 'lead_count' => (int) $p->lead_count,
1928 ];
1929 }
1930 }
1931
1932 wp_send_json([
1933 'success' => true,
1934 'leads' => $leads,
1935 'page' => $page,
1936 'per_page' => $per_page,
1937 'total_count' => $total_count,
1938 'total_pages' => $total_pages,
1939 'showing_start' => $total_count === 0 ? 0 : ($offset + 1),
1940 'showing_end' => min($offset + $per_page, $total_count),
1941 'stats' => $stats,
1942 'top_pages' => $top_pages,
1943 ]);
1944 wp_die();
1945 }
1946
1947 /**
1948 * Delete one or more leads by email. Removes every transcripts row for that
1949 * email and cleans up related wp_options (mxchat_email_{sid}, mxchat_name_{sid},
1950 * mxchat_history_{sid}) and any orphan option entries matching the email.
1951 */
1952 public function mxchat_delete_leads() {
1953 if (!current_user_can('manage_options')) {
1954 wp_send_json_error(['message' => 'Insufficient permissions']);
1955 wp_die();
1956 }
1957 check_ajax_referer('mxchat_delete_leads', 'security');
1958
1959 $emails_raw = isset($_POST['emails']) ? (array) wp_unslash($_POST['emails']) : [];
1960 $emails = [];
1961 foreach ($emails_raw as $e) {
1962 $clean = sanitize_email((string) $e);
1963 if ($clean) {
1964 $emails[] = $clean;
1965 }
1966 }
1967 if (empty($emails)) {
1968 wp_send_json_error(['message' => 'No emails provided']);
1969 wp_die();
1970 }
1971
1972 $summary = self::mxchat_wipe_leads_by_email($emails);
1973
1974 wp_send_json([
1975 'success' => true,
1976 'deleted_leads' => count($emails),
1977 'deleted_sessions' => $summary['deleted_sessions'],
1978 'deleted_rows' => $summary['deleted_rows'],
1979 ]);
1980 wp_die();
1981 }
1982
1983 /**
1984 * Fully wipe one or more leads by email: every transcripts row, every related wp_options
1985 * entry (history, pre-chat capture, chat_deleted preservation, agent name, translations).
1986 *
1987 * Shared between the Leads-tab Delete button and the transcript-delete opt-in checkbox.
1988 * Input emails must already be sanitized with sanitize_email().
1989 */
1990 private static function mxchat_wipe_leads_by_email(array $emails) {
1991 global $wpdb;
1992 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
1993 $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
1994 $has_translations = $wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table;
1995
1996 $deleted_sessions = 0;
1997 $deleted_rows = 0;
1998 $emails_lc = array_map('strtolower', $emails);
1999
2000 foreach ($emails as $email) {
2001 $session_ids = $wpdb->get_col($wpdb->prepare(
2002 "SELECT DISTINCT session_id FROM {$table} WHERE user_email = %s",
2003 $email
2004 ));
2005
2006 $rows_removed = $wpdb->delete($table, ['user_email' => $email], ['%s']);
2007 if ($rows_removed !== false) {
2008 $deleted_rows += (int) $rows_removed;
2009 }
2010
2011 foreach ($session_ids as $sid) {
2012 $deleted_sessions++;
2013 wp_cache_delete('chat_session_' . $sid, 'mxchat_chat_sessions');
2014 delete_option('mxchat_history_' . $sid);
2015 delete_option('mxchat_email_' . $sid);
2016 delete_option('mxchat_name_' . $sid);
2017 delete_option('mxchat_agent_name_' . $sid);
2018 delete_option('mxchat_lead_del_email_' . $sid);
2019 delete_option('mxchat_lead_del_name_' . $sid);
2020 delete_option('mxchat_lead_del_ts_' . $sid);
2021 if ($has_translations) {
2022 $wpdb->delete($translations_table, ['session_id' => $sid], ['%s']);
2023 }
2024 }
2025 }
2026
2027 // Clean up any lingering option entries (orphan pre-chat captures + chat_deleted
2028 // preservations) whose stored value matches one of the emails being wiped.
2029 $lingering = $wpdb->get_results(
2030 "SELECT option_name, option_value FROM {$wpdb->options}
2031 WHERE option_name LIKE 'mxchat_email_%' OR option_name LIKE 'mxchat_lead_del_email_%'"
2032 );
2033 foreach ($lingering as $opt) {
2034 if (!in_array(strtolower(trim($opt->option_value)), $emails_lc, true)) {
2035 continue;
2036 }
2037 if (strpos($opt->option_name, 'mxchat_lead_del_email_') === 0) {
2038 $sid = substr($opt->option_name, strlen('mxchat_lead_del_email_'));
2039 delete_option('mxchat_lead_del_email_' . $sid);
2040 delete_option('mxchat_lead_del_name_' . $sid);
2041 delete_option('mxchat_lead_del_ts_' . $sid);
2042 } else {
2043 $sid = substr($opt->option_name, strlen('mxchat_email_'));
2044 delete_option('mxchat_email_' . $sid);
2045 delete_option('mxchat_name_' . $sid);
2046 }
2047 }
2048
2049 wp_cache_delete('all_chat_sessions', 'mxchat_chat_sessions');
2050
2051 return [
2052 'deleted_sessions' => $deleted_sessions,
2053 'deleted_rows' => $deleted_rows,
2054 ];
2055 }
2056
2057 /**
2058 * Stream a leads CSV. scope=all exports every lead under current filters is not
2059 * supported to keep semantics simple; caller either exports all leads or a
2060 * specific set of selected emails.
2061 */
2062 public function mxchat_export_leads() {
2063 if (!current_user_can('manage_options')) {
2064 wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'mxchat'));
2065 }
2066 check_ajax_referer('mxchat_export_leads', 'security');
2067
2068 $scope = isset($_POST['scope']) ? sanitize_key($_POST['scope']) : 'all';
2069 $fields_mode = isset($_POST['fields']) ? sanitize_key($_POST['fields']) : 'email_and_name';
2070 $emails_in = isset($_POST['emails']) ? (array) wp_unslash($_POST['emails']) : [];
2071
2072 $emails_in_clean = [];
2073 foreach ($emails_in as $e) {
2074 $clean = sanitize_email((string) $e);
2075 if ($clean) {
2076 $emails_in_clean[] = $clean;
2077 }
2078 }
2079
2080 global $wpdb;
2081 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2082 $has_page_url_column = !empty($wpdb->get_results("SHOW COLUMNS FROM $table LIKE 'originating_page_url'"));
2083
2084 // Collect leads from transcripts.
2085 $transcripts_sql = "SELECT user_email AS email,
2086 MAX(timestamp) AS last_seen,
2087 COUNT(DISTINCT session_id) AS conversation_count
2088 FROM {$table}
2089 WHERE user_email IS NOT NULL AND user_email != ''";
2090 $params = [];
2091 if ($scope === 'selected' && !empty($emails_in_clean)) {
2092 $placeholders = implode(',', array_fill(0, count($emails_in_clean), '%s'));
2093 $transcripts_sql .= " AND user_email IN ({$placeholders})";
2094 $params = $emails_in_clean;
2095 }
2096 $transcripts_sql .= " GROUP BY user_email ORDER BY last_seen DESC";
2097
2098 $rows = !empty($params)
2099 ? $wpdb->get_results($wpdb->prepare($transcripts_sql, $params))
2100 : $wpdb->get_results($transcripts_sql);
2101
2102 // Hydrate each row with name + top page.
2103 $export_rows = [];
2104 foreach ($rows as $row) {
2105 $detail = $has_page_url_column
2106 ? $wpdb->get_row($wpdb->prepare(
2107 "SELECT user_name, originating_page_url FROM {$table}
2108 WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
2109 $row->email
2110 ))
2111 : $wpdb->get_row($wpdb->prepare(
2112 "SELECT user_name FROM {$table}
2113 WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
2114 $row->email
2115 ));
2116 $export_rows[] = [
2117 'email' => $row->email,
2118 'name' => isset($detail->user_name) ? (string) $detail->user_name : '',
2119 'conversation_count' => (int) $row->conversation_count,
2120 'last_seen' => $row->last_seen,
2121 'top_page_url' => isset($detail->originating_page_url) ? $detail->originating_page_url : '',
2122 ];
2123 }
2124
2125 // Include orphan + chat_deleted leads when exporting all.
2126 if ($scope === 'all') {
2127 $transcripts_emails_lc = array_flip(array_map(
2128 function ($r) { return strtolower($r['email']); },
2129 $export_rows
2130 ));
2131 foreach (self::mxchat_collect_chat_deleted_leads('') as $cd) {
2132 if (isset($transcripts_emails_lc[strtolower($cd['email'])])) continue;
2133 $transcripts_emails_lc[strtolower($cd['email'])] = true;
2134 $export_rows[] = [
2135 'email' => $cd['email'],
2136 'name' => $cd['name'],
2137 'conversation_count' => 0,
2138 'last_seen' => $cd['last_seen'],
2139 'top_page_url' => '',
2140 ];
2141 }
2142 foreach (self::mxchat_collect_orphan_leads('') as $orphan) {
2143 if (isset($transcripts_emails_lc[strtolower($orphan['email'])])) continue;
2144 $transcripts_emails_lc[strtolower($orphan['email'])] = true;
2145 $export_rows[] = [
2146 'email' => $orphan['email'],
2147 'name' => $orphan['name'],
2148 'conversation_count' => 0,
2149 'last_seen' => '',
2150 'top_page_url' => '',
2151 ];
2152 }
2153 }
2154
2155 if (empty($export_rows)) {
2156 wp_send_json_error(['message' => 'No leads to export.']);
2157 wp_die();
2158 }
2159
2160 $filename = 'mxchat-leads-' . date('Y-m-d') . '.csv';
2161 header('Content-Type: text/csv');
2162 header('Content-Disposition: attachment; filename="' . $filename . '"');
2163 header('Pragma: no-cache');
2164 header('Expires: 0');
2165
2166 $output = fopen('php://output', 'w');
2167 fputs($output, "\xEF\xBB\xBF"); // UTF-8 BOM for Excel
2168
2169 if ($fields_mode === 'email_only') {
2170 fputcsv($output, ['Email']);
2171 foreach ($export_rows as $r) {
2172 fputcsv($output, [$r['email']]);
2173 }
2174 } else {
2175 fputcsv($output, ['Email', 'Name', 'Conversations', 'Last seen', 'Top page']);
2176 foreach ($export_rows as $r) {
2177 fputcsv($output, [
2178 $r['email'],
2179 $r['name'],
2180 $r['conversation_count'],
2181 $r['last_seen'],
2182 $r['top_page_url'],
2183 ]);
2184 }
2185 }
2186
2187 fclose($output);
2188 wp_die();
2189 }
2190
2191 /**
2192 * Stats strip payload (independent of filters).
2193 */
2194 private static function mxchat_leads_stats($table, $has_page_url_column) {
2195 global $wpdb;
2196
2197 $total_transcripts_emails = (int) $wpdb->get_var(
2198 "SELECT COUNT(DISTINCT user_email) FROM {$table}
2199 WHERE user_email IS NOT NULL AND user_email != ''"
2200 );
2201
2202 $new_this_week = (int) $wpdb->get_var($wpdb->prepare(
2203 "SELECT COUNT(*) FROM (
2204 SELECT user_email FROM {$table}
2205 WHERE user_email IS NOT NULL AND user_email != ''
2206 GROUP BY user_email
2207 HAVING MIN(timestamp) >= %s
2208 ) AS new_leads",
2209 gmdate('Y-m-d H:i:s', strtotime('-7 days'))
2210 ));
2211
2212 $total_convos = (int) $wpdb->get_var(
2213 "SELECT COUNT(DISTINCT session_id) FROM {$table}
2214 WHERE user_email IS NOT NULL AND user_email != ''"
2215 );
2216
2217 $orphan_count = count(self::mxchat_collect_orphan_leads(''));
2218 $chat_deleted_count = self::mxchat_count_chat_deleted_leads();
2219
2220 // Total leads = unique emails across all three sources (dedup priority: transcripts > chat_deleted > orphan
2221 // is already enforced at collection time in mxchat_fetch_leads; stats re-apply it here).
2222 $total_leads = $total_transcripts_emails + $chat_deleted_count + $orphan_count;
2223
2224 $avg = $total_transcripts_emails > 0
2225 ? round($total_convos / $total_transcripts_emails, 1)
2226 : 0;
2227
2228 // Orphan % reflects *true* orphans only (pre-chat dropoffs). Chat-deleted leads are
2229 // excluded so the metric stays meaningful — admins shouldn't see their cleanups
2230 // inflate this number.
2231 $orphan_pct = $total_leads > 0
2232 ? (int) round(($orphan_count / $total_leads) * 100)
2233 : 0;
2234
2235 return [
2236 'total_leads' => $total_leads,
2237 'new_this_week' => $new_this_week,
2238 'avg_convos' => $avg,
2239 'orphan_pct' => $orphan_pct,
2240 'orphan_count' => $orphan_count,
2241 'chat_deleted_count' => $chat_deleted_count,
2242 ];
2243 }
2244
2245 /**
2246 * Collect leads who had a conversation that an admin later deleted (preserved via
2247 * mxchat_lead_del_* options). Returns rows tagged status='chat_deleted' with the
2248 * original last-seen timestamp so they still sort and filter sensibly.
2249 *
2250 * @param string $search Optional email/name substring filter.
2251 * @param string $date_cutoff Optional 'Y-m-d H:i:s' cutoff — only rows with last_ts >= cutoff.
2252 * @return array
2253 */
2254 private static function mxchat_collect_chat_deleted_leads($search = '', $date_cutoff = '') {
2255 global $wpdb;
2256 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2257
2258 $rows = $wpdb->get_results(
2259 "SELECT option_name, option_value FROM {$wpdb->options}
2260 WHERE option_name LIKE 'mxchat_lead_del_email_%'"
2261 );
2262 if (empty($rows)) {
2263 return [];
2264 }
2265
2266 // Emails that currently have transcripts rows should not appear as chat_deleted —
2267 // they've come back and chatted, so they're active leads again.
2268 $emails_in_transcripts = array_map(
2269 'strtolower',
2270 (array) $wpdb->get_col(
2271 "SELECT DISTINCT user_email FROM {$table}
2272 WHERE user_email IS NOT NULL AND user_email != ''"
2273 )
2274 );
2275 $emails_in_transcripts = array_flip($emails_in_transcripts);
2276
2277 $needle = strtolower(trim((string) $search));
2278 $by_email = [];
2279
2280 foreach ($rows as $opt) {
2281 $email = sanitize_email(trim((string) $opt->option_value));
2282 if (!$email) {
2283 continue;
2284 }
2285 if (isset($emails_in_transcripts[strtolower($email)])) {
2286 continue;
2287 }
2288 $sid = substr($opt->option_name, strlen('mxchat_lead_del_email_'));
2289 if (!$sid) {
2290 continue;
2291 }
2292 $name = (string) get_option('mxchat_lead_del_name_' . $sid, '');
2293 $ts = (string) get_option('mxchat_lead_del_ts_' . $sid, '');
2294
2295 if ($date_cutoff !== '' && ($ts === '' || $ts < $date_cutoff)) {
2296 continue;
2297 }
2298 if ($needle !== '') {
2299 $hay = strtolower($email . ' ' . $name);
2300 if (strpos($hay, $needle) === false) {
2301 continue;
2302 }
2303 }
2304
2305 $key = strtolower($email);
2306 if (!isset($by_email[$key]) || (isset($by_email[$key]['last_seen']) && $ts > $by_email[$key]['last_seen'])) {
2307 $by_email[$key] = [
2308 'email' => $email,
2309 'name' => $name,
2310 'conversation_count' => 0,
2311 'last_seen' => $ts,
2312 'last_seen_display' => $ts ? self::mxchat_leads_format_relative($ts) : __('Chat deleted', 'mxchat'),
2313 'first_seen' => $ts,
2314 'latest_session_id' => '',
2315 'top_page_url' => '',
2316 'top_page_title' => '',
2317 'is_orphan' => false,
2318 'status' => 'chat_deleted',
2319 ];
2320 }
2321 }
2322
2323 // Newest chat_deleted first.
2324 usort($by_email, function ($a, $b) {
2325 return strcmp((string) $b['last_seen'], (string) $a['last_seen']);
2326 });
2327 return array_values($by_email);
2328 }
2329
2330 /**
2331 * Count unique emails preserved as "chat deleted" (for the stats strip).
2332 */
2333 private static function mxchat_count_chat_deleted_leads() {
2334 global $wpdb;
2335 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2336
2337 $emails = $wpdb->get_col(
2338 "SELECT DISTINCT option_value FROM {$wpdb->options}
2339 WHERE option_name LIKE 'mxchat_lead_del_email_%'"
2340 );
2341 if (empty($emails)) {
2342 return 0;
2343 }
2344
2345 $transcripts_emails = array_map(
2346 'strtolower',
2347 (array) $wpdb->get_col(
2348 "SELECT DISTINCT user_email FROM {$table}
2349 WHERE user_email IS NOT NULL AND user_email != ''"
2350 )
2351 );
2352 $transcripts_emails = array_flip($transcripts_emails);
2353
2354 $count = 0;
2355 $seen = [];
2356 foreach ($emails as $raw) {
2357 $email = strtolower(trim((string) $raw));
2358 if (!$email || isset($seen[$email]) || isset($transcripts_emails[$email])) {
2359 continue;
2360 }
2361 $seen[$email] = true;
2362 $count++;
2363 }
2364 return $count;
2365 }
2366
2367 /**
2368 * Find leads who submitted the pre-chat form but never produced a transcripts row.
2369 * Returned rows have no conversation_count, no timestamp.
2370 *
2371 * @param string $search Optional email/name substring filter.
2372 * @return array
2373 */
2374 private static function mxchat_collect_orphan_leads($search = '') {
2375 global $wpdb;
2376 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2377
2378 $option_rows = $wpdb->get_results(
2379 "SELECT option_name, option_value FROM {$wpdb->options}
2380 WHERE option_name LIKE 'mxchat_email_%'"
2381 );
2382 if (empty($option_rows)) {
2383 return [];
2384 }
2385
2386 // Collect all session_ids that have real transcripts rows so we can exclude them.
2387 $session_ids_with_rows = $wpdb->get_col(
2388 "SELECT DISTINCT session_id FROM {$table}
2389 WHERE user_email IS NOT NULL AND user_email != ''"
2390 );
2391 $session_ids_with_rows = array_flip($session_ids_with_rows);
2392
2393 // Seen emails in transcripts (so orphans only include truly never-chatted leads).
2394 $emails_in_transcripts = array_map(
2395 'strtolower',
2396 (array) $wpdb->get_col(
2397 "SELECT DISTINCT user_email FROM {$table}
2398 WHERE user_email IS NOT NULL AND user_email != ''"
2399 )
2400 );
2401 $emails_in_transcripts = array_flip($emails_in_transcripts);
2402
2403 $orphans_by_email = [];
2404 $needle = strtolower(trim((string) $search));
2405
2406 foreach ($option_rows as $opt) {
2407 $email = sanitize_email(trim((string) $opt->option_value));
2408 if (!$email) {
2409 continue;
2410 }
2411 $sid = substr($opt->option_name, strlen('mxchat_email_'));
2412 if (!$sid) {
2413 continue;
2414 }
2415 // Exclude leads who have any transcripts rows (they appear in the main list).
2416 if (isset($emails_in_transcripts[strtolower($email)])) {
2417 continue;
2418 }
2419 if (isset($session_ids_with_rows[$sid])) {
2420 continue;
2421 }
2422
2423 $name_option = get_option('mxchat_name_' . $sid, '');
2424 $name = is_string($name_option) ? trim($name_option) : '';
2425
2426 if ($needle !== '') {
2427 $hay = strtolower($email . ' ' . $name);
2428 if (strpos($hay, $needle) === false) {
2429 continue;
2430 }
2431 }
2432
2433 $key = strtolower($email);
2434 if (!isset($orphans_by_email[$key])) {
2435 $orphans_by_email[$key] = [
2436 'email' => $email,
2437 'name' => $name,
2438 'conversation_count' => 0,
2439 'last_seen' => '',
2440 'last_seen_display' => __('No conversation yet', 'mxchat'),
2441 'first_seen' => '',
2442 'latest_session_id' => '',
2443 'top_page_url' => '',
2444 'top_page_title' => '',
2445 'is_orphan' => true,
2446 'status' => 'orphan',
2447 ];
2448 }
2449 }
2450
2451 return array_values($orphans_by_email);
2452 }
2453
2454 /**
2455 * Map a date_range key to a SQL-comparable cutoff string, or '' for all-time.
2456 */
2457 private static function mxchat_leads_date_cutoff($date_range) {
2458 switch ($date_range) {
2459 case 'today': return gmdate('Y-m-d H:i:s', strtotime('-24 hours'));
2460 case '7d': return gmdate('Y-m-d H:i:s', strtotime('-7 days'));
2461 case '30d': return gmdate('Y-m-d H:i:s', strtotime('-30 days'));
2462 case '90d': return gmdate('Y-m-d H:i:s', strtotime('-90 days'));
2463 case 'all':
2464 default: return '';
2465 }
2466 }
2467
2468 /**
2469 * Turn a UTC timestamp into a short relative display like "2h ago" or "Apr 12".
2470 */
2471 private static function mxchat_leads_format_relative($timestamp) {
2472 if (!$timestamp) {
2473 return '';
2474 }
2475 $ts = strtotime($timestamp . ' UTC');
2476 if (!$ts) {
2477 return '';
2478 }
2479 $diff = time() - $ts;
2480 if ($diff < 60) return __('just now', 'mxchat');
2481 if ($diff < 3600) return floor($diff / 60) . __('m ago', 'mxchat');
2482 if ($diff < 86400) return floor($diff / 3600) . __('h ago', 'mxchat');
2483 if ($diff < 604800) return floor($diff / 86400) . __('d ago', 'mxchat');
2484 return wp_date('M j', $ts);
2485 }
2486
2487 /**
2488 * Handle translation of chat messages via AJAX
2489 */
2490 public function mxchat_translate_messages() {
2491 if (!current_user_can('manage_options')) {
2492 wp_send_json_error(['error' => 'Insufficient permissions']);
2493 wp_die();
2494 }
2495
2496 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
2497 $target_lang = isset($_POST['target_lang']) ? sanitize_text_field($_POST['target_lang']) : 'en';
2498 $messages_json = isset($_POST['messages']) ? wp_unslash($_POST['messages']) : '[]';
2499 $messages = json_decode($messages_json, true);
2500
2501 if (empty($session_id)) {
2502 wp_send_json_error(['error' => 'No session ID provided']);
2503 wp_die();
2504 }
2505
2506 if (empty($messages) || !is_array($messages)) {
2507 wp_send_json_error(['error' => 'No messages to translate']);
2508 wp_die();
2509 }
2510
2511 // Language names for prompting
2512 $languages = [
2513 'en' => 'English',
2514 'es' => 'Spanish',
2515 'fr' => 'French',
2516 'de' => 'German',
2517 'it' => 'Italian',
2518 'pt' => 'Portuguese',
2519 'nl' => 'Dutch',
2520 'ru' => 'Russian',
2521 'zh' => 'Chinese',
2522 'ja' => 'Japanese',
2523 'ko' => 'Korean',
2524 'ar' => 'Arabic',
2525 'hi' => 'Hindi',
2526 'tr' => 'Turkish',
2527 'pl' => 'Polish',
2528 'vi' => 'Vietnamese',
2529 'th' => 'Thai',
2530 'id' => 'Indonesian',
2531 'sv' => 'Swedish',
2532 'da' => 'Danish'
2533 ];
2534
2535 $target_lang_name = isset($languages[$target_lang]) ? $languages[$target_lang] : 'English';
2536
2537 // Build combined text for translation (numbered for parsing)
2538 $numbered_messages = [];
2539 foreach ($messages as $i => $msg) {
2540 $content = isset($msg['content']) ? trim($msg['content']) : '';
2541 if (!empty($content)) {
2542 $numbered_messages[] = "[MSG" . $i . "]" . $content . "[/MSG" . $i . "]";
2543 }
2544 }
2545
2546 if (empty($numbered_messages)) {
2547 wp_send_json_error(['error' => 'No valid messages to translate']);
2548 wp_die();
2549 }
2550
2551 $combined_text = implode("\n\n", $numbered_messages);
2552
2553 // Prepare the translation prompt
2554 $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.";
2555
2556 // Get user's selected model and determine provider
2557 $options = get_option('mxchat_options', []);
2558 $selected_model = $options['model'] ?? 'gpt-5.1-chat-latest';
2559
2560 // Check if using OpenRouter
2561 if ($selected_model === 'openrouter') {
2562 $provider = 'openrouter';
2563 $selected_model = $options['openrouter_selected_model'] ?? '';
2564 $api_key = $options['openrouter_api_key'] ?? '';
2565
2566 if (empty($selected_model)) {
2567 wp_send_json_error(['error' => 'No OpenRouter model selected']);
2568 wp_die();
2569 }
2570 } else {
2571 // Determine provider from model name
2572 $model_parts = explode('-', $selected_model);
2573 $provider = strtolower($model_parts[0]);
2574
2575 // Get the appropriate API key based on provider
2576 $api_key = '';
2577 switch ($provider) {
2578 case 'gpt':
2579 case 'o1':
2580 $api_key = $options['api_key'] ?? '';
2581 break;
2582 case 'claude':
2583 $api_key = $options['claude_api_key'] ?? '';
2584 break;
2585 case 'grok':
2586 $api_key = $options['xai_api_key'] ?? '';
2587 break;
2588 case 'deepseek':
2589 $api_key = $options['deepseek_api_key'] ?? '';
2590 break;
2591 case 'gemini':
2592 $api_key = $options['gemini_api_key'] ?? '';
2593 break;
2594 default:
2595 // Default to OpenAI
2596 $api_key = $options['api_key'] ?? '';
2597 $provider = 'gpt';
2598 break;
2599 }
2600 }
2601
2602 if (empty($api_key)) {
2603 wp_send_json_error(['error' => 'No API key configured for ' . $provider]);
2604 wp_die();
2605 }
2606
2607 // Make API request based on provider
2608 $response = $this->translate_with_provider($provider, $selected_model, $api_key, $system_prompt, $combined_text);
2609
2610 if (is_wp_error($response)) {
2611 wp_send_json_error(['error' => $response->get_error_message()]);
2612 wp_die();
2613 }
2614
2615 // Parse the response to extract translated messages
2616 $translations = [];
2617 foreach ($messages as $msg) {
2618 $index = $msg['index'];
2619 $pattern = '/\[MSG' . $index . '\](.*?)\[\/MSG' . $index . '\]/s';
2620 if (preg_match($pattern, $response, $matches)) {
2621 $translations[] = [
2622 'index' => $index,
2623 'translated' => trim($matches[1])
2624 ];
2625 }
2626 }
2627
2628 // Save translations to database
2629 if (!empty($translations)) {
2630 $this->save_transcript_translation($session_id, $target_lang, $translations);
2631 }
2632
2633 wp_send_json(['success' => true, 'translations' => $translations, 'language' => $target_lang]);
2634 wp_die();
2635 }
2636
2637 /**
2638 * Save transcript translation to database
2639 */
2640 private function save_transcript_translation($session_id, $language_code, $translations) {
2641 global $wpdb;
2642 $table_name = $wpdb->prefix . 'mxchat_transcript_translations';
2643
2644 // Check if table exists, create if not
2645 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") !== $table_name) {
2646 mxchat_create_translations_table();
2647 }
2648
2649 $now = current_time('mysql');
2650 $translations_json = wp_json_encode($translations);
2651
2652 // Use REPLACE to insert or update
2653 $wpdb->query($wpdb->prepare(
2654 "REPLACE INTO $table_name (session_id, language_code, translations, created_at, updated_at)
2655 VALUES (%s, %s, %s, %s, %s)",
2656 $session_id,
2657 $language_code,
2658 $translations_json,
2659 $now,
2660 $now
2661 ));
2662 }
2663
2664 /**
2665 * Get saved translation for a session
2666 */
2667 public function mxchat_get_transcript_translation() {
2668 if (!current_user_can('manage_options')) {
2669 wp_send_json_error(['error' => 'Insufficient permissions']);
2670 wp_die();
2671 }
2672
2673 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
2674
2675 if (empty($session_id)) {
2676 wp_send_json_error(['error' => 'No session ID provided']);
2677 wp_die();
2678 }
2679
2680 global $wpdb;
2681 $table_name = $wpdb->prefix . 'mxchat_transcript_translations';
2682
2683 // Check if table exists
2684 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") !== $table_name) {
2685 wp_send_json(['success' => true, 'has_translation' => false]);
2686 wp_die();
2687 }
2688
2689 // Get the most recent translation for this session
2690 $result = $wpdb->get_row($wpdb->prepare(
2691 "SELECT language_code, translations FROM $table_name WHERE session_id = %s ORDER BY updated_at DESC LIMIT 1",
2692 $session_id
2693 ));
2694
2695 if ($result) {
2696 $translations = json_decode($result->translations, true);
2697 wp_send_json([
2698 'success' => true,
2699 'has_translation' => true,
2700 'language' => $result->language_code,
2701 'translations' => $translations
2702 ]);
2703 } else {
2704 wp_send_json(['success' => true, 'has_translation' => false]);
2705 }
2706 wp_die();
2707 }
2708
2709 /**
2710 * Translate text using the user's selected provider and model
2711 */
2712 private function translate_with_provider($provider, $model, $api_key, $system_prompt, $text) {
2713 switch ($provider) {
2714 case 'claude':
2715 return $this->translate_with_claude($api_key, $model, $system_prompt, $text);
2716 case 'grok':
2717 return $this->translate_with_xai($api_key, $model, $system_prompt, $text);
2718 case 'deepseek':
2719 return $this->translate_with_deepseek($api_key, $model, $system_prompt, $text);
2720 case 'gemini':
2721 return $this->translate_with_gemini($api_key, $model, $system_prompt, $text);
2722 case 'openrouter':
2723 return $this->translate_with_openrouter($api_key, $model, $system_prompt, $text);
2724 case 'gpt':
2725 case 'o1':
2726 default:
2727 return $this->translate_with_openai($api_key, $model, $system_prompt, $text);
2728 }
2729 }
2730
2731 /**
2732 * Translate text using OpenAI API
2733 */
2734 private function translate_with_openai($api_key, $model, $system_prompt, $text) {
2735 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', [
2736 'timeout' => 60,
2737 'headers' => [
2738 'Authorization' => 'Bearer ' . $api_key,
2739 'Content-Type' => 'application/json'
2740 ],
2741 'body' => wp_json_encode([
2742 'model' => $model,
2743 'messages' => [
2744 ['role' => 'system', 'content' => $system_prompt],
2745 ['role' => 'user', 'content' => $text]
2746 ],
2747 'temperature' => 0.3
2748 ])
2749 ]);
2750
2751 if (is_wp_error($response)) {
2752 return $response;
2753 }
2754
2755 $body = json_decode(wp_remote_retrieve_body($response), true);
2756
2757 if (isset($body['error'])) {
2758 return new WP_Error('api_error', $body['error']['message']);
2759 }
2760
2761 if (isset($body['choices'][0]['message']['content'])) {
2762 return $body['choices'][0]['message']['content'];
2763 }
2764
2765 return new WP_Error('api_error', 'Invalid API response');
2766 }
2767
2768 /**
2769 * Translate text using Claude API
2770 */
2771 private function translate_with_claude($api_key, $model, $system_prompt, $text) {
2772 $response = wp_remote_post('https://api.anthropic.com/v1/messages', [
2773 'timeout' => 60,
2774 'headers' => [
2775 'x-api-key' => $api_key,
2776 'anthropic-version' => '2023-06-01',
2777 'Content-Type' => 'application/json'
2778 ],
2779 'body' => wp_json_encode([
2780 'model' => $model,
2781 'max_tokens' => 4096,
2782 'system' => $system_prompt,
2783 'messages' => [
2784 ['role' => 'user', 'content' => $text]
2785 ]
2786 ])
2787 ]);
2788
2789 if (is_wp_error($response)) {
2790 return $response;
2791 }
2792
2793 $body = json_decode(wp_remote_retrieve_body($response), true);
2794
2795 if (isset($body['error'])) {
2796 return new WP_Error('api_error', $body['error']['message']);
2797 }
2798
2799 if (isset($body['content'][0]['text'])) {
2800 return $body['content'][0]['text'];
2801 }
2802
2803 return new WP_Error('api_error', 'Invalid API response');
2804 }
2805
2806 /**
2807 * Translate text using xAI (Grok) API
2808 */
2809 private function translate_with_xai($api_key, $model, $system_prompt, $text) {
2810 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', [
2811 'timeout' => 60,
2812 'headers' => [
2813 'Authorization' => 'Bearer ' . $api_key,
2814 'Content-Type' => 'application/json'
2815 ],
2816 'body' => wp_json_encode([
2817 'model' => $model,
2818 'messages' => [
2819 ['role' => 'system', 'content' => $system_prompt],
2820 ['role' => 'user', 'content' => $text]
2821 ],
2822 'temperature' => 0.3
2823 ])
2824 ]);
2825
2826 if (is_wp_error($response)) {
2827 return $response;
2828 }
2829
2830 $body = json_decode(wp_remote_retrieve_body($response), true);
2831
2832 if (isset($body['error'])) {
2833 return new WP_Error('api_error', $body['error']['message']);
2834 }
2835
2836 if (isset($body['choices'][0]['message']['content'])) {
2837 return $body['choices'][0]['message']['content'];
2838 }
2839
2840 return new WP_Error('api_error', 'Invalid API response');
2841 }
2842
2843 /**
2844 * Translate text using DeepSeek API
2845 */
2846 private function translate_with_deepseek($api_key, $model, $system_prompt, $text) {
2847 $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', [
2848 'timeout' => 60,
2849 'headers' => [
2850 'Authorization' => 'Bearer ' . $api_key,
2851 'Content-Type' => 'application/json'
2852 ],
2853 'body' => wp_json_encode([
2854 'model' => $model,
2855 'messages' => [
2856 ['role' => 'system', 'content' => $system_prompt],
2857 ['role' => 'user', 'content' => $text]
2858 ],
2859 'temperature' => 0.3
2860 ])
2861 ]);
2862
2863 if (is_wp_error($response)) {
2864 return $response;
2865 }
2866
2867 $body = json_decode(wp_remote_retrieve_body($response), true);
2868
2869 if (isset($body['error'])) {
2870 return new WP_Error('api_error', $body['error']['message']);
2871 }
2872
2873 if (isset($body['choices'][0]['message']['content'])) {
2874 return $body['choices'][0]['message']['content'];
2875 }
2876
2877 return new WP_Error('api_error', 'Invalid API response');
2878 }
2879
2880 /**
2881 * Translate text using Google Gemini API
2882 */
2883 private function translate_with_gemini($api_key, $model, $system_prompt, $text) {
2884 $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':generateContent?key=' . $api_key;
2885
2886 $response = wp_remote_post($url, [
2887 'timeout' => 60,
2888 'headers' => [
2889 'Content-Type' => 'application/json'
2890 ],
2891 'body' => wp_json_encode([
2892 'contents' => [
2893 [
2894 'parts' => [
2895 ['text' => $system_prompt . "\n\n" . $text]
2896 ]
2897 ]
2898 ],
2899 'generationConfig' => [
2900 'temperature' => 0.3
2901 ]
2902 ])
2903 ]);
2904
2905 if (is_wp_error($response)) {
2906 return $response;
2907 }
2908
2909 $body = json_decode(wp_remote_retrieve_body($response), true);
2910
2911 if (isset($body['error'])) {
2912 return new WP_Error('api_error', $body['error']['message']);
2913 }
2914
2915 if (isset($body['candidates'][0]['content']['parts'][0]['text'])) {
2916 return $body['candidates'][0]['content']['parts'][0]['text'];
2917 }
2918
2919 return new WP_Error('api_error', 'Invalid API response');
2920 }
2921
2922 /**
2923 * Translate text using OpenRouter API
2924 */
2925 private function translate_with_openrouter($api_key, $model, $system_prompt, $text) {
2926 $response = wp_remote_post('https://openrouter.ai/api/v1/chat/completions', [
2927 'timeout' => 60,
2928 'headers' => [
2929 'Authorization' => 'Bearer ' . $api_key,
2930 'Content-Type' => 'application/json',
2931 'HTTP-Referer' => home_url(),
2932 'X-Title' => 'MxChat Translation'
2933 ],
2934 'body' => wp_json_encode([
2935 'model' => $model,
2936 'messages' => [
2937 ['role' => 'system', 'content' => $system_prompt],
2938 ['role' => 'user', 'content' => $text]
2939 ],
2940 'temperature' => 0.3
2941 ])
2942 ]);
2943
2944 if (is_wp_error($response)) {
2945 return $response;
2946 }
2947
2948 $body = json_decode(wp_remote_retrieve_body($response), true);
2949
2950 if (isset($body['error'])) {
2951 return new WP_Error('api_error', $body['error']['message']);
2952 }
2953
2954 if (isset($body['choices'][0]['message']['content'])) {
2955 return $body['choices'][0]['message']['content'];
2956 }
2957
2958 return new WP_Error('api_error', 'Invalid API response');
2959 }
2960
2961 public function mxchat_fetch_chat_history() {
2962 global $wpdb;
2963 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
2964 $url_clicks_table = $wpdb->prefix . 'mxchat_url_clicks';
2965
2966 if (!current_user_can('manage_options')) {
2967 wp_send_json_error(['message' => 'Insufficient permissions']);
2968 wp_die();
2969 }
2970
2971 $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
2972 $per_page = isset($_POST['per_page']) ? absint($_POST['per_page']) : 50;
2973 $offset = ($page - 1) * $per_page;
2974 $search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
2975 $sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] === 'asc' ? 'ASC' : 'DESC';
2976
2977 // Build search condition
2978 $search_condition = '';
2979 $search_params = [];
2980 if (!empty($search)) {
2981 $search_condition = "WHERE (
2982 session_id LIKE %s
2983 OR user_email LIKE %s
2984 OR user_name LIKE %s
2985 OR user_identifier LIKE %s
2986 OR message LIKE %s
2987 )";
2988 $search_params = array_fill(0, 5, '%' . $wpdb->esc_like($search) . '%');
2989 }
2990
2991 // Get total count
2992 $count_query = !empty($search)
2993 ? $wpdb->prepare("SELECT COUNT(DISTINCT session_id) FROM {$table_name} {$search_condition}", $search_params)
2994 : "SELECT COUNT(DISTINCT session_id) FROM {$table_name}";
2995 $total_sessions = (int) $wpdb->get_var($count_query);
2996
2997 // Get session IDs for current page (sorted by newest or oldest)
2998 $session_query = !empty($search)
2999 ? $wpdb->prepare(
3000 "SELECT DISTINCT t.session_id FROM {$table_name} t {$search_condition}
3001 GROUP BY t.session_id ORDER BY MAX(t.timestamp) {$sort_order} LIMIT %d OFFSET %d",
3002 array_merge($search_params, [$per_page, $offset])
3003 )
3004 : $wpdb->prepare(
3005 "SELECT DISTINCT session_id FROM {$table_name}
3006 GROUP BY session_id ORDER BY MAX(timestamp) {$sort_order} LIMIT %d OFFSET %d",
3007 $per_page, $offset
3008 );
3009 $session_ids = $wpdb->get_col($session_query);
3010
3011 $total_pages = ceil($total_sessions / $per_page);
3012
3013 if (empty($session_ids)) {
3014 wp_send_json([
3015 'success' => true,
3016 'sessions' => [],
3017 'page' => $page,
3018 'total_pages' => 0,
3019 'total_sessions' => 0,
3020 'showing_start' => 0,
3021 'showing_end' => 0
3022 ]);
3023 wp_die();
3024 }
3025
3026 // Check for optional columns/tables
3027 $url_table_exists = $wpdb->get_var("SHOW TABLES LIKE '$url_clicks_table'") === $url_clicks_table;
3028 $originating_columns_exist = !empty($wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'"));
3029
3030 // Build session list data
3031 $sessions = [];
3032 foreach ($session_ids as $session_id) {
3033 // Get session metadata
3034 $session_data = $originating_columns_exist
3035 ? $wpdb->get_row($wpdb->prepare(
3036 "SELECT user_email, user_name, user_identifier, originating_page_url, originating_page_title, timestamp
3037 FROM {$table_name} WHERE session_id = %s ORDER BY timestamp ASC LIMIT 1",
3038 $session_id
3039 ))
3040 : $wpdb->get_row($wpdb->prepare(
3041 "SELECT user_email, user_name, user_identifier, timestamp FROM {$table_name}
3042 WHERE session_id = %s LIMIT 1",
3043 $session_id
3044 ));
3045
3046 // Get message count and latest timestamp
3047 $message_stats = $wpdb->get_row($wpdb->prepare(
3048 "SELECT COUNT(*) as count, MAX(timestamp) as latest FROM {$table_name} WHERE session_id = %s",
3049 $session_id
3050 ));
3051
3052 // Get first user message as preview
3053 $first_user_msg = $wpdb->get_var($wpdb->prepare(
3054 "SELECT message FROM {$table_name} WHERE session_id = %s AND role = 'user' ORDER BY timestamp ASC LIMIT 1",
3055 $session_id
3056 ));
3057 $preview = $first_user_msg ? wp_trim_words(wp_strip_all_tags(stripslashes($first_user_msg)), 12, '...') : 'No messages';
3058
3059 // Build display name
3060 $user_email = !empty($session_data->user_email) ? $session_data->user_email : '';
3061 $user_name = !empty($session_data->user_name) ? $session_data->user_name : '';
3062 $user_identifier = !empty($session_data->user_identifier) ? $session_data->user_identifier : 'Guest';
3063
3064 $display_name = $user_name ?: ($user_email ? explode('@', $user_email)[0] : $user_identifier);
3065 $display_sub = $user_email ?: ('ID: ' . $user_identifier);
3066
3067 // Format time - show relative for recent, date for older
3068 $timestamp = strtotime($message_stats->latest . ' UTC');
3069 $now = time();
3070 $diff = $now - $timestamp;
3071 if ($diff < 3600) {
3072 $time_display = floor($diff / 60) . 'm ago';
3073 } elseif ($diff < 86400) {
3074 $time_display = floor($diff / 3600) . 'h ago';
3075 } elseif ($diff < 604800) {
3076 $time_display = floor($diff / 86400) . 'd ago';
3077 } else {
3078 $time_display = wp_date('M j', $timestamp);
3079 }
3080
3081 // Get initials for avatar
3082 $initials = strtoupper(substr($display_name, 0, 2));
3083 if (strlen($display_name) > 2 && strpos($display_name, ' ') !== false) {
3084 $parts = explode(' ', $display_name);
3085 $initials = strtoupper(substr($parts[0], 0, 1) . substr(end($parts), 0, 1));
3086 }
3087
3088 $sessions[] = [
3089 'session_id' => $session_id,
3090 'display_name' => $display_name,
3091 'display_sub' => $display_sub,
3092 'initials' => $initials,
3093 'preview' => $preview,
3094 'message_count' => (int) $message_stats->count,
3095 'time_display' => $time_display,
3096 'timestamp' => $message_stats->latest
3097 ];
3098 }
3099
3100 wp_send_json([
3101 'success' => true,
3102 'sessions' => $sessions,
3103 'page' => $page,
3104 'total_pages' => $total_pages,
3105 'total_sessions' => $total_sessions,
3106 'showing_start' => $offset + 1,
3107 'showing_end' => min($offset + $per_page, $total_sessions)
3108 ]);
3109 wp_die();
3110 }
3111
3112 /**
3113 * Fetch single conversation details for split-panel view
3114 */
3115 public function mxchat_fetch_conversation() {
3116 global $wpdb;
3117 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
3118 $url_clicks_table = $wpdb->prefix . 'mxchat_url_clicks';
3119
3120 if (!current_user_can('manage_options')) {
3121 wp_send_json_error(['message' => 'Insufficient permissions']);
3122 wp_die();
3123 }
3124
3125 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
3126 if (empty($session_id)) {
3127 wp_send_json_error(['message' => 'No session ID provided']);
3128 wp_die();
3129 }
3130
3131 // Check for optional columns/tables
3132 $url_table_exists = $wpdb->get_var("SHOW TABLES LIKE '$url_clicks_table'") === $url_clicks_table;
3133 $originating_columns_exist = !empty($wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'"));
3134
3135 // Get session metadata
3136 $session_data = $originating_columns_exist
3137 ? $wpdb->get_row($wpdb->prepare(
3138 "SELECT user_email, user_name, user_identifier, originating_page_url, originating_page_title, timestamp
3139 FROM {$table_name} WHERE session_id = %s ORDER BY timestamp ASC LIMIT 1",
3140 $session_id
3141 ))
3142 : $wpdb->get_row($wpdb->prepare(
3143 "SELECT user_email, user_name, user_identifier, timestamp FROM {$table_name}
3144 WHERE session_id = %s LIMIT 1",
3145 $session_id
3146 ));
3147
3148 if (!$session_data) {
3149 wp_send_json_error(['message' => 'Session not found']);
3150 wp_die();
3151 }
3152
3153 // Get clicked URLs
3154 $clicked_urls = [];
3155 if ($url_table_exists) {
3156 $url_clicks = $wpdb->get_results($wpdb->prepare(
3157 "SELECT DISTINCT clicked_url FROM {$url_clicks_table}
3158 WHERE session_id = %s ORDER BY click_timestamp ASC",
3159 $session_id
3160 ));
3161 foreach ($url_clicks as $click) {
3162 $clicked_urls[] = $click->clicked_url;
3163 }
3164 }
3165
3166 // Get all messages
3167 $messages = $wpdb->get_results($wpdb->prepare(
3168 "SELECT * FROM {$table_name} WHERE session_id = %s ORDER BY timestamp ASC",
3169 $session_id
3170 ));
3171
3172 // Build user info
3173 $user_email = !empty($session_data->user_email) ? $session_data->user_email : '';
3174 $user_name = !empty($session_data->user_name) ? $session_data->user_name : '';
3175 $user_identifier = !empty($session_data->user_identifier) ? $session_data->user_identifier : 'Guest';
3176
3177 $display_name = $user_name ?: ($user_email ? explode('@', $user_email)[0] : $user_identifier);
3178 $display_sub = $user_email ?: ('ID: ' . $user_identifier);
3179
3180 // Get initials
3181 $initials = strtoupper(substr($display_name, 0, 2));
3182 if (strlen($display_name) > 2 && strpos($display_name, ' ') !== false) {
3183 $parts = explode(' ', $display_name);
3184 $initials = strtoupper(substr($parts[0], 0, 1) . substr(end($parts), 0, 1));
3185 }
3186
3187 // Page info
3188 $page_url = $originating_columns_exist && !empty($session_data->originating_page_url) ? $session_data->originating_page_url : '';
3189 $page_title = '';
3190 if ($page_url) {
3191 $page_title = !empty($session_data->originating_page_title) ? $session_data->originating_page_title : parse_url($page_url, PHP_URL_PATH);
3192 }
3193
3194 // Format messages for output
3195 $formatted_messages = [];
3196 foreach ($messages as $msg) {
3197 $is_user = ($msg->role === 'user');
3198 $is_bot = ($msg->role === 'bot' || $msg->role === 'assistant');
3199
3200 $content = wp_kses(
3201 stripslashes($msg->message),
3202 [
3203 'b' => [], 'strong' => [], 'i' => [], 'em' => [], 'u' => [],
3204 'br' => [], 'p' => [], 'ul' => [], 'ol' => [], 'li' => [],
3205 'a' => ['href' => [], 'title' => [], 'target' => [], 'class' => []],
3206 'div' => ['class' => [], 'id' => [], 'data-nonce' => []],
3207 'img' => ['src' => [], 'alt' => [], 'class' => []],
3208 'h3' => ['class' => []],
3209 'h4' => ['class' => []],
3210 'button' => ['type' => [], 'class' => [], 'data-product-id' => [], 'data-nonce' => [], 'data-product-type' => [], 'data-original-text' => [], 'data-mxchat-action' => []],
3211 'select' => ['class' => [], 'data-attribute' => []],
3212 'option' => ['value' => []],
3213 'span' => ['class' => []],
3214 'del' => [], 'ins' => [],
3215 ]
3216 );
3217 $formatted_content = $this->format_transcript_message($content);
3218
3219 $has_rag = $is_bot && !empty($msg->rag_context);
3220
3221 $formatted_messages[] = [
3222 'id' => $msg->id,
3223 'role' => $msg->role,
3224 'is_user' => $is_user,
3225 'is_bot' => $is_bot,
3226 'content' => $formatted_content,
3227 'timestamp' => wp_date('g:i A', strtotime($msg->timestamp . ' UTC')),
3228 'full_timestamp' => wp_date('F j, Y g:i A', strtotime($msg->timestamp . ' UTC')),
3229 'has_rag' => $has_rag
3230 ];
3231 }
3232
3233 // First message timestamp for "started" display
3234 $started = !empty($messages) ? wp_date('M j, Y g:i A', strtotime($messages[0]->timestamp . ' UTC')) : '-';
3235
3236 wp_send_json([
3237 'success' => true,
3238 'session_id' => $session_id,
3239 'user' => [
3240 'name' => $display_name,
3241 'sub' => $display_sub,
3242 'initials' => $initials,
3243 'email' => $user_email,
3244 'identifier' => $user_identifier
3245 ],
3246 'page' => [
3247 'url' => $page_url,
3248 'title' => $page_title
3249 ],
3250 'clicked_urls' => $clicked_urls,
3251 'messages' => $formatted_messages,
3252 'message_count' => count($messages),
3253 'started' => $started
3254 ]);
3255 wp_die();
3256 }
3257
3258
3259 /**
3260 * ALTERNATIVE: Simpler helper method using string replacement
3261 */
3262 private function highlight_clicked_links($message_content, $clicked_urls) {
3263 if (empty($clicked_urls)) {
3264 return wp_kses(
3265 $message_content,
3266 [
3267 'b' => [], 'strong' => [], 'i' => [], 'em' => [], 'u' => [],
3268 'br' => [], 'p' => [], 'ul' => [], 'ol' => [], 'li' => [],
3269 'a' => ['href' => [], 'title' => [], 'target' => [], 'class' => []]
3270 ]
3271 );
3272 }
3273
3274 // First apply standard sanitization
3275 $message_content = wp_kses(
3276 $message_content,
3277 [
3278 'b' => [], 'strong' => [], 'i' => [], 'em' => [], 'u' => [],
3279 'br' => [], 'p' => [], 'ul' => [], 'ol' => [], 'li' => [],
3280 'a' => ['href' => [], 'title' => [], 'target' => [], 'class' => []]
3281 ]
3282 );
3283
3284 // Process each clicked URL
3285 foreach ($clicked_urls as $clicked_url) {
3286 // Try multiple patterns to catch different link formats
3287 $patterns = [
3288 // Standard link format
3289 '/<a([^>]*href=["\']' . preg_quote($clicked_url, '/') . '["\'][^>]*)>/i',
3290 // Link with trailing slash
3291 '/<a([^>]*href=["\']' . preg_quote(rtrim($clicked_url, '/'), '/') . '\/?["\'][^>]*)>/i',
3292 // Encoded entities version
3293 '/<a([^>]*href=["\']' . preg_quote(htmlentities($clicked_url), '/') . '["\'][^>]*)>/i',
3294 ];
3295
3296 foreach ($patterns as $pattern) {
3297 if (preg_match($pattern, $message_content)) {
3298 $message_content = preg_replace_callback(
3299 $pattern,
3300 function($matches) {
3301 $full_match = $matches[0];
3302 $attributes = $matches[1];
3303
3304 // Check if it already has the class
3305 if (strpos($full_match, 'mxchat-clicked-link') !== false) {
3306 return $full_match;
3307 }
3308
3309 // Check if class attribute exists
3310 if (preg_match('/class=["\']([^"\']*)["\']/', $attributes, $class_matches)) {
3311 // Add to existing class
3312 $new_attributes = preg_replace(
3313 '/class=["\']([^"\']*)["\']/',
3314 'class="$1 mxchat-clicked-link"',
3315 $attributes
3316 );
3317 } else {
3318 // Add new class attribute
3319 $new_attributes = $attributes . ' class="mxchat-clicked-link"';
3320 }
3321
3322 // Add title if not present
3323 if (strpos($new_attributes, 'title=') === false) {
3324 $new_attributes .= ' title="User clicked this link"';
3325 }
3326
3327 return '<a' . $new_attributes . '>';
3328 },
3329 $message_content
3330 );
3331
3332 // If we found and replaced, break out of the patterns loop
3333 break;
3334 }
3335 }
3336 }
3337
3338 return $message_content;
3339 }
3340
3341 public function mxchat_create_prompts_page() {
3342 //error_log('=== DEBUG: mxchat_create_prompts_page started ===');
3343
3344 global $wpdb;
3345 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3346
3347 $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
3348 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
3349
3350 // Display success message if all prompts were deleted
3351 if (isset($_GET['all_deleted']) && $_GET['all_deleted'] === 'true') {
3352 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>';
3353 }
3354
3355 // Set up pagination, search query, and content type filter
3356 $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field($_GET['_wpnonce']) : '';
3357 $search_query = (!empty($nonce) && wp_verify_nonce($nonce, 'mxchat_prompts_search_nonce') && isset($_GET['search'])) ? sanitize_text_field($_GET['search']) : '';
3358 $content_type_filter = isset($_GET['content_type']) ? sanitize_key($_GET['content_type']) : ''; // ADDED 2.5.6
3359 $current_page = isset($_GET['paged']) ? absint($_GET['paged']) : 1;
3360 $per_page = 25;
3361
3362 //error_log('DEBUG: Search query: ' . $search_query);
3363 //error_log('DEBUG: Content type filter: ' . $content_type_filter);
3364 //error_log('DEBUG: Current page: ' . $current_page);
3365 //error_log('DEBUG: Per page: ' . $per_page);
3366
3367 // ================================
3368 // MULTI-BOT CONFIGURATION
3369 // ================================
3370
3371 // Check for multi-bot and set up bot selection
3372 if (class_exists('MxChat_Multi_Bot_Manager')) {
3373 $multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
3374 $available_bots = $multi_bot_manager->get_available_bots();
3375
3376 // Get saved bot selection (user-specific first, then site-wide default)
3377 $user_id = get_current_user_id();
3378 $saved_bot_id = get_user_meta($user_id, 'mxchat_selected_knowledge_bot', true);
3379 if (empty($saved_bot_id)) {
3380 $saved_bot_id = get_option('mxchat_current_knowledge_bot', 'default');
3381 }
3382
3383 // Allow URL override but default to saved selection
3384 $current_bot_id = isset($_GET['bot_id']) ? sanitize_key($_GET['bot_id']) : $saved_bot_id;
3385 $multibot_active = true;
3386 } else {
3387 $current_bot_id = 'default';
3388 $multibot_active = false;
3389 }
3390
3391 // ================================
3392 // DATA SOURCE CONFIGURATION
3393 // ================================
3394
3395 // Get bot-specific Pinecone settings
3396 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($current_bot_id);
3397 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3398 $pinecone_api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3399
3400 // Get OpenAI Vector Store settings
3401 $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
3402 $use_vectorstore = ($vectorstore_options['mxchat_use_openai_vectorstore'] ?? '0') === '1';
3403
3404 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Use Pinecone: ' . ($use_pinecone ? 'YES' : 'NO'));
3405 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Has API Key: ' . (!empty($pinecone_api_key) ? 'YES' : 'NO'));
3406 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Host: ' . ($pinecone_options['mxchat_pinecone_host'] ?? 'NOT SET'));
3407 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Namespace: ' . ($pinecone_options['mxchat_pinecone_namespace'] ?? 'NOT SET'));
3408
3409 if ($use_pinecone && !empty($pinecone_api_key)) {
3410 //error_log('DEBUG: Using PINECONE data source with bot-specific config');
3411 // PINECONE DATA SOURCE
3412 $data_source = 'pinecone';
3413
3414 // IMPORTANT: Pass the bot-specific $pinecone_options, not default options!
3415 // UPDATED 2.5.6: Added content_type_filter parameter
3416 $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, $search_query, $current_page, $per_page, $current_bot_id, $content_type_filter);
3417
3418 // TEMPORARY DEBUG - Add this right after the fetch call
3419 //error_log('=== DEBUG: Bot switching issue ===');
3420 //error_log('Current bot ID: ' . $current_bot_id);
3421 //error_log('Use Pinecone: ' . ($use_pinecone ? 'YES' : 'NO'));
3422 //error_log('Records returned: ' . count($records['data'] ?? []));
3423 //error_log('Total records: ' . ($records['total'] ?? 0));
3424
3425 // Check first few records to see their bot_id
3426 if (!empty($records['data'])) {
3427 foreach (array_slice($records['data'], 0, 3) as $i => $record) {
3428 $record_bot_id = $record->bot_id ?? 'NOT_SET';
3429 //error_log('Record ' . ($i+1) . ' bot_id: ' . $record_bot_id . ', content preview: ' . substr($record->article_content ?? '', 0, 30) . '...');
3430 }
3431 }
3432 //error_log('=== END DEBUG ===');
3433
3434 $total_records = $records['total'] ?? 0;
3435 $prompts = $records['data'] ?? array();
3436 $total_in_database = $records['total_in_database'] ?? 0;
3437 $showing_recent_only = $records['showing_recent_only'] ?? false;
3438
3439 $total_pages = ceil($total_records / $per_page);
3440
3441 } else {
3442 //error_log('DEBUG: Using WORDPRESS DB data source');
3443 // WORDPRESS DB DATA SOURCE (your existing logic)
3444 $data_source = 'wordpress';
3445
3446 // Initialize these variables for WordPress DB
3447 $total_in_database = 0;
3448 $showing_recent_only = false;
3449
3450 $offset = ($current_page - 1) * $per_page;
3451
3452 // UPDATED 2.5.6: Build WHERE clause for search and content type filtering
3453 $where_clauses = array();
3454 $where_values = array();
3455
3456 if ($search_query) {
3457 $where_clauses[] = "article_content LIKE %s";
3458 $where_values[] = '%' . $wpdb->esc_like($search_query) . '%';
3459 }
3460
3461 if ($content_type_filter) {
3462 $where_clauses[] = "content_type = %s";
3463 $where_values[] = $content_type_filter;
3464 }
3465
3466 $sql_where = "";
3467 if (!empty($where_clauses)) {
3468 $sql_where = "WHERE " . implode(" AND ", $where_clauses);
3469 }
3470
3471 // UPDATED 2.6.3: Count unique entries (by source_url) instead of individual rows
3472 // This ensures pagination shows X entries per page, not X chunks
3473 // Entries with empty source_url are counted individually
3474 if (!empty($where_values)) {
3475 // Count unique source_urls + count of rows with empty source_url
3476 $count_query = $wpdb->prepare(
3477 "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$sql_where} AND source_url != '') +
3478 (SELECT COUNT(*) FROM {$table_name} {$sql_where} AND (source_url = '' OR source_url IS NULL))",
3479 array_merge($where_values, $where_values)
3480 );
3481 } else {
3482 $count_query = "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '') +
3483 (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL)";
3484 }
3485 $total_records = $wpdb->get_var($count_query);
3486 $total_pages = ceil($total_records / $per_page);
3487
3488 // UPDATED 2.6.3: Get unique source_urls for pagination, then fetch all their rows
3489 // Step 1: Get the source_urls for this page (distinct URLs ordered by latest timestamp)
3490 if (!empty($where_values)) {
3491 $urls_query = $wpdb->prepare(
3492 "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name} {$sql_where}
3493 GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
3494 array_merge($where_values, array($per_page, $offset))
3495 );
3496 } else {
3497 $urls_query = $wpdb->prepare(
3498 "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
3499 GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
3500 $per_page, $offset
3501 );
3502 }
3503 $page_urls = $wpdb->get_results($urls_query);
3504
3505 // Step 2: Fetch all rows for these source_urls
3506 $prompts = array();
3507 if (!empty($page_urls)) {
3508 // Build URL order map to preserve newest-first ordering from step 1
3509 $url_order_map = array();
3510 $url_list = array();
3511 $has_empty_url = false;
3512 $order_index = 0;
3513 foreach ($page_urls as $url_row) {
3514 if (empty($url_row->source_url)) {
3515 $has_empty_url = true;
3516 $url_order_map['__empty__'] = $order_index++;
3517 } else {
3518 $url_list[] = $url_row->source_url;
3519 $url_order_map[$url_row->source_url] = $order_index++;
3520 }
3521 }
3522
3523 // Build query to fetch all rows for these URLs
3524 $url_conditions = array();
3525 $url_values = array();
3526
3527 if (!empty($url_list)) {
3528 $placeholders = implode(',', array_fill(0, count($url_list), '%s'));
3529 $url_conditions[] = "source_url IN ($placeholders)";
3530 $url_values = array_merge($url_values, $url_list);
3531 }
3532
3533 if ($has_empty_url) {
3534 $url_conditions[] = "(source_url = '' OR source_url IS NULL)";
3535 }
3536
3537 if (!empty($url_conditions)) {
3538 $url_where = "WHERE (" . implode(" OR ", $url_conditions) . ")";
3539
3540 // Add original filters back
3541 if (!empty($where_clauses)) {
3542 $url_where .= " AND " . implode(" AND ", $where_clauses);
3543 $url_values = array_merge($url_values, $where_values);
3544 }
3545
3546 if (!empty($url_values)) {
3547 $prompts_query = $wpdb->prepare(
3548 "SELECT * FROM {$table_name} {$url_where} ORDER BY timestamp DESC",
3549 $url_values
3550 );
3551 } else {
3552 $prompts_query = "SELECT * FROM {$table_name} {$url_where} ORDER BY timestamp DESC";
3553 }
3554
3555 $prompts = $wpdb->get_results($prompts_query);
3556
3557 // Sort prompts by the original URL order (newest first), then by timestamp within each URL group
3558 usort($prompts, function($a, $b) use ($url_order_map) {
3559 $url_a = empty($a->source_url) ? '__empty__' : $a->source_url;
3560 $url_b = empty($b->source_url) ? '__empty__' : $b->source_url;
3561 $order_a = $url_order_map[$url_a] ?? PHP_INT_MAX;
3562 $order_b = $url_order_map[$url_b] ?? PHP_INT_MAX;
3563
3564 // First sort by URL order (newest URLs first)
3565 if ($order_a !== $order_b) {
3566 return $order_a - $order_b;
3567 }
3568
3569 // Within same URL, sort by timestamp DESC (newest chunks first)
3570 return strtotime($b->timestamp) - strtotime($a->timestamp);
3571 });
3572 }
3573 }
3574 }
3575
3576 // ================================
3577 // PAGINATION GENERATION
3578 // ================================
3579
3580 // Generate pagination links
3581 if ($total_pages > 1) {
3582 // Build a clean base URL with only necessary parameters
3583 $pagination_args = array('page' => 'mxchat-prompts');
3584
3585 // Preserve bot_id parameter if multi-bot is active
3586 if ($multibot_active && !empty($current_bot_id) && $current_bot_id !== 'default') {
3587 $pagination_args['bot_id'] = $current_bot_id;
3588 }
3589
3590 // Preserve search query and nonce if present
3591 if ($search_query) {
3592 $pagination_args['search'] = $search_query;
3593 if (!empty($nonce)) {
3594 $pagination_args['_wpnonce'] = $nonce;
3595 }
3596 }
3597
3598 // Preserve content type filter if present
3599 if ($content_type_filter) {
3600 $pagination_args['content_type'] = $content_type_filter;
3601 }
3602
3603 // Build clean base URL
3604 $base_url = add_query_arg($pagination_args, admin_url('admin.php'));
3605
3606 $page_links = paginate_links(array(
3607 'base' => $base_url . '%_%',
3608 'format' => '&paged=%#%',
3609 'prev_text' => __('&laquo; Previous', 'mxchat'),
3610 'next_text' => __('Next &raquo;', 'mxchat'),
3611 'total' => $total_pages,
3612 'current' => $current_page,
3613 'type' => 'plain',
3614 ));
3615 } else {
3616 $page_links = '';
3617 }
3618
3619 // ================================
3620 // PROCESSING STATUS RETRIEVAL
3621 // ================================
3622
3623 // Retrieve processing statuses using queue-based method
3624 $processing_statuses = $knowledge_manager->mxchat_get_processing_statuses();
3625 $pdf_status = $processing_statuses['pdf_status'];
3626 $sitemap_status = $processing_statuses['sitemap_status'];
3627 $is_processing = $processing_statuses['is_processing'];
3628
3629 //error_log('=== DEBUG: mxchat_create_prompts_page data preparation completed ===');
3630
3631 // ================================
3632 // RENDER PAGE WITH NEW SIDEBAR LAYOUT
3633 // ================================
3634
3635 // Include the new knowledge page template
3636 require_once plugin_dir_path(__FILE__) . 'admin-knowledge-page.php';
3637
3638 // Package all page data for the render function
3639 $page_data = array(
3640 'prompts' => $prompts,
3641 'total_records' => $total_records,
3642 'total_pages' => $total_pages,
3643 'current_page' => $current_page,
3644 'per_page' => $per_page,
3645 'page_links' => $page_links,
3646 'search_query' => $search_query,
3647 'content_type_filter' => $content_type_filter,
3648 'data_source' => $data_source,
3649 'use_pinecone' => $use_pinecone,
3650 'use_vectorstore' => $use_vectorstore,
3651 'multibot_active' => $multibot_active,
3652 'current_bot_id' => $current_bot_id,
3653 'pdf_status' => $pdf_status,
3654 'sitemap_status' => $sitemap_status,
3655 'is_processing' => $is_processing,
3656 'total_in_database' => $total_in_database ?? 0,
3657 'showing_recent_only' => $showing_recent_only ?? false,
3658 );
3659
3660 // Render the new sidebar-based page
3661 mxchat_render_knowledge_page($this, $knowledge_manager, $page_data);
3662 }
3663
3664 /**
3665 * Get bot-specific Pinecone configuration
3666 * Used in the knowledge retrieval functions
3667 */
3668 private function get_bot_pinecone_config($bot_id = 'default') {
3669 //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
3670
3671 // If default bot or multi-bot add-on not active, use default Pinecone config
3672 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
3673 //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
3674 $addon_options = get_option('mxchat_pinecone_addon_options', array());
3675 $config = array(
3676 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
3677 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
3678 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
3679 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
3680 );
3681 //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
3682 return $config;
3683 }
3684
3685 //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
3686
3687 // Hook for multi-bot add-on to provide bot-specific Pinecone config
3688 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
3689
3690 if (!empty($bot_pinecone_config)) {
3691 //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
3692 //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
3693 //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
3694 //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
3695 } else {
3696 //error_log("MXCHAT DEBUG: Filter returned empty config!");
3697 }
3698
3699 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
3700 }
3701
3702 public function mxchat_delete_chat_history() {
3703 if (!current_user_can('manage_options')) {
3704 echo wp_json_encode(['error' => esc_html__('You do not have sufficient permissions.', 'mxchat')]);
3705 wp_die();
3706 }
3707 check_ajax_referer('mxchat_delete_chat_history', 'security');
3708
3709 if (!isset($_POST['delete_session_ids']) || !is_array($_POST['delete_session_ids'])) {
3710 echo wp_json_encode(['error' => esc_html__('No chat sessions selected for deletion.', 'mxchat')]);
3711 wp_die();
3712 }
3713
3714 global $wpdb;
3715 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
3716 $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
3717 $has_translations = $wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table;
3718
3719 // When true, any lead attached to these sessions is fully wiped (all their sessions,
3720 // across the whole table). Default false: the chat rows go away but the lead is
3721 // preserved as a separate "chat deleted" lead in the Leads tab.
3722 $also_delete_lead = !empty($_POST['also_delete_lead']) && $_POST['also_delete_lead'] !== 'false';
3723
3724 $deleted_count = 0;
3725 $preserved_as_deleted_leads = 0;
3726 $emails_to_fully_wipe = [];
3727
3728 foreach ((array) $_POST['delete_session_ids'] as $session_id) {
3729 $session_id_sanitized = sanitize_text_field($session_id);
3730 if ($session_id_sanitized === '') {
3731 continue;
3732 }
3733
3734 // Capture the lead info attached to this session *before* we delete the rows.
3735 $lead_row = $wpdb->get_row($wpdb->prepare(
3736 "SELECT user_email, user_name, MAX(timestamp) AS last_ts
3737 FROM {$table_name}
3738 WHERE session_id = %s AND user_email IS NOT NULL AND user_email != ''
3739 GROUP BY user_email, user_name
3740 ORDER BY last_ts DESC LIMIT 1",
3741 $session_id_sanitized
3742 ));
3743
3744 wp_cache_delete('chat_session_' . $session_id_sanitized, 'mxchat_chat_sessions');
3745 $wpdb->delete($table_name, ['session_id' => $session_id_sanitized]);
3746
3747 if ($has_translations) {
3748 $wpdb->delete($translations_table, ['session_id' => $session_id_sanitized]);
3749 }
3750
3751 delete_option('mxchat_history_' . $session_id_sanitized);
3752 delete_option('mxchat_agent_name_' . $session_id_sanitized);
3753
3754 if ($lead_row && !empty($lead_row->user_email)) {
3755 if ($also_delete_lead) {
3756 // Full-wipe requested — queue the email so that all their sessions and
3757 // related options get swept below. Also clear this session's pre-chat
3758 // capture options (they're no longer meaningful).
3759 $emails_to_fully_wipe[strtolower($lead_row->user_email)] = $lead_row->user_email;
3760 delete_option('mxchat_email_' . $session_id_sanitized);
3761 delete_option('mxchat_name_' . $session_id_sanitized);
3762 } else {
3763 // Preserve the lead in a "Chat deleted" state via distinct option keys so
3764 // they stay out of the orphan bucket (orphan = pre-chat form dropoff).
3765 update_option('mxchat_lead_del_email_' . $session_id_sanitized, $lead_row->user_email, false);
3766 if (!empty($lead_row->user_name)) {
3767 update_option('mxchat_lead_del_name_' . $session_id_sanitized, $lead_row->user_name, false);
3768 }
3769 if (!empty($lead_row->last_ts)) {
3770 update_option('mxchat_lead_del_ts_' . $session_id_sanitized, $lead_row->last_ts, false);
3771 }
3772 // Clean up pre-chat capture options for this session — chat_deleted supersedes.
3773 delete_option('mxchat_email_' . $session_id_sanitized);
3774 delete_option('mxchat_name_' . $session_id_sanitized);
3775 $preserved_as_deleted_leads++;
3776 }
3777 } else {
3778 // No lead attached — nothing to preserve. Clean up any orphan options anyway.
3779 delete_option('mxchat_email_' . $session_id_sanitized);
3780 delete_option('mxchat_name_' . $session_id_sanitized);
3781 }
3782
3783 $deleted_count++;
3784 }
3785
3786 // Opt-in full-lead wipe: sweep every remaining row + every option key (including
3787 // chat_deleted preservation) for each affected email. Reuses the same internal
3788 // helper as the Leads-tab Delete button for consistency.
3789 if (!empty($emails_to_fully_wipe)) {
3790 self::mxchat_wipe_leads_by_email(array_values($emails_to_fully_wipe));
3791 }
3792
3793 wp_cache_delete('all_chat_sessions', 'mxchat_chat_sessions');
3794
3795 echo wp_json_encode([
3796 'success' => sprintf(
3797 esc_html__('%d chat session(s) have been deleted.', 'mxchat'),
3798 $deleted_count
3799 ),
3800 'preserved_as_deleted_leads' => $preserved_as_deleted_leads,
3801 'leads_fully_wiped' => count($emails_to_fully_wipe),
3802 ]);
3803 wp_die();
3804 }
3805
3806 /**
3807 * Format transcript message content with markdown processing
3808 * Converts markdown links, bold, italic, code blocks, and plain URLs to HTML
3809 */
3810 private function format_transcript_message($text) {
3811 if (empty($text)) {
3812 return '';
3813 }
3814
3815 // Normalize line endings and clean up excessive whitespace
3816 $text = str_replace("\r\n", "\n", $text);
3817 $text = str_replace("\r", "\n", $text);
3818
3819 // Clean up existing <br> tags that may have been saved (legacy data)
3820 // Convert <br>, <br/>, <br /> back to newlines for consistent processing
3821 $text = preg_replace('/<br\s*\/?>\s*/i', "\n", $text);
3822
3823 // Collapse 3+ consecutive newlines to just 2 (paragraph break)
3824 $text = preg_replace('/\n{3,}/', "\n\n", $text);
3825
3826 // Process markdown headers (# Header)
3827 $text = preg_replace_callback('/^(#{1,6})\s+(.+)$/m', function($matches) {
3828 $level = strlen($matches[1]);
3829 $content = esc_html(trim($matches[2]));
3830 return "<h{$level}>{$content}</h{$level}>";
3831 }, $text);
3832
3833 // Process code blocks with triple backticks
3834 $text = preg_replace_callback('/```(\w+)?\n?([\s\S]*?)```/', function($matches) {
3835 $language = !empty($matches[1]) ? ' class="language-' . esc_attr($matches[1]) . '"' : '';
3836 $code = esc_html($matches[2]);
3837 return "<pre><code{$language}>{$code}</code></pre>";
3838 }, $text);
3839
3840 // Process inline code with single backticks
3841 $text = preg_replace('/`([^`]+)`/', '<code>$1</code>', $text);
3842
3843 // Process bold text **text** or __text__
3844 $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
3845 $text = preg_replace('/__(.+?)__/', '<strong>$1</strong>', $text);
3846
3847 // Process italic text *text* or _text_ (but not if part of URL)
3848 $text = preg_replace('/(?<![*_\w])\*([^*]+)\*(?![*\w])/', '<em>$1</em>', $text);
3849 $text = preg_replace('/(?<![*_\w])_([^_]+)_(?![*\w])/', '<em>$1</em>', $text);
3850
3851 // Process markdown links [text](url)
3852 $text = preg_replace_callback('/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/', function($matches) {
3853 $link_text = esc_html($matches[1]);
3854 $url = esc_url($matches[2]);
3855 return "<a href=\"{$url}\" target=\"_blank\" rel=\"noopener\">{$link_text}</a>";
3856 }, $text);
3857
3858 // Process citation-style brackets [URL]
3859 $text = preg_replace_callback('/\[(https?:\/\/[^\]]+)\]/', function($matches) {
3860 $url = esc_url($matches[1]);
3861 return "<a href=\"{$url}\" target=\"_blank\" rel=\"noopener\">{$url}</a>";
3862 }, $text);
3863
3864 // Process standalone URLs (not already in links or img src)
3865 $text = preg_replace_callback(
3866 '/(?<!href="|src="|">)(https?:\/\/[^\s<>"]+)(?![^<]*<\/a>)/',
3867 function($matches) {
3868 $url = esc_url($matches[1]);
3869 // Truncate display URL if too long
3870 $display = strlen($matches[1]) > 50 ? substr($matches[1], 0, 47) . '...' : $matches[1];
3871 return "<a href=\"{$url}\" target=\"_blank\" rel=\"noopener\">{$display}</a>";
3872 },
3873 $text
3874 );
3875
3876 // Process mailto links
3877 $text = preg_replace_callback('/\[([^\]]+)\]\((mailto:[^)]+)\)/', function($matches) {
3878 $link_text = esc_html($matches[1]);
3879 $mailto = esc_url($matches[2]);
3880 return "<a href=\"{$mailto}\">{$link_text}</a>";
3881 }, $text);
3882
3883 // Convert paragraphs: split by double newlines, wrap in <p> tags
3884 // This creates proper paragraph structure instead of excessive <br> tags
3885 $paragraphs = preg_split('/\n\n+/', $text);
3886
3887 // Filter out empty paragraphs but preserve content like "0"
3888 $paragraphs = array_values(array_filter(array_map('trim', $paragraphs), function($p) {
3889 return $p !== '';
3890 }));
3891
3892 if (empty($paragraphs)) {
3893 // No content after filtering
3894 return '';
3895 } elseif (count($paragraphs) > 1) {
3896 // Multiple paragraphs - wrap each in <p> tags, convert single newlines to <br>
3897 $formatted_paragraphs = array_map(function($p) {
3898 return nl2br($p);
3899 }, $paragraphs);
3900 $text = '<p>' . implode('</p><p>', $formatted_paragraphs) . '</p>';
3901 } else {
3902 // Single paragraph - just convert newlines to <br>
3903 $text = nl2br($paragraphs[0]);
3904 }
3905
3906 return $text;
3907 }
3908
3909 /**
3910 * AJAX handler to fetch RAG context for a specific message
3911 * Used by the transcript viewer to show retrieved documents
3912 */
3913 public function mxchat_get_rag_context() {
3914 // Check permissions
3915 if (!current_user_can('manage_options')) {
3916 wp_send_json_error(['message' => esc_html__('You do not have sufficient permissions.', 'mxchat')]);
3917 wp_die();
3918 }
3919
3920 // Validate message ID
3921 if (!isset($_POST['message_id']) || empty($_POST['message_id'])) {
3922 wp_send_json_error(['message' => esc_html__('Message ID is required.', 'mxchat')]);
3923 wp_die();
3924 }
3925
3926 $message_id = absint($_POST['message_id']);
3927
3928 global $wpdb;
3929 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
3930
3931 // Fetch the RAG context for this message
3932 $result = $wpdb->get_row($wpdb->prepare(
3933 "SELECT rag_context FROM {$table_name} WHERE id = %d",
3934 $message_id
3935 ));
3936
3937 if (!$result || empty($result->rag_context)) {
3938 wp_send_json_error(['message' => esc_html__('No RAG context found for this message.', 'mxchat')]);
3939 wp_die();
3940 }
3941
3942 // Decode the JSON data
3943 $rag_context = json_decode($result->rag_context, true);
3944
3945 if (json_last_error() !== JSON_ERROR_NONE) {
3946 wp_send_json_error(['message' => esc_html__('Invalid RAG context data.', 'mxchat')]);
3947 wp_die();
3948 }
3949
3950 wp_send_json_success($rag_context);
3951 wp_die();
3952 }
3953
3954 public function display_admin_notices() {
3955 // Check if we're on a MXChat admin page
3956 $screen = get_current_screen();
3957 if (!$screen || strpos($screen->base, 'mxchat') === false) {
3958 return;
3959 }
3960
3961 $dismiss_button = '<button type="button" class="notice-dismiss"><span class="screen-reader-text">' . esc_html__('Dismiss this notice.', 'mxchat') . '</span></button>';
3962
3963 // Check for error notices
3964 $error_notice = get_transient('mxchat_admin_notice_error');
3965 if ($error_notice) {
3966 echo '<div class="notice notice-error is-dismissible"><p>' . wp_kses_post($error_notice) . '</p>' . $dismiss_button . '</div>';
3967 delete_transient('mxchat_admin_notice_error');
3968 }
3969
3970 // Check for success notices
3971 $success_notice = get_transient('mxchat_admin_notice_success');
3972 if ($success_notice) {
3973 echo '<div class="notice notice-success is-dismissible"><p>' . wp_kses_post($success_notice) . '</p>' . $dismiss_button . '</div>';
3974 delete_transient('mxchat_admin_notice_success');
3975 }
3976
3977 // Check for info notices
3978 $info_notice = get_transient('mxchat_admin_notice_info');
3979 if ($info_notice) {
3980 echo '<div class="notice notice-info is-dismissible"><p>' . wp_kses_post($info_notice) . '</p>' . $dismiss_button . '</div>';
3981 delete_transient('mxchat_admin_notice_info');
3982 }
3983 }
3984
3985
3986 public function mxchat_create_activation_page() {
3987 // Include the new Pro & Extensions page template
3988 require_once plugin_dir_path(__FILE__) . 'admin-pro-page.php';
3989
3990 // Get addons configuration from the MxChat_Addons class
3991 require_once plugin_dir_path(__FILE__) . 'class-mxchat-addons.php';
3992 $addons_instance = new MxChat_Addons();
3993 $addons_config = $addons_instance->get_addons_config();
3994
3995 // Render the consolidated Pro & Extensions page
3996 mxchat_render_pro_page($this, $addons_config);
3997 }
3998
3999 /**
4000 * Check if current activation is linked to a domain
4001 * This checks YOUR website's database, not the user's local database
4002 */
4003 public function is_current_activation_linked($domain) {
4004 $license_key = get_option('mxchat_activation_key');
4005 $email = get_option('mxchat_pro_email');
4006
4007 if (empty($license_key) || empty($email)) {
4008 return false;
4009 }
4010
4011 // Check with YOUR website's API
4012 $response = wp_remote_post('https://mxchat.ai/mxchat-api/check-domain', array(
4013 'body' => array(
4014 'license_key' => $license_key,
4015 'email' => $email,
4016 'domain' => $domain
4017 ),
4018 'timeout' => 10,
4019 'sslverify' => false
4020 ));
4021
4022 if (is_wp_error($response)) {
4023 return false;
4024 }
4025
4026 $body = json_decode(wp_remote_retrieve_body($response), true);
4027 return isset($body['success']) && $body['success'] && isset($body['data']['linked']) && $body['data']['linked'];
4028 }
4029
4030
4031 public function mxchat_actions_page_html() {
4032 if (!current_user_can('manage_options')) {
4033 return;
4034 }
4035
4036 global $wpdb;
4037 $table_name = $wpdb->prefix . 'mxchat_intents';
4038
4039 // Get stats for dashboard
4040 $total_actions = $wpdb->get_var("SELECT COUNT(*) FROM $table_name");
4041 $enabled_actions = $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE enabled = 1");
4042 $disabled_actions = $total_actions - $enabled_actions;
4043
4044 // Get unique action types count
4045 $action_types_count = $wpdb->get_var("SELECT COUNT(DISTINCT callback_function) FROM $table_name");
4046
4047 // Get action type distribution
4048 $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");
4049 $available_callbacks = $this->mxchat_get_available_callbacks();
4050 $callback_groups = $this->mxchat_get_available_callbacks(true, true);
4051
4052 $action_type_distribution = array();
4053 foreach ($type_distribution_raw as $row) {
4054 $label = isset($available_callbacks[$row->callback_function]['label'])
4055 ? $available_callbacks[$row->callback_function]['label']
4056 : $row->callback_function;
4057 $action_type_distribution[$label] = $row->count;
4058 }
4059
4060 // Prepare page data
4061 $page_data = array(
4062 'total_actions' => $total_actions,
4063 'enabled_actions' => $enabled_actions,
4064 'disabled_actions' => $disabled_actions,
4065 'action_types_count' => $action_types_count,
4066 'action_type_distribution' => $action_type_distribution,
4067 'available_callbacks' => $available_callbacks,
4068 'callback_groups' => $callback_groups,
4069 );
4070
4071 // Include and render the new template
4072 require_once plugin_dir_path(__FILE__) . 'admin-actions-page.php';
4073 mxchat_render_actions_page($this, $page_data);
4074 }
4075
4076 /**
4077 * LEGACY HTML - Preserved below for reference, to be removed in future update
4078 */
4079 function mxchat_actions_page_legacy_html() {
4080 // This function is deprecated and no longer used
4081 // The new template is in includes/admin-actions-page.php
4082 ?>
4083 <div class="wrap mxchat-wrapper">
4084 <!-- Hero Section -->
4085 <div class="mxchat-hero">
4086 <h1 class="mxchat-main-title">
4087 <span class="mxchat-gradient-text">Actions</span> Manager
4088 </h1>
4089 <p class="mxchat-hero-subtitle">
4090 <?php esc_html_e('Create and manage custom actions to enhance your chatbot\'s capabilities.', 'mxchat'); ?>
4091 </p>
4092 </div>
4093
4094 <!-- Actions Header with Search and Filter -->
4095 <div class="mxchat-actions-header">
4096 <div class="mxchat-actions-filters">
4097 <form method="get" class="mxchat-search-form">
4098 <input type="hidden" name="page" value="mxchat-actions">
4099 <div class="mxchat-search-group">
4100 <span class="dashicons dashicons-search"></span>
4101 <input type="text" name="s" class="mxchat-search-input"
4102 placeholder="<?php esc_attr_e('Search Actions', 'mxchat'); ?>"
4103 value="<?php echo esc_attr($search_term); ?>">
4104 </div>
4105 <select name="callback_filter" class="mxchat-action-filter">
4106 <option value=""><?php esc_html_e('All Action Types', 'mxchat'); ?></option>
4107 <?php foreach ($available_callbacks as $function => $callback_data) :
4108 $label = $callback_data['label']; ?>
4109 <option value="<?php echo esc_attr($function); ?>"
4110 <?php selected($callback_filter, $function); ?>>
4111 <?php echo esc_html($label); ?>
4112 </option>
4113 <?php endforeach; ?>
4114 </select>
4115 <button type="submit" class="mxchat-button-secondary">
4116 <?php esc_html_e('Filter', 'mxchat'); ?>
4117 </button>
4118 </form>
4119 </div>
4120 <div class="mxchat-actions-controls">
4121 <button type="button" id="mxchat-add-action-btn" class="mxchat-button-primary">
4122 <span class="dashicons dashicons-plus-alt"></span>
4123 <?php esc_html_e('Add New Action', 'mxchat'); ?>
4124 </button>
4125 </div>
4126 </div>
4127
4128 <!-- Actions Grid Layout - All actions in a single grid -->
4129 <div class="mxchat-actions-grid">
4130 <div class="mxchat-cards-container">
4131 <?php if (!empty($actions)) : ?>
4132 <?php foreach ($actions as $action) :
4133 $callback_function = $action->callback_function;
4134 $callback_label = isset($available_callbacks[$callback_function]['label'])
4135 ? $available_callbacks[$callback_function]['label']
4136 : $callback_function;
4137 $threshold_value = isset($action->similarity_threshold)
4138 ? round($action->similarity_threshold * 100)
4139 : 85;
4140
4141 // Check if this is a form action
4142 $is_form_action = strpos($action->intent_label, 'Form ') === 0;
4143
4144 // Get action status (enabled/disabled) - default to true if column doesn't exist
4145 $is_enabled = isset($action->enabled) ? (bool)$action->enabled : true;
4146
4147 // Get enabled bots for display
4148 $enabled_bots = [];
4149 if (isset($action->enabled_bots) && !empty($action->enabled_bots)) {
4150 $enabled_bots = json_decode($action->enabled_bots, true);
4151 if (!is_array($enabled_bots)) {
4152 $enabled_bots = ['default'];
4153 }
4154 } else {
4155 $enabled_bots = ['default']; // Backward compatibility
4156 }
4157 ?>
4158 <div class="mxchat-action-card <?php echo $is_form_action ? 'mxchat-form-action' : ''; ?>">
4159 <div class="mxchat-card-header">
4160 <div class="mxchat-card-title"><?php echo esc_html($action->intent_label); ?></div>
4161 <div class="mxchat-card-toggle">
4162 <label class="mxchat-switch">
4163 <input type="checkbox" class="mxchat-action-toggle"
4164 data-action-id="<?php echo esc_attr($action->id); ?>"
4165 <?php checked($is_enabled); ?>>
4166 <span class="mxchat-slider round"></span>
4167 </label>
4168 </div>
4169 </div>
4170
4171 <div class="mxchat-card-body">
4172 <div class="mxchat-card-description">
4173 <strong><?php esc_html_e('Type:', 'mxchat'); ?></strong>
4174 <?php echo esc_html($callback_label); ?>
4175 </div>
4176
4177 <div class="mxchat-card-phrases">
4178 <strong><?php esc_html_e('Trigger phrases:', 'mxchat'); ?></strong>
4179 <div class="mxchat-phrases-preview">
4180 <?php
4181 // Check if the helper function exists, otherwise use a simple substring
4182 if (method_exists($this, 'get_trimmed_phrases')) {
4183 echo esc_html($this->get_trimmed_phrases($action->phrases));
4184 } else {
4185 echo esc_html(strlen($action->phrases) > 100 ?
4186 substr($action->phrases, 0, 97) . '...' :
4187 $action->phrases);
4188 }
4189 ?>
4190 </div>
4191 </div>
4192
4193 <div class="mxchat-threshold-control">
4194 <div class="mxchat-threshold-label">
4195 <?php esc_html_e('Similarity Threshold:', 'mxchat'); ?>
4196 <span class="mxchat-threshold-value"><?php echo esc_html($threshold_value); ?>%</span>
4197 </div>
4198 </div>
4199
4200 <!-- Bot Availability Display (Read-only) -->
4201 <div class="mxchat-card-bots">
4202 <strong><?php esc_html_e('Assigned bots:', 'mxchat'); ?></strong>
4203 <div class="mxchat-bot-badges">
4204 <?php
4205 foreach ($enabled_bots as $bot_id) {
4206 if ($bot_id === 'default') {
4207 echo '<span class="mxchat-bot-badge">' . esc_html__('Default Bot', 'mxchat') . '</span>';
4208 } else {
4209 // Try to get bot name from multi-bot manager
4210 if (class_exists('MxChat_Multi_Bot_Core_Manager')) {
4211 $multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
4212 $available_bots = $multi_bot_manager->get_available_bots();
4213 $bot_name = isset($available_bots[$bot_id]) ? $available_bots[$bot_id] : $bot_id;
4214 echo '<span class="mxchat-bot-badge">' . esc_html($bot_name) . '</span>';
4215 } else {
4216 echo '<span class="mxchat-bot-badge">' . esc_html($bot_id) . '</span>';
4217 }
4218 }
4219 }
4220 ?>
4221 </div>
4222 </div>
4223 </div>
4224
4225 <div class="mxchat-card-footer">
4226 <?php
4227 // Check if it's a form action
4228 $is_form_action = preg_match('/Form (\d+)/', $action->intent_label, $form_matches);
4229
4230 // Check if it's a recommendation flow action
4231 $is_flow_action = preg_match('/Recommendation Flow (\d+)/', $action->intent_label, $flow_matches);
4232
4233 if ($is_form_action) {
4234 $form_id = isset($form_matches[1]) ? $form_matches[1] : '';
4235 ?>
4236 <a href="<?php echo esc_url(admin_url('admin.php?page=mxchat-forms&action=edit&form_id=' . $form_id)); ?>"
4237 class="mxchat-button-primary">
4238 <span class="dashicons dashicons-feedback"></span>
4239 <?php esc_html_e('Edit Form', 'mxchat'); ?>
4240 </a>
4241 <?php } elseif ($is_flow_action) {
4242 ?>
4243 <a href="<?php echo esc_url(admin_url('admin.php?page=mxchat-smart-recommender')); ?>"
4244 class="mxchat-button-primary">
4245 <span class="dashicons dashicons-list-view"></span>
4246 <?php esc_html_e('Manage Flows', 'mxchat'); ?>
4247 </a>
4248 <?php } else { ?>
4249 <button type="button"
4250 class="mxchat-button-secondary mxchat-edit-button"
4251 data-action-id="<?php echo esc_attr($action->id); ?>"
4252 data-phrases="<?php echo esc_attr($action->phrases); ?>"
4253 data-label="<?php echo esc_attr($action->intent_label); ?>"
4254 data-threshold="<?php echo esc_attr(round($action->similarity_threshold * 100)); ?>"
4255 data-callback-function="<?php echo esc_attr($action->callback_function); ?>"
4256 data-enabled-bots="<?php echo esc_attr(json_encode($enabled_bots)); ?>">
4257 <span class="dashicons dashicons-edit"></span>
4258 <?php esc_html_e('Edit', 'mxchat'); ?>
4259 </button>
4260 <form method="post"
4261 action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
4262 class="mxchat-delete-form"
4263 onsubmit="return confirm('<?php esc_attr_e('Are you sure you want to delete this action?', 'mxchat'); ?>');">
4264 <?php wp_nonce_field('mxchat_delete_intent_nonce'); ?>
4265 <input type="hidden" name="action" value="mxchat_delete_intent">
4266 <input type="hidden" name="intent_id" value="<?php echo esc_attr($action->id); ?>">
4267 <button type="submit" class="mxchat-button-text mxchat-delete-button">
4268 <span class="dashicons dashicons-trash"></span>
4269 <?php esc_html_e('Delete', 'mxchat'); ?>
4270 </button>
4271 </form>
4272 <?php } ?>
4273 </div>
4274 </div>
4275 <?php endforeach; ?>
4276 <?php else : ?>
4277 <!-- If no actions found -->
4278 <div class="mxchat-no-actions">
4279 <div class="mxchat-empty-state">
4280 <span class="dashicons dashicons-format-chat"></span>
4281 <h2><?php esc_html_e('No actions found', 'mxchat'); ?></h2>
4282 <p><?php esc_html_e('Get started by creating your first action to enhance your chatbot.', 'mxchat'); ?></p>
4283 <button type="button" id="mxchat-create-first-action" class="mxchat-button-primary">
4284 <?php esc_html_e('Create Your First Action', 'mxchat'); ?>
4285 </button>
4286 </div>
4287 </div>
4288 <?php endif; ?>
4289 </div>
4290 </div>
4291
4292 <?php if ($total_pages > 1) : ?>
4293 <div class="mxchat-pagination">
4294 <?php
4295 echo paginate_links(array(
4296 'base' => add_query_arg('paged', '%#%'),
4297 'format' => '',
4298 'prev_text' => __('&laquo; Previous', 'mxchat'),
4299 'next_text' => __('Next &raquo;', 'mxchat'),
4300 'total' => $total_pages,
4301 'current' => $page
4302 ));
4303 ?>
4304 </div>
4305 <?php endif; ?>
4306
4307 <!-- Add/Edit Action Modal with Step-Based Approach -->
4308 <!-- Complete Modal HTML with Defined Groups Variable -->
4309 <div id="mxchat-action-modal" class="mxchat-modal" style="display: none;">
4310 <div class="mxchat-modal-content">
4311 <span class="mxchat-modal-close">&times;</span>
4312
4313 <form id="mxchat-action-form" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
4314 <!-- Dynamic nonce field -->
4315 <div id="action-nonce-container">
4316 <?php wp_nonce_field('mxchat_add_intent_nonce', 'add_intent_nonce'); ?>
4317 </div>
4318 <input type="hidden" name="action" id="form_action_type" value="mxchat_add_intent">
4319 <input type="hidden" name="intent_id" id="edit_action_id" value="">
4320 <input type="hidden" name="callback_function" id="callback_function" value="">
4321
4322 <!-- Step 1: Action Type Selection -->
4323 <div id="mxchat-action-step-1" class="mxchat-action-step active">
4324 <div class="mxchat-step-indicator">
4325 <div class="mxchat-step-number">1</div>
4326 <div class="mxchat-step-title"><?php esc_html_e('Select Action Type', 'mxchat'); ?></div>
4327 </div>
4328
4329 <div id="mxchat-action-type-selector" class="mxchat-action-type-selector">
4330 <div class="mxchat-action-type-search">
4331 <span class="dashicons dashicons-search"></span>
4332 <input type="text" id="action-type-search" placeholder="<?php esc_attr_e('Search action types...', 'mxchat'); ?>" class="mxchat-action-type-search-input">
4333 </div>
4334
4335 <?php
4336 // Get the callbacks - IMPORTANT: Define the $groups variable here
4337 $groups = $this->mxchat_get_available_callbacks(true, true);
4338 ?>
4339
4340 <div class="mxchat-action-type-categories">
4341 <button type="button" class="mxchat-category-button active" data-category="all"><?php esc_html_e('All', 'mxchat'); ?></button>
4342 <?php
4343 // Get unique categories from the defined groups
4344 foreach ($groups as $group_label => $group_callbacks) :
4345 $category_slug = sanitize_title($group_label);
4346 ?>
4347 <button type="button" class="mxchat-category-button" data-category="<?php echo esc_attr($category_slug); ?>"><?php echo esc_html($group_label); ?></button>
4348 <?php endforeach; ?>
4349 </div>
4350
4351 <div class="mxchat-action-types-grid">
4352 <?php
4353 // Generate action cards from available callbacks
4354 foreach ($groups as $group_label => $group_callbacks) :
4355 $category_slug = sanitize_title($group_label);
4356
4357 foreach ($group_callbacks as $function => $data) :
4358 $label = $data['label'];
4359 $icon = isset($data['icon']) ? $data['icon'] : 'admin-generic';
4360 $description = isset($data['description']) ? $data['description'] : '';
4361 $is_addon = isset($data['addon']) && $data['addon'] !== false;
4362 $addon_name = isset($data['addon_name']) ? $data['addon_name'] : '';
4363 $is_installed = isset($data['installed']) ? $data['installed'] : true;
4364 $is_promo = !empty($data['addon_promo']) && !$is_installed;
4365
4366 // Determine card status and styling
4367 $card_class = 'mxchat-action-type-card';
4368 $icon_class = 'mxchat-action-type-icon';
4369 $status_badge = '';
4370
4371 if ($is_promo) {
4372 // Promotional card — not selectable, just informational
4373 $card_class .= ' not-installed mxchat-promo-card';
4374 $status_badge = '<span class="mxchat-addon-badge">' . esc_html__('Add-on Required', 'mxchat') . '</span>';
4375 } elseif ($is_addon && !$is_installed) {
4376 // Add-on not installed
4377 $card_class .= ' not-installed';
4378 $status_badge .= '<span class="mxchat-addon-badge">' . esc_html__('Add-on Required', 'mxchat') . '</span>';
4379 }
4380
4381 // Default description if none provided
4382 if (empty($description)) {
4383 $description = sprintf(
4384 esc_html__('Use the %s action in your chatbot', 'mxchat'),
4385 $label
4386 );
4387 }
4388 ?>
4389 <div class="<?php echo esc_attr($card_class); ?>"
4390 data-category="<?php echo esc_attr($category_slug); ?>"
4391 <?php if (!$is_promo) : ?>
4392 data-value="<?php echo esc_attr($function); ?>"
4393 data-label="<?php echo esc_attr($label); ?>"
4394 <?php endif; ?>
4395 data-pro="false"
4396 data-addon="<?php echo esc_attr($is_addon ? $data['addon'] : ''); ?>"
4397 data-installed="<?php echo $is_installed ? 'true' : 'false'; ?>"
4398 <?php if ($is_promo) : ?>data-promo="true"<?php endif; ?>>
4399 <div class="<?php echo esc_attr($icon_class); ?>">
4400 <span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span>
4401 </div>
4402 <div class="mxchat-action-type-info">
4403 <h4><?php echo esc_html($label); ?></h4>
4404 <p><?php echo esc_html($description); ?></p>
4405 <?php if (!empty($status_badge)) : ?>
4406 <?php echo $status_badge; ?>
4407 <?php endif; ?>
4408
4409 <?php if ($is_promo || ($is_addon && !$is_installed)) : ?>
4410 <div class="mxchat-addon-info">
4411 <?php echo esc_html(sprintf(
4412 __('Requires %s', 'mxchat'),
4413 $addon_name
4414 )); ?>
4415 — <a href="https://mxchat.ai/" target="_blank"><?php esc_html_e('Get Add-on', 'mxchat'); ?></a>
4416 </div>
4417 <?php endif; ?>
4418 </div>
4419 </div>
4420 <?php endforeach;
4421 endforeach; ?>
4422 </div>
4423 </div>
4424
4425 <div class="mxchat-modal-actions">
4426 <button type="button" class="mxchat-button-secondary mxchat-modal-cancel">
4427 <?php esc_html_e('Cancel', 'mxchat'); ?>
4428 </button>
4429 </div>
4430 </div>
4431
4432 <!-- Step 2: Action Configuration -->
4433 <div id="mxchat-action-step-2" class="mxchat-action-step">
4434 <div class="mxchat-step-indicator">
4435 <div class="mxchat-step-number">2</div>
4436 <div class="mxchat-step-title"><?php esc_html_e('Configure Action', 'mxchat'); ?></div>
4437 </div>
4438
4439 <div class="mxchat-selected-action">
4440 <button type="button" class="mxchat-back-button" id="mxchat-back-to-step-1">
4441 <span class="dashicons dashicons-arrow-left-alt"></span>
4442 <?php esc_html_e('Back to Action Types', 'mxchat'); ?>
4443 </button>
4444 <div class="mxchat-selected-action-info">
4445 <div id="selected-action-icon" class="mxchat-action-type-icon">
4446 <span class="dashicons dashicons-admin-generic"></span>
4447 </div>
4448 <div class="mxchat-selected-action-details">
4449 <h3 id="selected-action-title"><?php esc_html_e('Selected Action', 'mxchat'); ?></h3>
4450 <p id="selected-action-description"><?php esc_html_e('Configure this action for your chatbot', 'mxchat'); ?></p>
4451 </div>
4452 </div>
4453 </div>
4454
4455 <div class="mxchat-form-group">
4456 <label for="intent_label">
4457 <?php esc_html_e('Action Label (For your reference only)', 'mxchat'); ?>
4458 </label>
4459 <input name="intent_label" type="text" id="intent_label" required
4460 class="mxchat-intent-input"
4461 placeholder="<?php esc_attr_e('Example: Newsletter Signup', 'mxchat'); ?>">
4462 </div>
4463
4464 <div class="mxchat-form-group">
4465 <label for="phrases">
4466 <?php esc_html_e('Trigger Phrases (comma-separated)', 'mxchat'); ?>
4467 </label>
4468 <textarea name="phrases" id="action_phrases" rows="5" required
4469 class="mxchat-intent-textarea"
4470 placeholder="<?php esc_attr_e('Example: sign me up, subscribe me, I want to join, add me to the newsletter', 'mxchat'); ?>"></textarea>
4471 </div>
4472
4473 <div class="mxchat-form-group">
4474 <label for="similarity_threshold">
4475 <?php esc_html_e('Similarity Threshold', 'mxchat'); ?>
4476 <span class="mxchat-threshold-value-display">85%</span>
4477 </label>
4478 <div class="mxchat-slider-group modal-slider">
4479 <input type="range"
4480 name="similarity_threshold"
4481 id="similarity_threshold"
4482 min="10"
4483 max="95"
4484 value="85"
4485 class="mxchat-intent-slider"
4486 oninput="document.querySelector('.mxchat-threshold-value-display').textContent = this.value + '%'">
4487 </div>
4488 <div class="mxchat-threshold-hint">
4489 <?php esc_html_e('Lower values (10-30) make the action trigger more easily. Higher values (70-95) require more exact matches.', 'mxchat'); ?>
4490 </div>
4491 </div>
4492
4493 <!-- Bot Selection Section -->
4494 <div class="mxchat-form-group">
4495 <label for="enabled_bots">
4496 <?php esc_html_e('Which bots should we enable this action for?', 'mxchat'); ?>
4497 </label>
4498 <div class="mxchat-bot-selector">
4499 <div class="mxchat-bot-option">
4500 <label class="mxchat-checkbox-label">
4501 <input type="checkbox"
4502 name="enabled_bots[]"
4503 value="default"
4504 id="bot_default"
4505 checked="checked">
4506 <span class="mxchat-checkmark"></span>
4507 <?php esc_html_e('Default Bot', 'mxchat'); ?>
4508 </label>
4509 </div>
4510
4511 <?php if (class_exists('MxChat_Multi_Bot_Core_Manager')) :
4512 $multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
4513 $available_bots = $multi_bot_manager->get_available_bots();
4514
4515 foreach ($available_bots as $bot_id => $bot_name) :
4516 if ($bot_id === 'default') continue; // Skip default, already shown above
4517 ?>
4518 <div class="mxchat-bot-option">
4519 <label class="mxchat-checkbox-label">
4520 <input type="checkbox"
4521 name="enabled_bots[]"
4522 value="<?php echo esc_attr($bot_id); ?>"
4523 id="bot_<?php echo esc_attr($bot_id); ?>">
4524 <span class="mxchat-checkmark"></span>
4525 <?php echo esc_html($bot_name); ?>
4526 </label>
4527 </div>
4528 <?php
4529 endforeach;
4530 endif; ?>
4531 </div>
4532 </div>
4533
4534 <div class="mxchat-modal-actions">
4535 <button type="button" class="mxchat-button-secondary mxchat-modal-cancel">
4536 <?php esc_html_e('Cancel', 'mxchat'); ?>
4537 </button>
4538 <button type="submit" class="mxchat-button-primary" id="mxchat-save-action-btn">
4539 <?php esc_html_e('Save Action', 'mxchat'); ?>
4540 </button>
4541 </div>
4542 </div>
4543 </form>
4544 </div>
4545 </div>
4546
4547 <div id="mxchat-action-loading" class="mxchat-action-loading" style="display: none;">
4548 <div class="mxchat-action-loading-spinner"></div>
4549 <div class="mxchat-action-loading-text">
4550 <?php esc_html_e('Saving action, please wait...', 'mxchat'); ?>
4551 </div>
4552 </div>
4553 </div><!-- .mxchat-wrapper -->
4554 <?php
4555 }
4556 private function get_trimmed_phrases($phrases, $max_length = 100) {
4557 if (strlen($phrases) <= $max_length) {
4558 return $phrases;
4559 }
4560
4561 $trimmed = substr($phrases, 0, $max_length);
4562 $last_comma = strrpos($trimmed, ',');
4563
4564 if ($last_comma !== false) {
4565 $trimmed = substr($trimmed, 0, $last_comma);
4566 }
4567
4568 return $trimmed . '...';
4569 }
4570 public function mxchat_add_enabled_column_to_intents() {
4571 global $wpdb;
4572 $table_name = $wpdb->prefix . 'mxchat_intents';
4573
4574 // Check if the column already exists
4575 $columns = $wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'enabled'");
4576
4577 if (empty($columns)) {
4578 // Add the column with default value of 1 (enabled)
4579 $wpdb->query("ALTER TABLE $table_name ADD COLUMN enabled TINYINT(1) NOT NULL DEFAULT 1");
4580 }
4581 }
4582
4583 public function mxchat_handle_delete_intent() {
4584 if ( ! current_user_can( 'manage_options' ) ) {
4585 wp_die( esc_html__('Unauthorized user', 'mxchat') );
4586 }
4587
4588 check_admin_referer('mxchat_delete_intent_nonce');
4589
4590 if (isset($_POST['intent_id'])) {
4591 global $wpdb;
4592 $table_name = $wpdb->prefix . 'mxchat_intents';
4593 $intent_id = intval($_POST['intent_id']);
4594
4595 $wpdb->delete($table_name, ['id' => $intent_id], ['%d']);
4596 }
4597
4598 wp_safe_redirect(admin_url('admin.php?page=mxchat-actions'));
4599 exit;
4600 }
4601 public function mxchat_handle_edit_intent() {
4602 // Security checks (nonce and permissions)
4603 if (!current_user_can('manage_options')) {
4604 wp_die(esc_html__('Unauthorized user', 'mxchat'));
4605 }
4606 check_admin_referer('mxchat_edit_intent');
4607
4608 // Get POST data
4609 $intent_id = isset($_POST['intent_id']) ? absint($_POST['intent_id']) : 0;
4610 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
4611 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
4612 $threshold_percentage = isset($_POST['similarity_threshold']) ? intval($_POST['similarity_threshold']) : 85;
4613 $similarity_threshold = min(95, max(10, $threshold_percentage)) / 100; // Convert to 0.10–0.95
4614
4615 // Handle enabled_bots
4616 $enabled_bots = isset($_POST['enabled_bots']) ? $_POST['enabled_bots'] : array('default');
4617 $enabled_bots = array_map('sanitize_text_field', $enabled_bots);
4618
4619 // Ensure default is always included for backward compatibility
4620 if (!in_array('default', $enabled_bots)) {
4621 $enabled_bots[] = 'default';
4622 }
4623
4624 $enabled_bots_json = json_encode($enabled_bots);
4625
4626 // Validate inputs
4627 if (!$intent_id || empty($intent_label) || empty($phrases_input)) {
4628 $this->handle_embedding_error(__('Invalid input. Please ensure all fields are filled out.', 'mxchat'));
4629 return;
4630 }
4631
4632 $phrases_array = array_map('sanitize_text_field', array_filter(array_map('trim', explode(',', $phrases_input))));
4633 if (empty($phrases_array)) {
4634 $this->handle_embedding_error(__('Please enter at least one valid phrase.', 'mxchat'));
4635 return;
4636 }
4637
4638 // Generate embeddings with improved error handling
4639 $vectors = [];
4640 $failed_phrases = [];
4641
4642 foreach ($phrases_array as $phrase) {
4643 $embedding_vector = $this->mxchat_generate_embedding($phrase, $this->options['api_key']);
4644 if (is_array($embedding_vector)) {
4645 $vectors[] = $embedding_vector;
4646 } else {
4647 $failed_phrases[] = $phrase;
4648 }
4649 }
4650
4651 if (!empty($failed_phrases)) {
4652 $this->handle_embedding_error(
4653 sprintf(
4654 __('Error generating embeddings for phrases: %s. Check your embedding API.', 'mxchat'),
4655 implode(', ', $failed_phrases)
4656 )
4657 );
4658 return;
4659 }
4660
4661 if (empty($vectors)) {
4662 $this->handle_embedding_error(__('No valid embeddings generated. Please check your phrases.', 'mxchat'));
4663 return;
4664 }
4665
4666 $combined_vector = $this->mxchat_average_vectors($vectors);
4667 $serialized_vector = maybe_serialize($combined_vector);
4668
4669 // Update the database
4670 global $wpdb;
4671 $table_name = $wpdb->prefix . 'mxchat_intents';
4672
4673 $result = $wpdb->update(
4674 $table_name,
4675 array(
4676 'intent_label' => $intent_label,
4677 'phrases' => implode(', ', $phrases_array),
4678 'embedding_vector' => $serialized_vector,
4679 'similarity_threshold' => $similarity_threshold,
4680 'enabled_bots' => $enabled_bots_json, // Include enabled_bots in update
4681 ),
4682 array('id' => $intent_id),
4683 array('%s', '%s', '%s', '%f', '%s'), // Format: string, string, string, float, string
4684 array('%d') // Where format: integer
4685 );
4686
4687 if (false === $result) {
4688 $this->handle_embedding_error(__('Failed to update action in database.', 'mxchat'));
4689 return;
4690 }
4691
4692 // Set success message and redirect
4693 set_transient('mxchat_admin_notice_success', __('Intent updated successfully!', 'mxchat'), 60);
4694
4695 $redirect_url = add_query_arg(
4696 array(
4697 'page' => 'mxchat-actions'
4698 ),
4699 admin_url('admin.php')
4700 );
4701 wp_safe_redirect($redirect_url);
4702 exit;
4703 }
4704 public function mxchat_handle_add_intent() {
4705 if (!current_user_can('manage_options')) {
4706 wp_die(esc_html__('Unauthorized user', 'mxchat'));
4707 }
4708 check_admin_referer('mxchat_add_intent_nonce');
4709 global $wpdb;
4710 $table_name = $wpdb->prefix . 'mxchat_intents';
4711
4712 // Sanitize and get form data
4713 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
4714 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
4715 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
4716
4717 // Get similarity threshold from form (convert percentage to decimal)
4718 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
4719
4720 // Handle enabled_bots
4721 $enabled_bots = isset($_POST['enabled_bots']) ? $_POST['enabled_bots'] : array('default');
4722 $enabled_bots = array_map('sanitize_text_field', $enabled_bots);
4723
4724 // Ensure default is always included for backward compatibility with existing actions
4725 if (!in_array('default', $enabled_bots)) {
4726 $enabled_bots[] = 'default';
4727 }
4728
4729 $enabled_bots_json = json_encode($enabled_bots);
4730
4731 // Validate required fields
4732 if (empty($intent_label) || empty($callback_function) || empty($phrases_input)) {
4733 $this->handle_embedding_error(__('Invalid input. Please ensure all fields are filled out.', 'mxchat'));
4734 return;
4735 }
4736
4737 // Validate callback function
4738 $available_callbacks = $this->mxchat_get_available_callbacks();
4739 if (!array_key_exists($callback_function, $available_callbacks)) {
4740 $this->handle_embedding_error(__('Invalid callback function selected.', 'mxchat'));
4741 return;
4742 }
4743
4744 // Check if this is an add-on promotional placeholder (not a real action)
4745 if (!empty($available_callbacks[$callback_function]['addon_promo'])) {
4746 $addon_name = isset($available_callbacks[$callback_function]['addon_name']) ? $available_callbacks[$callback_function]['addon_name'] : __('an add-on', 'mxchat');
4747 $this->handle_embedding_error(sprintf(
4748 __('This action requires the %s to be installed and activated.', 'mxchat'),
4749 $addon_name
4750 ));
4751 return;
4752 }
4753
4754 // Process phrases
4755 $phrases_array = array_map('sanitize_text_field', array_filter(array_map('trim', explode(',', $phrases_input))));
4756 if (empty($phrases_array)) {
4757 $this->handle_embedding_error(__('Please enter at least one valid phrase.', 'mxchat'));
4758 return;
4759 }
4760
4761 // Generate embeddings with improved error handling
4762 $vectors = [];
4763 $failed_phrases = [];
4764 foreach ($phrases_array as $phrase) {
4765 $embedding_vector = $this->mxchat_generate_embedding($phrase, $this->options['api_key']);
4766 if (is_array($embedding_vector)) {
4767 $vectors[] = $embedding_vector;
4768 } else {
4769 $failed_phrases[] = $phrase;
4770 }
4771 }
4772
4773 // Check for embedding failures
4774 if (!empty($failed_phrases)) {
4775 $this->handle_embedding_error(
4776 sprintf(
4777 __('Error generating embeddings for phrases: %s. Check your embedding API.', 'mxchat'),
4778 implode(', ', $failed_phrases)
4779 )
4780 );
4781 return;
4782 }
4783
4784 if (empty($vectors)) {
4785 $this->handle_embedding_error(__('No valid embeddings generated. Please check your phrases.', 'mxchat'));
4786 return;
4787 }
4788
4789 // Create combined vector and insert into database
4790 $combined_vector = $this->mxchat_average_vectors($vectors);
4791 $serialized_vector = maybe_serialize($combined_vector);
4792
4793 $result = $wpdb->insert($table_name, [
4794 'intent_label' => $intent_label,
4795 'phrases' => implode(', ', $phrases_array),
4796 'embedding_vector' => $serialized_vector,
4797 'callback_function' => $callback_function,
4798 'similarity_threshold' => $similarity_threshold,
4799 'enabled_bots' => $enabled_bots_json, // NEW field
4800 ]);
4801
4802 if ($result === false) {
4803 $this->handle_embedding_error(__('Database error: ', 'mxchat') . $wpdb->last_error);
4804 return;
4805 }
4806
4807 // Set success message and redirect
4808 set_transient('mxchat_admin_notice_success', __('New intent added successfully!', 'mxchat'), 60);
4809 wp_safe_redirect(admin_url('admin.php?page=mxchat-actions'));
4810 exit;
4811 }
4812
4813
4814
4815
4816 private function handle_embedding_error($message, $redirect = true) {
4817 // Store the error message in the existing transient
4818 set_transient('mxchat_admin_notice_error', $message, 60);
4819
4820 if ($redirect) {
4821 // Redirect back to the actions page
4822 $redirect_url = add_query_arg(
4823 array(
4824 'page' => 'mxchat-actions'
4825 ),
4826 admin_url('admin.php')
4827 );
4828 wp_safe_redirect($redirect_url);
4829 exit;
4830 }
4831 }
4832
4833 /**
4834 * AJAX handler to fetch actions list for the new split-panel UI
4835 */
4836 public function mxchat_fetch_actions_list() {
4837 check_ajax_referer('mxchat_actions_nonce', 'security');
4838
4839 if (!current_user_can('manage_options')) {
4840 wp_send_json_error(__('Unauthorized', 'mxchat'));
4841 }
4842
4843 global $wpdb;
4844 $table_name = $wpdb->prefix . 'mxchat_intents';
4845
4846 $page = isset($_POST['page']) ? max(1, intval($_POST['page'])) : 1;
4847 $per_page = isset($_POST['per_page']) ? min(100, max(1, intval($_POST['per_page']))) : 50;
4848 $offset = ($page - 1) * $per_page;
4849 $search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
4850 $callback_filter = isset($_POST['callback_filter']) ? sanitize_text_field($_POST['callback_filter']) : '';
4851 $sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] === 'asc' ? 'ASC' : 'DESC';
4852
4853 // Build WHERE clause
4854 $where = '1=1';
4855 $params = array();
4856
4857 if ($search) {
4858 $search_like = '%' . $wpdb->esc_like($search) . '%';
4859 $where .= ' AND (intent_label LIKE %s OR phrases LIKE %s)';
4860 $params[] = $search_like;
4861 $params[] = $search_like;
4862 }
4863
4864 if ($callback_filter) {
4865 $where .= ' AND callback_function = %s';
4866 $params[] = $callback_filter;
4867 }
4868
4869 // Get total count
4870 $count_query = "SELECT COUNT(*) FROM $table_name WHERE $where";
4871 if (!empty($params)) {
4872 $count_query = $wpdb->prepare($count_query, $params);
4873 }
4874 $total_actions = $wpdb->get_var($count_query);
4875
4876 // Get actions
4877 $query = "SELECT * FROM $table_name WHERE $where ORDER BY id $sort_order LIMIT %d OFFSET %d";
4878 $all_params = array_merge($params, array($per_page, $offset));
4879 $actions = $wpdb->get_results($wpdb->prepare($query, $all_params));
4880
4881 // Get available callbacks for labels/icons
4882 $available_callbacks = $this->mxchat_get_available_callbacks();
4883
4884 // Prefetch individual phrase counts for all fetched actions
4885 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
4886 $phrase_counts = array();
4887 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
4888 $action_ids = wp_list_pluck($actions, 'id');
4889 if (!empty($action_ids)) {
4890 $id_placeholders = implode(',', array_fill(0, count($action_ids), '%d'));
4891 $count_results = $wpdb->get_results($wpdb->prepare(
4892 "SELECT intent_id, COUNT(*) as cnt FROM $phrases_table WHERE intent_id IN ($id_placeholders) GROUP BY intent_id",
4893 $action_ids
4894 ));
4895 foreach ($count_results as $row) {
4896 $phrase_counts[$row->intent_id] = intval($row->cnt);
4897 }
4898 }
4899 }
4900
4901 // Format actions for response
4902 $formatted_actions = array();
4903 foreach ($actions as $action) {
4904 $callback_data = isset($available_callbacks[$action->callback_function])
4905 ? $available_callbacks[$action->callback_function]
4906 : array('label' => $action->callback_function, 'icon' => 'admin-generic');
4907
4908 $enabled_bots = json_decode($action->enabled_bots, true);
4909 if (!is_array($enabled_bots)) {
4910 $enabled_bots = array('default');
4911 }
4912
4913 $formatted_actions[] = array(
4914 'id' => intval($action->id),
4915 'label' => $action->intent_label,
4916 'phrases' => $action->phrases,
4917 'callback_function' => $action->callback_function,
4918 'callback_label' => $callback_data['label'],
4919 'icon' => isset($callback_data['icon']) ? $callback_data['icon'] : 'admin-generic',
4920 'threshold' => round($action->similarity_threshold * 100),
4921 'enabled' => (bool) $action->enabled,
4922 'enabled_bots' => $enabled_bots,
4923 'has_legacy_vector' => !empty($action->embedding_vector),
4924 'individual_phrase_count' => isset($phrase_counts[$action->id]) ? $phrase_counts[$action->id] : 0,
4925 );
4926 }
4927
4928 $total_pages = ceil($total_actions / $per_page);
4929 $showing_start = $total_actions > 0 ? $offset + 1 : 0;
4930 $showing_end = min($offset + $per_page, $total_actions);
4931
4932 wp_send_json_success(array(
4933 'actions' => $formatted_actions,
4934 'page' => $page,
4935 'per_page' => $per_page,
4936 'total_actions' => intval($total_actions),
4937 'total_pages' => $total_pages,
4938 'showing_start' => $showing_start,
4939 'showing_end' => $showing_end,
4940 ));
4941 }
4942
4943 /**
4944 * AJAX handler to toggle action enabled status
4945 */
4946 public function mxchat_toggle_action_status() {
4947 check_ajax_referer('mxchat_actions_nonce', 'security');
4948
4949 if (!current_user_can('manage_options')) {
4950 wp_send_json_error(__('Unauthorized', 'mxchat'));
4951 }
4952
4953 $action_id = isset($_POST['action_id']) ? intval($_POST['action_id']) : 0;
4954 $enabled = isset($_POST['enabled']) ? intval($_POST['enabled']) : 0;
4955
4956 if (!$action_id) {
4957 wp_send_json_error(__('Invalid action ID', 'mxchat'));
4958 }
4959
4960 global $wpdb;
4961 $table_name = $wpdb->prefix . 'mxchat_intents';
4962
4963 $result = $wpdb->update(
4964 $table_name,
4965 array('enabled' => $enabled ? 1 : 0),
4966 array('id' => $action_id),
4967 array('%d'),
4968 array('%d')
4969 );
4970
4971 if ($result === false) {
4972 wp_send_json_error(__('Failed to update action status', 'mxchat'));
4973 }
4974
4975 wp_send_json_success(array('enabled' => (bool) $enabled));
4976 }
4977
4978 /**
4979 * AJAX handler to bulk delete actions
4980 */
4981 public function mxchat_bulk_delete_actions() {
4982 check_ajax_referer('mxchat_delete_intent_nonce', 'security');
4983
4984 if (!current_user_can('manage_options')) {
4985 wp_send_json_error(__('Unauthorized', 'mxchat'));
4986 }
4987
4988 $action_ids = isset($_POST['action_ids']) ? array_map('intval', (array) $_POST['action_ids']) : array();
4989
4990 if (empty($action_ids)) {
4991 wp_send_json_error(__('No actions selected', 'mxchat'));
4992 }
4993
4994 global $wpdb;
4995 $table_name = $wpdb->prefix . 'mxchat_intents';
4996
4997 $placeholders = implode(',', array_fill(0, count($action_ids), '%d'));
4998 $query = $wpdb->prepare("DELETE FROM $table_name WHERE id IN ($placeholders)", $action_ids);
4999 $result = $wpdb->query($query);
5000
5001 if ($result === false) {
5002 wp_send_json_error(__('Failed to delete actions', 'mxchat'));
5003 }
5004
5005 // Also delete individual phrases for these intents
5006 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5007 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5008 $wpdb->query($wpdb->prepare("DELETE FROM $phrases_table WHERE intent_id IN ($placeholders)", $action_ids));
5009 }
5010
5011 wp_send_json_success(array('deleted' => $result));
5012 }
5013
5014 /**
5015 * AJAX handler to add a new intent/action
5016 */
5017 public function mxchat_add_intent_ajax() {
5018 check_ajax_referer('mxchat_add_intent_nonce', 'security');
5019
5020 if (!current_user_can('manage_options')) {
5021 wp_send_json_error(__('Unauthorized', 'mxchat'));
5022 }
5023
5024 global $wpdb;
5025 $table_name = $wpdb->prefix . 'mxchat_intents';
5026
5027 // Sanitize input
5028 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
5029 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
5030 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
5031 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
5032 $enabled_bots = isset($_POST['enabled_bots']) ? array_map('sanitize_text_field', (array) $_POST['enabled_bots']) : array('default');
5033
5034 // Validate
5035 // Check if using new individual phrases mode or legacy mode
5036 $individual_phrases = isset($_POST['individual_phrases']) ? array_filter(array_map('sanitize_text_field', (array) $_POST['individual_phrases'])) : array();
5037 $use_individual = !empty($individual_phrases);
5038
5039 // Validate required fields (phrases not required when using individual mode)
5040 if (empty($intent_label) || empty($callback_function)) {
5041 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
5042 }
5043 if (!$use_individual && empty($phrases_input)) {
5044 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
5045 }
5046
5047 // Ensure default bot is included
5048 if (!in_array('default', $enabled_bots)) {
5049 $enabled_bots[] = 'default';
5050 }
5051 $enabled_bots_json = json_encode($enabled_bots);
5052
5053 if ($use_individual) {
5054 // New mode: individual phrases each get their own vector
5055 // Insert the intent row with empty legacy fields
5056 $result = $wpdb->insert(
5057 $table_name,
5058 array(
5059 'intent_label' => $intent_label,
5060 'phrases' => '',
5061 'embedding_vector' => '',
5062 'similarity_threshold' => $similarity_threshold,
5063 'callback_function' => $callback_function,
5064 'enabled' => 1,
5065 'enabled_bots' => $enabled_bots_json,
5066 )
5067 );
5068
5069 if ($result === false) {
5070 wp_send_json_error(__('Failed to add action to database.', 'mxchat'));
5071 }
5072
5073 $intent_id = $wpdb->insert_id;
5074
5075 // Insert each phrase individually with its own embedding
5076 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5077 $failed_phrases = array();
5078 foreach ($individual_phrases as $phrase) {
5079 $phrase = trim($phrase);
5080 if (empty($phrase)) continue;
5081
5082 $embedding_vector = $this->mxchat_generate_embedding($phrase);
5083 if (is_wp_error($embedding_vector)) {
5084 $failed_phrases[] = $phrase;
5085 continue;
5086 }
5087
5088 $wpdb->insert(
5089 $phrases_table,
5090 array(
5091 'intent_id' => $intent_id,
5092 'phrase' => $phrase,
5093 'embedding_vector' => maybe_serialize($embedding_vector),
5094 )
5095 );
5096 }
5097
5098 $response = array('id' => $intent_id);
5099 if (!empty($failed_phrases)) {
5100 $response['failed_phrases'] = $failed_phrases;
5101 }
5102 wp_send_json_success($response);
5103
5104 } else {
5105 // Legacy mode: combine all phrases into one embedding (backwards compatible)
5106 $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
5107 if (empty($phrases_array)) {
5108 wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
5109 }
5110
5111 // Generate embedding (combine phrases into single string for embedding)
5112 $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
5113 if (is_wp_error($embedding_vector)) {
5114 // Fallback: store without embedding
5115 $serialized_vector = null;
5116 } else {
5117 $serialized_vector = maybe_serialize($embedding_vector);
5118 }
5119
5120 // Insert
5121 $result = $wpdb->insert(
5122 $table_name,
5123 array(
5124 'intent_label' => $intent_label,
5125 'phrases' => implode(', ', $phrases_array),
5126 'embedding_vector' => $serialized_vector,
5127 'similarity_threshold' => $similarity_threshold,
5128 'callback_function' => $callback_function,
5129 'enabled' => 1,
5130 'enabled_bots' => $enabled_bots_json,
5131 )
5132 );
5133
5134 if ($result === false) {
5135 wp_send_json_error(__('Failed to add action to database.', 'mxchat'));
5136 }
5137
5138 wp_send_json_success(array('id' => $wpdb->insert_id));
5139 }
5140 }
5141
5142 /**
5143 * AJAX handler to edit an existing intent/action
5144 */
5145 public function mxchat_edit_intent_ajax() {
5146 check_ajax_referer('mxchat_edit_intent', 'security');
5147
5148 if (!current_user_can('manage_options')) {
5149 wp_send_json_error(__('Unauthorized', 'mxchat'));
5150 }
5151
5152 global $wpdb;
5153 $table_name = $wpdb->prefix . 'mxchat_intents';
5154
5155 // Sanitize input
5156 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5157 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
5158 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
5159 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
5160 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
5161 $enabled_bots = isset($_POST['enabled_bots']) ? array_map('sanitize_text_field', (array) $_POST['enabled_bots']) : array('default');
5162
5163 // Validate
5164 if (!$intent_id || empty($intent_label)) {
5165 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
5166 }
5167
5168 // Check if phrases are managed individually (empty phrases_input means individual mode)
5169 $uses_individual_phrases = empty($phrases_input);
5170
5171 if ($uses_individual_phrases) {
5172 // Individual phrase mode: only update non-phrase fields, skip embedding regeneration
5173 $update_data = array(
5174 'intent_label' => $intent_label,
5175 'similarity_threshold' => $similarity_threshold,
5176 'callback_function' => $callback_function,
5177 'enabled_bots' => json_encode($enabled_bots),
5178 );
5179 } else {
5180 // Legacy mode: process phrases and regenerate embedding (backwards compatible for add-ons)
5181 $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
5182 if (empty($phrases_array)) {
5183 wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
5184 }
5185
5186 // Generate new embedding (combine phrases into single string for embedding)
5187 $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
5188 if (is_wp_error($embedding_vector)) {
5189 // Keep existing embedding
5190 $update_data = array(
5191 'intent_label' => $intent_label,
5192 'phrases' => implode(', ', $phrases_array),
5193 'similarity_threshold' => $similarity_threshold,
5194 'callback_function' => $callback_function,
5195 'enabled_bots' => json_encode($enabled_bots),
5196 );
5197 } else {
5198 $serialized_vector = maybe_serialize($embedding_vector);
5199 $update_data = array(
5200 'intent_label' => $intent_label,
5201 'phrases' => implode(', ', $phrases_array),
5202 'embedding_vector' => $serialized_vector,
5203 'similarity_threshold' => $similarity_threshold,
5204 'callback_function' => $callback_function,
5205 'enabled_bots' => json_encode($enabled_bots),
5206 );
5207 }
5208 }
5209
5210 // Ensure default bot is included
5211 if (!in_array('default', $enabled_bots)) {
5212 $enabled_bots[] = 'default';
5213 }
5214 $update_data['enabled_bots'] = json_encode($enabled_bots);
5215
5216 $result = $wpdb->update(
5217 $table_name,
5218 $update_data,
5219 array('id' => $intent_id),
5220 null,
5221 array('%d')
5222 );
5223
5224 if ($result === false) {
5225 wp_send_json_error(__('Failed to update action.', 'mxchat'));
5226 }
5227
5228 wp_send_json_success(array('updated' => true));
5229 }
5230
5231 /**
5232 * AJAX handler to delete a single intent/action
5233 */
5234 public function mxchat_delete_intent_ajax() {
5235 check_ajax_referer('mxchat_delete_intent_nonce', 'security');
5236
5237 if (!current_user_can('manage_options')) {
5238 wp_send_json_error(__('Unauthorized', 'mxchat'));
5239 }
5240
5241 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5242
5243 if (!$intent_id) {
5244 wp_send_json_error(__('Invalid action ID', 'mxchat'));
5245 }
5246
5247 global $wpdb;
5248 $table_name = $wpdb->prefix . 'mxchat_intents';
5249
5250 $result = $wpdb->delete($table_name, array('id' => $intent_id), array('%d'));
5251
5252 if ($result === false) {
5253 wp_send_json_error(__('Failed to delete action', 'mxchat'));
5254 }
5255
5256 // Also delete individual phrases for this intent
5257 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5258 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5259 $wpdb->delete($phrases_table, array('intent_id' => $intent_id), array('%d'));
5260 }
5261
5262 wp_send_json_success(array('deleted' => true));
5263 }
5264
5265 /**
5266 * AJAX handler to add a single phrase with its own embedding to an intent
5267 */
5268 public function mxchat_add_phrase_ajax() {
5269 check_ajax_referer('mxchat_add_phrase_nonce', 'security');
5270
5271 if (!current_user_can('manage_options')) {
5272 wp_send_json_error(__('Unauthorized', 'mxchat'));
5273 }
5274
5275 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5276 $phrase = isset($_POST['phrase']) ? sanitize_text_field($_POST['phrase']) : '';
5277
5278 if (!$intent_id || empty($phrase)) {
5279 wp_send_json_error(__('Please provide an intent ID and phrase.', 'mxchat'));
5280 }
5281
5282 // Verify the intent exists
5283 global $wpdb;
5284 $intents_table = $wpdb->prefix . 'mxchat_intents';
5285 $intent = $wpdb->get_row($wpdb->prepare("SELECT id FROM $intents_table WHERE id = %d", $intent_id));
5286 if (!$intent) {
5287 wp_send_json_error(__('Action not found.', 'mxchat'));
5288 }
5289
5290 // Generate embedding for this single phrase
5291 $embedding_vector = $this->mxchat_generate_embedding($phrase);
5292 if (is_wp_error($embedding_vector)) {
5293 wp_send_json_error(__('Failed to generate embedding: ', 'mxchat') . $embedding_vector->get_error_message());
5294 }
5295
5296 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5297 $result = $wpdb->insert(
5298 $phrases_table,
5299 array(
5300 'intent_id' => $intent_id,
5301 'phrase' => $phrase,
5302 'embedding_vector' => maybe_serialize($embedding_vector),
5303 )
5304 );
5305
5306 if ($result === false) {
5307 wp_send_json_error(__('Failed to add phrase.', 'mxchat'));
5308 }
5309
5310 wp_send_json_success(array('id' => $wpdb->insert_id, 'phrase' => $phrase));
5311 }
5312
5313 /**
5314 * AJAX handler to delete a single phrase from wp_mxchat_intent_phrases
5315 */
5316 public function mxchat_delete_phrase_ajax() {
5317 check_ajax_referer('mxchat_delete_phrase_nonce', 'security');
5318
5319 if (!current_user_can('manage_options')) {
5320 wp_send_json_error(__('Unauthorized', 'mxchat'));
5321 }
5322
5323 $phrase_id = isset($_POST['phrase_id']) ? intval($_POST['phrase_id']) : 0;
5324 if (!$phrase_id) {
5325 wp_send_json_error(__('Invalid phrase ID.', 'mxchat'));
5326 }
5327
5328 global $wpdb;
5329 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5330 $result = $wpdb->delete($phrases_table, array('id' => $phrase_id), array('%d'));
5331
5332 if ($result === false) {
5333 wp_send_json_error(__('Failed to delete phrase.', 'mxchat'));
5334 }
5335
5336 wp_send_json_success(array('deleted' => true));
5337 }
5338
5339 /**
5340 * AJAX handler to fetch individual phrases for an intent
5341 */
5342 public function mxchat_get_phrases_ajax() {
5343 check_ajax_referer('mxchat_get_phrases_nonce', 'security');
5344
5345 if (!current_user_can('manage_options')) {
5346 wp_send_json_error(__('Unauthorized', 'mxchat'));
5347 }
5348
5349 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5350 if (!$intent_id) {
5351 wp_send_json_error(__('Invalid intent ID.', 'mxchat'));
5352 }
5353
5354 global $wpdb;
5355 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5356
5357 $phrases = array();
5358 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5359 $phrases = $wpdb->get_results($wpdb->prepare(
5360 "SELECT id, phrase, created_at FROM $phrases_table WHERE intent_id = %d ORDER BY created_at ASC",
5361 $intent_id
5362 ));
5363 }
5364
5365 wp_send_json_success(array('phrases' => $phrases));
5366 }
5367
5368 /**
5369 * AJAX handler to clear legacy phrases and embedding from the main intents table
5370 */
5371 public function mxchat_delete_legacy_phrases_ajax() {
5372 check_ajax_referer('mxchat_delete_legacy_nonce', 'security');
5373
5374 if (!current_user_can('manage_options')) {
5375 wp_send_json_error(__('Unauthorized', 'mxchat'));
5376 }
5377
5378 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5379 if (!$intent_id) {
5380 wp_send_json_error(__('Invalid intent ID.', 'mxchat'));
5381 }
5382
5383 global $wpdb;
5384 $table_name = $wpdb->prefix . 'mxchat_intents';
5385
5386 $result = $wpdb->update(
5387 $table_name,
5388 array('phrases' => '', 'embedding_vector' => ''),
5389 array('id' => $intent_id),
5390 array('%s', '%s'),
5391 array('%d')
5392 );
5393
5394 if ($result === false) {
5395 wp_send_json_error(__('Failed to clear legacy phrases.', 'mxchat'));
5396 }
5397
5398 wp_send_json_success(array('cleared' => true));
5399 }
5400
5401 /**
5402 * Enhanced get_available_callbacks function with form action exclusion
5403 *
5404 * @param bool $grouped Whether to return callbacks grouped by category
5405 * @param bool $include_all Whether to include all potential actions (even if add-on not installed)
5406 * @return array Callbacks data with icons, descriptions and availability status
5407 */
5408 private function mxchat_get_available_callbacks($grouped = false, $include_all = true) {
5409 // Load WordPress plugin functions if needed
5410 if (!function_exists('get_plugins')) {
5411 require_once ABSPATH . 'wp-admin/includes/plugin.php';
5412 }
5413
5414 // Get active plugins
5415 $active_plugins = get_option('active_plugins', array());
5416
5417 // Functions to exclude from the action selector only if Pro is activated
5418 // If user doesn't have Pro, show these so they can see what they're missing
5419 $excluded_when_pro_active_functions = array(
5420 'mxchat_handle_form_collection' // Forms add-on action
5421 );
5422
5423 // Always excluded functions (regardless of Pro status)
5424 $always_excluded_functions = array();
5425
5426 // Combine exclusion lists based on Pro activation status
5427 $excluded_functions = $always_excluded_functions;
5428 if ($this->is_activated) {
5429 // Only exclude add-on managed functions if Pro is active
5430 $excluded_functions = array_merge($excluded_functions, $excluded_when_pro_active_functions);
5431 }
5432
5433 // Define add-on plugin files and their corresponding action functions
5434 $addon_plugins = array(
5435 'mxchat-woo/mxchat-woo.php' => array(
5436 'functions' => array(
5437 'mxchat_handle_product_recommendations',
5438 'mxchat_handle_order_history',
5439 'mxchat_show_product_card',
5440 'mxchat_add_to_cart',
5441 'mxchat_checkout_redirect',
5442 'mxchat_handle_featured_products'
5443 ),
5444 'name' => __('WooCommerce Add-on', 'mxchat'),
5445 'pro_required' => true
5446 ),
5447 'mxchat-perplexity/mxchat-perplexity.php' => array(
5448 'functions' => array('mxchat_perplexity_research'),
5449 'name' => __('Perplexity Add-on', 'mxchat'),
5450 'pro_required' => true
5451 ),
5452 'mxchat-forms/mxchat-forms.php' => array(
5453 'functions' => array('mxchat_handle_form_collection'),
5454 'name' => __('Forms Add-on', 'mxchat'),
5455 'pro_required' => true
5456 ),
5457 // Add other add-ons and their functions here
5458 );
5459
5460 // Get the functions that are provided by active add-ons
5461 $addon_provided_functions = array();
5462 $addon_function_mapping = array(); // Maps functions to their add-on info
5463
5464 // Check which add-ons are active
5465 foreach ($addon_plugins as $plugin_file => $addon_info) {
5466 $is_active = in_array($plugin_file, $active_plugins);
5467
5468 // For each function in this addon
5469 foreach ($addon_info['functions'] as $function) {
5470 // Consider a function installed only if:
5471 // 1. The add-on is active AND
5472 // 2. Either it doesn't require Pro OR Pro is activated
5473 $is_installed = $is_active && (!$addon_info['pro_required'] || $this->is_activated);
5474
5475 // If the add-on is installed, mark this function as provided by an add-on
5476 if ($is_installed) {
5477 $addon_provided_functions[] = $function;
5478 }
5479
5480 // Store addon info for this function regardless of installation status
5481 $addon_function_mapping[$function] = array(
5482 'addon' => basename(dirname($plugin_file)),
5483 'addon_name' => $addon_info['name'],
5484 'pro_required' => $addon_info['pro_required'],
5485 'is_active' => $is_active,
5486 'is_installed' => $is_installed
5487 );
5488 }
5489 }
5490
5491 // Core callbacks - always available in the base plugin
5492 $core_callbacks = array(
5493 'mxchat_handle_email_capture' => array(
5494 'label' => __('Loops Email Capture', 'mxchat'),
5495 'pro_only' => false,
5496 'group' => __('Customer Engagement', 'mxchat'),
5497 'icon' => 'email-alt',
5498 'description' => __('Collect visitor emails for your mailing list in Loops', 'mxchat'),
5499 'addon' => false, // Not from an add-on
5500 'installed' => true // Always installed with base plugin
5501 ),
5502 'mxchat_handle_search_request' => array(
5503 'label' => __('Brave Web Search', 'mxchat'),
5504 'pro_only' => false,
5505 'group' => __('Search Features', 'mxchat'),
5506 'icon' => 'search',
5507 'description' => __('Let users search the web directly from the chat', 'mxchat'),
5508 'addon' => false,
5509 'installed' => true
5510 ),
5511 'mxchat_handle_image_search_request' => array(
5512 'label' => __('Brave Image Search', 'mxchat'),
5513 'pro_only' => false,
5514 'group' => __('Search Features', 'mxchat'),
5515 'icon' => 'format-image',
5516 'description' => __('Search and display images in the chat conversation', 'mxchat'),
5517 'addon' => false,
5518 'installed' => true
5519 ),
5520 // Pro core features - check is_activated property
5521 'mxchat_generate_image' => array(
5522 'label' => __('Generate Image (OpenAI)', 'mxchat'),
5523 'pro_only' => false,
5524 'group' => __('Other Features', 'mxchat'),
5525 'icon' => 'art',
5526 'description' => __('Create images with GPT Image from OpenAI (requires OpenAI API key)', 'mxchat'),
5527 'addon' => false,
5528 'installed' => true
5529 ),
5530 'mxchat_generate_gemini_image' => array(
5531 'label' => __('Generate Image (Gemini)', 'mxchat'),
5532 'pro_only' => false,
5533 'group' => __('Other Features', 'mxchat'),
5534 'icon' => 'art',
5535 'description' => __('Create images with Imagen from Google (requires Gemini API key)', 'mxchat'),
5536 'addon' => false,
5537 'installed' => true
5538 ),
5539 'mxchat_handle_pdf_discussion' => array(
5540 'label' => __('Chat with PDF', 'mxchat'),
5541 'pro_only' => false,
5542 'group' => __('Other Features', 'mxchat'),
5543 'icon' => 'media-document',
5544 'description' => __('Answer questions about uploaded PDF documents', 'mxchat'),
5545 'addon' => false,
5546 'installed' => true
5547 ),
5548 'mxchat_live_agent_handover' => array(
5549 'label' => __('Slack Live Agent', 'mxchat'),
5550 'pro_only' => false,
5551 'group' => __('Customer Engagement', 'mxchat'),
5552 'icon' => 'admin-users',
5553 'description' => __('Transfer conversation to a human support agent on Slack', 'mxchat'),
5554 'addon' => false,
5555 'installed' => true
5556 ),
5557 'mxchat_telegram_live_agent_handover' => array(
5558 'label' => __('Telegram Live Agent', 'mxchat'),
5559 'pro_only' => false,
5560 'group' => __('Customer Engagement', 'mxchat'),
5561 'icon' => 'format-chat',
5562 'description' => __('Transfer conversation to a human support agent on Telegram', 'mxchat'),
5563 'addon' => false,
5564 'installed' => true
5565 ),
5566 'mxchat_handle_switch_to_chatbot_intent' => array(
5567 'label' => __('Back to Chatbot', 'mxchat'),
5568 'pro_only' => false,
5569 'group' => __('Customer Engagement', 'mxchat'),
5570 'icon' => 'backup',
5571 'description' => __('Return from live agent mode to AI chatbot', 'mxchat'),
5572 'addon' => false,
5573 'installed' => true
5574 ),
5575 );
5576
5577 // Add-on callbacks with placeholders - only include if the add-on is NOT active
5578 // These are promotional/informational only — not selectable as real actions
5579 $addon_callbacks = array(
5580 // WooCommerce Add-on
5581 'mxchat_handle_product_recommendations' => array(
5582 'label' => __('Product Recommendations', 'mxchat'),
5583 'pro_only' => false,
5584 'addon_promo' => true,
5585 'group' => __('WooCommerce Features', 'mxchat'),
5586 'icon' => 'cart',
5587 'description' => __('Suggest products based on customer preferences', 'mxchat'),
5588 ),
5589 'mxchat_handle_order_history' => array(
5590 'label' => __('Order History', 'mxchat'),
5591 'pro_only' => false,
5592 'addon_promo' => true,
5593 'group' => __('WooCommerce Features', 'mxchat'),
5594 'icon' => 'clipboard',
5595 'description' => __('Allow customers to check their order status', 'mxchat'),
5596 ),
5597 'mxchat_show_product_card' => array(
5598 'label' => __('Show Product Card', 'mxchat'),
5599 'pro_only' => false,
5600 'addon_promo' => true,
5601 'group' => __('WooCommerce Features', 'mxchat'),
5602 'icon' => 'products',
5603 'description' => __('Display product information in the chat', 'mxchat'),
5604 ),
5605 'mxchat_add_to_cart' => array(
5606 'label' => __('Add to Cart', 'mxchat'),
5607 'pro_only' => false,
5608 'addon_promo' => true,
5609 'group' => __('WooCommerce Features', 'mxchat'),
5610 'icon' => 'plus-alt',
5611 'description' => __('Add products to cart directly from chat', 'mxchat'),
5612 ),
5613 'mxchat_checkout_redirect' => array(
5614 'label' => __('Proceed to Checkout', 'mxchat'),
5615 'pro_only' => false,
5616 'addon_promo' => true,
5617 'group' => __('WooCommerce Features', 'mxchat'),
5618 'icon' => 'arrow-right-alt',
5619 'description' => __('Redirect customer to checkout page', 'mxchat'),
5620 ),
5621 'mxchat_handle_featured_products' => array(
5622 'label' => __('Featured Products Showcase', 'mxchat'),
5623 'pro_only' => false,
5624 'addon_promo' => true,
5625 'group' => __('WooCommerce Features', 'mxchat'),
5626 'icon' => 'star-filled',
5627 'description' => __('Display a curated selection of products with an AI-generated message', 'mxchat'),
5628 ),
5629
5630 // Perplexity Add-on
5631 'mxchat_perplexity_research' => array(
5632 'label' => __('Perplexity Research', 'mxchat'),
5633 'pro_only' => false,
5634 'addon_promo' => true,
5635 'group' => __('Search Features', 'mxchat'),
5636 'icon' => 'book-alt',
5637 'description' => __('Allows the chatbot to search the web for accurate, up-to-date answers', 'mxchat'),
5638 ),
5639
5640 // Forms Add-on
5641 'mxchat_handle_form_collection' => array(
5642 'label' => __('Form Collection', 'mxchat'),
5643 'pro_only' => false,
5644 'addon_promo' => true,
5645 'group' => __('Form Features', 'mxchat'),
5646 'icon' => 'feedback',
5647 'description' => __('Collect user information through custom forms in chat', 'mxchat'),
5648 ),
5649 );
5650
5651 // Enhance add-on callbacks with installation status and addon info
5652 foreach ($addon_callbacks as $function => $data) {
5653 if (isset($addon_function_mapping[$function])) {
5654 $addon_info = $addon_function_mapping[$function];
5655
5656 $addon_callbacks[$function]['addon'] = $addon_info['addon'];
5657 $addon_callbacks[$function]['addon_name'] = $addon_info['addon_name'];
5658 $addon_callbacks[$function]['installed'] = $addon_info['is_installed'];
5659
5660 // Set pro_only based on add-on configuration
5661 $addon_callbacks[$function]['pro_only'] = $addon_info['pro_required'];
5662 } else {
5663 $addon_callbacks[$function]['addon'] = 'unknown';
5664 $addon_callbacks[$function]['addon_name'] = __('Unknown Add-on', 'mxchat');
5665 $addon_callbacks[$function]['installed'] = false;
5666 }
5667 }
5668
5669 // Initialize callbacks with core features
5670 $callbacks = $core_callbacks;
5671
5672 // Get callbacks from active add-ons
5673 $active_addon_callbacks = apply_filters('mxchat_available_callbacks', array());
5674
5675 // Add placeholder callbacks only for add-ons that aren't active
5676 if ($include_all) {
5677 foreach ($addon_callbacks as $function => $data) {
5678 // Skip placeholders for functions provided by active add-ons
5679 if (in_array($function, $addon_provided_functions)) {
5680 continue;
5681 }
5682
5683 // Skip excluded functions
5684 if (in_array($function, $excluded_functions)) {
5685 continue;
5686 }
5687
5688 // Add the placeholder
5689 $callbacks[$function] = $data;
5690 }
5691 }
5692
5693 // Add callbacks from active add-ons (will override placeholders)
5694 foreach ($active_addon_callbacks as $function => $data) {
5695 // Skip excluded functions
5696 if (in_array($function, $excluded_functions)) {
5697 continue;
5698 }
5699
5700 // Always include callbacks from add-ons
5701 $callbacks[$function] = $data;
5702
5703 // Ensure they have the proper add-on info
5704 if (isset($addon_function_mapping[$function])) {
5705 $addon_info = $addon_function_mapping[$function];
5706 $callbacks[$function]['addon'] = $addon_info['addon'];
5707 $callbacks[$function]['addon_name'] = $addon_info['addon_name'];
5708 $callbacks[$function]['installed'] = $addon_info['is_installed'];
5709 $callbacks[$function]['pro_only'] = $addon_info['pro_required'];
5710 }
5711 }
5712
5713 // Just before returning callbacks, sort them to prioritize free features
5714 if (!$grouped) {
5715 // Create temporary arrays for sorting
5716 $free_callbacks = array();
5717 $pro_callbacks = array();
5718
5719 // Split callbacks into free and pro
5720 foreach ($callbacks as $key => $data) {
5721 if (isset($data['pro_only']) && $data['pro_only']) {
5722 $pro_callbacks[$key] = $data;
5723 } else {
5724 $free_callbacks[$key] = $data;
5725 }
5726 }
5727
5728 // Merge with free callbacks first
5729 $callbacks = array_merge($free_callbacks, $pro_callbacks);
5730 }
5731
5732 // Return grouped structure if requested
5733 if ($grouped) {
5734 $grouped_callbacks = array();
5735 foreach ($callbacks as $key => $data) {
5736 $group_label = isset($data['group']) ? $data['group'] : __('Other Features', 'mxchat');
5737
5738 // Ensure we carry forward all the new fields in grouped mode
5739 $callback_data = array(
5740 'label' => $data['label'],
5741 'pro_only' => isset($data['pro_only']) ? $data['pro_only'] : false,
5742 'icon' => isset($data['icon']) ? $data['icon'] : 'admin-generic',
5743 'description' => isset($data['description']) ? $data['description'] : __('Custom action for your chatbot', 'mxchat'),
5744 'addon' => isset($data['addon']) ? $data['addon'] : false,
5745 'addon_name' => isset($data['addon_name']) ? $data['addon_name'] : '',
5746 'installed' => isset($data['installed']) ? $data['installed'] : true
5747 );
5748
5749 $grouped_callbacks[$group_label][$key] = $callback_data;
5750 }
5751
5752 // Sort within each group to prioritize free features
5753 foreach ($grouped_callbacks as $group => $items) {
5754 $free_items = array();
5755 $pro_items = array();
5756
5757 foreach ($items as $key => $data) {
5758 if (isset($data['pro_only']) && $data['pro_only']) {
5759 $pro_items[$key] = $data;
5760 } else {
5761 $free_items[$key] = $data;
5762 }
5763 }
5764
5765 $grouped_callbacks[$group] = array_merge($free_items, $pro_items);
5766 }
5767
5768 return $grouped_callbacks;
5769 }
5770
5771 return $callbacks;
5772 }
5773
5774 public function mxchat_page_init() {
5775 register_setting(
5776 'mxchat_option_group',
5777 'mxchat_options',
5778 array($this, 'mxchat_sanitize')
5779 );
5780
5781 register_setting(
5782 'mxchat_option_group',
5783 'mxchat_similarity_threshold',
5784 array(
5785 'type' => 'number',
5786 'sanitize_callback' => function($value) {
5787 $value = absint($value);
5788 return min(max($value, 20), 95);
5789 },
5790 'default' => 80,
5791 )
5792 );
5793
5794 // Chatbot Settings Section
5795 add_settings_section(
5796 'mxchat_chatbot_section',
5797 esc_html__('Chatbot Settings', 'mxchat'),
5798 null,
5799 'mxchat-chatbot'
5800 );
5801
5802 // API Keys Settings Section
5803 add_settings_section(
5804 'mxchat_api_keys_section',
5805 esc_html__('API Keys', 'mxchat'),
5806 array($this, 'mxchat_api_keys_section_callback'),
5807 'mxchat-api-keys'
5808 );
5809
5810 // OpenAI API Key
5811 add_settings_field(
5812 'api_key',
5813 esc_html__('OpenAI API Key', 'mxchat'),
5814 array($this, 'api_key_callback'),
5815 'mxchat-api-keys',
5816 'mxchat_api_keys_section'
5817 );
5818
5819 // X.AI API Key
5820 add_settings_field(
5821 'xai_api_key',
5822 esc_html__('X.AI API Key', 'mxchat'),
5823 array($this, 'xai_api_key_callback'),
5824 'mxchat-api-keys',
5825 'mxchat_api_keys_section'
5826 );
5827
5828 // Claude API Key
5829 add_settings_field(
5830 'claude_api_key',
5831 esc_html__('Claude API Key', 'mxchat'),
5832 array($this, 'claude_api_key_callback'),
5833 'mxchat-api-keys',
5834 'mxchat_api_keys_section'
5835 );
5836
5837 // DeepSeek API Key
5838 add_settings_field(
5839 'deepseek_api_key',
5840 esc_html__('DeepSeek API Key', 'mxchat'),
5841 array($this, 'deepseek_api_key_callback'),
5842 'mxchat-api-keys',
5843 'mxchat_api_keys_section'
5844 );
5845
5846 // Google Gemini API Key
5847 add_settings_field(
5848 'gemini_api_key',
5849 esc_html__('Google Gemini API Key', 'mxchat'),
5850 array($this, 'gemini_api_key_callback'),
5851 'mxchat-api-keys',
5852 'mxchat_api_keys_section'
5853 );
5854
5855 // Voyage AI API Key
5856 add_settings_field(
5857 'voyage_api_key',
5858 esc_html__('Voyage AI API Key', 'mxchat'),
5859 array($this, 'voyage_api_key_callback'),
5860 'mxchat-api-keys',
5861 'mxchat_api_keys_section'
5862 );
5863
5864 // OpenRouter API Key
5865 add_settings_field(
5866 'openrouter_api_key',
5867 esc_html__('OpenRouter API Key', 'mxchat'),
5868 array($this, 'openrouter_api_key_callback'),
5869 'mxchat-api-keys',
5870 'mxchat_api_keys_section'
5871 );
5872
5873 // Loops API Key
5874 add_settings_field(
5875 'loops_api_key',
5876 esc_html__('Loops API Key', 'mxchat'),
5877 array($this, 'mxchat_loops_api_key_callback'),
5878 'mxchat-api-keys',
5879 'mxchat_api_keys_section'
5880 );
5881
5882 // Brave Search API Key
5883 add_settings_field(
5884 'brave_api_key',
5885 __('Brave API Key', 'mxchat'),
5886 array($this, 'mxchat_brave_api_key_callback'),
5887 'mxchat-api-keys',
5888 'mxchat_api_keys_section'
5889 );
5890
5891 // Similarity Threshold Slider
5892 add_settings_field(
5893 'similarity_threshold', // Field ID
5894 esc_html__('Similarity Threshold', 'mxchat'), // Field title
5895 array($this, 'mxchat_similarity_threshold_callback'), // Callback function
5896 'mxchat-chatbot', // Page
5897 'mxchat_chatbot_section' // Section
5898 );
5899
5900 // RAG Sources Limit Slider
5901 add_settings_field(
5902 'rag_sources_limit', // Field ID
5903 esc_html__('RAG Sources Limit', 'mxchat'), // Field title
5904 array($this, 'mxchat_rag_sources_limit_callback'), // Callback function
5905 'mxchat-chatbot', // Page
5906 'mxchat_chatbot_section' // Section
5907 );
5908
5909 // RAG Chunks Limit Slider
5910 add_settings_field(
5911 'rag_chunks_limit', // Field ID
5912 esc_html__('RAG Chunks Limit', 'mxchat'), // Field title
5913 array($this, 'mxchat_rag_chunks_limit_callback'), // Callback function
5914 'mxchat-chatbot', // Page
5915 'mxchat_chatbot_section' // Section
5916 );
5917
5918 add_settings_field(
5919 'append_to_body',
5920 esc_html__('Auto-Display Chatbot', 'mxchat'),
5921 array($this, 'mxchat_append_to_body_callback'),
5922 'mxchat-chatbot',
5923 'mxchat_chatbot_section'
5924 );
5925
5926 add_settings_field(
5927 'contextual_awareness_toggle',
5928 esc_html__('Contextual Awareness', 'mxchat'),
5929 array($this, 'mxchat_contextual_awareness_callback'),
5930 'mxchat-chatbot',
5931 'mxchat_chatbot_section'
5932 );
5933
5934 add_settings_field(
5935 'citation_links_toggle',
5936 esc_html__('Citation Links', 'mxchat'),
5937 array($this, 'mxchat_citation_links_toggle_callback'),
5938 'mxchat-chatbot',
5939 'mxchat_chatbot_section'
5940 );
5941
5942 add_settings_field(
5943 'enable_streaming_toggle',
5944 esc_html__('Enable Streaming', 'mxchat'),
5945 array($this, 'enable_streaming_toggle_callback'),
5946 'mxchat-chatbot',
5947 'mxchat_chatbot_section', // Same section as your working toggle
5948 array(
5949 'class' => 'mxchat-setting-row streaming-setting',
5950 'style' => 'display: none;' // Hidden by default, shown when OpenAI/Claude selected
5951 )
5952 );
5953
5954
5955 add_settings_field(
5956 'model',
5957 esc_html__('Chat Model', 'mxchat'),
5958 array($this, 'mxchat_model_callback'),
5959 'mxchat-chatbot',
5960 'mxchat_chatbot_section'
5961 );
5962
5963 add_settings_field(
5964 'embedding_model',
5965 esc_html__('Embedding Model', 'mxchat'),
5966 array($this, 'embedding_model_callback'),
5967 'mxchat-chatbot',
5968 'mxchat_chatbot_section'
5969 );
5970
5971 add_settings_field(
5972 'system_prompt_instructions',
5973 esc_html__('AI Instructions (Behavior)', 'mxchat'),
5974 array($this, 'system_prompt_instructions_callback'),
5975 'mxchat-chatbot',
5976 'mxchat_chatbot_section'
5977 );
5978
5979
5980 add_settings_field(
5981 'top_bar_title',
5982 esc_html__('Top Bar Title', 'mxchat'),
5983 array($this, 'mxchat_top_bar_title_callback'),
5984 'mxchat-chatbot',
5985 'mxchat_chatbot_section'
5986 );
5987
5988 add_settings_field(
5989 'ai_agent_text',
5990 esc_html__('AI Agent Text', 'mxchat'),
5991 array($this, 'mxchat_ai_agent_text_callback'),
5992 'mxchat-chatbot',
5993 'mxchat_chatbot_section'
5994 );
5995
5996 add_settings_field(
5997 'enable_email_block',
5998 esc_html__('Require Email To Chat', 'mxchat'),
5999 array($this, 'enable_email_block_callback'),
6000 'mxchat-chatbot',
6001 'mxchat_chatbot_section'
6002 );
6003
6004 add_settings_field(
6005 'email_blocker_header_content',
6006 esc_html__('Require Email Chat Content', 'mxchat'),
6007 array($this, 'email_blocker_header_content_callback'),
6008 'mxchat-chatbot',
6009 'mxchat_chatbot_section'
6010 );
6011
6012 add_settings_field(
6013 'email_blocker_button_text',
6014 esc_html__('Require Email Chat Button Text', 'mxchat'),
6015 [$this, 'email_blocker_button_text_callback'],
6016 'mxchat-chatbot',
6017 'mxchat_chatbot_section'
6018 );
6019
6020 add_settings_field(
6021 'enable_name_field',
6022 esc_html__('Require Name Field', 'mxchat'),
6023 array($this, 'enable_name_field_callback'),
6024 'mxchat-chatbot',
6025 'mxchat_chatbot_section'
6026 );
6027
6028 add_settings_field(
6029 'name_field_placeholder',
6030 esc_html__('Name Field Placeholder', 'mxchat'),
6031 array($this, 'name_field_placeholder_callback'),
6032 'mxchat-chatbot',
6033 'mxchat_chatbot_section'
6034 );
6035
6036 add_settings_field(
6037 'intro_message',
6038 esc_html__('Introductory Message', 'mxchat'),
6039 array($this, 'mxchat_intro_message_callback'),
6040 'mxchat-chatbot',
6041 'mxchat_chatbot_section'
6042 );
6043
6044 add_settings_field(
6045 'input_copy',
6046 esc_html__('Input Copy', 'mxchat'),
6047 array($this, 'mxchat_input_copy_callback'),
6048 'mxchat-chatbot',
6049 'mxchat_chatbot_section'
6050 );
6051
6052 add_settings_field(
6053 'pre_chat_message',
6054 esc_html__('Chat Teaser Pop-up', 'mxchat'),
6055 array($this, 'mxchat_pre_chat_message_callback'),
6056 'mxchat-chatbot',
6057 'mxchat_chatbot_section'
6058 );
6059
6060 add_settings_field(
6061 'privacy_toggle',
6062 esc_html__('Toggle Privacy Notice', 'mxchat'),
6063 array($this, 'mxchat_privacy_toggle_callback'),
6064 'mxchat-chatbot',
6065 'mxchat_chatbot_section'
6066 );
6067
6068 add_settings_field(
6069 'complianz_toggle',
6070 esc_html__('Enable Complianz', 'mxchat'),
6071 array($this, 'mxchat_complianz_toggle_callback'),
6072 'mxchat-chatbot',
6073 'mxchat_chatbot_section'
6074 );
6075
6076 add_settings_field(
6077 'link_target_toggle',
6078 esc_html__('Open Links in a New Tab', 'mxchat'),
6079 array($this, 'mxchat_link_target_toggle_callback'),
6080 'mxchat-chatbot',
6081 'mxchat_chatbot_section'
6082 );
6083
6084 add_settings_field(
6085 'chat_persistence_toggle',
6086 esc_html__('Enable Chat Persistence', 'mxchat'),
6087 array($this, 'mxchat_chat_persistence_toggle_callback'),
6088 'mxchat-chatbot',
6089 'mxchat_chatbot_section'
6090 );
6091
6092 add_settings_field(
6093 'popular_question_1',
6094 esc_html__('Quick Question 1', 'mxchat'),
6095 array($this, 'mxchat_popular_question_1_callback'),
6096 'mxchat-chatbot',
6097 'mxchat_chatbot_section'
6098 );
6099
6100 add_settings_field(
6101 'popular_question_2',
6102 esc_html__('Quick Question 2', 'mxchat'),
6103 array($this, 'mxchat_popular_question_2_callback'),
6104 'mxchat-chatbot',
6105 'mxchat_chatbot_section'
6106 );
6107
6108 add_settings_field(
6109 'popular_question_3',
6110 esc_html__('Quick Question 3', 'mxchat'),
6111 array($this, 'mxchat_popular_question_3_callback'),
6112 'mxchat-chatbot',
6113 'mxchat_chatbot_section'
6114 );
6115
6116 add_settings_field(
6117 'additional_popular_questions',
6118 esc_html__('Additional Quick Questions', 'mxchat'),
6119 array($this, 'mxchat_additional_popular_questions_callback'),
6120 'mxchat-chatbot',
6121 'mxchat_chatbot_section'
6122 );
6123
6124
6125 add_settings_field(
6126 'rate_limits',
6127 __('Rate Limits Settings', 'mxchat'),
6128 array($this, 'mxchat_rate_limits_callback'),
6129 'mxchat-chatbot',
6130 'mxchat_chatbot_section'
6131 );
6132
6133 // Loops Settings Section
6134 add_settings_section(
6135 'mxchat_loops_section',
6136 esc_html__('Loops Settings', 'mxchat'),
6137 null,
6138 'mxchat-embed'
6139 );
6140
6141 // Loops Settings Fields (API Key moved to API Keys tab)
6142 add_settings_field(
6143 'loops_mailing_list',
6144 esc_html__('Loops Mailing List', 'mxchat'),
6145 array($this, 'mxchat_loops_mailing_list_callback'),
6146 'mxchat-embed',
6147 'mxchat_loops_section'
6148 );
6149
6150 add_settings_field(
6151 'triggered_phrase_response',
6152 esc_html__('Triggered Phrase Response', 'mxchat'),
6153 array($this, 'mxchat_triggered_phrase_response_callback'),
6154 'mxchat-embed',
6155 'mxchat_loops_section'
6156 );
6157
6158 add_settings_field(
6159 'email_capture_response',
6160 esc_html__('Email Capture Response', 'mxchat'),
6161 array($this, 'mxchat_email_capture_response_callback'),
6162 'mxchat-embed',
6163 'mxchat_loops_section'
6164 );
6165
6166 // Brave Search Settings Fields
6167 add_settings_section(
6168 'mxchat_brave_section',
6169 __('Brave Search Settings', 'mxchat'),
6170 array($this, 'mxchat_brave_section_callback'),
6171 'mxchat-embed'
6172 );
6173
6174 // Brave API Key moved to API Keys tab
6175 add_settings_field(
6176 'brave_image_count',
6177 __('Number of Images to Return', 'mxchat'),
6178 array($this, 'mxchat_brave_image_count_callback'),
6179 'mxchat-embed',
6180 'mxchat_brave_section'
6181 );
6182
6183 add_settings_field(
6184 'brave_safe_search',
6185 __('Safe Search', 'mxchat'),
6186 array($this, 'mxchat_brave_safe_search_callback'),
6187 'mxchat-embed',
6188 'mxchat_brave_section'
6189 );
6190
6191 add_settings_field(
6192 'brave_news_count',
6193 __('Number of News Articles', 'mxchat'),
6194 array($this, 'mxchat_brave_news_count_callback'),
6195 'mxchat-embed',
6196 'mxchat_brave_section'
6197 );
6198
6199 add_settings_field(
6200 'brave_country',
6201 __('Country', 'mxchat'),
6202 array($this, 'mxchat_brave_country_callback'),
6203 'mxchat-embed',
6204 'mxchat_brave_section'
6205 );
6206
6207 add_settings_field(
6208 'brave_language',
6209 __('Language', 'mxchat'),
6210 array($this, 'mxchat_brave_language_callback'),
6211 'mxchat-embed',
6212 'mxchat_brave_section'
6213 );
6214
6215 // Chat with PDF Intent Settings Fields
6216 add_settings_section(
6217 'mxchat_pdf_intent_section',
6218 __('Toolbar Settings & Intents', 'mxchat'),
6219 array($this, 'mxchat_pdf_intent_section_callback'),
6220 'mxchat-embed'
6221 );
6222
6223 add_settings_field(
6224 'chat_toolbar_toggle',
6225 __('Show Chat Toolbar', 'mxchat'),
6226 array($this, 'mxchat_chat_toolbar_toggle_callback'),
6227 'mxchat-embed',
6228 'mxchat_pdf_intent_section'
6229 );
6230
6231 // PDF Upload Button Toggle
6232 add_settings_field(
6233 'show_pdf_upload_button',
6234 __('Show PDF Upload Button', 'mxchat'),
6235 array($this, 'mxchat_show_pdf_upload_button_callback'),
6236 'mxchat-embed',
6237 'mxchat_pdf_intent_section'
6238 );
6239
6240 // Word Upload Button Toggle
6241 add_settings_field(
6242 'show_word_upload_button',
6243 __('Show Word Upload Button', 'mxchat'),
6244 array($this, 'mxchat_show_word_upload_button_callback'),
6245 'mxchat-embed',
6246 'mxchat_pdf_intent_section'
6247 );
6248
6249 add_settings_field(
6250 'pdf_intent_trigger_text',
6251 __('Intent Trigger Text', 'mxchat'),
6252 array($this, 'mxchat_pdf_intent_trigger_text_callback'),
6253 'mxchat-embed',
6254 'mxchat_pdf_intent_section'
6255 );
6256
6257 add_settings_field(
6258 'pdf_intent_success_text',
6259 __('Success Text', 'mxchat'),
6260 array($this, 'mxchat_pdf_intent_success_text_callback'),
6261 'mxchat-embed',
6262 'mxchat_pdf_intent_section'
6263 );
6264
6265 add_settings_field(
6266 'pdf_intent_error_text',
6267 __('Error Text', 'mxchat'),
6268 array($this, 'mxchat_pdf_intent_error_text_callback'),
6269 'mxchat-embed',
6270 'mxchat_pdf_intent_section'
6271 );
6272
6273 // Add PDF Maximum Pages Field
6274 add_settings_field(
6275 'pdf_max_pages',
6276 __('Maximum Document Pages', 'mxchat'),
6277 array($this, 'mxchat_pdf_max_pages_callback'),
6278 'mxchat-embed',
6279 'mxchat_pdf_intent_section'
6280 );
6281
6282 // Live Agent Settings Fields
6283 add_settings_section(
6284 'mxchat_live_agent_section',
6285 __('Live Agent Settings', 'mxchat'),
6286 array($this, 'mxchat_live_agent_section_callback'),
6287 'mxchat-embed'
6288 );
6289
6290 // Live Agent Status Fields (add at top of live agent settings)
6291 add_settings_field(
6292 'live_agent_status',
6293 __('Live Agent Status', 'mxchat'),
6294 array($this, 'mxchat_live_agent_status_callback'),
6295 'mxchat-embed',
6296 'mxchat_live_agent_section'
6297 );
6298
6299 add_settings_field(
6300 'live_agent_notification_message',
6301 __('Notification Message', 'mxchat'),
6302 array($this, 'mxchat_live_agent_notification_message_callback'),
6303 'mxchat-embed',
6304 'mxchat_live_agent_section'
6305 );
6306
6307 add_settings_field(
6308 'live_agent_away_message',
6309 __('Away Message', 'mxchat'),
6310 array($this, 'mxchat_live_agent_away_message_callback'),
6311 'mxchat-embed',
6312 'mxchat_live_agent_section'
6313 );
6314
6315 add_settings_field(
6316 'live_agent_user_ids',
6317 __('Slack Agent User IDs', 'mxchat'),
6318 array($this, 'mxchat_live_agent_user_ids_callback'),
6319 'mxchat-embed',
6320 'mxchat_live_agent_section'
6321 );
6322
6323 add_settings_field(
6324 'live_agent_webhook_url',
6325 __('Slack Webhook URL', 'mxchat'),
6326 array($this, 'mxchat_live_agent_webhook_url_callback'),
6327 'mxchat-embed',
6328 'mxchat_live_agent_section'
6329 );
6330
6331 add_settings_field(
6332 'live_agent_secret_key',
6333 __('Slack Secret Key', 'mxchat'),
6334 array($this, 'mxchat_live_agent_secret_key_callback'),
6335 'mxchat-embed',
6336 'mxchat_live_agent_section'
6337 );
6338
6339 // Live Agent Integration Fields
6340 add_settings_field(
6341 'live_agent_bot_token',
6342 __('Slack Bot OAuth Token', 'mxchat'),
6343 array($this, 'mxchat_live_agent_bot_token_callback'),
6344 'mxchat-embed',
6345 'mxchat_live_agent_section'
6346 );
6347
6348 // Telegram Integration Section
6349 add_settings_section(
6350 'mxchat_telegram_section',
6351 __('Telegram Settings', 'mxchat'),
6352 array($this, 'mxchat_telegram_section_callback'),
6353 'mxchat-embed'
6354 );
6355
6356 add_settings_field(
6357 'telegram_status',
6358 __('Live Agent Status', 'mxchat'),
6359 array($this, 'mxchat_telegram_status_callback'),
6360 'mxchat-embed',
6361 'mxchat_telegram_section'
6362 );
6363
6364 add_settings_field(
6365 'telegram_notification_message',
6366 __('Notification Message', 'mxchat'),
6367 array($this, 'mxchat_telegram_notification_message_callback'),
6368 'mxchat-embed',
6369 'mxchat_telegram_section'
6370 );
6371
6372 add_settings_field(
6373 'telegram_away_message',
6374 __('Away Message', 'mxchat'),
6375 array($this, 'mxchat_telegram_away_message_callback'),
6376 'mxchat-embed',
6377 'mxchat_telegram_section'
6378 );
6379
6380 add_settings_field(
6381 'telegram_bot_token',
6382 __('Telegram Bot Token', 'mxchat'),
6383 array($this, 'mxchat_telegram_bot_token_callback'),
6384 'mxchat-embed',
6385 'mxchat_telegram_section'
6386 );
6387
6388 add_settings_field(
6389 'telegram_group_id',
6390 __('Telegram Group ID', 'mxchat'),
6391 array($this, 'mxchat_telegram_group_id_callback'),
6392 'mxchat-embed',
6393 'mxchat_telegram_section'
6394 );
6395
6396 add_settings_field(
6397 'telegram_webhook_secret',
6398 __('Webhook Secret Token', 'mxchat'),
6399 array($this, 'mxchat_telegram_webhook_secret_callback'),
6400 'mxchat-embed',
6401 'mxchat_telegram_section'
6402 );
6403
6404 // General Settings Section
6405 add_settings_section(
6406 'mxchat_general_section',
6407 esc_html__('YouTube Tutorials', 'mxchat'),
6408 null,
6409 'mxchat-general'
6410 );
6411 }
6412
6413 public function mxchat_prompts_page_init() {
6414 register_setting(
6415 'mxchat_prompts_options',
6416 'mxchat_prompts_options',
6417 array(
6418 'type' => 'array',
6419 'description' => __('MXChat Knowledge Base Settings', 'mxchat'),
6420 'default' => array(
6421 'mxchat_auto_sync_posts' => 0,
6422 'mxchat_auto_sync_pages' => 0,
6423 'mxchat_use_pinecone' => 0,
6424 'mxchat_pinecone_api_key' => '',
6425 'mxchat_pinecone_environment' => '',
6426 'mxchat_pinecone_index' => '',
6427 'mxchat_pinecone_host' => '',
6428 ),
6429 'sanitize_callback' => array($this, 'sanitize_prompts_options'),
6430 )
6431 );
6432
6433 add_action('admin_notices', array($this, 'sync_settings_notice'));
6434 }
6435
6436 public function mxchat_transcripts_page_init() {
6437 register_setting(
6438 'mxchat_transcripts_options',
6439 'mxchat_transcripts_options',
6440 array(
6441 'type' => 'array',
6442 'description' => __('MXChat Transcripts Notification Settings', 'mxchat'),
6443 'default' => array(
6444 'mxchat_enable_notifications' => 0,
6445 'mxchat_notification_email' => get_option('admin_email'),
6446 'mxchat_auto_delete_transcripts' => 'never',
6447 ),
6448 'sanitize_callback' => array($this, 'sanitize_transcripts_options'),
6449 )
6450 );
6451
6452 add_settings_section(
6453 'mxchat_transcripts_notification_section',
6454 esc_html__('Chat Notification Settings', 'mxchat'),
6455 array($this, 'mxchat_transcripts_notification_section_callback'),
6456 'mxchat-transcripts'
6457 );
6458
6459 add_settings_field(
6460 'mxchat_enable_notifications',
6461 esc_html__('Enable Chat Notifications', 'mxchat'),
6462 array($this, 'mxchat_enable_notifications_callback'),
6463 'mxchat-transcripts',
6464 'mxchat_transcripts_notification_section'
6465 );
6466
6467 add_settings_field(
6468 'mxchat_notification_email',
6469 esc_html__('Notification Email Address', 'mxchat'),
6470 array($this, 'mxchat_notification_email_callback'),
6471 'mxchat-transcripts',
6472 'mxchat_transcripts_notification_section'
6473 );
6474
6475 add_settings_field(
6476 'mxchat_auto_delete_transcripts',
6477 esc_html__('Auto-Delete Old Transcripts', 'mxchat'),
6478 array($this, 'mxchat_auto_delete_transcripts_callback'),
6479 'mxchat-transcripts',
6480 'mxchat_transcripts_notification_section'
6481 );
6482
6483 add_settings_field(
6484 'mxchat_retention_days',
6485 esc_html__('Custom Retention (Days)', 'mxchat'),
6486 array($this, 'mxchat_retention_days_callback'),
6487 'mxchat-transcripts',
6488 'mxchat_transcripts_notification_section'
6489 );
6490
6491 add_settings_field(
6492 'mxchat_auto_email_transcript',
6493 esc_html__('Auto-Email Full Transcript', 'mxchat'),
6494 array($this, 'mxchat_auto_email_transcript_callback'),
6495 'mxchat-transcripts',
6496 'mxchat_transcripts_notification_section'
6497 );
6498 }
6499
6500
6501 /**
6502 * Sanitize all prompts options
6503 *
6504 * @param array $input The unsanitized options array
6505 * @return array The sanitized options array
6506 */
6507 public function sanitize_prompts_options($input) {
6508 // Log the incoming input.
6509 //error_log('Sanitizing inputs: ' . print_r($input, true));
6510
6511 $sanitized = array();
6512
6513 // Boolean options
6514 $sanitized['mxchat_auto_sync_posts'] = isset($input['mxchat_auto_sync_posts']) ? 1 : 0;
6515 $sanitized['mxchat_auto_sync_pages'] = isset($input['mxchat_auto_sync_pages']) ? 1 : 0;
6516 $sanitized['mxchat_use_pinecone'] = !empty($input['mxchat_use_pinecone']) ? 1 : 0;
6517
6518 // API Key: if less than 32 characters, flag as invalid.
6519 $api_key = sanitize_text_field($input['mxchat_pinecone_api_key'] ?? '');
6520 if (!empty($api_key) && strlen($api_key) < 32) {
6521 add_settings_error(
6522 'mxchat_prompts_options',
6523 'invalid_api_key',
6524 __('The Pinecone API key appears to be invalid. Please check your API key.', 'mxchat')
6525 );
6526 $existing_options = get_option('mxchat_prompts_options', array());
6527 $sanitized['mxchat_pinecone_api_key'] = $existing_options['mxchat_pinecone_api_key'] ?? '';
6528 } else {
6529 $sanitized['mxchat_pinecone_api_key'] = $api_key;
6530 }
6531
6532 // Environment and Index Name
6533 $sanitized['mxchat_pinecone_environment'] = sanitize_text_field($input['mxchat_pinecone_environment'] ?? '');
6534 $sanitized['mxchat_pinecone_index'] = sanitize_text_field($input['mxchat_pinecone_index'] ?? '');
6535
6536 // Host: Remove protocol and validate format.
6537 $host = sanitize_text_field($input['mxchat_pinecone_host'] ?? '');
6538 $host = preg_replace('#^https?://#', '', $host);
6539 //error_log('Host after removing protocol: ' . $host);
6540 if (!empty($host)) {
6541 if (!preg_match('/^[\w-]+\.svc\.[\w-]+\.pinecone\.io$/', $host)) {
6542 add_settings_error(
6543 'mxchat_prompts_options',
6544 'invalid_host',
6545 __('The Pinecone host appears to be invalid. It should look like "mxchat-vectors-zrmsquq.svc.aped-4627-b74a.pinecone.io"', 'mxchat')
6546 );
6547 $existing_options = get_option('mxchat_prompts_options', array());
6548 $sanitized['mxchat_pinecone_host'] = $existing_options['mxchat_pinecone_host'] ?? '';
6549 } else {
6550 $sanitized['mxchat_pinecone_host'] = $host;
6551 }
6552 } else {
6553 $sanitized['mxchat_pinecone_host'] = '';
6554 }
6555
6556 //error_log('Final sanitized array: ' . print_r($sanitized, true));
6557
6558 return $sanitized;
6559 }
6560
6561 public function sync_settings_notice() {
6562 // Only show notice on our plugin page
6563 if (!isset($_GET['page']) || $_GET['page'] !== 'mxchat-prompts') {
6564 return;
6565 }
6566
6567 // Check if settings were updated
6568 if (isset($_GET['settings-updated'])) {
6569
6570 ?>
6571 <div class="notice notice-success is-dismissible">
6572 <p><?php esc_html_e('Sync settings updated successfully.', 'mxchat'); ?></p>
6573 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php esc_html_e('Dismiss this notice.', 'mxchat'); ?></span></button>
6574 </div>
6575 <?php
6576
6577 }
6578 }
6579 // Add this sanitization function to your class
6580 public function sanitize_sync_setting($input) {
6581 return (bool)$input ? __('1', 'mxchat') : __('', 'mxchat');
6582 }
6583
6584 public function mxchat_rate_limits_callback() {
6585 $all_options = get_option('mxchat_options', []);
6586
6587 // Define available rate limits
6588 $rate_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited');
6589
6590 // Define available timeframes
6591 $timeframes = array(
6592 'hourly' => __('Per Hour', 'mxchat'),
6593 'daily' => __('Per Day', 'mxchat'),
6594 'weekly' => __('Per Week', 'mxchat'),
6595 'monthly' => __('Per Month', 'mxchat')
6596 );
6597
6598 // Get all roles plus a "logged_out" pseudo-role
6599 $roles = wp_roles()->get_names();
6600 $roles['logged_out'] = __('Logged Out Users', 'mxchat');
6601
6602 // Start the wrapper
6603 echo '<div class="pro-feature-wrapper active">';
6604 echo '<div class="mxchat-rate-limits-container">';
6605
6606 echo '<p class="description" style="margin-bottom: 20px;">' .
6607 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') .
6608 '</p>';
6609
6610 // Add markdown link documentation
6611 echo '<div class="notice notice-info inline" style="margin-bottom: 20px; padding: 10px;">';
6612 echo '<p><strong>' . esc_html__('Markdown Links Supported:', 'mxchat') . '</strong></p>';
6613 echo '<p>' . esc_html__('You can include clickable links in your custom messages using markdown syntax:', 'mxchat') . '</p>';
6614 echo '<ul style="margin-left: 20px;">';
6615 echo '<li><code>[Link text](https://example.com)</code> - Creates a clickable link</li>';
6616 echo '<li><code>[Visit our pricing](https://example.com/pricing)</code> - Link with custom text</li>';
6617 echo '<li><code>Plain URLs like https://example.com will also become clickable</code></li>';
6618 echo '</ul>';
6619 echo '</div>';
6620
6621 // Output the controls for each role
6622 foreach ($roles as $role_id => $role_name) {
6623 // Get saved options or defaults
6624 $default_limit = ($role_id === 'logged_out') ? '10' : '100';
6625 $default_timeframe = 'daily';
6626 $default_message = __('Rate limit exceeded. Please try again later.', 'mxchat');
6627
6628 $selected_limit = isset($all_options['rate_limits'][$role_id]['limit'])
6629 ? $all_options['rate_limits'][$role_id]['limit']
6630 : $default_limit;
6631
6632 $selected_timeframe = isset($all_options['rate_limits'][$role_id]['timeframe'])
6633 ? $all_options['rate_limits'][$role_id]['timeframe']
6634 : $default_timeframe;
6635
6636 $custom_message = isset($all_options['rate_limits'][$role_id]['message'])
6637 ? $all_options['rate_limits'][$role_id]['message']
6638 : $default_message;
6639
6640 // Output the row
6641 echo '<div class="mxchat-rate-limit-row mxchat-autosave-section">';
6642
6643 // Role label
6644 echo '<div class="mxchat-rate-limit-role">' . esc_html($role_name) . '</div>';
6645
6646 // Controls section
6647 echo '<div class="mxchat-rate-limit-controls-wrapper">';
6648
6649 // Rate limit and timeframe controls
6650 echo '<div class="mxchat-rate-limit-controls">';
6651
6652 // Limit dropdown
6653 echo '<div>';
6654 echo '<label for="rate_limits_' . esc_attr($role_id) . '_limit">' . esc_html__('Limit:', 'mxchat') . '</label>';
6655 echo '<select
6656 id="rate_limits_' . esc_attr($role_id) . '_limit"
6657 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][limit]"
6658 class="mxchat-autosave-field">';
6659 foreach ($rate_limits as $limit) {
6660 echo '<option value="' . esc_attr($limit) . '" ' . selected($selected_limit, $limit, false) . '>' . esc_html($limit) . '</option>';
6661 }
6662 echo '</select>';
6663 echo '</div>';
6664
6665 // Timeframe dropdown
6666 echo '<div>';
6667 echo '<label for="rate_limits_' . esc_attr($role_id) . '_timeframe">' . esc_html__('Timeframe:', 'mxchat') . '</label>';
6668 echo '<select
6669 id="rate_limits_' . esc_attr($role_id) . '_timeframe"
6670 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][timeframe]"
6671 class="mxchat-autosave-field">';
6672 foreach ($timeframes as $value => $label) {
6673 echo '<option value="' . esc_attr($value) . '" ' . selected($selected_timeframe, $value, false) . '>' . esc_html($label) . '</option>';
6674 }
6675 echo '</select>';
6676 echo '</div>';
6677
6678 echo '</div>'; // End controls
6679
6680 // Custom message textarea
6681 echo '<div class="mxchat-rate-limit-message">';
6682 echo '<label for="rate_limits_' . esc_attr($role_id) . '_message">' . esc_html__('Custom Message:', 'mxchat') . '</label>';
6683 echo '<textarea
6684 id="rate_limits_' . esc_attr($role_id) . '_message"
6685 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][message]"
6686 class="mxchat-autosave-field"
6687 placeholder="' . esc_attr__('Enter custom message when rate limit is exceeded', 'mxchat') . '">' .
6688 esc_textarea($custom_message) .
6689 '</textarea>';
6690 echo '<p class="description">' .
6691 esc_html__('Example: Rate limit reached! [Visit our pricing page](https://example.com/pricing) to upgrade.', 'mxchat') .
6692 '</p>';
6693 echo '</div>'; // End message
6694
6695 echo '</div>'; // End controls wrapper
6696
6697 echo '</div>'; // End row
6698 }
6699
6700 echo '</div>'; // End container
6701
6702 echo '</div>'; // End pro-feature-wrapper
6703 }
6704
6705 private function mxchat_add_option_field($id, $title, $callback = '') {
6706 add_settings_field(
6707 $id,
6708 __($title, 'mxchat'),
6709 $callback ? array($this, $callback) : array($this, $id . '_callback'),
6710 'mxchat-max',
6711 'mxchat_setting_section_id',
6712 $id === 'model' ? ['label_for' => 'model'] : []
6713 );
6714 }
6715
6716 // API Keys Section Callback
6717 public function mxchat_api_keys_section_callback() {
6718 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>';
6719 }
6720
6721 // OpenAI API Key
6722 public function api_key_callback() {
6723 $apiKey = isset($this->options['api_key']) ? esc_attr($this->options['api_key']) : '';
6724 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6725
6726 echo '<div class="api-key-wrapper">';
6727 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 . '" />';
6728 echo '<button type="button" id="toggleApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6729 echo '<p class="description">' . esc_html__('Required for OpenAI GPT models and OpenAI embeddings. Get your API key from OpenAI Platform.', 'mxchat') . '</p>';
6730 echo '</div>';
6731 }
6732
6733 // X.AI API Key
6734 public function xai_api_key_callback() {
6735 $xaiApiKey = isset($this->options['xai_api_key']) ? esc_attr($this->options['xai_api_key']) : '';
6736 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6737
6738 echo '<div class="api-key-wrapper">';
6739 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 . '" />';
6740 echo '<button type="button" id="toggleXaiApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6741 echo '<p class="description">' . esc_html__('Required for X.AI Grok models. Get your API key from X.AI Console.', 'mxchat') . '</p>';
6742 echo '</div>';
6743 }
6744 // Claude API Key
6745 public function claude_api_key_callback() {
6746 $claudeApiKey = isset($this->options['claude_api_key']) ? esc_attr($this->options['claude_api_key']) : '';
6747 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6748
6749 echo '<div class="api-key-wrapper">';
6750 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 . '" />';
6751 echo '<button type="button" id="toggleClaudeApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6752 echo '<p class="description">' . esc_html__('Required for Anthropic Claude models. Get your API key from Anthropic Console.', 'mxchat') . '</p>';
6753 echo '</div>';
6754 }
6755
6756 // DeepSeek API Key
6757 public function deepseek_api_key_callback() {
6758 $apiKey = isset($this->options['deepseek_api_key']) ? esc_attr($this->options['deepseek_api_key']) : '';
6759 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6760
6761 echo '<div class="api-key-wrapper">';
6762 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 . '" />';
6763 echo '<button type="button" id="toggleDeepSeekApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6764 echo '<p class="description">' . esc_html__('Required for DeepSeek models. Get your API key from DeepSeek Platform.', 'mxchat') . '</p>';
6765 echo '</div>';
6766 }
6767
6768 // Gemini API Key
6769 public function gemini_api_key_callback() {
6770 $geminiApiKey = isset($this->options['gemini_api_key']) ? esc_attr($this->options['gemini_api_key']) : '';
6771 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6772
6773 echo '<div class="api-key-wrapper">';
6774 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 . '" />';
6775 echo '<button type="button" id="toggleGeminiApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6776 echo '<p class="description">' . esc_html__('Required for Google Gemini models and embeddings. Get your API key from Google AI Studio.', 'mxchat') . '</p>';
6777 echo '</div>';
6778 }
6779
6780
6781 // OpenRouter API Key
6782 public function openrouter_api_key_callback() {
6783 $openrouterApiKey = isset($this->options['openrouter_api_key']) ? esc_attr($this->options['openrouter_api_key']) : '';
6784 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6785
6786 echo '<div class="api-key-wrapper">';
6787 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 . '" />';
6788 echo '<button type="button" id="toggleOpenRouterApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6789 echo '<p class="description">' . esc_html__('Required for OpenRouter models. Get your API key from OpenRouter.ai', 'mxchat') . '</p>';
6790 echo '</div>';
6791 }
6792
6793 // Voyage API Key
6794 public function voyage_api_key_callback() {
6795 $apiKey = isset($this->options['voyage_api_key']) ? esc_attr($this->options['voyage_api_key']) : '';
6796 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6797
6798 echo '<div class="api-key-wrapper">';
6799 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 . '" />';
6800 echo '<button type="button" id="toggleVoyageAPIKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6801 echo '<p class="description">' . esc_html__('Required for Voyage AI embedding models. Get your API key from Voyage AI.', 'mxchat') . '</p>';
6802 echo '</div>';
6803 }
6804
6805 public function mxchat_loops_api_key_callback() {
6806 $loops_api_key = isset($this->options['loops_api_key']) ? esc_attr($this->options['loops_api_key']) : '';
6807 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6808
6809 echo '<div class="api-key-wrapper">';
6810 echo sprintf(
6811 '<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" />',
6812 $loops_api_key,
6813 $nonce
6814 );
6815 echo '<button type="button" id="toggleLoopsApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6816 echo '</div>';
6817 echo '<p class="description">' . esc_html__('Required for Loops email integration. Get your API key from Loops.so', 'mxchat') . '</p>';
6818 }
6819 public function mxchat_loops_mailing_list_callback() {
6820 // Add error handling and type checking
6821 $loops_api_key = '';
6822 $selected_list = '';
6823 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6824
6825 // Safely get the API key
6826 if (isset($this->options['loops_api_key']) && is_string($this->options['loops_api_key'])) {
6827 $loops_api_key = $this->options['loops_api_key'];
6828 }
6829
6830 // Safely get the selected list
6831 if (isset($this->options['loops_mailing_list']) && is_string($this->options['loops_mailing_list'])) {
6832 $selected_list = $this->options['loops_mailing_list'];
6833 }
6834
6835 if (!empty($loops_api_key)) {
6836 $lists = $this->mxchat_fetch_loops_mailing_lists($loops_api_key);
6837 if (is_array($lists) && !empty($lists)) {
6838 echo '<div class="mxchat-field-wrapper">';
6839 echo '<select id="loops_mailing_list" name="loops_mailing_list" class="mxchat-autosave-field" data-nonce="' . $nonce . '">';
6840
6841 // Add a default "Select a list" option
6842 echo '<option value="" ' . selected($selected_list, '', false) . '>' . esc_html__('Select a list', 'mxchat') . '</option>';
6843
6844 foreach ($lists as $list) {
6845 if (is_array($list) && isset($list['id']) && isset($list['name'])) {
6846 echo sprintf(
6847 '<option value="%s" %s>%s</option>',
6848 esc_attr($list['id']),
6849 selected($selected_list, $list['id'], false),
6850 esc_html($list['name'])
6851 );
6852 }
6853 }
6854 echo '</select>';
6855 echo '</div>';
6856 echo '<p class="description">' . esc_html__('Please select a mailing list to use with Loops.', 'mxchat') . '</p>';
6857 } else {
6858 echo '<p class="description">' . esc_html__('No lists found. Please verify your API Key.', 'mxchat') . '</p>';
6859 }
6860 } else {
6861 echo '<p class="description">' . esc_html__('Enter a valid Loops API Key to load mailing lists.', 'mxchat') . '</p>';
6862 }
6863 }
6864 public function mxchat_triggered_phrase_response_callback() {
6865 $default_response = __('Would you like to join our mailing list? Please provide your email below.', 'mxchat');
6866 $triggered_response = isset($this->options['triggered_phrase_response'])
6867 ? $this->options['triggered_phrase_response']
6868 : $default_response;
6869 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6870
6871 echo '<div class="mxchat-field-wrapper">';
6872 echo sprintf(
6873 '<textarea id="triggered_phrase_response" name="triggered_phrase_response" rows="3" cols="50" class="mxchat-autosave-field" data-nonce="%s">%s</textarea>',
6874 $nonce,
6875 esc_textarea($triggered_response)
6876 );
6877 echo '</div>';
6878 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>';
6879 }
6880
6881 public function mxchat_email_capture_response_callback() {
6882 $default_response = __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
6883 $email_capture_response = isset($this->options['email_capture_response'])
6884 ? $this->options['email_capture_response']
6885 : $default_response;
6886 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6887
6888 echo '<div class="mxchat-field-wrapper">';
6889 echo sprintf(
6890 '<textarea id="email_capture_response" name="email_capture_response" rows="3" cols="50" class="mxchat-autosave-field" data-nonce="%s">%s</textarea>',
6891 $nonce,
6892 esc_textarea($email_capture_response)
6893 );
6894 echo '</div>';
6895 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>';
6896 }
6897 public function mxchat_pre_chat_message_callback() {
6898 // Load the entire 'mxchat_options' array
6899 $all_options = get_option('mxchat_options', []);
6900
6901 // Retrieve the saved message or use the default value
6902 $default_message = __('Hey there! Ask me anything!', 'mxchat');
6903 $pre_chat_message = isset($all_options['pre_chat_message']) ? $all_options['pre_chat_message'] : $default_message;
6904
6905 // Output the textarea
6906 printf(
6907 '<textarea id="pre_chat_message" name="pre_chat_message" rows="5" cols="50">%s</textarea>',
6908 esc_textarea($pre_chat_message)
6909 );
6910 }
6911
6912 // Callback for AI Instructions textarea
6913 public function system_prompt_instructions_callback() {
6914 // Retrieve the current value of the system prompt instructions
6915 $instructions = isset($this->options['system_prompt_instructions']) ? esc_textarea($this->options['system_prompt_instructions']) : '';
6916 // Render the textarea field
6917 printf(
6918 '<textarea id="system_prompt_instructions" name="system_prompt_instructions" rows="5" cols="50">%s</textarea>',
6919 $instructions
6920 );
6921 // Personalization hint
6922 echo '<p class="description" style="margin-top: 8px;">';
6923 echo esc_html__('Use {visitor_name} to personalize AI responses when lead capture is enabled.', 'mxchat') . '<br>';
6924 echo '<code style="font-size: 12px;">' . esc_html__('Example: The visitor\'s name is {visitor_name}. Address them by name.', 'mxchat') . '</code>';
6925 echo '</p>';
6926 // Sample instructions button
6927 echo '<div class="mxchat-instructions-container">';
6928 echo '<button type="button" class="mxchat-instructions-btn" id="mxchatViewSampleBtn">';
6929 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">';
6930 echo '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>';
6931 echo '<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>';
6932 echo '</svg>';
6933 echo esc_html__('View Sample Instructions', 'mxchat');
6934 echo '</button>';
6935 echo '</div>';
6936
6937 // Add modal to WordPress admin footer instead of inline
6938 add_action('admin_footer', array($this, 'render_sample_instructions_modal'));
6939 }
6940
6941 // New method to render modal in admin footer
6942 public function render_sample_instructions_modal() {
6943 static $modal_rendered = false;
6944 if ($modal_rendered) return; // Prevent duplicate modals
6945 $modal_rendered = true;
6946
6947 echo '<div class="mxchat-instructions-modal-overlay" id="mxchatSampleModal">';
6948 echo '<div class="mxchat-instructions-modal-content">';
6949 echo '<div class="mxchat-instructions-modal-header">';
6950 echo '<h3 class="mxchat-instructions-modal-title">';
6951 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">';
6952 echo '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>';
6953 echo '<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>';
6954 echo '</svg>';
6955 echo esc_html__('Sample AI Instructions', 'mxchat');
6956 echo '</h3>';
6957 echo '<button type="button" class="mxchat-instructions-modal-close" id="mxchatModalClose">';
6958 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">';
6959 echo '<line x1="18" y1="6" x2="6" y2="18"/>';
6960 echo '<line x1="6" y1="6" x2="18" y2="18"/>';
6961 echo '</svg>';
6962 echo '</button>';
6963 echo '</div>';
6964 echo '<div class="mxchat-instructions-modal-body">';
6965 echo '<div class="mxchat-instructions-content">';
6966 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:
6967
6968 # Response Style - CRITICALLY IMPORTANT
6969 - MAXIMUM LENGTH: 1-3 short sentences per response
6970 - Ultra-concise: Get straight to the answer with no filler
6971 - No introductions like "Sure!" or "I\'d be happy to help"
6972 - No phrases like "based on my knowledge" or "according to information"
6973 - No explanatory text before giving the answer
6974 - No summaries or repetition
6975 - Hyperlink all URLs
6976 - Respond in user\'s language
6977 - Minor chit chat or conversation is okay, but try to keep it focused on [insert topic]
6978
6979 # Knowledge Base Requirements - PREVENT HALLUCINATIONS
6980 - ONLY answer using information explicitly provided in OFFICIAL KNOWLEDGE DATABASE CONTENT sections marked with ===== delimiters
6981 - If required information is NOT in the knowledge database: "I don\'t have enough information in my knowledge base to answer that question accurately."
6982 - NEVER invent or hallucinate URLs, links, product specs, procedures, dates, statistics, names, contacts, or company information
6983 - When knowledge base information is unclear or contradictory, acknowledge the limitation rather than guessing
6984 - Better to admit insufficient information than provide inaccurate answers');
6985 echo '</div>';
6986 echo '<button type="button" class="mxchat-instructions-copy-btn" id="mxchatCopyBtn">';
6987 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">';
6988 echo '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>';
6989 echo '<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>';
6990 echo '</svg>';
6991 echo esc_html__('Copy Instructions', 'mxchat');
6992 echo '</button>';
6993 echo '</div>';
6994 echo '<div class="mxchat-instructions-modal-footer">';
6995 echo '<button type="button" class="mxchat-instructions-btn-secondary" id="mxchatCloseBtn">' . esc_html__('Close', 'mxchat') . '</button>';
6996 echo '</div>';
6997 echo '</div>';
6998 echo '</div>';
6999 }
7000
7001
7002 public function mxchat_model_callback() {
7003 // Define available models grouped by provider
7004 $models = array(
7005 esc_html__('OpenRouter (100+ Models)', 'mxchat') => array(
7006 'openrouter' => esc_html__('OpenRouter - Select after entering API key', 'mxchat'),
7007 ),
7008 esc_html__('Google Gemini Models', 'mxchat') => array(
7009 'gemini-3-pro-preview' => esc_html__('Gemini 3 Pro (Most Intelligent, Multimodal)', 'mxchat'),
7010 'gemini-3-flash-preview' => esc_html__('Gemini 3 Flash (Balanced Speed & Scale)', 'mxchat'),
7011 'gemini-2.5-pro' => esc_html__('Gemini 2.5 Pro (Advanced Thinking)', 'mxchat'),
7012 'gemini-2.5-flash' => esc_html__('Gemini 2.5 Flash (Best Price-Performance)', 'mxchat'),
7013 'gemini-2.5-flash-lite' => esc_html__('Gemini 2.5 Flash-Lite (Ultra Fast)', 'mxchat'),
7014 'gemini-2.0-flash' => esc_html__('Gemini 2.0 Flash (Deprecated Mar 2026)', 'mxchat'),
7015 'gemini-2.0-flash-lite' => esc_html__('Gemini 2.0 Flash-Lite (Deprecated Mar 2026)', 'mxchat'),
7016 'gemini-1.5-pro' => esc_html__('Gemini 1.5 Pro (Deprecated Sep 2025)', 'mxchat'),
7017 'gemini-1.5-flash' => esc_html__('Gemini 1.5 Flash (Deprecated Sep 2025)', 'mxchat'),
7018 ),
7019 esc_html__('X.AI Models', 'mxchat') => array(
7020 'grok-3-beta' => esc_html__('Grok-3 (Powerful)', 'mxchat'),
7021 'grok-3-fast-beta' => esc_html__('Grok-3 Fast (High Performance)', 'mxchat'),
7022 'grok-3-mini-beta' => esc_html__('Grok-3 Mini (Affordable)', 'mxchat'),
7023 'grok-3-mini-fast-beta' => esc_html__('Grok-3 Mini Fast (Quick Response)', 'mxchat'),
7024 'grok-2' => esc_html__('Grok 2', 'mxchat'),
7025 'grok-4-0709' => esc_html__('Grok 4 (Latest Flagship)', 'mxchat'),
7026 'grok-4-1-fast-reasoning' => esc_html__('Grok 4.1 Fast (Reasoning)', 'mxchat'),
7027 'grok-4-1-fast-non-reasoning' => esc_html__('Grok 4.1 Fast (Non-Reasoning)', 'mxchat'),
7028 ),
7029 esc_html__('DeepSeek Models', 'mxchat') => array(
7030 'deepseek-chat' => esc_html__('DeepSeek-V3', 'mxchat'),
7031 ),
7032 esc_html__('Claude Models', 'mxchat') => array(
7033 'claude-opus-4-6' => esc_html__('Claude Opus 4.6 (Most Capable - Recommended)', 'mxchat'),
7034 'claude-sonnet-4-6' => esc_html__('Claude Sonnet 4.6 (Latest Sonnet - Fast & Capable)', 'mxchat'),
7035 'claude-opus-4-5' => esc_html__('Claude Opus 4.5 (Highly Capable)', 'mxchat'),
7036 'claude-sonnet-4-5-20250929' => esc_html__('Claude Sonnet 4.5 (Best for Agents & Coding)', 'mxchat'),
7037 'claude-opus-4-1-20250805' => esc_html__('Claude Opus 4.1 (Exceptional for Complex Tasks)', 'mxchat'),
7038 'claude-haiku-4-5-20251001' => esc_html__('Claude Haiku 4.5 (Fastest & Most Intelligent)', 'mxchat'),
7039 'claude-opus-4-20250514' => esc_html__('Claude 4 Opus (Complex Tasks)', 'mxchat'),
7040 'claude-sonnet-4-20250514' => esc_html__('Claude 4 Sonnet (High Performance)', 'mxchat'),
7041 ),
7042 esc_html__('OpenAI Models', 'mxchat') => array(
7043 'gpt-5.4' => esc_html__('GPT-5.4 (Flagship Reasoning & Coding)', 'mxchat'),
7044 'gpt-5.4-mini' => esc_html__('GPT-5.4 Mini (Fast, 400K Context)', 'mxchat'),
7045 'gpt-5.4-nano' => esc_html__('GPT-5.4 Nano (Fastest & Cheapest)', 'mxchat'),
7046 'gpt-5.3-chat-latest' => esc_html__('GPT-5.3 Chat (Conversational)', 'mxchat'),
7047 'gpt-5.2' => esc_html__('GPT-5.2 (Best General-Purpose & Agentic Model)', 'mxchat'),
7048 'gpt-5.1-chat-latest' => esc_html__('GPT-5.1 Chat Latest (Recommended)', 'mxchat'),
7049 'gpt-5.1-2025-11-13' => esc_html__('GPT-5.1 (Flagship for Coding & Agentic Tasks)', 'mxchat'),
7050 'gpt-5' => esc_html__('GPT-5 (Flagship for Coding, Reasoning & Agents)', 'mxchat'),
7051 'gpt-5-mini' => esc_html__('GPT-5 Mini (Fast and Lightweight)', 'mxchat'),
7052 'gpt-5-nano' => esc_html__('GPT-5 Nano (Fastest & Cheapest for Summarization/Classification)', 'mxchat'),
7053 ),
7054 );
7055
7056 // Retrieve the currently selected model from saved options
7057 $selected_model = isset($this->options['model']) ? esc_attr($this->options['model']) : 'gpt-5.1-chat-latest';
7058
7059 // Begin the select dropdown
7060 echo '<select id="model" name="model">';
7061
7062 // Iterate over groups of models
7063 foreach ($models as $group_label => $group_models) {
7064 echo '<optgroup label="' . esc_attr($group_label) . '">';
7065
7066 foreach ($group_models as $model_value => $model_label) {
7067 echo '<option value="' . esc_attr($model_value) . '" ' . selected($selected_model, $model_value, false) . '>' . esc_html($model_label) . '</option>';
7068 }
7069
7070 echo '</optgroup>';
7071 }
7072
7073 echo '</select>';
7074
7075 // Add a note for OpenRouter
7076 echo '<p class="description" id="openrouter-model-note" style="display:none; color: #d63638; font-weight: 500;">';
7077 echo '<span class="dashicons dashicons-info" style="font-size: 16px; vertical-align: middle;"></span> ';
7078 echo esc_html__('After entering your OpenRouter API key above, click the button below to load available models.', 'mxchat');
7079 echo '</p>';
7080
7081 // API Key Status Messages (hidden by default, shown by JS based on selected model)
7082 $has_openai_key = !empty($this->options['api_key']);
7083 $has_claude_key = !empty($this->options['claude_api_key']);
7084 $has_xai_key = !empty($this->options['xai_api_key']);
7085 $has_deepseek_key = !empty($this->options['deepseek_api_key']);
7086 $has_gemini_key = !empty($this->options['gemini_api_key']);
7087 $has_openrouter_key = !empty($this->options['openrouter_api_key']);
7088
7089 // OpenAI/GPT models
7090 echo '<p class="mxchat-api-status" data-provider="openai" style="display:none;">';
7091 if ($has_openai_key) {
7092 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenAI detected', 'mxchat') . '</span>';
7093 } else {
7094 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenAI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7095 }
7096 echo '</p>';
7097
7098 // Claude models
7099 echo '<p class="mxchat-api-status" data-provider="claude" style="display:none;">';
7100 if ($has_claude_key) {
7101 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Anthropic (Claude) detected', 'mxchat') . '</span>';
7102 } else {
7103 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Anthropic (Claude) detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7104 }
7105 echo '</p>';
7106
7107 // X.AI models
7108 echo '<p class="mxchat-api-status" data-provider="xai" style="display:none;">';
7109 if ($has_xai_key) {
7110 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for X.AI (Grok) detected', 'mxchat') . '</span>';
7111 } else {
7112 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>';
7113 }
7114 echo '</p>';
7115
7116 // DeepSeek models
7117 echo '<p class="mxchat-api-status" data-provider="deepseek" style="display:none;">';
7118 if ($has_deepseek_key) {
7119 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for DeepSeek detected', 'mxchat') . '</span>';
7120 } else {
7121 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for DeepSeek detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7122 }
7123 echo '</p>';
7124
7125 // Gemini models
7126 echo '<p class="mxchat-api-status" data-provider="gemini" style="display:none;">';
7127 if ($has_gemini_key) {
7128 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Google Gemini detected', 'mxchat') . '</span>';
7129 } else {
7130 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Google Gemini detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7131 }
7132 echo '</p>';
7133
7134 // OpenRouter models
7135 echo '<p class="mxchat-api-status" data-provider="openrouter" style="display:none;">';
7136 if ($has_openrouter_key) {
7137 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenRouter detected', 'mxchat') . '</span>';
7138 } else {
7139 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenRouter detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7140 }
7141 echo '</p>';
7142
7143 // ADD THESE HIDDEN FIELDS RIGHT HERE:
7144 $openrouter_model = isset($this->options['openrouter_selected_model']) ? esc_attr($this->options['openrouter_selected_model']) : '';
7145 $openrouter_model_name = isset($this->options['openrouter_selected_model_name']) ? esc_attr($this->options['openrouter_selected_model_name']) : '';
7146
7147 echo '<input type="hidden" id="openrouter_selected_model" name="openrouter_selected_model" value="' . $openrouter_model . '" />';
7148 echo '<input type="hidden" id="openrouter_selected_model_name" name="openrouter_selected_model_name" value="' . $openrouter_model_name . '" />';
7149 }
7150
7151 // Update your existing callback method
7152 public function enable_streaming_toggle_callback() {
7153 // Get value from options array, default to 'on'
7154 $enabled = isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on';
7155 $checked = ($enabled === 'on') ? 'checked' : '';
7156
7157 echo '<label class="toggle-switch">';
7158 echo sprintf(
7159 '<input type="checkbox" id="enable_streaming_toggle" name="enable_streaming_toggle" value="on" %s />',
7160 esc_attr($checked)
7161 );
7162 echo '<span class="slider"></span>';
7163 echo '</label>';
7164
7165 // Test button with better styling
7166 echo '<div style="margin-top: 15px;">';
7167 echo '<button type="button" id="mxchat-test-streaming-btn" class="button button-secondary">';
7168 echo '<span class="dashicons dashicons-admin-tools" style="vertical-align: middle; margin-right: 5px;"></span>';
7169 echo esc_html__('Test Streaming Compatibility', 'mxchat');
7170 echo '</button>';
7171 echo '<p id="mxchat-test-streaming-result" style="margin-top:10px; font-weight: bold;"></p>';
7172 echo '</div>';
7173 }
7174
7175 // Web Search toggle callback
7176 public function enable_web_search_toggle_callback() {
7177 // Get value from options array, default to 'off'
7178 $enabled = isset($this->options['enable_web_search']) ? $this->options['enable_web_search'] : 'off';
7179 $checked = ($enabled === 'on') ? 'checked' : '';
7180
7181 // Get current model to determine if we should show/enable the toggle
7182 $current_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest';
7183
7184 // Models that DON'T support web search (per OpenAI docs)
7185 // gpt-5 with minimal reasoning is handled at API level, gpt-4.1-nano doesn't support it
7186 $unsupported_models = array('gpt-4.1-nano');
7187
7188 // Check if current model is an OpenAI model that supports web search
7189 $openai_models = array(
7190 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.4-nano', 'gpt-5.3-chat-latest',
7191 'gpt-5.2', 'gpt-5.1-chat-latest', 'gpt-5.1-2025-11-13', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano'
7192 );
7193
7194 $is_openai = in_array($current_model, $openai_models);
7195 $is_supported = $is_openai && !in_array($current_model, $unsupported_models);
7196
7197 // Wrapper div with data attributes for JS to show/hide
7198 echo '<div id="web-search-toggle-wrapper" data-openai-models="' . esc_attr(implode(',', $openai_models)) . '" data-unsupported-models="' . esc_attr(implode(',', $unsupported_models)) . '"' . (!$is_supported ? ' style="display:none;"' : '') . '>';
7199
7200 echo '<label class="toggle-switch">';
7201 echo sprintf(
7202 '<input type="checkbox" id="enable_web_search" name="enable_web_search" value="on" %s />',
7203 esc_attr($checked)
7204 );
7205 echo '<span class="slider"></span>';
7206 echo '</label>';
7207
7208 echo '</div>';
7209
7210 // Message shown when non-OpenAI model is selected
7211 echo '<p id="web-search-unavailable-message" class="description" style="color: #666;' . ($is_supported ? ' display:none;' : '') . '">';
7212 echo '<span class="dashicons dashicons-info" style="font-size: 16px; vertical-align: middle; margin-right: 4px;"></span>';
7213 echo esc_html__('Web search is only available for OpenAI models.', 'mxchat');
7214 echo '</p>';
7215 }
7216
7217 // AJAX handler to fetch OpenRouter models
7218 public function fetch_openrouter_models() {
7219 check_ajax_referer('mxchat_fetch_openrouter_models', 'nonce');
7220
7221 $api_key = isset($_POST['api_key']) ? sanitize_text_field($_POST['api_key']) : '';
7222
7223 if (empty($api_key)) {
7224 wp_send_json_error(array('message' => 'API key is required'));
7225 }
7226
7227 $response = wp_remote_get('https://openrouter.ai/api/v1/models', array(
7228 'headers' => array(
7229 'Authorization' => 'Bearer ' . $api_key,
7230 'Content-Type' => 'application/json',
7231 ),
7232 'timeout' => 15,
7233 ));
7234
7235 if (is_wp_error($response)) {
7236 wp_send_json_error(array('message' => $response->get_error_message()));
7237 }
7238
7239 $body = wp_remote_retrieve_body($response);
7240 $data = json_decode($body, true);
7241
7242 if (isset($data['data']) && is_array($data['data'])) {
7243 // Format the models for the frontend
7244 $models = array_map(function($model) {
7245 return array(
7246 'id' => $model['id'],
7247 'name' => $model['name'] ?? $model['id'],
7248 'description' => $model['description'] ?? '',
7249 'context_length' => $model['context_length'] ?? 0,
7250 'pricing' => array(
7251 'prompt' => $model['pricing']['prompt'] ?? 0,
7252 'completion' => $model['pricing']['completion'] ?? 0,
7253 ),
7254 );
7255 }, $data['data']);
7256
7257 wp_send_json_success(array('models' => $models));
7258 } else {
7259 wp_send_json_error(array('message' => 'Invalid response from OpenRouter'));
7260 }
7261 }
7262
7263 // Callback function for embedding model selection
7264 public function embedding_model_callback() {
7265 $models = array(
7266 esc_html__('OpenAI Embeddings', 'mxchat') => array(
7267 'text-embedding-3-small' => esc_html__('TE3 Small (1536, Efficient)', 'mxchat'),
7268 'text-embedding-ada-002' => esc_html__('Ada 2 (1536, Recommended)', 'mxchat'),
7269 'text-embedding-3-large' => esc_html__('TE3 Large (3072, Powerful)', 'mxchat'),
7270 ),
7271 esc_html__('Voyage AI Embeddings', 'mxchat') => array(
7272 'voyage-3-large' => esc_html__('Voyage-3 Large (2048, Most Capable)', 'mxchat'),
7273 ),
7274 esc_html__('Google Gemini Embeddings', 'mxchat') => array(
7275 'gemini-embedding-001' => esc_html__('Gemini Embedding (1536, Stable)', 'mxchat'),
7276 )
7277 );
7278 $selected_model = isset($this->options['embedding_model']) ? esc_attr($this->options['embedding_model']) : 'text-embedding-ada-002';
7279 echo '<select id="embedding_model" name="embedding_model">';
7280 foreach ($models as $group_label => $group_models) {
7281 echo '<optgroup label="' . esc_attr($group_label) . '">';
7282 foreach ($group_models as $model_value => $model_label) {
7283 echo '<option value="' . esc_attr($model_value) . '" ' . selected($selected_model, $model_value, false) . '>' . esc_html($model_label) . '</option>';
7284 }
7285 echo '</optgroup>';
7286 }
7287 echo '</select>';
7288
7289 // API Key Status Messages for Embedding Models
7290 $has_openai_key = !empty($this->options['api_key']);
7291 $has_voyage_key = !empty($this->options['voyage_api_key']);
7292 $has_gemini_key = !empty($this->options['gemini_api_key']);
7293
7294 // OpenAI Embeddings
7295 echo '<p class="mxchat-embedding-api-status" data-provider="openai" style="display:none;">';
7296 if ($has_openai_key) {
7297 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenAI detected', 'mxchat') . '</span>';
7298 } else {
7299 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenAI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7300 }
7301 echo '</p>';
7302
7303 // Voyage AI Embeddings
7304 echo '<p class="mxchat-embedding-api-status" data-provider="voyage" style="display:none;">';
7305 if ($has_voyage_key) {
7306 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Voyage AI detected', 'mxchat') . '</span>';
7307 } else {
7308 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Voyage AI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7309 }
7310 echo '</p>';
7311
7312 // Gemini Embeddings
7313 echo '<p class="mxchat-embedding-api-status" data-provider="gemini" style="display:none;">';
7314 if ($has_gemini_key) {
7315 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Google Gemini detected', 'mxchat') . '</span>';
7316 } else {
7317 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Google Gemini detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7318 }
7319 echo '</p>';
7320
7321 }
7322
7323
7324 public function mxchat_top_bar_title_callback() {
7325 // Retrieve the current value of the top bar title from saved options
7326 $top_bar_title = isset($this->options['top_bar_title']) ? esc_attr($this->options['top_bar_title']) : '';
7327
7328 // Render the input field
7329 echo '<input type="text" id="top_bar_title" name="top_bar_title" value="' . $top_bar_title . '" />';
7330 }
7331 public function mxchat_ai_agent_text_callback() {
7332 // Retrieve the current value of the AI agent text from saved options
7333 $ai_agent_text = isset($this->options['ai_agent_text']) ? esc_attr($this->options['ai_agent_text']) : '';
7334 // Render the input field
7335 echo '<input type="text" id="ai_agent_text" name="ai_agent_text" value="' . $ai_agent_text . '" />';
7336 }
7337
7338
7339 public function enable_email_block_callback() {
7340 // Load full plugin options array
7341 $all_options = get_option('mxchat_options', []);
7342
7343 // Get the value, default to 'off'
7344 $enable_email_block = isset($all_options['enable_email_block']) ? $all_options['enable_email_block'] : 'off';
7345
7346 // Check if it's 'on'
7347 $checked = ($enable_email_block === 'on') ? 'checked' : '';
7348
7349 echo '<label class="toggle-switch">';
7350 echo sprintf(
7351 '<input type="checkbox" id="enable_email_block" name="enable_email_block" value="on" %s />',
7352 esc_attr($checked)
7353 );
7354 echo '<span class="slider"></span>';
7355 echo '</label>';
7356 }
7357
7358 public function email_blocker_header_content_callback() {
7359 // Load the entire 'mxchat_options' array
7360 $all_options = get_option('mxchat_options', []);
7361
7362 // Retrieve the saved content or default to empty
7363 $content = isset($all_options['email_blocker_header_content'])
7364 ? $all_options['email_blocker_header_content']
7365 : '';
7366
7367 // Render the textarea - IMPORTANT: name should be just "email_blocker_header_content"
7368 echo '<textarea
7369 id="email_blocker_header_content"
7370 name="email_blocker_header_content"
7371 rows="5"
7372 cols="70"
7373 data-setting="email_blocker_header_content"
7374 >' . esc_textarea($content) . '</textarea>';
7375 }
7376
7377 public function email_blocker_button_text_callback() {
7378 // Load the entire 'mxchat_options' array
7379 $all_options = get_option('mxchat_options', []);
7380
7381 // Retrieve the saved button text or default to empty
7382 $button_text = isset($all_options['email_blocker_button_text'])
7383 ? $all_options['email_blocker_button_text']
7384 : '';
7385
7386 // Use esc_attr to safely render the existing text
7387 echo '<input type="text" id="email_blocker_button_text" name="email_blocker_button_text" value="' . esc_attr($button_text) . '" style="width: 300px;" />';
7388 }
7389
7390 //Enable name field callback
7391 public function enable_name_field_callback() {
7392 // Load full plugin options array
7393 $all_options = get_option('mxchat_options', []);
7394 // Get the value, default to 'off'
7395 $enable_name_field = isset($all_options['enable_name_field']) ? $all_options['enable_name_field'] : 'off';
7396 // Check if it's 'on'
7397 $checked = ($enable_name_field === 'on') ? 'checked' : '';
7398 echo '<label class="toggle-switch">';
7399 echo sprintf(
7400 '<input type="checkbox" id="enable_name_field" name="enable_name_field" value="on" %s />',
7401 esc_attr($checked)
7402 );
7403 echo '<span class="slider"></span>';
7404 echo '</label>';
7405 }
7406 //Name field placeholder callback
7407 public function name_field_placeholder_callback() {
7408 $all_options = get_option('mxchat_options', []);
7409 $placeholder = isset($all_options['name_field_placeholder'])
7410 ? $all_options['name_field_placeholder']
7411 : esc_html__('Enter your name', 'mxchat');
7412
7413 echo '<input type="text" id="name_field_placeholder" name="name_field_placeholder" value="' . esc_attr($placeholder) . '" style="width: 300px;" />';
7414 }
7415
7416
7417
7418 public function mxchat_intro_message_callback() {
7419 // Load the entire 'mxchat_options' array
7420 $all_options = get_option('mxchat_options', []);
7421 // Retrieve the saved intro message or use the default
7422 $default_message = __('Hello! How can I assist you today?', 'mxchat');
7423 $saved_message = isset($all_options['intro_message']) ? $all_options['intro_message'] : $default_message;
7424 // Output the textarea with the saved value without escaping HTML
7425 ?>
7426 <textarea id="intro_message" name="intro_message" rows="5" cols="50"><?php echo $saved_message; ?></textarea>
7427 <p class="description" style="margin-top: 8px;">
7428 <?php esc_html_e('Use {visitor_name} to personalize greetings when lead capture is enabled.', 'mxchat'); ?><br>
7429 <code style="font-size: 12px;"><?php esc_html_e('Example: Hello {visitor_name}! How can I help you today?', 'mxchat'); ?></code>
7430 </p>
7431 <?php
7432 }
7433
7434 public function mxchat_input_copy_callback() {
7435 // Load the entire 'mxchat_options' array
7436 $all_options = get_option('mxchat_options', []);
7437
7438 // Retrieve the saved input copy or use the default value
7439 $default_copy = __('How can I assist?', 'mxchat');
7440 $input_copy = isset($all_options['input_copy']) ? $all_options['input_copy'] : $default_copy;
7441
7442 // Output the input field with the saved value
7443 printf(
7444 '<input type="text" id="input_copy" name="input_copy" value="%s" placeholder="%s" />',
7445 esc_attr($input_copy),
7446 esc_attr__('How can I assist?', 'mxchat')
7447 );
7448 }
7449
7450
7451 public function mxchat_append_to_body_callback() {
7452 // Fetch fresh options to ensure we have the latest saved values
7453 $options = get_option('mxchat_options', array());
7454
7455 // Get value from options array, default to 'off'
7456 $append_to_body = isset($options['append_to_body']) ? $options['append_to_body'] : 'off';
7457 $checked = ($append_to_body === 'on') ? 'checked' : '';
7458
7459 // Get post type visibility settings
7460 $visibility_mode = isset($options['post_type_visibility_mode']) ? $options['post_type_visibility_mode'] : 'all';
7461 $visibility_list = isset($options['post_type_visibility_list']) ? $options['post_type_visibility_list'] : array();
7462 if (!is_array($visibility_list)) {
7463 $visibility_list = array();
7464 }
7465
7466 echo '<div class="mxchat-autosave-section">';
7467
7468 // Main toggle
7469 echo '<label class="toggle-switch">';
7470 echo sprintf(
7471 '<input type="checkbox" id="append_to_body" name="append_to_body" value="on" %s />',
7472 esc_attr($checked)
7473 );
7474 echo '<span class="slider"></span>';
7475 echo '</label>';
7476
7477 // Post Type Visibility Options (only visible when auto-display is ON)
7478 $display_style = ($append_to_body === 'on') ? '' : 'display: none;';
7479 echo '<div id="post-type-visibility-options" class="mxchat-sub-options" style="' . esc_attr($display_style) . '">';
7480
7481 echo '<div class="mxchat-post-type-visibility-header">';
7482 echo '<h4>' . esc_html__('Post Type Visibility', 'mxchat') . '</h4>';
7483 echo '</div>';
7484
7485 // Mode selector (radio buttons)
7486 echo '<div class="mxchat-visibility-mode">';
7487
7488 echo '<label class="mxchat-radio-label">';
7489 echo '<input type="radio" name="post_type_visibility_mode" value="all" ' . checked($visibility_mode, 'all', false) . ' />';
7490 echo '<span>' . esc_html__('Show on all post types', 'mxchat') . '</span>';
7491 echo '</label>';
7492
7493 echo '<label class="mxchat-radio-label">';
7494 echo '<input type="radio" name="post_type_visibility_mode" value="include" ' . checked($visibility_mode, 'include', false) . ' />';
7495 echo '<span>' . esc_html__('Only show on selected post types', 'mxchat') . '</span>';
7496 echo '</label>';
7497
7498 echo '<label class="mxchat-radio-label">';
7499 echo '<input type="radio" name="post_type_visibility_mode" value="exclude" ' . checked($visibility_mode, 'exclude', false) . ' />';
7500 echo '<span>' . esc_html__('Hide on selected post types', 'mxchat') . '</span>';
7501 echo '</label>';
7502
7503 echo '</div>';
7504
7505 // Post type checkboxes (only visible when mode is include or exclude)
7506 $list_display = ($visibility_mode !== 'all') ? '' : 'display: none;';
7507 echo '<div id="post-type-list" class="mxchat-post-type-list" style="' . esc_attr($list_display) . '">';
7508
7509 // Get all public post types
7510 $post_types = get_post_types(array('public' => true), 'objects');
7511
7512 foreach ($post_types as $post_type) {
7513 // Skip attachments
7514 if ($post_type->name === 'attachment') {
7515 continue;
7516 }
7517
7518 $is_checked = in_array($post_type->name, $visibility_list) ? 'checked' : '';
7519
7520 echo '<label class="mxchat-checkbox-label">';
7521 echo '<input type="checkbox" name="post_type_visibility_list[]" value="' . esc_attr($post_type->name) . '" ' . $is_checked . ' />';
7522 echo '<span>' . esc_html($post_type->label) . '</span>';
7523 echo '</label>';
7524 }
7525
7526 echo '</div>'; // End post-type-list
7527 echo '</div>'; // End post-type-visibility-options
7528 echo '</div>'; // End mxchat-autosave-section
7529 }
7530
7531 public function mxchat_contextual_awareness_callback() {
7532 // Get value from options array, default to 'off'
7533 $contextual_awareness = isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off';
7534 $checked = ($contextual_awareness === 'on') ? 'checked' : '';
7535 echo '<label class="toggle-switch">';
7536 echo sprintf(
7537 '<input type="checkbox" id="contextual_awareness_toggle" name="contextual_awareness_toggle" value="on" %s />',
7538 esc_attr($checked)
7539 );
7540 echo '<span class="slider"></span>';
7541 echo '</label>';
7542 }
7543
7544 public function mxchat_citation_links_toggle_callback() {
7545 // Get value from options array, default to 'on' (enabled by default)
7546 $citation_links = isset($this->options['citation_links_toggle']) ? $this->options['citation_links_toggle'] : 'on';
7547 $checked = ($citation_links === 'on') ? 'checked' : '';
7548 echo '<label class="toggle-switch">';
7549 echo sprintf(
7550 '<input type="checkbox" id="citation_links_toggle" name="citation_links_toggle" value="on" %s />',
7551 esc_attr($checked)
7552 );
7553 echo '<span class="slider"></span>';
7554 echo '</label>';
7555 }
7556
7557 public function mxchat_privacy_toggle_callback() {
7558 // Load from mxchat_options array
7559 $options = get_option('mxchat_options', []);
7560
7561 // Get privacy toggle value with fallback
7562 $privacy_toggle = isset($options['privacy_toggle']) ? $options['privacy_toggle'] : 'off';
7563 $checked = ($privacy_toggle === 'on') ? 'checked' : '';
7564
7565 // Get privacy text with fallback
7566 $privacy_text = isset($options['privacy_text'])
7567 ? $options['privacy_text']
7568 : __('By chatting, you agree to our <a href="https://example.com/privacy-policy" target="_blank">privacy policy</a>.', 'mxchat');
7569
7570 // Output the toggle switch
7571 echo '<label class="toggle-switch">';
7572 echo sprintf(
7573 '<input type="checkbox" id="privacy_toggle" name="privacy_toggle" value="on" %s />',
7574 esc_attr($checked)
7575 );
7576 echo '<span class="slider"></span>';
7577 echo '</label>';
7578
7579 // Output the custom text input field
7580 echo sprintf(
7581 '<textarea id="privacy_text" name="privacy_text" rows="5" cols="50" class="regular-text">%s</textarea>',
7582 esc_textarea($privacy_text)
7583 );
7584 }
7585
7586
7587 public function mxchat_complianz_toggle_callback() {
7588 // Load from mxchat_options array
7589 $options = get_option('mxchat_options', []);
7590
7591 // Get complianz toggle value with fallback
7592 $complianz_toggle = isset($options['complianz_toggle']) ? $options['complianz_toggle'] : 'off';
7593 $checked = ($complianz_toggle === 'on') ? 'checked' : '';
7594
7595 // Output the toggle switch
7596 echo '<label class="toggle-switch">';
7597 echo sprintf(
7598 '<input type="checkbox" id="complianz_toggle" name="complianz_toggle" value="on" %s />',
7599 esc_attr($checked)
7600 );
7601 echo '<span class="slider"></span>';
7602 echo '</label>';
7603 }
7604
7605 public function mxchat_link_target_toggle_callback() {
7606 // Load from mxchat_options array
7607 $options = get_option('mxchat_options', []);
7608
7609 // Get link target toggle value with fallback
7610 $link_target_toggle = isset($options['link_target_toggle']) ? $options['link_target_toggle'] : 'off';
7611 $checked = ($link_target_toggle === 'on') ? 'checked' : '';
7612
7613 // Output the toggle switch
7614 echo '<label class="toggle-switch">';
7615 echo sprintf(
7616 '<input type="checkbox" id="link_target_toggle" name="link_target_toggle" value="on" %s />',
7617 esc_attr($checked)
7618 );
7619 echo '<span class="slider"></span>';
7620 echo '</label>';
7621 }
7622
7623 public function mxchat_chat_persistence_toggle_callback() {
7624 // Load from mxchat_options array
7625 $options = get_option('mxchat_options', []);
7626
7627 // Get chat persistence toggle value with fallback
7628 $chat_persistence_toggle = isset($options['chat_persistence_toggle']) ? $options['chat_persistence_toggle'] : 'off';
7629 $checked = ($chat_persistence_toggle === 'on') ? 'checked' : '';
7630
7631 // Output the toggle switch
7632 echo '<label class="toggle-switch">';
7633 echo sprintf(
7634 '<input type="checkbox" id="chat_persistence_toggle" name="chat_persistence_toggle" value="on" %s />',
7635 esc_attr($checked)
7636 );
7637 echo '<span class="slider"></span>';
7638 echo '</label>';
7639 }
7640
7641 public function mxchat_popular_question_1_callback() {
7642 // Load the full plugin options array
7643 $all_options = get_option('mxchat_options', []);
7644
7645 // Retrieve the specific option for popular_question_1
7646 $popular_question_1 = isset($all_options['popular_question_1']) ? $all_options['popular_question_1'] : '';
7647
7648 // Render the input field
7649 printf(
7650 '<input type="text" id="popular_question_1" name="popular_question_1" value="%s" placeholder="%s" class="regular-text" />',
7651 esc_attr($popular_question_1),
7652 esc_attr__('Enter Quick Question 1', 'mxchat')
7653 );
7654 }
7655
7656
7657 public function mxchat_popular_question_2_callback() {
7658 // Load the full plugin options array
7659 $all_options = get_option('mxchat_options', []);
7660
7661 // Retrieve the specific option for popular_question_2
7662 $popular_question_2 = isset($all_options['popular_question_2']) ? $all_options['popular_question_2'] : '';
7663
7664 // Render the input field
7665 printf(
7666 '<input type="text" id="popular_question_2" name="popular_question_2" value="%s" placeholder="%s" class="regular-text" />',
7667 esc_attr($popular_question_2),
7668 esc_attr__('Enter Quick Question 2', 'mxchat')
7669 );
7670 }
7671
7672
7673 public function mxchat_popular_question_3_callback() {
7674 // Load the full plugin options array
7675 $all_options = get_option('mxchat_options', []);
7676
7677 // Retrieve the specific option for popular_question_3
7678 $popular_question_3 = isset($all_options['popular_question_3']) ? $all_options['popular_question_3'] : '';
7679
7680 // Render the input field
7681 printf(
7682 '<input type="text" id="popular_question_3" name="popular_question_3" value="%s" placeholder="%s" class="regular-text" />',
7683 esc_attr($popular_question_3),
7684 esc_attr(__('Enter Quick Question 3', 'mxchat'))
7685 );
7686 }
7687
7688 public function mxchat_additional_popular_questions_callback() {
7689 $options = get_option('mxchat_options', []);
7690 $additional_questions = isset($options['additional_popular_questions'])
7691 ? $options['additional_popular_questions']
7692 : get_option('additional_popular_questions', array());
7693
7694 echo '<div id="mxchat-additional-questions-container">';
7695 if (!empty($additional_questions)) {
7696 foreach ($additional_questions as $index => $question) {
7697 printf(
7698 '<div class="mxchat-question-row">
7699 <input type="text" name="additional_popular_questions[]"
7700 value="%s"
7701 placeholder="%s"
7702 class="regular-text mxchat-question-input"
7703 data-question-index="%d" />
7704 <button type="button" class="button mxchat-remove-question"
7705 aria-label="%s">%s</button>
7706 </div>',
7707 esc_attr($question),
7708 esc_attr(sprintf(__('Enter Additional Quick Question %d', 'mxchat'), $index + 4)),
7709 $index,
7710 esc_attr(__('Remove question', 'mxchat')),
7711 esc_html__('Remove', 'mxchat')
7712 );
7713 }
7714 } else {
7715 printf(
7716 '<div class="mxchat-question-row">
7717 <input type="text" name="additional_popular_questions[]"
7718 value=""
7719 placeholder="%s"
7720 class="regular-text mxchat-question-input"
7721 data-question-index="0" />
7722 <button type="button" class="button mxchat-remove-question"
7723 aria-label="%s">%s</button>
7724 </div>',
7725 esc_attr(__('Enter Additional Quick Question 4', 'mxchat')),
7726 esc_attr(__('Remove question', 'mxchat')),
7727 esc_html__('Remove', 'mxchat')
7728 );
7729 }
7730 echo '</div>';
7731 printf(
7732 '<button type="button" class="button mxchat-add-question" aria-label="%s">%s</button>',
7733 esc_attr(__('Add question', 'mxchat')),
7734 esc_html__('Add Question', 'mxchat')
7735 );
7736 }
7737
7738 public function mxchat_brave_api_key_callback() {
7739 $brave_api_key = isset($this->options['brave_api_key']) ? esc_attr($this->options['brave_api_key']) : '';
7740 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7741
7742 echo '<div class="api-key-wrapper">';
7743 echo sprintf(
7744 '<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" />',
7745 $brave_api_key,
7746 $nonce
7747 );
7748 echo '<button type="button" id="toggleBraveApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7749 echo '</div>';
7750 echo '<p class="description">' . __('Required for Brave Search integration. Get your API key from Brave Search API.', 'mxchat') . '</p>';
7751 }
7752
7753 public function mxchat_brave_image_count_callback() {
7754 $brave_image_count = isset($this->options['brave_image_count'])
7755 ? intval($this->options['brave_image_count'])
7756 : 4;
7757 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7758
7759 echo '<div class="mxchat-field-wrapper">';
7760 echo sprintf(
7761 '<input type="number" id="brave_image_count" name="brave_image_count"
7762 value="%d" min="1" max="6" class="small-text mxchat-autosave-field" data-nonce="%s" />',
7763 $brave_image_count,
7764 $nonce
7765 );
7766 echo '</div>';
7767 echo '<p class="description">' . __('Select the number of images to return (1-6).', 'mxchat') . '</p>';
7768 }
7769
7770 public function mxchat_brave_safe_search_callback() {
7771 $brave_safe_search = isset($this->options['brave_safe_search'])
7772 ? esc_attr($this->options['brave_safe_search'])
7773 : 'strict';
7774 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7775
7776 echo '<div class="mxchat-field-wrapper">';
7777 echo '<select id="brave_safe_search" name="brave_safe_search" class="mxchat-autosave-field" data-nonce="' . $nonce . '">';
7778 echo sprintf(
7779 '<option value="strict" %s>%s</option>',
7780 selected($brave_safe_search, 'strict', false),
7781 __('Strict', 'mxchat')
7782 );
7783 echo sprintf(
7784 '<option value="off" %s>%s</option>',
7785 selected($brave_safe_search, 'off', false),
7786 __('Off', 'mxchat')
7787 );
7788 echo '</select>';
7789 echo '</div>';
7790 echo '<p class="description">' .
7791 esc_html__('Set the Safe Search level for image searches. Brave Search only supports "Strict" and "Off" options.', 'mxchat') .
7792 '</p>';
7793 }
7794
7795 public function mxchat_brave_news_count_callback() {
7796 $brave_news_count = isset($this->options['brave_news_count'])
7797 ? intval($this->options['brave_news_count'])
7798 : 3;
7799 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7800
7801 echo '<div class="mxchat-field-wrapper">';
7802 echo sprintf(
7803 '<input type="number" id="brave_news_count" name="brave_news_count"
7804 value="%d" min="1" max="10" class="small-text mxchat-autosave-field" data-nonce="%s" />',
7805 $brave_news_count,
7806 $nonce
7807 );
7808 echo '</div>';
7809 echo '<p class="description">' . esc_html__('Select the number of news articles to retrieve (1-10).', 'mxchat') . '</p>';
7810 }
7811
7812 public function mxchat_brave_country_callback() {
7813 $brave_country = isset($this->options['brave_country'])
7814 ? esc_attr($this->options['brave_country'])
7815 : 'us';
7816 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7817
7818 echo '<div class="mxchat-field-wrapper">';
7819 echo sprintf(
7820 '<input type="text" id="brave_country" name="brave_country"
7821 value="%s" maxlength="2" class="small-text mxchat-autosave-field" data-nonce="%s" />',
7822 $brave_country,
7823 $nonce
7824 );
7825 echo '</div>';
7826 echo '<p class="description">' . esc_html__('Enter the country code (e.g., "us" for United States).', 'mxchat') . '</p>';
7827 }
7828
7829 public function mxchat_brave_language_callback() {
7830 $brave_language = isset($this->options['brave_language'])
7831 ? esc_attr($this->options['brave_language'])
7832 : 'en';
7833 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7834
7835 echo '<div class="mxchat-field-wrapper">';
7836 echo sprintf(
7837 '<input type="text" id="brave_language" name="brave_language"
7838 value="%s" maxlength="2" class="small-text mxchat-autosave-field" data-nonce="%s" />',
7839 $brave_language,
7840 $nonce
7841 );
7842 echo '</div>';
7843 echo '<p class="description">' . esc_html__('Enter the language code (e.g., "en" for English).', 'mxchat') . '</p>';
7844 }
7845
7846
7847
7848
7849
7850 // Section Callback
7851 public function mxchat_pdf_intent_section_callback() {
7852 echo '<p>' . esc_html__('Configure the intent settings for the Chat with PDF feature.', 'mxchat') . '</p>';
7853 }
7854
7855 public function mxchat_chat_toolbar_toggle_callback() {
7856 // Get chat toolbar toggle value with fallback
7857 $chat_toolbar_toggle = isset($this->options['chat_toolbar_toggle']) ? $this->options['chat_toolbar_toggle'] : 'off';
7858 $checked = ($chat_toolbar_toggle === 'on') ? 'checked' : '';
7859
7860 // Output the toggle switch
7861 echo '<label class="toggle-switch">';
7862 echo sprintf(
7863 '<input type="checkbox" id="chat_toolbar_toggle" name="chat_toolbar_toggle" value="on" %s />',
7864 esc_attr($checked)
7865 );
7866 echo '<span class="slider"></span>';
7867 echo '</label>';
7868
7869 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>';
7870 }
7871
7872 /**
7873 * Callback for PDF upload button toggle setting
7874 */
7875 public function mxchat_show_pdf_upload_button_callback() {
7876 // Get toggle value with fallback
7877 $show_pdf_button = isset($this->options['show_pdf_upload_button']) ? $this->options['show_pdf_upload_button'] : 'on';
7878 $checked = ($show_pdf_button === 'on') ? 'checked' : '';
7879
7880 // Output the toggle switch
7881 echo '<label class="toggle-switch">';
7882 echo sprintf(
7883 '<input type="checkbox" id="show_pdf_upload_button" name="show_pdf_upload_button" value="on" %s />',
7884 esc_attr($checked)
7885 );
7886 echo '<span class="slider"></span>';
7887 echo '</label>';
7888
7889 echo '<p class="description">' . esc_html__('Enable to show the PDF upload button in the chatbot toolbar. Disable to hide it.', 'mxchat') . '</p>';
7890 }
7891
7892 /**
7893 * Callback for Word upload button toggle setting
7894 */
7895 public function mxchat_show_word_upload_button_callback() {
7896 // Get toggle value with fallback
7897 $show_word_button = isset($this->options['show_word_upload_button']) ? $this->options['show_word_upload_button'] : 'on';
7898 $checked = ($show_word_button === 'on') ? 'checked' : '';
7899
7900 // Output the toggle switch
7901 echo '<label class="toggle-switch">';
7902 echo sprintf(
7903 '<input type="checkbox" id="show_word_upload_button" name="show_word_upload_button" value="on" %s />',
7904 esc_attr($checked)
7905 );
7906 echo '<span class="slider"></span>';
7907 echo '</label>';
7908
7909 echo '<p class="description">' . esc_html__('Enable to show the Word document upload button in the chatbot toolbar. Disable to hide it.', 'mxchat') . '</p>';
7910 }
7911
7912 public function mxchat_pdf_intent_trigger_text_callback() {
7913 $default_text = __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat');
7914
7915 echo sprintf(
7916 '<textarea id="pdf_intent_trigger_text"
7917 name="pdf_intent_trigger_text"
7918 rows="3"
7919 cols="50"
7920 placeholder="%s">%s</textarea>',
7921 esc_attr__('Enter trigger text', 'mxchat'),
7922 isset($this->options['pdf_intent_trigger_text'])
7923 ? esc_textarea($this->options['pdf_intent_trigger_text'])
7924 : esc_textarea($default_text)
7925 );
7926 echo '<p class="description">' . esc_html__('Text displayed when the intent is triggered.', 'mxchat') . '</p>';
7927 }
7928
7929 public function mxchat_pdf_intent_success_text_callback() {
7930 $default_text = __("I've processed the PDF. What questions do you have about it?", 'mxchat');
7931
7932 echo sprintf(
7933 '<textarea id="pdf_intent_success_text"
7934 name="pdf_intent_success_text"
7935 rows="3"
7936 cols="50"
7937 placeholder="%s">%s</textarea>',
7938 esc_attr__('Enter success text', 'mxchat'),
7939 isset($this->options['pdf_intent_success_text'])
7940 ? esc_textarea($this->options['pdf_intent_success_text'])
7941 : esc_textarea($default_text)
7942 );
7943 echo '<p class="description">' . esc_html__('Text displayed when the intent is successful.', 'mxchat') . '</p>';
7944 }
7945
7946 public function mxchat_pdf_intent_error_text_callback() {
7947 $default_text = __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
7948
7949 echo sprintf(
7950 '<textarea id="pdf_intent_error_text"
7951 name="pdf_intent_error_text"
7952 rows="3"
7953 cols="50"
7954 placeholder="%s">%s</textarea>',
7955 esc_attr__('Enter error text', 'mxchat'),
7956 isset($this->options['pdf_intent_error_text'])
7957 ? esc_textarea($this->options['pdf_intent_error_text'])
7958 : esc_textarea($default_text)
7959 );
7960 echo '<p class="description">' . esc_html__('Text displayed when an error occurs during the intent.', 'mxchat') . '</p>';
7961 }
7962
7963 public function mxchat_pdf_max_pages_callback() {
7964 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
7965
7966 echo sprintf(
7967 '<input type="range"
7968 id="pdf_max_pages"
7969 name="pdf_max_pages"
7970 min="1"
7971 max="69"
7972 value="%d"
7973 class="range-slider" />',
7974 esc_attr($max_pages)
7975 );
7976 echo '<span id="pdf_max_pages_output">' . esc_html($max_pages) . '</span>';
7977 echo '<p class="description">' . esc_html__('Set the maximum number of document pages users can upload for processing. (1-69 pages)', 'mxchat') . '</p>';
7978 }
7979
7980 public function mxchat_live_agent_status_callback() {
7981 // Always get fresh options instead of using cached $this->options
7982 $fresh_options = get_option('mxchat_options');
7983 $status = isset($fresh_options['live_agent_status']) ? $fresh_options['live_agent_status'] : 'off';
7984
7985 echo '<label class="toggle-switch">';
7986 echo sprintf(
7987 '<input type="checkbox" id="live_agent_status" name="live_agent_status" value="on" %s />',
7988 checked($status, 'on', false)
7989 );
7990 echo '<span class="slider"></span>';
7991 echo '</label>';
7992 echo '<label for="live_agent_status" class="mxchat-status-label">';
7993 echo '<span class="status-text">' . ($status === 'on' ? esc_html__('Online', 'mxchat') : esc_html__('Offline', 'mxchat')) . '</span>';
7994 echo '</label>';
7995 }
7996
7997 public function mxchat_live_agent_away_message_callback() {
7998 $message = isset($this->options['live_agent_away_message'])
7999 ? $this->options['live_agent_away_message']
8000 : __('Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.', 'mxchat');
8001
8002 printf(
8003 '<textarea id="live_agent_away_message" name="live_agent_away_message" rows="3" cols="50">%s</textarea>',
8004 esc_textarea($message)
8005 );
8006 echo '<p class="description">' . esc_html__('Message shown when live agents are offline.', 'mxchat') . '</p>';
8007 }
8008
8009 public function mxchat_live_agent_notification_message_callback() {
8010 $message = isset($this->options['live_agent_notification_message'])
8011 ? $this->options['live_agent_notification_message']
8012 : __('Live agent has been notified.', 'mxchat');
8013
8014 printf(
8015 '<textarea id="live_agent_notification_message" name="live_agent_notification_message" rows="3" cols="50">%s</textarea>',
8016 esc_textarea($message)
8017 );
8018 echo '<p class="description">' . esc_html__('Message shown when live transfer activated.', 'mxchat') . '</p>';
8019 }
8020
8021 public function mxchat_live_agent_webhook_url_callback() {
8022 $webhook_url = isset($this->options['live_agent_webhook_url'])
8023 ? esc_url($this->options['live_agent_webhook_url'])
8024 : esc_url(get_option('live_agent_webhook_url', ''));
8025
8026 printf(
8027 '<input type="password" id="live_agent_webhook_url" name="live_agent_webhook_url" value="%s" class="regular-text" />',
8028 $webhook_url
8029 );
8030 echo '<button type="button" id="toggleWebhookUrlVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
8031 echo '<p class="description">' . esc_html__('Enter your Slack webhook URL for live agent notifications.', 'mxchat') . '</p>';
8032 }
8033
8034 public function mxchat_live_agent_secret_key_callback() {
8035 printf(
8036 '<input type="password" id="live_agent_secret_key" name="live_agent_secret_key" value="%s" class="regular-text" />',
8037 isset($this->options['live_agent_secret_key']) ? esc_attr($this->options['live_agent_secret_key']) : ''
8038 );
8039 echo '<button type="button" id="toggleSecretKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
8040 echo '<p class="description">' . esc_html__('Secret key for validating Slack requests. Keep this secure.', 'mxchat') . '</p>';
8041 }
8042
8043 public function mxchat_live_agent_bot_token_callback() {
8044 printf(
8045 '<input type="password" id="live_agent_bot_token" name="live_agent_bot_token" value="%s" class="regular-text" />',
8046 isset($this->options['live_agent_bot_token']) ? esc_attr($this->options['live_agent_bot_token']) : ''
8047 );
8048 echo '<button type="button" id="toggleBotTokenVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
8049 echo '<p class="description">' . esc_html__('Your Slack Bot OAuth Token (starts with xoxb-). Keep this secure.', 'mxchat') . '</p>';
8050 }
8051
8052 public function mxchat_live_agent_user_ids_callback() {
8053 $user_ids = isset($this->options['live_agent_user_ids'])
8054 ? esc_textarea($this->options['live_agent_user_ids'])
8055 : '';
8056
8057 printf(
8058 '<textarea id="live_agent_user_ids" name="live_agent_user_ids" rows="4" class="large-text">%s</textarea>',
8059 $user_ids
8060 );
8061 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>';
8062 }
8063
8064 /**
8065 * Telegram Integration Callbacks
8066 */
8067 public function mxchat_telegram_section_callback() {
8068 echo '<p>' . esc_html__('Configure Telegram integration for live agent support.', 'mxchat') . '</p>';
8069 }
8070
8071 public function mxchat_telegram_status_callback() {
8072 $fresh_options = get_option('mxchat_options');
8073 $status = isset($fresh_options['telegram_status']) ? $fresh_options['telegram_status'] : 'off';
8074
8075 echo '<label class="toggle-switch">';
8076 echo sprintf(
8077 '<input type="checkbox" id="telegram_status" name="telegram_status" value="on" %s />',
8078 checked($status, 'on', false)
8079 );
8080 echo '<span class="slider"></span>';
8081 echo '</label>';
8082 echo '<label for="telegram_status" class="mxchat-status-label">';
8083 echo '<span class="status-text">' . ($status === 'on' ? esc_html__('Online', 'mxchat') : esc_html__('Offline', 'mxchat')) . '</span>';
8084 echo '</label>';
8085 }
8086
8087 public function mxchat_telegram_notification_message_callback() {
8088 $message = isset($this->options['telegram_notification_message'])
8089 ? $this->options['telegram_notification_message']
8090 : __("I've notified a support agent. Please allow a moment for them to respond.", 'mxchat');
8091
8092 printf(
8093 '<textarea id="telegram_notification_message" name="telegram_notification_message" rows="3" cols="50">%s</textarea>',
8094 esc_textarea($message)
8095 );
8096 echo '<p class="description">' . esc_html__('Message shown when live transfer activated.', 'mxchat') . '</p>';
8097 }
8098
8099 public function mxchat_telegram_away_message_callback() {
8100 $message = isset($this->options['telegram_away_message'])
8101 ? $this->options['telegram_away_message']
8102 : __('Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.', 'mxchat');
8103
8104 printf(
8105 '<textarea id="telegram_away_message" name="telegram_away_message" rows="3" cols="50">%s</textarea>',
8106 esc_textarea($message)
8107 );
8108 echo '<p class="description">' . esc_html__('Message shown when live agents are offline.', 'mxchat') . '</p>';
8109 }
8110
8111 public function mxchat_telegram_bot_token_callback() {
8112 printf(
8113 '<input type="password" id="telegram_bot_token" name="telegram_bot_token" value="%s" class="regular-text" />',
8114 isset($this->options['telegram_bot_token']) ? esc_attr($this->options['telegram_bot_token']) : ''
8115 );
8116 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>';
8117 echo '<p class="description">' . esc_html__('Your Telegram Bot Token from @BotFather (e.g., 123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ).', 'mxchat') . '</p>';
8118 }
8119
8120 public function mxchat_telegram_group_id_callback() {
8121 printf(
8122 '<input type="text" id="telegram_group_id" name="telegram_group_id" value="%s" class="regular-text" />',
8123 isset($this->options['telegram_group_id']) ? esc_attr($this->options['telegram_group_id']) : ''
8124 );
8125 echo '<p class="description">' . esc_html__('Your Telegram supergroup ID (starts with -100). The group must have forum topics enabled.', 'mxchat') . '</p>';
8126 }
8127
8128 public function mxchat_telegram_webhook_secret_callback() {
8129 $secret = isset($this->options['telegram_webhook_secret']) ? $this->options['telegram_webhook_secret'] : '';
8130
8131 // Auto-generate secret if empty
8132 if (empty($secret)) {
8133 $secret = wp_generate_password(64, false, false);
8134 $options = get_option('mxchat_options', []);
8135 $options['telegram_webhook_secret'] = $secret;
8136 update_option('mxchat_options', $options);
8137 $this->options['telegram_webhook_secret'] = $secret;
8138 }
8139
8140 printf(
8141 '<input type="password" id="telegram_webhook_secret" name="telegram_webhook_secret" value="%s" class="regular-text" />',
8142 esc_attr($secret)
8143 );
8144 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>';
8145 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>';
8146 }
8147
8148 public function mxchat_similarity_threshold_callback() {
8149 // Load from mxchat_options array
8150 $options = get_option('mxchat_options', []);
8151
8152 // Get value from options array with default of 35
8153 $threshold = isset($options['similarity_threshold']) ? $options['similarity_threshold'] : 35;
8154
8155 echo '<div class="slider-container">';
8156 echo sprintf(
8157 '<input type="range"
8158 id="similarity_threshold"
8159 name="similarity_threshold"
8160 min="20"
8161 max="85"
8162 step="1"
8163 value="%s"
8164 class="range-slider" />',
8165 esc_attr($threshold)
8166 );
8167 echo sprintf(
8168 '<span id="threshold_value" class="range-value">%s</span>',
8169 esc_html($threshold)
8170 );
8171 echo '</div>';
8172 }
8173
8174 public function mxchat_rag_sources_limit_callback() {
8175 // Load from mxchat_options array
8176 $options = get_option('mxchat_options', []);
8177
8178 // Get value from options array with default of 3
8179 $rag_sources_limit = isset($options['rag_sources_limit']) ? intval($options['rag_sources_limit']) : 3;
8180
8181 echo '<div class="slider-container">';
8182 echo sprintf(
8183 '<input type="range"
8184 id="rag_sources_limit"
8185 name="rag_sources_limit"
8186 min="3"
8187 max="10"
8188 step="1"
8189 value="%s"
8190 class="range-slider" />',
8191 esc_attr($rag_sources_limit)
8192 );
8193 echo sprintf(
8194 '<span id="rag_sources_limit_value" class="range-value">%s</span>',
8195 esc_html($rag_sources_limit)
8196 );
8197 echo '</div>';
8198 }
8199
8200 public function mxchat_rag_chunks_limit_callback() {
8201 // Load from mxchat_options array
8202 $options = get_option('mxchat_options', []);
8203
8204 // Get value from options array with default of 15
8205 $rag_chunks_limit = isset($options['rag_chunks_limit']) ? intval($options['rag_chunks_limit']) : 15;
8206
8207 echo '<div class="slider-container">';
8208 echo sprintf(
8209 '<input type="range"
8210 id="rag_chunks_limit"
8211 name="rag_chunks_limit"
8212 min="8"
8213 max="20"
8214 step="1"
8215 value="%s"
8216 class="range-slider" />',
8217 esc_attr($rag_chunks_limit)
8218 );
8219 echo sprintf(
8220 '<span id="rag_chunks_limit_value" class="range-value">%s</span>',
8221 esc_html($rag_chunks_limit)
8222 );
8223 echo '</div>';
8224 }
8225
8226 public function mxchat_enqueue_admin_assets() {
8227 // Get plugin version
8228 $version = MXCHAT_VERSION;
8229
8230 // Use file modification time for development (remove in production)
8231 if (defined('WP_DEBUG') && WP_DEBUG) {
8232 $version = filemtime(plugin_dir_path(__FILE__) . '../mxchat-basic.php');
8233 }
8234
8235 $current_page = isset($_GET['page']) ? $_GET['page'] : '';
8236 $plugin_url = plugin_dir_url(__FILE__) . '../';
8237
8238 // Only load on MxChat pages
8239 if (strpos($current_page, 'mxchat') === false) {
8240 return;
8241 }
8242
8243 // Always load these on all MxChat pages
8244 $this->enqueue_core_admin_assets($plugin_url, $version);
8245 $this->enqueue_page_specific_assets($current_page, $plugin_url, $version);
8246 $this->localize_admin_scripts($current_page);
8247 }
8248 private function enqueue_core_admin_assets($plugin_url, $version) {
8249 // Core admin styles
8250 wp_enqueue_style('mxchat-admin-css', $plugin_url . 'css/admin-style.css', array(), $version);
8251 wp_enqueue_style('mxchat-knowledge-css', $plugin_url . 'css/knowledge-style.css', array(), $version);
8252
8253 // New sidebar navigation styles (for main settings page)
8254 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8255
8256 // Core admin scripts
8257 wp_enqueue_script('mxchat-admin-js', $plugin_url . 'js/mxchat-admin.js', array('jquery'), $version, true);
8258 }
8259 private function enqueue_page_specific_assets($current_page, $plugin_url, $version) {
8260 switch ($current_page) {
8261 case 'mxchat-prompts':
8262 // Knowledge processing page assets
8263 wp_enqueue_style('mxchat-content-selector-css', $plugin_url . 'css/content-selector.css', array(), $version);
8264 wp_enqueue_script('mxchat-content-selector-js', $plugin_url . 'js/content-selector.js', array('jquery'), $version, true);
8265 // Add the knowledge processing script (common script needed for WordPress dismiss functionality)
8266 wp_enqueue_script('mxchat-knowledge-processing', $plugin_url . 'js/knowledge-processing.js', array('jquery', 'common'), $version, true);
8267 break;
8268
8269 case 'mxchat-transcripts':
8270 // Load admin sidebar CSS first (shared styles for sidebar navigation)
8271 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8272 // Load transcripts-specific styles
8273 wp_enqueue_style('mxchat-chat-transcripts-css', $plugin_url . 'css/chat-transcripts.css', array('mxchat-admin-sidebar-css'), $version);
8274 wp_enqueue_script('mxchat-transcripts-js', $plugin_url . 'js/mxchat_transcripts.js', array('jquery'), $version, true);
8275 break;
8276
8277 case 'mxchat-actions':
8278 // Load admin sidebar CSS first (shared styles for sidebar navigation)
8279 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8280 // Load actions-specific styles
8281 wp_enqueue_style('mxchat-actions-css', $plugin_url . 'css/actions.css', array('mxchat-admin-sidebar-css'), $version);
8282 wp_enqueue_script('mxchat-actions-js', $plugin_url . 'js/mxchat_actions.js', array('jquery'), $version, true);
8283
8284 // Localize script data for actions page
8285 $is_activated = get_option('mxchat_pro_license_status') === 'active';
8286 wp_localize_script('mxchat-actions-js', 'mxchActionsData', array(
8287 'ajaxUrl' => admin_url('admin-ajax.php'),
8288 'nonce' => wp_create_nonce('mxchat_actions_nonce'),
8289 'addNonce' => wp_create_nonce('mxchat_add_intent_nonce'),
8290 'editNonce' => wp_create_nonce('mxchat_edit_intent'),
8291 'deleteNonce' => wp_create_nonce('mxchat_delete_intent_nonce'),
8292 'toggleNonce' => wp_create_nonce('mxchat_actions_nonce'),
8293 'addPhraseNonce' => wp_create_nonce('mxchat_add_phrase_nonce'),
8294 'deletePhraseNonce' => wp_create_nonce('mxchat_delete_phrase_nonce'),
8295 'getPhrasesNonce' => wp_create_nonce('mxchat_get_phrases_nonce'),
8296 'deleteLegacyNonce' => wp_create_nonce('mxchat_delete_legacy_nonce'),
8297 'isActivated' => $is_activated,
8298 'i18n' => array(
8299 'confirmDelete' => __('Are you sure you want to delete this action?', 'mxchat'),
8300 'confirmBulkDelete' => __('Are you sure you want to delete the selected actions?', 'mxchat'),
8301 'saving' => __('Saving...', 'mxchat'),
8302 'saved' => __('Saved successfully!', 'mxchat'),
8303 'error' => __('An error occurred. Please try again.', 'mxchat'),
8304 'proRequired' => __('This feature requires MxChat Pro.', 'mxchat'),
8305 'addonRequired' => __('This feature requires an add-on.', 'mxchat')
8306 )
8307 ));
8308 break;
8309
8310 case 'mxchat-activation':
8311 // Load admin sidebar CSS first (shared styles for sidebar navigation)
8312 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8313 // Load pro page-specific styles
8314 wp_enqueue_style('mxchat-pro-css', $plugin_url . 'css/admin-pro.css', array('mxchat-admin-sidebar-css'), $version);
8315 // Load pro page JavaScript
8316 wp_enqueue_script('mxchat-pro-js', $plugin_url . 'js/mxchat_pro.js', array('jquery'), $version, true);
8317 // Load activation script for license activation/deactivation
8318 wp_enqueue_script('mxchat-activation-js', $plugin_url . 'js/activation-script.js', array('jquery'), $version, true);
8319 break;
8320
8321 case 'mxchat-content':
8322 // Load admin sidebar CSS (shared styles for sidebar navigation)
8323 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8324 // Load content page-specific styles
8325 wp_enqueue_style('mxchat-content-css', $plugin_url . 'css/admin-content.css', array('mxchat-admin-sidebar-css'), $version);
8326 // Load content page JavaScript
8327 wp_enqueue_script('mxchat-content-js', $plugin_url . 'js/mxchat-content.js', array('jquery'), $version, true);
8328 break;
8329
8330 case 'mxchat-api-access':
8331 // Shared sidebar styles — page handles its own tab switching inline.
8332 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8333 break;
8334 default:
8335 wp_enqueue_script(
8336 'mxchat-test-streaming-js',
8337 $plugin_url . 'js/mxchat-test-streaming.js',
8338 ['jquery'],
8339 $version,
8340 true
8341 );
8342
8343 wp_localize_script('mxchat-test-streaming-js', 'mxchatTestStreamingAjax', [
8344 'ajax_url' => admin_url('admin-ajax.php'),
8345 'nonce' => wp_create_nonce('mxchat_test_streaming_nonce'),
8346 'settings_nonce' => wp_create_nonce('mxchat_save_setting_nonce')
8347 ]);
8348
8349 // Testing Tab: Load chatbot assets for the embedded testing chatbot
8350 wp_enqueue_style('mxchat-chat-css', $plugin_url . 'css/chat-style.css', array(), $version);
8351 wp_enqueue_style('mxchat-admin-testing-css', $plugin_url . 'css/admin-testing-tab.css', array('mxchat-admin-sidebar-css', 'mxchat-chat-css'), $version);
8352
8353 wp_enqueue_script('mxchat-chat-js', $plugin_url . 'js/chat-script.js', array('jquery'), $version, true);
8354 wp_enqueue_script('mxchat-admin-testing-js', $plugin_url . 'js/admin-testing-tab.js', array('jquery', 'mxchat-chat-js'), $version, true);
8355
8356 // Allow add-ons to enqueue their public CSS/JS for the testing chatbot
8357 do_action('mxchat_enqueue_testing_tab_assets');
8358
8359 // Localize mxchatChat for the chatbot JS (same settings as frontend)
8360 $options = get_option('mxchat_options', array());
8361 $prompts_options = get_option('mxchat_prompts_options', array());
8362 $theme_options = get_option('mxchat_theme_options', array());
8363 $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
8364 $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
8365 $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
8366
8367 wp_localize_script('mxchat-chat-js', 'mxchatChat', array(
8368 'ajax_url' => admin_url('admin-ajax.php'),
8369 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
8370 'model' => isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest',
8371 'enable_streaming_toggle' => isset($options['enable_streaming_toggle']) ? $options['enable_streaming_toggle'] : 'on',
8372 'contextual_awareness_toggle' => isset($options['contextual_awareness_toggle']) ? $options['contextual_awareness_toggle'] : 'off',
8373 'link_target_toggle' => $options['link_target_toggle'] ?? 'off',
8374 'rate_limit_message' => $options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
8375 'complianz_toggle' => isset($options['complianz_toggle']) && $options['complianz_toggle'] === 'on',
8376 'user_message_bg_color' => $options['user_message_bg_color'] ?? '#fff',
8377 'user_message_font_color' => $options['user_message_font_color'] ?? '#212121',
8378 'bot_message_bg_color' => $options['bot_message_bg_color'] ?? '#212121',
8379 'bot_message_font_color' => $options['bot_message_font_color'] ?? '#fff',
8380 'top_bar_bg_color' => $options['top_bar_bg_color'] ?? '#212121',
8381 'send_button_font_color' => $options['send_button_font_color'] ?? '#212121',
8382 'close_button_color' => $options['close_button_color'] ?? '#fff',
8383 'chatbot_background_color' => $options['chatbot_background_color'] ?? '#212121',
8384 'chatbot_bg_color' => $options['chatbot_bg_color'] ?? '#fff',
8385 'icon_color' => $options['icon_color'] ?? '#fff',
8386 'chat_input_font_color' => $options['chat_input_font_color'] ?? '#212121',
8387 'chat_persistence_toggle' => 'off', // Always off for testing chatbot
8388 'appendWidgetToBody' => 'off',
8389 'live_agent_message_bg_color' => $options['live_agent_message_bg_color'] ?? '#ffffff',
8390 'live_agent_message_font_color' => $options['live_agent_message_font_color'] ?? '#333333',
8391 'chat_toolbar_toggle' => $options['chat_toolbar_toggle'] ?? 'off',
8392 'mode_indicator_bg_color' => $options['mode_indicator_bg_color'] ?? '#767676',
8393 'mode_indicator_font_color' => $options['mode_indicator_font_color'] ?? '#ffffff',
8394 'toolbar_icon_color' => $options['toolbar_icon_color'] ?? '#212121',
8395 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
8396 'email_collection_enabled' => 'off', // No email collection in testing
8397 'initial_email_state' => null,
8398 'skip_email_check' => true,
8399 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
8400 'skip_inline_colors' => $skip_inline_colors,
8401 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array()
8402 ));
8403
8404 // Localize testing tab script data
8405 wp_localize_script('mxchat-admin-testing-js', 'mxchatAdminTestData', array(
8406 'ajaxUrl' => admin_url('admin-ajax.php'),
8407 'nonce' => wp_create_nonce('mxchat_test_nonce'),
8408 'isAdmin' => true,
8409 'testingEnabled' => true
8410 ));
8411
8412 // Add testing enabled flag for the chatbot to return debug data
8413 add_action('admin_footer', function() {
8414 echo '<script>window.mxchatTestingEnabled = true;</script>';
8415 });
8416
8417 break;
8418 }
8419 }
8420 private function localize_admin_scripts($current_page) {
8421 // Base localization data for main admin script
8422 $base_data = array(
8423 'ajax_url' => admin_url('admin-ajax.php'),
8424 'nonce' => wp_create_nonce('mxchat_admin_nonce'),
8425 'admin_url' => admin_url(),
8426 'license_nonce' => wp_create_nonce('mxchat_activate_license_nonce'),
8427 'inline_edit_nonce' => wp_create_nonce('mxchat_save_inline_nonce'),
8428 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
8429 'export_nonce' => wp_create_nonce('mxchat_export_transcripts'),
8430 'actions_nonce' => wp_create_nonce('mxchat_actions_nonce'),
8431 'add_intent_nonce' => wp_create_nonce('mxchat_add_intent_nonce'),
8432 'edit_intent_nonce' => wp_create_nonce('mxchat_edit_intent'),
8433 'add_phrase_nonce' => wp_create_nonce('mxchat_add_phrase_nonce'),
8434 'delete_phrase_nonce' => wp_create_nonce('mxchat_delete_phrase_nonce'),
8435 'get_phrases_nonce' => wp_create_nonce('mxchat_get_phrases_nonce'),
8436 'delete_legacy_nonce' => wp_create_nonce('mxchat_delete_legacy_nonce'),
8437 'toggle_action_nonce' => wp_create_nonce('mxchat_actions_nonce'),
8438 'fetch_openrouter_models_nonce' => wp_create_nonce('mxchat_fetch_openrouter_models'),
8439 'is_activated' => $this->is_activated ? '1' : '0',
8440 'status_refresh_interval' => 5000,
8441 'prompts_setting_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
8442 'ajaxurl' => admin_url('admin-ajax.php')
8443 );
8444
8445 // Localize main admin script with base data
8446 wp_localize_script('mxchat-admin-js', 'mxchatAdmin', $base_data);
8447
8448 // Page-specific localizations
8449 $this->localize_page_specific_scripts($current_page);
8450 }
8451 private function localize_page_specific_scripts($current_page) {
8452 switch ($current_page) {
8453 case 'mxchat-prompts':
8454 // Status updater localization
8455 wp_localize_script('mxchat-status-updater', 'mxchat_status_data', array(
8456 'ajax_url' => admin_url('admin-ajax.php'),
8457 'nonce' => wp_create_nonce('mxchat_status_nonce')
8458 ));
8459
8460 // Content selector localization
8461 wp_localize_script('mxchat-content-selector-js', 'mxchatSelector', array(
8462 'ajaxurl' => admin_url('admin-ajax.php'),
8463 'nonce' => wp_create_nonce('mxchat_content_selector_nonce'),
8464 'i18n' => array(
8465 'searchPlaceholder' => __('Search posts and pages...', 'mxchat'),
8466 'selectAll' => __('Select All', 'mxchat'),
8467 'process' => __('Process Selected', 'mxchat'),
8468 'cancel' => __('Cancel', 'mxchat'),
8469 'noResults' => __('No content found.', 'mxchat')
8470 )
8471 ));
8472
8473 wp_localize_script('mxchat-knowledge-processing', 'mxchatAdmin', array(
8474 'ajax_url' => admin_url('admin-ajax.php'),
8475 'status_nonce' => wp_create_nonce('mxchat_status_nonce'),
8476 'queue_nonce' => wp_create_nonce('mxchat_queue_nonce'),
8477 'stop_nonce' => wp_create_nonce('mxchat_stop_processing_action'),
8478 'settings_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
8479 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
8480 'entries_nonce' => wp_create_nonce('mxchat_entries_nonce'),
8481 'admin_url' => admin_url(),
8482 'ajaxurl' => admin_url('admin-ajax.php'),
8483 'status_refresh_interval' => 2000,
8484 'bot_id' => isset($_GET['bot_id']) ? sanitize_text_field($_GET['bot_id']) : 'default'
8485 ));
8486 break;
8487
8488 case 'mxchat-activation':
8489 wp_localize_script('mxchat-activation-js', 'mxchatAdmin', array(
8490 'ajax_url' => admin_url('admin-ajax.php'),
8491 'license_nonce' => wp_create_nonce('mxchat_activate_license_nonce')
8492 ));
8493 break;
8494
8495 case 'mxchat-content':
8496 wp_localize_script('mxchat-content-js', 'mxchatContent', array(
8497 'ajaxUrl' => admin_url('admin-ajax.php'),
8498 'nonce' => wp_create_nonce('mxchat_content_nonce'),
8499 'settingNonce' => wp_create_nonce('mxchat_save_setting_nonce'),
8500 'previewUrl' => home_url('/?p='),
8501 'isActivated' => $this->is_activated(),
8502 'hasAdvancedContent' => apply_filters('mxchat_content_pro_feature', false, 'seo_readability'),
8503 'hasGSC' => apply_filters('mxchat_content_pro_feature', false, 'gsc_integration'),
8504 'seoOptimize' => array(
8505 'meta_description' => ($options['seo_optimize_meta_desc'] ?? 'on') === 'on',
8506 'seo_title' => ($options['seo_optimize_seo_title'] ?? 'on') === 'on',
8507 'slug' => ($options['seo_optimize_slug'] ?? 'on') === 'on',
8508 'readability' => ($options['seo_optimize_readability'] ?? 'on') === 'on',
8509 'internal_links' => ($options['seo_optimize_internal_links'] ?? 'on') === 'on',
8510 'img_alt' => ($options['seo_optimize_img_alt'] ?? 'on') === 'on',
8511 'featured_img' => ($options['seo_optimize_featured_img'] ?? 'on') === 'on',
8512 ),
8513 'i18n' => array(
8514 'generating' => __('Generating...', 'mxchat'),
8515 'planning' => __('Planning content structure...', 'mxchat'),
8516 'images' => __('Generating images...', 'mxchat'),
8517 'writing' => __('Writing full content...', 'mxchat'),
8518 'creating' => __('Creating WordPress post...', 'mxchat'),
8519 'done' => __('Content generated successfully!', 'mxchat'),
8520 'error' => __('An error occurred. Please try again.', 'mxchat'),
8521 'editSuccess' => __('Content updated successfully.', 'mxchat'),
8522 'promptEmpty' => __('Please enter a prompt.', 'mxchat'),
8523 )
8524 ));
8525 break;
8526
8527 case 'mxchat-transcripts':
8528 // Get chart data for localization
8529 $chart_data = $this->get_transcripts_chart_data();
8530
8531 wp_localize_script('mxchat-transcripts-js', 'mxchatAdmin', array(
8532 'ajax_url' => admin_url('admin-ajax.php'),
8533 'export_nonce' => wp_create_nonce('mxchat_export_transcripts'),
8534 'delete_nonce' => wp_create_nonce('mxchat_delete_chat_history'),
8535 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
8536 'translate_nonce' => wp_create_nonce('mxchat_translate_messages')
8537 ));
8538
8539 // Localize chart data separately - use array_values to ensure proper JSON array encoding
8540 wp_localize_script('mxchat-transcripts-js', 'mxchatChartData', array(
8541 'labels' => array_values($chart_data['labels']),
8542 'chats' => array_values($chart_data['chats']),
8543 'messages' => array_values($chart_data['messages'])
8544 ));
8545 break;
8546
8547 case 'mxchat-settings':
8548 default:
8549 // Color picker and settings localization
8550 wp_localize_script('mxchat-color-picker', 'mxchatStyleSettings', array(
8551 'ajax_url' => admin_url('admin-ajax.php'),
8552 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
8553 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
8554 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
8555 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
8556 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
8557 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
8558 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
8559 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
8560 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
8561 'icon_color' => $this->options['icon_color'] ?? '#fff',
8562 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
8563 'pre_chat_message' => $this->options['pre_chat_message'] ?? esc_html__('Hey there! Ask me anything!', 'mxchat'),
8564 'rate_limit_message' => $this->options['rate_limit_message'] ?? esc_html__('Rate limit exceeded. Please try again later.', 'mxchat'),
8565 'loops_api_key' => $this->options['loops_api_key'] ?? '',
8566 'loops_mailing_list' => $this->options['loops_mailing_list'] ?? '',
8567 'triggered_phrase_response' => $this->options['triggered_phrase_response'] ?? esc_html__('Would you like to join our mailing list? Please provide your email below.', 'mxchat'),
8568 'email_capture_response' => $this->options['email_capture_response'] ?? esc_html__('Thank you for providing your email! You\'ve been added to our list.', 'mxchat'),
8569 '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'),
8570 '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'),
8571 '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'),
8572 'pdf_max_pages' => $this->options['pdf_max_pages'] ?? 69,
8573 'live_agent_webhook_url' => $this->options['live_agent_webhook_url'] ?? '',
8574 'live_agent_secret_key' => $this->options['live_agent_secret_key'] ?? '',
8575 'live_agent_bot_token' => $this->options['live_agent_bot_token'] ?? '',
8576 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
8577 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
8578 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
8579 'show_pdf_upload_button' => $this->options['show_pdf_upload_button'] ?? 'on',
8580 'show_word_upload_button' => $this->options['show_word_upload_button'] ?? 'on',
8581 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
8582 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
8583 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
8584 ));
8585 break;
8586 }
8587
8588 // Additional localization that was in the original code
8589 wp_localize_script('mxchat-admin-js', 'mxchatPromptsAdmin', array(
8590 'ajax_url' => admin_url('admin-ajax.php'),
8591 'prompts_setting_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
8592 ));
8593 }
8594
8595 public function mxchat_sanitize($input) {
8596 $new_input = array();
8597
8598 if (isset($input['api_key'])) {
8599 $new_input['api_key'] = sanitize_text_field($input['api_key']);
8600 }
8601
8602 if (isset($input['similarity_threshold'])) {
8603 $new_input['similarity_threshold'] = absint($input['similarity_threshold']); // Ensure it's an integer
8604 $new_input['similarity_threshold'] = min(max($new_input['similarity_threshold'], 20), 85); // Enforce range
8605 }
8606
8607 if (isset($input['rag_sources_limit'])) {
8608 $new_input['rag_sources_limit'] = absint($input['rag_sources_limit']); // Ensure it's an integer
8609 $new_input['rag_sources_limit'] = min(max($new_input['rag_sources_limit'], 3), 10); // Enforce range 3-10
8610 }
8611
8612 if (isset($input['rag_chunks_limit'])) {
8613 $new_input['rag_chunks_limit'] = absint($input['rag_chunks_limit']); // Ensure it's an integer
8614 $new_input['rag_chunks_limit'] = min(max($new_input['rag_chunks_limit'], 8), 20); // Enforce range 8-20
8615 }
8616
8617 if (isset($input['xai_api_key'])) {
8618 $new_input['xai_api_key'] = sanitize_text_field($input['xai_api_key']);
8619 }
8620
8621 if (isset($input['claude_api_key'])) {
8622 $new_input['claude_api_key'] = sanitize_text_field($input['claude_api_key']);
8623 }
8624
8625 if (isset($input['enable_streaming_toggle'])) {
8626 $new_input['enable_streaming_toggle'] = ($input['enable_streaming_toggle'] === 'on') ? 'on' : 'off';
8627 } else {
8628 // If checkbox not checked, it won't be in $input, so set to 'off'
8629 $new_input['enable_streaming_toggle'] = 'off';
8630 }
8631
8632 if (isset($input['enable_web_search'])) {
8633 $new_input['enable_web_search'] = ($input['enable_web_search'] === 'on') ? 'on' : 'off';
8634 } else {
8635 // If checkbox not checked, it won't be in $input, so set to 'off'
8636 $new_input['enable_web_search'] = 'off';
8637 }
8638
8639 if (isset($input['deepseek_api_key'])) {
8640 $new_input['deepseek_api_key'] = sanitize_text_field($input['deepseek_api_key']);
8641 }
8642
8643 if (isset($input['gemini_api_key'])) {
8644 $new_input['gemini_api_key'] = sanitize_text_field($input['gemini_api_key']);
8645 }
8646
8647 if (isset($input['enable_woocommerce_integration'])) {
8648 $new_input['enable_woocommerce_integration'] = $input['enable_woocommerce_integration'] === 'on' ? 'on' : 'off';
8649 }
8650
8651 if (isset($input['privacy_toggle'])) {
8652 $new_input['privacy_toggle'] = $input['privacy_toggle'];
8653 }
8654
8655 if (isset($input['complianz_toggle'])) {
8656 $new_input['complianz_toggle'] = $input['complianz_toggle'];
8657 }
8658
8659 // Handle custom privacy text input
8660 if (isset($input['privacy_text'])) {
8661 // Allow basic HTML for links
8662 $new_input['privacy_text'] = wp_kses_post($input['privacy_text']);
8663 }
8664
8665 if (isset($input['system_prompt_instructions'])) {
8666 $new_input['system_prompt_instructions'] = sanitize_textarea_field($input['system_prompt_instructions']);
8667 }
8668
8669 if (isset($input['mxchat_pro_email'])) {
8670 $new_input['mxchat_pro_email'] = sanitize_email($input['mxchat_pro_email']);
8671 }
8672
8673 if (isset($input['mxchat_activation_key'])) {
8674 $new_input['mxchat_activation_key'] = sanitize_text_field($input['mxchat_activation_key']);
8675 }
8676
8677 if (isset($input['append_to_body'])) {
8678 $new_input['append_to_body'] = $input['append_to_body'] === 'on' ? 'on' : 'off';
8679 }
8680
8681 // Post type visibility settings
8682 if (isset($input['post_type_visibility_mode'])) {
8683 $allowed_modes = array('all', 'include', 'exclude');
8684 $new_input['post_type_visibility_mode'] = in_array($input['post_type_visibility_mode'], $allowed_modes)
8685 ? $input['post_type_visibility_mode']
8686 : 'all';
8687 }
8688
8689 if (isset($input['post_type_visibility_list'])) {
8690 if (is_array($input['post_type_visibility_list'])) {
8691 $new_input['post_type_visibility_list'] = array_map('sanitize_key', $input['post_type_visibility_list']);
8692 } else {
8693 $new_input['post_type_visibility_list'] = array();
8694 }
8695 }
8696
8697 if (isset($input['contextual_awareness_toggle'])) {
8698 $new_input['contextual_awareness_toggle'] = $input['contextual_awareness_toggle'] === 'on' ? 'on' : 'off';
8699 }
8700
8701 if (isset($input['citation_links_toggle'])) {
8702 $new_input['citation_links_toggle'] = $input['citation_links_toggle'] === 'on' ? 'on' : 'off';
8703 }
8704
8705 if (isset($input['top_bar_title'])) {
8706 $new_input['top_bar_title'] = sanitize_text_field($input['top_bar_title']);
8707 }
8708
8709 if (isset($input['ai_agent_text'])) {
8710 $new_input['ai_agent_text'] = sanitize_text_field($input['ai_agent_text']);
8711 }
8712
8713 if (isset($input['enable_email_block'])) {
8714 $new_input['enable_email_block'] = sanitize_text_field($input['enable_email_block']);
8715 }
8716
8717 if (isset($input['email_blocker_header_content'])) {
8718 // wp_kses_post() allows standard HTML tags permitted by WordPress
8719 $new_input['email_blocker_header_content'] = wp_kses_post($input['email_blocker_header_content']);
8720 }
8721 if (isset($input['email_blocker_button_text'])) {
8722 $new_input['email_blocker_button_text'] = sanitize_text_field($input['email_blocker_button_text']);
8723 }
8724 // Sanitize name field toggle
8725 if (isset($input['enable_name_field'])) {
8726 $new_input['enable_name_field'] = ($input['enable_name_field'] === 'on') ? 'on' : 'off';
8727 } else {
8728 $new_input['enable_name_field'] = 'off';
8729 }
8730 // Sanitize name field placeholder
8731 if (isset($input['name_field_placeholder'])) {
8732 $new_input['name_field_placeholder'] = sanitize_text_field($input['name_field_placeholder']);
8733 }
8734 if (isset($input['intro_message'])) {
8735 $new_input['intro_message'] = wp_kses_post($input['intro_message']); // Use wp_kses_post instead
8736 }
8737
8738 if (isset($input['input_copy'])) {
8739 $new_input['input_copy'] = sanitize_text_field($input['input_copy']);
8740 }
8741
8742 if (isset($input['rate_limit_message'])) {
8743 $new_input['rate_limit_message'] = sanitize_text_field($input['rate_limit_message']);
8744 }
8745
8746 // Handle the new rate limits format
8747 if (isset($input['rate_limits']) && is_array($input['rate_limits'])) {
8748 $new_input['rate_limits'] = array();
8749 $allowed_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited');
8750 $allowed_timeframes = array('hourly', 'daily', 'weekly', 'monthly');
8751
8752 foreach ($input['rate_limits'] as $role_id => $settings) {
8753 $new_input['rate_limits'][$role_id] = array();
8754
8755 // Sanitize limit
8756 if (isset($settings['limit'])) {
8757 $limit = sanitize_text_field($settings['limit']);
8758 if (in_array($limit, $allowed_limits, true)) {
8759 $new_input['rate_limits'][$role_id]['limit'] = $limit;
8760 } else {
8761 $new_input['rate_limits'][$role_id]['limit'] = ($role_id === 'logged_out') ? '10' : '100'; // Default
8762 }
8763 }
8764
8765 // Sanitize timeframe
8766 if (isset($settings['timeframe'])) {
8767 $timeframe = sanitize_text_field($settings['timeframe']);
8768 if (in_array($timeframe, $allowed_timeframes, true)) {
8769 $new_input['rate_limits'][$role_id]['timeframe'] = $timeframe;
8770 } else {
8771 $new_input['rate_limits'][$role_id]['timeframe'] = 'daily'; // Default
8772 }
8773 }
8774
8775 // Sanitize message
8776 if (isset($settings['message'])) {
8777 $new_input['rate_limits'][$role_id]['message'] = sanitize_textarea_field($settings['message']);
8778 }
8779 }
8780 }
8781
8782 if (isset($input['pre_chat_message'])) {
8783 $new_input['pre_chat_message'] = sanitize_textarea_field($input['pre_chat_message']);
8784 }
8785
8786 if (isset($input['voyage_api_key'])) {
8787 $new_input['voyage_api_key'] = sanitize_text_field($input['voyage_api_key']);
8788 }
8789
8790 // Add to your sanitize function
8791 if (isset($input['embedding_model'])) {
8792 $allowed_models = array(
8793 'text-embedding-ada-002',
8794 'text-embedding-3-small',
8795 'text-embedding-3-large',
8796 'voyage-3-large',
8797 'gemini-embedding-001'
8798 );
8799 if (in_array($input['embedding_model'], $allowed_models)) {
8800 $new_input['embedding_model'] = sanitize_text_field($input['embedding_model']);
8801 }
8802 }
8803
8804 if (isset($input['model'])) {
8805 if ($input['model'] === 'openrouter') {
8806 $new_input['model'] = 'openrouter';
8807 } else {
8808 $allowed_models = array(
8809 'gemini-3-pro-preview',
8810 'gemini-3-flash-preview',
8811 'gemini-2.5-pro',
8812 'gemini-2.5-flash',
8813 'gemini-2.5-flash-lite',
8814 'gemini-2.0-flash',
8815 'gemini-2.0-flash-lite',
8816 'gemini-1.5-pro',
8817 'gemini-1.5-flash',
8818 'grok-4-0709',
8819 'grok-4-1-fast-reasoning',
8820 'grok-4-1-fast-non-reasoning',
8821 'grok-3-beta',
8822 'grok-3-fast-beta',
8823 'grok-3-mini-beta',
8824 'grok-3-mini-fast-beta',
8825 'grok-2',
8826 'deepseek-chat',
8827 'claude-opus-4-6',
8828 'claude-sonnet-4-6',
8829 'claude-opus-4-5',
8830 'claude-sonnet-4-5-20250929',
8831 'claude-opus-4-1-20250805',
8832 'claude-haiku-4-5-20251001',
8833 'claude-opus-4-20250514',
8834 'claude-sonnet-4-20250514',
8835 'gpt-5.2',
8836 'gpt-5.1-chat-latest',
8837 'gpt-5.1-2025-11-13',
8838 'gpt-5',
8839 'gpt-5-mini',
8840 'gpt-5-nano',
8841 'gpt-5.4',
8842 'gpt-5.4-mini',
8843 'gpt-5.4-nano',
8844 'gpt-5.3-chat-latest',
8845 );
8846
8847 if (in_array($input['model'], $allowed_models)) {
8848 $new_input['model'] = sanitize_text_field($input['model']);
8849 } else {
8850 // Fallback for any deprecated model
8851 $new_input['model'] = 'gpt-5.1-chat-latest';
8852 }
8853 }
8854 }
8855
8856 if (isset($input['openrouter_selected_model'])) {
8857 // Just sanitize it, don't validate against a whitelist
8858 $new_input['openrouter_selected_model'] = sanitize_text_field($input['openrouter_selected_model']);
8859 }
8860
8861 // ADD THIS:
8862 if (isset($input['openrouter_selected_model_name'])) {
8863 $new_input['openrouter_selected_model_name'] = sanitize_text_field($input['openrouter_selected_model_name']);
8864 }
8865
8866 if (isset($input['openrouter_api_key'])) {
8867 $new_input['openrouter_api_key'] = sanitize_text_field($input['openrouter_api_key']);
8868 }
8869
8870
8871
8872 if (isset($input['woocommerce_consumer_key'])) {
8873 $new_input['woocommerce_consumer_key'] = sanitize_text_field($input['woocommerce_consumer_key']);
8874 }
8875
8876 if (isset($input['woocommerce_consumer_secret'])) {
8877 $new_input['woocommerce_consumer_secret'] = sanitize_text_field($input['woocommerce_consumer_secret']);
8878 }
8879
8880
8881 // Sanitize link_target_toggle
8882 if (isset($input['link_target_toggle'])) {
8883 $new_input['link_target_toggle'] = $input['link_target_toggle'] === 'on' ? 'on' : 'off';
8884 }
8885
8886 // Sanitize Loops API Key
8887 if (isset($input['loops_api_key'])) {
8888 $new_input['loops_api_key'] = sanitize_text_field($input['loops_api_key']);
8889 }
8890
8891 if (isset($input['chat_persistence_toggle'])) {
8892 $new_input['chat_persistence_toggle'] = $input['chat_persistence_toggle'] === 'on' ? 'on' : 'off';
8893 }
8894
8895 if (isset($input['popular_question_1'])) {
8896 $new_input['popular_question_1'] = sanitize_text_field($input['popular_question_1']);
8897 }
8898
8899 if (isset($input['popular_question_2'])) {
8900 $new_input['popular_question_2'] = sanitize_text_field($input['popular_question_2']);
8901 }
8902
8903 if (isset($input['popular_question_3'])) {
8904 $new_input['popular_question_3'] = sanitize_text_field($input['popular_question_3']);
8905 }
8906
8907 if (isset($input['additional_popular_questions']) && is_array($input['additional_popular_questions'])) {
8908 $new_input['additional_popular_questions'] = array_map('sanitize_text_field', $input['additional_popular_questions']);
8909 }
8910
8911 // Sanitize Loops Mailing List
8912 if (isset($input['loops_mailing_list'])) {
8913 $new_input['loops_mailing_list'] = sanitize_text_field($input['loops_mailing_list']);
8914 }
8915
8916 // Sanitize Triggered Phrase Response
8917 if (isset($input['triggered_phrase_response'])) {
8918 $new_input['triggered_phrase_response'] = wp_kses_post($input['triggered_phrase_response']);
8919 }
8920 if (isset($input['email_capture_response'])) {
8921 $new_input['email_capture_response'] = wp_kses_post($input['email_capture_response']);
8922 }
8923
8924 // Sanitize Brave Search Settings
8925 if (isset($input['brave_api_key'])) {
8926 $new_input['brave_api_key'] = sanitize_text_field($input['brave_api_key']);
8927 }
8928
8929 if (isset($input['brave_image_count'])) {
8930 $image_count = intval($input['brave_image_count']);
8931 $new_input['brave_image_count'] = ($image_count >=1 && $image_count <=6) ? $image_count : 4;
8932 }
8933
8934 if (isset($input['brave_safe_search'])) {
8935 $allowed = array('strict', 'off');
8936 $new_input['brave_safe_search'] = in_array($input['brave_safe_search'], $allowed, true) ? $input['brave_safe_search'] : 'strict';
8937 }
8938
8939 if (isset($input['brave_news_count'])) {
8940 $news_count = intval($input['brave_news_count']);
8941 $new_input['brave_news_count'] = ($news_count >=1 && $news_count <=10) ? $news_count : 3;
8942 }
8943
8944 if (isset($input['brave_country'])) {
8945 $new_input['brave_country'] = sanitize_text_field($input['brave_country']);
8946 }
8947
8948 if (isset($input['brave_language'])) {
8949 $new_input['brave_language'] = sanitize_text_field($input['brave_language']);
8950 }
8951
8952 if (isset($input['chat_toolbar_toggle'])) {
8953 $new_input['chat_toolbar_toggle'] = $input['chat_toolbar_toggle'] === 'on' ? 'on' : 'off';
8954 }
8955
8956 // Sanitize PDF upload button toggle
8957 if (isset($input['show_pdf_upload_button'])) {
8958 $new_input['show_pdf_upload_button'] = $input['show_pdf_upload_button'] === 'on' ? 'on' : 'off';
8959 } else {
8960 $new_input['show_pdf_upload_button'] = 'off'; // If checkbox is unchecked
8961 }
8962
8963 // Sanitize Word upload button toggle
8964 if (isset($input['show_word_upload_button'])) {
8965 $new_input['show_word_upload_button'] = $input['show_word_upload_button'] === 'on' ? 'on' : 'off';
8966 } else {
8967 $new_input['show_word_upload_button'] = 'off'; // If checkbox is unchecked
8968 }
8969
8970 if (isset($input['pdf_intent_trigger_text'])) {
8971 $new_input['pdf_intent_trigger_text'] = sanitize_text_field($input['pdf_intent_trigger_text']);
8972 }
8973
8974 if (isset($input['pdf_intent_success_text'])) {
8975 $new_input['pdf_intent_success_text'] = sanitize_text_field($input['pdf_intent_success_text']);
8976 }
8977
8978 if (isset($input['pdf_intent_error_text'])) {
8979 $new_input['pdf_intent_error_text'] = sanitize_text_field($input['pdf_intent_error_text']);
8980 }
8981
8982 if (isset($input['pdf_max_pages'])) {
8983 $new_input['pdf_max_pages'] = intval($input['pdf_max_pages']);
8984 if ($new_input['pdf_max_pages'] < 1 || $new_input['pdf_max_pages'] > 69) {
8985 $new_input['pdf_max_pages'] = 69; // Default to 69 if out of range
8986 }
8987 }
8988
8989 if (isset($input['live_agent_webhook_url'])) {
8990 $new_input['live_agent_webhook_url'] = esc_url_raw($input['live_agent_webhook_url']);
8991 }
8992 if (isset($input['live_agent_secret_key'])) {
8993 $new_input['live_agent_secret_key'] = sanitize_text_field($input['live_agent_secret_key']);
8994 }
8995
8996 // Live Agent Integration
8997 if (isset($input['live_agent_bot_token'])) {
8998 $new_input['live_agent_bot_token'] = sanitize_text_field($input['live_agent_bot_token']);
8999 }
9000
9001 if (isset($input['live_agent_user_ids'])) {
9002 $new_input['live_agent_user_ids'] = sanitize_textarea_field($input['live_agent_user_ids']);
9003 }
9004
9005 if (isset($input['live_agent_status'])) {
9006 $new_input['live_agent_status'] = ($input['live_agent_status'] === 'on') ? 'on' : 'off';
9007 }
9008 if (isset($input['live_agent_away_message'])) {
9009 $new_input['live_agent_away_message'] = sanitize_textarea_field($input['live_agent_away_message']);
9010 }
9011 if (isset($input['live_agent_notification_message'])) {
9012 $new_input['live_agent_notification_message'] = sanitize_textarea_field($input['live_agent_notification_message']);
9013 }
9014
9015 // Telegram Integration
9016 if (isset($input['telegram_status'])) {
9017 $new_input['telegram_status'] = ($input['telegram_status'] === 'on') ? 'on' : 'off';
9018 }
9019 if (isset($input['telegram_bot_token'])) {
9020 $new_input['telegram_bot_token'] = sanitize_text_field($input['telegram_bot_token']);
9021 }
9022 if (isset($input['telegram_group_id'])) {
9023 $new_input['telegram_group_id'] = sanitize_text_field($input['telegram_group_id']);
9024 }
9025 if (isset($input['telegram_webhook_secret'])) {
9026 $new_input['telegram_webhook_secret'] = sanitize_text_field($input['telegram_webhook_secret']);
9027 }
9028 if (isset($input['telegram_notification_message'])) {
9029 $new_input['telegram_notification_message'] = sanitize_textarea_field($input['telegram_notification_message']);
9030 }
9031 if (isset($input['telegram_away_message'])) {
9032 $new_input['telegram_away_message'] = sanitize_textarea_field($input['telegram_away_message']);
9033 }
9034
9035 // Sanitize script loading strategy
9036 if (isset($input['script_loading_strategy'])) {
9037 $allowed_strategies = array('default', 'defer', 'delay_1s', 'delay_3s', 'delay_5s', 'on_interaction');
9038 $new_input['script_loading_strategy'] = in_array($input['script_loading_strategy'], $allowed_strategies)
9039 ? $input['script_loading_strategy']
9040 : 'default';
9041 }
9042
9043 // Sanitize debug mode
9044 if (isset($input['debug_mode'])) {
9045 $new_input['debug_mode'] = ($input['debug_mode'] === 'on') ? 'on' : 'off';
9046 }
9047
9048 // Content Generator Settings
9049 if (isset($input['content_model'])) {
9050 $new_input['content_model'] = sanitize_text_field($input['content_model']);
9051 }
9052 if (isset($input['content_image_model'])) {
9053 $new_input['content_image_model'] = sanitize_text_field($input['content_image_model']);
9054 }
9055 if (isset($input['content_enable_images'])) {
9056 $new_input['content_enable_images'] = ($input['content_enable_images'] === 'on') ? 'on' : 'off';
9057 }
9058 if (isset($input['content_use_placeholders'])) {
9059 $new_input['content_use_placeholders'] = ($input['content_use_placeholders'] === 'on') ? 'on' : 'off';
9060 }
9061 if (isset($input['content_internal_linking'])) {
9062 $new_input['content_internal_linking'] = ($input['content_internal_linking'] === 'on') ? 'on' : 'off';
9063 }
9064 if (isset($input['content_tool_use'])) {
9065 $new_input['content_tool_use'] = ($input['content_tool_use'] === 'on') ? 'on' : 'off';
9066 }
9067
9068 // SEO Optimize toggle fields
9069 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) {
9070 if (isset($input[$seo_key])) {
9071 $new_input[$seo_key] = ($input[$seo_key] === 'on') ? 'on' : 'off';
9072 }
9073 }
9074
9075 // Preserve content generator settings when saving from main settings page
9076 // (where content fields are not in the form submission)
9077 $existing = get_option('mxchat_options', array());
9078 foreach (array('content_model', 'content_image_model', 'content_enable_images', 'content_use_placeholders', 'content_internal_linking', 'content_tool_use', 'seo_optimize_meta_desc', 'seo_optimize_seo_title', 'seo_optimize_slug', 'seo_optimize_readability', 'seo_optimize_internal_links', 'seo_optimize_img_alt', 'seo_optimize_featured_img') as $key) {
9079 if (!isset($new_input[$key]) && isset($existing[$key])) {
9080 $new_input[$key] = $existing[$key];
9081 }
9082 }
9083
9084 return $new_input;
9085 }
9086
9087 /**
9088 * Log a debug message if debug mode is enabled
9089 *
9090 * @param string $type The type of log entry (settings_save, api_error, activation, etc.)
9091 * @param string $message The log message
9092 * @param array $data Optional additional data to log
9093 * @return bool Whether the message was logged
9094 */
9095 public static function mxchat_log_debug( $type, $message, $data = array() ) {
9096 $options = get_option( 'mxchat_options', array() );
9097
9098 // Check if debug mode is enabled
9099 if ( ! isset( $options['debug_mode'] ) || $options['debug_mode'] !== 'on' ) {
9100 return false;
9101 }
9102
9103 // Get current log
9104 $log = get_option( 'mxchat_debug_log', array() );
9105 if ( ! is_array( $log ) ) {
9106 $log = array();
9107 }
9108
9109 // Add new entry
9110 $entry = array(
9111 'time' => current_time( 'Y-m-d H:i:s' ),
9112 'type' => sanitize_key( $type ),
9113 'message' => sanitize_text_field( $message ),
9114 );
9115
9116 if ( ! empty( $data ) ) {
9117 $entry['data'] = $data;
9118 }
9119
9120 // Add to beginning of array (newest first)
9121 array_unshift( $log, $entry );
9122
9123 // Keep only last 100 entries
9124 if ( count( $log ) > 100 ) {
9125 $log = array_slice( $log, 0, 100 );
9126 }
9127
9128 // Save log
9129 update_option( 'mxchat_debug_log', $log, false );
9130
9131 return true;
9132 }
9133
9134 /**
9135 * Get the debug log entries
9136 *
9137 * @return array The debug log entries
9138 */
9139 public static function mxchat_get_debug_log() {
9140 $log = get_option( 'mxchat_debug_log', array() );
9141 return is_array( $log ) ? $log : array();
9142 }
9143
9144 /**
9145 * Clear the debug log
9146 *
9147 * @return bool Whether the log was cleared
9148 */
9149 public static function mxchat_clear_debug_log() {
9150 return delete_option( 'mxchat_debug_log' );
9151 }
9152
9153 /**
9154 * Export settings as JSON with masked API keys
9155 *
9156 * @return array The sanitized settings array
9157 */
9158 public static function mxchat_export_settings() {
9159 $options = get_option( 'mxchat_options', array() );
9160
9161 if ( ! is_array( $options ) ) {
9162 return array();
9163 }
9164
9165 // List of API key fields to mask
9166 $api_key_fields = array(
9167 'api_key',
9168 'xai_api_key',
9169 'claude_api_key',
9170 'deepseek_api_key',
9171 'voyage_api_key',
9172 'gemini_api_key',
9173 'openrouter_api_key',
9174 'loops_api_key',
9175 'brave_api_key',
9176 'live_agent_secret_key',
9177 'live_agent_bot_token',
9178 'telegram_bot_token',
9179 'telegram_webhook_secret',
9180 'woocommerce_consumer_key',
9181 'woocommerce_consumer_secret',
9182 );
9183
9184 // Mask API keys (show only last 4 characters)
9185 foreach ( $api_key_fields as $field ) {
9186 if ( isset( $options[ $field ] ) && ! empty( $options[ $field ] ) ) {
9187 $value = $options[ $field ];
9188 if ( strlen( $value ) > 4 ) {
9189 $options[ $field ] = str_repeat( '*', strlen( $value ) - 4 ) . substr( $value, -4 );
9190 } else {
9191 $options[ $field ] = '****';
9192 }
9193 }
9194 }
9195
9196 // Add metadata
9197 $export = array(
9198 'plugin_version' => defined( 'MXCHAT_VERSION' ) ? MXCHAT_VERSION : 'unknown',
9199 'export_date' => current_time( 'Y-m-d H:i:s' ),
9200 'wordpress_version' => get_bloginfo( 'version' ),
9201 'php_version' => phpversion(),
9202 'settings' => $options,
9203 );
9204
9205 return $export;
9206 }
9207
9208 /**
9209 * Reset all settings to defaults
9210 *
9211 * @return bool Whether the reset was successful
9212 */
9213 public static function mxchat_reset_all_settings() {
9214 // Delete main options
9215 $deleted = delete_option( 'mxchat_options' );
9216
9217 // Also clear the debug log
9218 delete_option( 'mxchat_debug_log' );
9219
9220 // Log the reset (will create new log since we just cleared it)
9221 // We need to temporarily enable debug mode to log this
9222 $temp_options = array( 'debug_mode' => 'on' );
9223 update_option( 'mxchat_options', $temp_options );
9224
9225 self::mxchat_log_debug( 'reset', 'All settings have been reset to defaults' );
9226
9227 // Now delete again to trigger re-initialization
9228 delete_option( 'mxchat_options' );
9229
9230 return $deleted;
9231 }
9232
9233 // Method to append the chatbot to the body
9234 public function mxchat_append_chatbot_to_body() {
9235 $options = get_option('mxchat_options');
9236 if (isset($options['append_to_body']) && $options['append_to_body'] === 'on') {
9237 echo do_shortcode('[mxchat_chatbot floating="yes"]');
9238 }
9239 }
9240
9241
9242
9243
9244
9245 private function mxchat_fetch_loops_mailing_lists($api_key) {
9246 $url = 'https://app.loops.so/api/v1/lists';
9247 $response = wp_remote_get($url, array(
9248 'headers' => array(
9249 'Authorization' => 'Bearer ' . $api_key,
9250 'Content-Type' => 'application/json'
9251 )
9252 ));
9253
9254 if (is_wp_error($response)) {
9255 return array();
9256 }
9257
9258 $body = wp_remote_retrieve_body($response);
9259 $lists = json_decode($body, true);
9260
9261 return isset($lists) && is_array($lists) ? $lists : array();
9262 }
9263
9264 function mxchat_calculate_cosine_similarity($vec1, $vec2) {
9265 if (empty($vec1) || empty($vec2)) {
9266 return 0.0;
9267 }
9268
9269 $dot_product = 0.0;
9270 $norm_a = 0.0;
9271 $norm_b = 0.0;
9272
9273 for ($i = 0; $i < count($vec1); $i++) {
9274 $dot_product += $vec1[$i] * $vec2[$i];
9275 $norm_a += pow($vec1[$i], 2);
9276 $norm_b += pow($vec2[$i], 2);
9277 }
9278
9279 if ($norm_a == 0.0 || $norm_b == 0.0) {
9280 return 0.0;
9281 } else {
9282 return $dot_product / (sqrt($norm_a) * sqrt($norm_b));
9283 }
9284 }
9285
9286 /**
9287 * Validates nonce and deletes all chat prompts
9288 */
9289 public function mxchat_handle_delete_all_prompts() {
9290 //error_log('=== DELETE ALL DEBUG START ===');
9291
9292 // Verify nonce
9293 if (!isset($_POST['mxchat_delete_all_prompts_nonce']) || !wp_verify_nonce($_POST['mxchat_delete_all_prompts_nonce'], 'mxchat_delete_all_prompts_action')) {
9294 //error_log('DEBUG: Nonce verification failed');
9295 wp_die(__('Nonce verification failed.', 'mxchat'));
9296 }
9297
9298 // Check permissions
9299 if (!current_user_can('manage_options')) {
9300 //error_log('DEBUG: Permission check failed');
9301 wp_die(__('You do not have sufficient permissions to delete all prompts.', 'mxchat'));
9302 }
9303
9304 // Get bot_id and content type filter from POST data
9305 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
9306 $content_type_filter = isset($_POST['content_type_filter']) ? sanitize_text_field($_POST['content_type_filter']) : '';
9307
9308 $success = true;
9309 $error_messages = array();
9310
9311 // Get bot-specific Pinecone configuration
9312 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
9313 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
9314
9315 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
9316
9317 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
9318 // Delete from Pinecone (with optional content type filter)
9319 $result = $pinecone_manager->mxchat_delete_all_from_pinecone($pinecone_options, $content_type_filter);
9320
9321 if (!$result['success']) {
9322 $success = false;
9323 $error_messages[] = $result['message'];
9324 }
9325
9326 } else {
9327 // Delete from WordPress database
9328 global $wpdb;
9329 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
9330
9331 // Build WHERE conditions
9332 $where_clauses = array();
9333 $where_values = array();
9334
9335 // Bot filter
9336 if ($bot_id !== 'default' && class_exists('MxChat_Multi_Bot_Manager')) {
9337 $where_clauses[] = 'bot_id = %s';
9338 $where_values[] = $bot_id;
9339 }
9340
9341 // Content type filter
9342 if (!empty($content_type_filter)) {
9343 $where_clauses[] = 'content_type = %s';
9344 $where_values[] = $content_type_filter;
9345 }
9346
9347 if (!empty($where_clauses)) {
9348 $where_sql = implode(' AND ', $where_clauses);
9349 $result = $wpdb->query($wpdb->prepare("DELETE FROM {$table_name} WHERE {$where_sql}", $where_values));
9350 } else {
9351 // No filters — delete all
9352 $result = $wpdb->query("DELETE FROM {$table_name}");
9353 }
9354
9355 if ($result === false) {
9356 $success = false;
9357 $error_messages[] = 'Failed to delete from WordPress database';
9358 }
9359 }
9360
9361 // Redirect back with a success message and bot_id
9362 $redirect_url = add_query_arg(array(
9363 'page' => 'mxchat-prompts',
9364 'bot_id' => $bot_id,
9365 'all_deleted' => $success ? 'true' : 'false'
9366 ), admin_url('admin.php'));
9367
9368 wp_safe_redirect($redirect_url);
9369 exit;
9370 }
9371
9372
9373
9374 /**
9375 * Handles deletion prompt with nonce validation
9376 */
9377 public function mxchat_handle_delete_prompt() {
9378 // Sanitize and validate nonce
9379 $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : '';
9380 if (empty($nonce) || !wp_verify_nonce($nonce, 'mxchat_delete_prompt_nonce')) {
9381 wp_die(esc_html__('Nonce verification failed.', 'mxchat'));
9382 }
9383
9384 // Check permissions
9385 if (!current_user_can('manage_options')) {
9386 wp_die(esc_html__('You do not have sufficient permissions to delete prompts.', 'mxchat'));
9387 }
9388
9389 // Get ID and source parameters
9390 $id = isset($_GET['id']) ? sanitize_text_field($_GET['id']) : '';
9391 $source = isset($_GET['source']) ? sanitize_text_field($_GET['source']) : '';
9392
9393 if (empty($id)) {
9394 wp_die(esc_html__('Invalid prompt ID.', 'mxchat'));
9395 }
9396
9397 $success = false;
9398 $error_message = '';
9399
9400 // Check if Pinecone is enabled and determine source automatically if not specified
9401 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
9402 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
9403
9404 // If source is not specified, determine based on Pinecone configuration
9405 if (empty($source)) {
9406 $source = $use_pinecone ? 'pinecone' : 'wordpress';
9407 }
9408
9409 if ($source === 'pinecone' || $use_pinecone) {
9410 //error_log('[MXCHAT-DELETE] Deleting from Pinecone, ID: ' . $id);
9411
9412 // Handle Pinecone deletion
9413 if (empty($pinecone_options['mxchat_pinecone_host']) ||
9414 empty($pinecone_options['mxchat_pinecone_api_key'])) {
9415 wp_die(esc_html__('Pinecone configuration is missing.', 'mxchat'));
9416 }
9417
9418 // Delete from Pinecone using the vector ID directly
9419 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
9420 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
9421 $id,
9422 $pinecone_options['mxchat_pinecone_api_key'],
9423 $pinecone_options['mxchat_pinecone_host']
9424 );
9425 if ($result['success']) {
9426 $success = true;
9427
9428 // Remove from vector cache
9429 $pinecone_manager->mxchat_remove_from_pinecone_vector_cache($id);
9430 $pinecone_manager->mxchat_remove_from_processed_content_caches($id);
9431
9432 set_transient('mxchat_admin_notice_success',
9433 esc_html__('Vector deleted successfully from Pinecone.', 'mxchat'), 30);
9434 } else {
9435 $error_message = $result['message'];
9436 set_transient('mxchat_admin_notice_error',
9437 esc_html__('Failed to delete from Pinecone: ', 'mxchat') . esc_html($error_message), 30);
9438 }
9439 } else {
9440 //error_log('[MXCHAT-DELETE] Deleting from WordPress database, ID: ' . $id);
9441
9442 // Handle WordPress database deletion
9443 global $wpdb;
9444 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
9445
9446 // Clear cache and delete prompt
9447 wp_cache_delete('prompt_' . $id, 'mxchat_prompts');
9448
9449 $result = $wpdb->delete(
9450 $table_name,
9451 array('id' => intval($id)),
9452 array('%d')
9453 );
9454
9455 if ($result !== false) {
9456 $success = true;
9457 set_transient('mxchat_admin_notice_success',
9458 esc_html__('Entry deleted successfully.', 'mxchat'), 30);
9459 } else {
9460 set_transient('mxchat_admin_notice_error',
9461 esc_html__('Failed to delete entry from database.', 'mxchat'), 30);
9462 }
9463 }
9464
9465 // Redirect back to the prompts page
9466 wp_safe_redirect(add_query_arg(
9467 array(
9468 'page' => 'mxchat-prompts',
9469 'deleted' => $success ? 'true' : 'false'
9470 ),
9471 admin_url('admin.php')
9472 ));
9473 exit;
9474 }
9475
9476
9477
9478
9479 /**
9480 * Averages multiple vectors into a single vector
9481 */
9482 private function mxchat_average_vectors($vectors) {
9483 $vector_length = count($vectors[0]);
9484 $sum_vector = array_fill(0, $vector_length, 0);
9485
9486 foreach ($vectors as $vector) {
9487 for ($i = 0; $i < $vector_length; $i++) {
9488 $sum_vector[$i] += $vector[$i];
9489 }
9490 }
9491
9492 // Divide each component by the number of vectors to get the average
9493 $num_vectors = count($vectors);
9494 for ($i = 0; $i < $vector_length; $i++) {
9495 $sum_vector[$i] /= $num_vectors;
9496 }
9497
9498 return $sum_vector;
9499 }
9500
9501
9502
9503
9504 /**
9505 * Generates embeddings from input text for MXChat
9506 */
9507 public function mxchat_generate_embedding($text) {
9508 // Enable detailed logging for debugging
9509 //error_log('[MXCHAT-EMBED] Starting embedding generation. Text length: ' . strlen($text) . ' bytes');
9510 //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
9511
9512 $options = get_option('mxchat_options');
9513 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
9514 //error_log('[MXCHAT-EMBED] Selected embedding model: ' . $selected_model);
9515
9516 // Determine provider and endpoint
9517 if (strpos($selected_model, 'voyage') === 0) {
9518 $api_key = $options['voyage_api_key'] ?? '';
9519 $endpoint = 'https://api.voyageai.com/v1/embeddings';
9520 $provider_name = 'Voyage AI';
9521 //error_log('[MXCHAT-EMBED] Using Voyage AI API');
9522 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
9523 $api_key = $options['gemini_api_key'] ?? '';
9524 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
9525 $provider_name = 'Google Gemini';
9526 //error_log('[MXCHAT-EMBED] Using Google Gemini API');
9527 } else {
9528 $api_key = $options['api_key'] ?? '';
9529 $endpoint = 'https://api.openai.com/v1/embeddings';
9530 $provider_name = 'OpenAI';
9531 //error_log('[MXCHAT-EMBED] Using OpenAI API');
9532 }
9533
9534 //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
9535
9536 if (empty($api_key)) {
9537 $error_message = sprintf('Missing %s API key. Please configure your API key in the MxChat settings.', $provider_name);
9538 //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
9539 return $error_message;
9540 }
9541
9542 // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
9543 $estimated_tokens = ceil(str_word_count($text) / 0.75);
9544 //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
9545
9546 if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
9547 //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
9548 // Consider truncating text here
9549 }
9550
9551 // Prepare request body based on provider
9552 if (strpos($selected_model, 'gemini-embedding') === 0) {
9553 // Gemini API format
9554 $request_body = array(
9555 'model' => 'models/' . $selected_model,
9556 'content' => array(
9557 'parts' => array(
9558 array('text' => $text)
9559 )
9560 )
9561 );
9562
9563 // Set output dimensionality to 1536 for consistency with other models
9564 $request_body['outputDimensionality'] = 1536;
9565 } else {
9566 // OpenAI/Voyage API format
9567 $request_body = array(
9568 'model' => $selected_model,
9569 'input' => $text
9570 );
9571
9572 // Add output_dimension for voyage-3-large model
9573 if ($selected_model === 'voyage-3-large') {
9574 $request_body['output_dimension'] = 2048;
9575 }
9576 }
9577
9578 //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
9579
9580 // Prepare headers based on provider
9581 if (strpos($selected_model, 'gemini-embedding') === 0) {
9582 // Gemini uses API key as query parameter
9583 $endpoint .= '?key=' . $api_key;
9584 $headers = array(
9585 'Content-Type' => 'application/json'
9586 );
9587 } else {
9588 // OpenAI/Voyage use Bearer token
9589 $headers = array(
9590 'Authorization' => 'Bearer ' . $api_key,
9591 'Content-Type' => 'application/json'
9592 );
9593 }
9594
9595 // Make API request
9596 //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
9597 $response = wp_remote_post($endpoint, array(
9598 'body' => wp_json_encode($request_body),
9599 'headers' => $headers,
9600 'timeout' => 60 // Increased timeout for large inputs
9601 ));
9602
9603 // Handle wp_remote_post errors
9604 if (is_wp_error($response)) {
9605 $error_message = $response->get_error_message();
9606 //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
9607 return 'Connection error: ' . $error_message;
9608 }
9609
9610 // Get and check HTTP response code
9611 $http_code = wp_remote_retrieve_response_code($response);
9612 //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
9613
9614 if ($http_code !== 200) {
9615 $error_body = wp_remote_retrieve_body($response);
9616 //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
9617
9618 // Try to parse error for more details
9619 $error_json = json_decode($error_body, true);
9620 if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
9621 $error_type = $error_json['error']['type'] ?? 'unknown';
9622 $error_message = $error_json['error']['message'] ?? 'No message';
9623 //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
9624 //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
9625
9626 // Customize error message for common API errors
9627 if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
9628 $error_message = sprintf('Invalid %s API key. Please check your API key in the MxChat settings.', $provider_name);
9629 } elseif ($error_type === 'authentication_error') {
9630 $error_message = sprintf('%s authentication failed. Please verify your API key in the MxChat settings.', $provider_name);
9631 }
9632
9633 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
9634 return $error_message;
9635 }
9636
9637 $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding", $http_code);
9638 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
9639 return $error_message;
9640 }
9641
9642 // Parse response body
9643 $response_body = wp_remote_retrieve_body($response);
9644 //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
9645
9646 $response_data = json_decode($response_body, true);
9647
9648 if (json_last_error() !== JSON_ERROR_NONE) {
9649 $error = json_last_error_msg();
9650 //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
9651 //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
9652 return "Failed to parse API response: $error";
9653 }
9654
9655 // Handle different response formats based on provider
9656 if (strpos($selected_model, 'gemini-embedding') === 0) {
9657 // Gemini API response format
9658 if (isset($response_data['embedding']['values'])) {
9659 $embedding_dimensions = count($response_data['embedding']['values']);
9660 //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
9661
9662 // Check if embedding dimensions are as expected (should be 1536)
9663 if ($embedding_dimensions !== 1536) {
9664 //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
9665 }
9666
9667 MxChat_Utils::stamp_active_embedding_model($selected_model);
9668 return $response_data['embedding']['values'];
9669 } else {
9670 //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
9671 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
9672
9673 if (isset($response_data['error'])) {
9674 $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
9675 //error_log('[MXCHAT-EMBED] ' . $error_message);
9676 return $error_message;
9677 }
9678
9679 $error_message = "Invalid Gemini API response format: No embedding found";
9680 //error_log('[MXCHAT-EMBED] ' . $error_message);
9681 return $error_message;
9682 }
9683 } else {
9684 // OpenAI/Voyage API response format
9685 if (isset($response_data['data'][0]['embedding'])) {
9686 $embedding_dimensions = count($response_data['data'][0]['embedding']);
9687 //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
9688
9689 // Check if embedding dimensions are as expected
9690 if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
9691 ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
9692 //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
9693 }
9694
9695 MxChat_Utils::stamp_active_embedding_model($selected_model);
9696 return $response_data['data'][0]['embedding'];
9697 } else {
9698 //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
9699 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
9700
9701 if (isset($response_data['error'])) {
9702 $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
9703 //error_log('[MXCHAT-EMBED] ' . $error_message);
9704 return $error_message;
9705 }
9706
9707 $error_message = "Invalid API response format: No embedding found";
9708 //error_log('[MXCHAT-EMBED] ' . $error_message);
9709 return $error_message;
9710 }
9711 }
9712 }
9713
9714
9715
9716 }
9717 ?>
9718