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

9,622 lines 393.3 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 add_action('admin_init', array($this, 'mxchat_page_init'));
42 add_action('admin_init', array($this, 'mxchat_prompts_page_init'));
43 add_action('admin_enqueue_scripts', array($this, 'mxchat_enqueue_admin_assets'));
44 add_action('wp_ajax_mxchat_delete_chat_history', array($this, 'mxchat_delete_chat_history'));
45 add_action('admin_post_mxchat_delete_prompt', array($this, 'mxchat_handle_delete_prompt'));
46 add_action('wp_ajax_mxchat_fetch_chat_history', array($this, 'mxchat_fetch_chat_history'));
47 add_action('wp_ajax_nopriv_mxchat_fetch_chat_history', array($this, 'mxchat_fetch_chat_history'));
48 add_action('wp_ajax_mxchat_fetch_conversation', array($this, 'mxchat_fetch_conversation'));
49 add_action('wp_footer', array($this, 'mxchat_append_chatbot_to_body'));
50 add_action('admin_head-mxchat-prompts', array($this, 'mxchat_enqueue_admin_assets'));
51 add_action('admin_head-toplevel_page_mxchat-max', array($this, 'mxchat_enqueue_admin_assets'));
52 add_action('admin_notices', array($this, 'mxchat_display_admin_notice'));
53 add_action('admin_post_mxchat_delete_all_prompts', array($this, 'mxchat_handle_delete_all_prompts'));
54 add_action('admin_post_mxchat_add_intent', array($this, 'mxchat_handle_add_intent'));
55 add_action('admin_post_mxchat_delete_intent', array($this, 'mxchat_handle_delete_intent'));
56 add_action('admin_post_mxchat_edit_intent', array($this, 'mxchat_handle_edit_intent'));
57 add_action('wp_ajax_mxchat_export_transcripts', array($this, 'export_chat_transcripts'));
58
59 // Leads tab (inside Transcripts)
60 add_action('wp_ajax_mxchat_fetch_leads', array($this, 'mxchat_fetch_leads'));
61 add_action('wp_ajax_mxchat_delete_leads', array($this, 'mxchat_delete_leads'));
62 add_action('wp_ajax_mxchat_export_leads', array($this, 'mxchat_export_leads'));
63
64 add_action('admin_init', array($this, 'mxchat_transcripts_page_init'));
65 add_action('wp_ajax_dismiss_live_agent_notice', array($this, 'dismiss_live_agent_notice'));
66 add_action('wp_ajax_dismiss_theme_migration_notice', array($this, 'dismiss_theme_migration_notice'));
67 add_action('mxchat_cleanup_old_transcripts', array($this, 'cleanup_old_transcripts'));
68
69 add_action('admin_init', array($this, 'register_pinecone_settings'));
70 add_action('admin_init', array($this, 'register_openai_vectorstore_settings'));
71
72 add_action('admin_notices', array($this, 'display_admin_notices'));
73
74 add_action('wp_ajax_mxchat_test_streaming_actual', [$this, 'mxchat_handle_test_streaming_actual']);
75 add_action('wp_ajax_mxchat_test_streaming', [$this, 'mxchat_handle_test_streaming']); // Keep existing as fallback
76 add_action('wp_ajax_mxchat_test_vectorstore_connection', array($this, 'mxchat_test_vectorstore_connection'));
77
78 add_action('wp_ajax_mxchat_save_selected_bot', array($this, 'mxchat_save_selected_bot'));
79 add_action('wp_ajax_mxchat_fetch_openrouter_models', array($this, 'fetch_openrouter_models'));
80 add_action('wp_ajax_mxchat_get_rag_context', array($this, 'mxchat_get_rag_context'));
81
82 // Actions page AJAX handlers
83 add_action('wp_ajax_mxchat_fetch_actions_list', array($this, 'mxchat_fetch_actions_list'));
84 add_action('wp_ajax_mxchat_toggle_action_status', array($this, 'mxchat_toggle_action_status'));
85 add_action('wp_ajax_mxchat_bulk_delete_actions', array($this, 'mxchat_bulk_delete_actions'));
86 add_action('wp_ajax_mxchat_add_intent_ajax', array($this, 'mxchat_add_intent_ajax'));
87 add_action('wp_ajax_mxchat_edit_intent_ajax', array($this, 'mxchat_edit_intent_ajax'));
88 add_action('wp_ajax_mxchat_delete_intent_ajax', array($this, 'mxchat_delete_intent_ajax'));
89 add_action('wp_ajax_mxchat_add_phrase', array($this, 'mxchat_add_phrase_ajax'));
90 add_action('wp_ajax_mxchat_delete_phrase', array($this, 'mxchat_delete_phrase_ajax'));
91 add_action('wp_ajax_mxchat_get_phrases', array($this, 'mxchat_get_phrases_ajax'));
92 add_action('wp_ajax_mxchat_delete_legacy_phrases', array($this, 'mxchat_delete_legacy_phrases_ajax'));
93
94 // Slack test connection
95 add_action('wp_ajax_mxchat_test_slack_connection', array($this, 'mxchat_test_slack_connection'));
96
97 // Translation handlers
98 add_action('wp_ajax_mxchat_translate_messages', array($this, 'mxchat_translate_messages'));
99 add_action('wp_ajax_mxchat_get_transcript_translation', array($this, 'mxchat_get_transcript_translation'));
100
101 // 3.2.3: Embedding model switch protection
102 add_action('wp_ajax_mxchat_check_embedding_switch', array($this, 'mxchat_check_embedding_switch_ajax'));
103 add_action('wp_ajax_mxchat_dismiss_embedding_mismatch', array($this, 'mxchat_dismiss_embedding_mismatch_ajax'));
104 add_action('admin_notices', array($this, 'mxchat_embedding_mismatch_notice'));
105 }
106
107 /**
108 * 3.2.3: Preflight check before allowing the embedding model dropdown to
109 * switch. Pure option comparison — no counts, no DB queries beyond the
110 * cached options. The dialog is shown whenever the user has previously
111 * embedded with a different model than the one they're switching to.
112 */
113 public function mxchat_check_embedding_switch_ajax() {
114 check_ajax_referer('mxchat_admin_nonce', 'security');
115 if (!current_user_can('manage_options')) {
116 wp_send_json_error(__('Unauthorized', 'mxchat'));
117 }
118
119 $new_model = isset($_POST['new_model']) ? sanitize_text_field(wp_unslash($_POST['new_model'])) : '';
120 $active_model = MxChat_Utils::get_active_embedding_model();
121
122 $is_mismatch = !empty($active_model) && !empty($new_model) && $active_model !== $new_model;
123
124 $active_dims = MxChat_Utils::embedding_model_dimensions($active_model);
125 $new_dims = MxChat_Utils::embedding_model_dimensions($new_model);
126
127 wp_send_json_success(array(
128 'is_mismatch' => $is_mismatch,
129 'active_model' => $active_model,
130 'active_label' => MxChat_Utils::embedding_model_label($active_model),
131 'new_model' => $new_model,
132 'new_label' => MxChat_Utils::embedding_model_label($new_model),
133 'dims_differ' => ($active_dims > 0 && $new_dims > 0 && $active_dims !== $new_dims),
134 'active_dims' => $active_dims,
135 'new_dims' => $new_dims,
136 ));
137 }
138
139 /**
140 * 3.2.3: Dismiss the persistent mismatch banner. Tied to the active+selected
141 * pair so the banner reappears on the next switch event.
142 */
143 public function mxchat_dismiss_embedding_mismatch_ajax() {
144 check_ajax_referer('mxchat_admin_nonce', 'security');
145 if (!current_user_can('manage_options')) {
146 wp_send_json_error(__('Unauthorized', 'mxchat'));
147 }
148
149 $options = get_option('mxchat_options', array());
150 $selected = $options['embedding_model'] ?? '';
151 $active = MxChat_Utils::get_active_embedding_model();
152 update_option('mxchat_dismissed_embedding_mismatch', $active . '|' . $selected, false);
153 wp_send_json_success();
154 }
155
156 /**
157 * 3.2.3: Persistent admin banner shown whenever the active embedding model
158 * (last used to actually embed something) differs from the currently
159 * selected model. Pure option comparison — no DB queries on every page
160 * load. The banner auto-clears once both match again, i.e. after a delete
161 * + re-embed cycle.
162 */
163 public function mxchat_embedding_mismatch_notice() {
164 if (!current_user_can('manage_options')) {
165 return;
166 }
167
168 $options = get_option('mxchat_options', array());
169 $selected = $options['embedding_model'] ?? '';
170 $active = MxChat_Utils::get_active_embedding_model();
171
172 if (empty($active) || empty($selected) || $active === $selected) {
173 return;
174 }
175
176 $dismissed = get_option('mxchat_dismissed_embedding_mismatch', '');
177 if ($dismissed === $active . '|' . $selected) {
178 return;
179 }
180
181 $active_label = MxChat_Utils::embedding_model_label($active);
182 $selected_label = MxChat_Utils::embedding_model_label($selected);
183 $active_dims = MxChat_Utils::embedding_model_dimensions($active);
184 $selected_dims = MxChat_Utils::embedding_model_dimensions($selected);
185 $dims_differ = ($active_dims > 0 && $selected_dims > 0 && $active_dims !== $selected_dims);
186
187 $kb_url = admin_url('admin.php?page=mxchat-prompts');
188 $actions_url = admin_url('admin.php?page=mxchat-actions');
189
190 ?>
191 <div class="notice notice-error is-dismissible mxchat-embedding-mismatch-notice"
192 data-active="<?php echo esc_attr($active); ?>"
193 data-selected="<?php echo esc_attr($selected); ?>">
194 <p><strong><?php esc_html_e('MxChat: Embedding model mismatch detected', 'mxchat'); ?></strong></p>
195 <p>
196 <?php
197 printf(
198 /* translators: 1: previously-used model name, 2: currently-selected model name */
199 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'),
200 '<code>' . esc_html($active_label) . '</code>',
201 '<code>' . esc_html($selected_label) . '</code>'
202 );
203 ?>
204 </p>
205 <?php if ($dims_differ) : ?>
206 <p>
207 <strong><?php esc_html_e('Dimension mismatch:', 'mxchat'); ?></strong>
208 <?php
209 printf(
210 /* translators: 1: old dim count, 2: new dim count */
211 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'),
212 (int) $active_dims,
213 (int) $selected_dims
214 );
215 ?>
216 </p>
217 <?php endif; ?>
218 <p>
219 <?php esc_html_e('To fix this:', 'mxchat'); ?>
220 <a href="<?php echo esc_url($kb_url); ?>"><?php esc_html_e('Delete all knowledge base entries', 'mxchat'); ?></a> ·
221 <a href="<?php echo esc_url($actions_url); ?>"><?php esc_html_e('Delete all actions', 'mxchat'); ?></a> ·
222 <?php esc_html_e('then re-import / re-add them with the new model selected.', 'mxchat'); ?>
223 </p>
224 </div>
225 <script>
226 (function($){
227 $(document).on('click', '.mxchat-embedding-mismatch-notice .notice-dismiss', function(){
228 $.post(ajaxurl, {
229 action: 'mxchat_dismiss_embedding_mismatch',
230 security: '<?php echo esc_js(wp_create_nonce('mxchat_admin_nonce')); ?>'
231 });
232 });
233 })(jQuery);
234 </script>
235 <?php
236 }
237
238 private function is_license_active() {
239 $license_status = get_option('mxchat_license_status', 'inactive');
240 return ($license_status === 'active');
241 }
242
243 private function initialize_default_options() {
244 $default_options = array(
245 'api_key' => '',
246 'xai_api_key' => '',
247 'claude_api_key' => '',
248 'deepseek_api_key' => '',
249 'voyage_api_key' => '',
250 'gemini_api_key' => '',
251 'enable_streaming_toggle' => 'off',
252 'enable_web_search' => 'off',
253 'embedding_model' => 'text-embedding-ada-002',
254 '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:
255
256 # Response Style - CRITICALLY IMPORTANT
257 - MAXIMUM LENGTH: 1-3 short sentences per response
258 - Ultra-concise: Get straight to the answer with no filler
259 - No introductions like "Sure!" or "I\'d be happy to help"
260 - No phrases like "based on my knowledge" or "according to information"
261 - No explanatory text before giving the answer
262 - No summaries or repetition
263 - Hyperlink all URLs
264 - Respond in user\'s language
265 - Minor chit chat or conversation is okay, but try to keep it focused on [insert topic]
266
267 # Knowledge Base Requirements - PREVENT HALLUCINATIONS
268 - ONLY answer questions using information explicitly provided in OFFICIAL KNOWLEDGE DATABASE CONTENT sections marked with ===== delimiters
269 - If required information is NOT in the knowledge database: "I don\'t have enough information in my knowledge base to answer that question accurately."
270 - NEVER invent or hallucinate URLs, links, product specs, procedures, dates, statistics, names, contacts, or company information
271 - When knowledge base information is unclear or contradictory, acknowledge the limitation rather than guessing
272 - Better to admit insufficient information than provide inaccurate answers',
273 'model' => esc_html__('gpt-5.1-chat-latest', 'mxchat'),
274 'rate_limit_logged_out' => esc_html__('100', 'mxchat'),
275 'role_rate_limits' => array(),
276 'rate_limit_message' => esc_html__('Rate limit exceeded. Please try again later.', 'mxchat'),
277 'enable_email_block' => '',
278 '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'),
279 'email_blocker_button_text' => esc_html__('Start Chat', 'mxchat'),
280 'enable_name_field' => 'off', // NEW
281 'name_field_placeholder' => esc_html__('Enter your name', 'mxchat'), // NEW
282 'top_bar_title' => esc_html__('MxChat', 'mxchat'),
283 'intro_message' => __('Hello! How can I assist you today?', 'mxchat'),
284 'ai_agent_text' => esc_html__('AI Agent', 'mxchat'),
285 'input_copy' => esc_html__('How can I assist?', 'mxchat'),
286 'append_to_body' => esc_html__('off', 'mxchat'),
287 'post_type_visibility_mode' => 'all', // 'all', 'include', 'exclude'
288 'post_type_visibility_list' => array(), // Array of post type slugs
289 'contextual_awareness_toggle' => 'off',
290 'citation_links_toggle' => 'on',
291 'close_button_color' => esc_html__('#fff', 'mxchat'),
292 'chatbot_bg_color' => esc_html__('#fff', 'mxchat'),
293 'user_message_bg_color' => esc_html__('#fff', 'mxchat'),
294 'user_message_font_color' => esc_html__('#212121', 'mxchat'),
295 'bot_message_bg_color' => esc_html__('#212121', 'mxchat'),
296 'bot_message_font_color' => esc_html__('#fff', 'mxchat'),
297 'top_bar_bg_color' => esc_html__('#212121', 'mxchat'),
298 'send_button_font_color' => esc_html__('#212121', 'mxchat'),
299 'chat_input_font_color' => esc_html__('#212121', 'mxchat'),
300 'chatbot_background_color' => esc_html__('#212121', 'mxchat'),
301 'icon_color' => esc_html__('#fff', 'mxchat'),
302 'enable_woocommerce_integration' => esc_html__('0', 'mxchat'),
303 'link_target_toggle' => esc_html__('off', 'mxchat'),
304 'pre_chat_message' => esc_html__('Hey there! Ask me anything!', 'mxchat'),
305
306 // New fields for Loops Integration
307 'loops_api_key' => '',
308 'loops_mailing_list' => '',
309 'triggered_phrase_response' => __('Would you like to join our mailing list? Please provide your email below.', 'mxchat'),
310 'email_capture_response' => __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat'),
311 'popular_question_1' => '',
312 'popular_question_2' => '',
313 'popular_question_3' => '',
314 'pdf_intent_trigger_text' => __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat'),
315 'pdf_intent_success_text' => __("I've processed the PDF. What questions do you have about it?", 'mxchat'),
316 'pdf_intent_error_text' => __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat'),
317 'pdf_max_pages' => 69,
318 'show_pdf_upload_button' => 'on',
319 'show_word_upload_button' => 'on',
320
321 // Live Agent Integration (Slack)
322 'live_agent_webhook_url' => '',
323 'live_agent_secret_key' => '',
324 'live_agent_bot_token' => '',
325 'live_agent_message_bg_color' => esc_html__('#ffffff', 'mxchat'),
326 'live_agent_message_font_color' => esc_html__('#333333', 'mxchat'),
327
328 // Telegram Integration
329 'telegram_status' => 'off',
330 'telegram_bot_token' => '',
331 'telegram_group_id' => '',
332 'telegram_webhook_secret' => '',
333 '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'),
334 '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'),
335
336 'chat_toolbar_toggle' => esc_html__('off', 'mxchat'),
337 'mode_indicator_bg_color' => esc_html__('#767676', 'mxchat'),
338 'mode_indicator_font_color' => esc_html__('#ffffff', 'mxchat'),
339 'toolbar_icon_color' => esc_html__('#212121', 'mxchat'),
340
341 // Optimization settings
342 'script_loading_strategy' => 'default',
343
344 // Debug settings
345 'debug_mode' => 'off',
346 );
347
348
349 // Merge existing options with defaults
350 $existing_options = get_option('mxchat_options', array());
351 $merged_options = wp_parse_args($existing_options, $default_options);
352
353 // Update the options if they have changed
354 if ($existing_options !== $merged_options) {
355 update_option('mxchat_options', $merged_options);
356 }
357
358 // Add default limits for each role
359 $roles = wp_roles()->get_names();
360 foreach ($roles as $role_id => $role_name) {
361 $default_options['role_rate_limits'][$role_id] = esc_html__('100', 'mxchat');
362 }
363
364 return $default_options;
365
366 // Update the $this->options property
367 $this->options = $merged_options;
368 }
369
370 public function mxchat_add_plugin_page() {
371 // Main menu page
372 add_menu_page(
373 esc_html__('MxChat Settings', 'mxchat'),
374 esc_html__('MxChat', 'mxchat'),
375 'manage_options',
376 'mxchat-max',
377 array($this, 'mxchat_create_admin_page'),
378 'dashicons-testimonial',
379 6
380 );
381
382 // Rename the first submenu item from "MxChat" to "Settings"
383 add_submenu_page(
384 'mxchat-max',
385 esc_html__('MxChat Settings', 'mxchat'),
386 esc_html__('Settings', 'mxchat'),
387 'manage_options',
388 'mxchat-max',
389 array($this, 'mxchat_create_admin_page')
390 );
391
392 // Submenu page for Knowledge
393 add_submenu_page(
394 'mxchat-max',
395 esc_html__('Prompts', 'mxchat'),
396 esc_html__('Knowledge', 'mxchat'),
397 'manage_options',
398 'mxchat-prompts',
399 array($this, 'mxchat_create_prompts_page')
400 );
401
402 add_submenu_page(
403 'mxchat-max',
404 esc_html__('Chat Transcripts', 'mxchat'),
405 esc_html__('Transcripts', 'mxchat'),
406 'manage_options',
407 'mxchat-transcripts',
408 array($this, 'mxchat_create_transcripts_page')
409 );
410
411 add_submenu_page(
412 'mxchat-max',
413 esc_html__('MxChat Actions', 'mxchat'),
414 esc_html__('Actions', 'mxchat'),
415 'manage_options',
416 'mxchat-actions',
417 array($this, 'mxchat_actions_page_html')
418 );
419
420 // Content Generator page
421 add_submenu_page(
422 'mxchat-max',
423 esc_html__('Content', 'mxchat'),
424 esc_html__('Content', 'mxchat'),
425 'manage_options',
426 'mxchat-content',
427 array($this, 'mxchat_create_content_page')
428 );
429
430 // Consolidated Pro & Extensions page (replaces separate Add Ons and Pro Upgrade pages)
431 add_submenu_page(
432 'mxchat-max',
433 esc_html__('Pro & Extensions', 'mxchat'),
434 esc_html__('Pro & Extensions', 'mxchat'),
435 'manage_options',
436 'mxchat-activation',
437 array($this, 'mxchat_create_activation_page')
438 );
439 }
440
441 public function mxchat_create_addons_page() {
442 require_once plugin_dir_path(__FILE__) . 'class-mxchat-addons.php';
443 $addons_page = new MxChat_Addons();
444 $addons_page->render_page();
445 }
446
447 /**
448 * Render the Content Generator admin page
449 */
450 public function mxchat_create_content_page() {
451 require_once plugin_dir_path(__FILE__) . 'admin-content-page.php';
452 mxchat_render_content_page($this);
453 }
454
455 /**
456 * Test actual streaming functionality in the WordPress environment
457 */
458 public function mxchat_handle_test_streaming_actual() {
459 check_ajax_referer('mxchat_test_streaming_nonce', 'nonce');
460
461 // Check if headers have already been sent
462 if (headers_sent()) {
463 wp_send_json_error(['message' => 'Headers already sent - streaming not possible']);
464 return;
465 }
466
467 // Check for required functions
468 if (!function_exists('curl_init')) {
469 wp_send_json_error(['message' => 'cURL not available - streaming requires cURL']);
470 return;
471 }
472
473 // Get user's selected model and API key
474 $options = get_option('mxchat_options', []);
475 $selected_model = $options['model'] ?? 'gpt-5.1-chat-latest';
476
477 // Get the provider from the model
478 $model_parts = explode('-', $selected_model);
479 $provider = strtolower($model_parts[0]);
480
481 // Get the appropriate API key
482 $api_key = '';
483 switch ($provider) {
484 case 'gpt':
485 case 'o1':
486 $api_key = $options['api_key'] ?? '';
487 break;
488 case 'claude':
489 $api_key = $options['claude_api_key'] ?? '';
490 break;
491 case 'grok':
492 $api_key = $options['xai_api_key'] ?? '';
493 break;
494 case 'deepseek':
495 $api_key = $options['deepseek_api_key'] ?? '';
496 break;
497 case 'gemini':
498 $api_key = $options['gemini_api_key'] ?? '';
499 break;
500 default:
501 // Default to OpenAI for unknown models
502 $api_key = $options['api_key'] ?? '';
503 $provider = 'gpt';
504 break;
505 }
506
507 if (empty($api_key)) {
508 wp_send_json_error(['message' => "API key not configured for {$provider} provider"]);
509 return;
510 }
511
512 // Test streaming with the selected model and provider
513 try {
514 $this->perform_streaming_test($provider, $selected_model, $api_key);
515 } catch (Exception $e) {
516 wp_send_json_error(['message' => 'Streaming test exception: ' . $e->getMessage()]);
517 }
518 }
519
520 /**
521 * Perform the actual streaming test
522 */
523 private function perform_streaming_test($provider, $model, $api_key) {
524 // Set streaming headers
525 header('Content-Type: text/event-stream');
526 header('Cache-Control: no-cache');
527 header('X-Accel-Buffering: no'); // Disable nginx buffering
528
529 // Prepare test message
530 $test_message = "Please respond with exactly: 'Streaming test successful!' - send this as a short response for testing.";
531
532 // Configure API request based on provider
533 $url = '';
534 $headers = [];
535 $body = [];
536
537 switch ($provider) {
538 case 'gpt':
539 case 'o1':
540 $url = 'https://api.openai.com/v1/chat/completions';
541 $headers = [
542 'Content-Type: application/json',
543 'Authorization: Bearer ' . $api_key
544 ];
545 $body = [
546 'model' => $model,
547 'messages' => [['role' => 'user', 'content' => $test_message]],
548 'max_tokens' => 50,
549 'temperature' => 0.3,
550 'stream' => true
551 ];
552 break;
553
554 case 'claude':
555 $url = 'https://api.anthropic.com/v1/messages';
556 $headers = [
557 'Content-Type: application/json',
558 'x-api-key: ' . $api_key,
559 'anthropic-version: 2023-06-01'
560 ];
561 $body = [
562 'model' => $model,
563 'messages' => [['role' => 'user', 'content' => $test_message]],
564 'max_tokens' => 50,
565 'temperature' => 0.3,
566 'stream' => true
567 ];
568 break;
569
570 case 'grok':
571 $url = 'https://api.x.ai/v1/chat/completions';
572 $headers = [
573 'Content-Type: application/json',
574 'Authorization: Bearer ' . $api_key
575 ];
576 $body = [
577 'model' => $model,
578 'messages' => [['role' => 'user', 'content' => $test_message]],
579 'max_tokens' => 50,
580 'temperature' => 0.3,
581 'stream' => true
582 ];
583 break;
584
585 case 'deepseek':
586 $url = 'https://api.deepseek.com/v1/chat/completions';
587 $headers = [
588 'Content-Type: application/json',
589 'Authorization: Bearer ' . $api_key
590 ];
591 $body = [
592 'model' => $model,
593 'messages' => [['role' => 'user', 'content' => $test_message]],
594 'max_tokens' => 50,
595 'temperature' => 0.3,
596 'stream' => true
597 ];
598 break;
599
600 default:
601 echo "data: " . json_encode(['error' => 'Unsupported provider for streaming test: ' . $provider]) . "\n\n";
602 flush();
603 return;
604 }
605
606 // Initialize cURL
607 $ch = curl_init();
608 curl_setopt($ch, CURLOPT_URL, $url);
609 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
610 curl_setopt($ch, CURLOPT_POST, true);
611 curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
612 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
613 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
614 curl_setopt($ch, CURLOPT_TIMEOUT, 30);
615 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use ($provider) {
616 return $this->process_streaming_test_data($data, $provider);
617 });
618
619 // Send initial test message
620 echo "data: " . json_encode(['content' => '[Starting streaming test...]']) . "\n\n";
621 flush();
622
623 $result = curl_exec($ch);
624 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
625 $curl_error = curl_error($ch);
626 curl_close($ch);
627
628 if ($curl_error) {
629 echo "data: " . json_encode(['error' => 'cURL Error: ' . $curl_error]) . "\n\n";
630 flush();
631 return;
632 }
633
634 if ($http_code !== 200) {
635 echo "data: " . json_encode(['error' => 'API returned HTTP ' . $http_code]) . "\n\n";
636 flush();
637 return;
638 }
639
640 // Send completion signal
641 echo "data: [DONE]\n\n";
642 flush();
643 }
644
645 /**
646 * Process streaming data for the test
647 */
648 private function process_streaming_test_data($data, $provider) {
649 static $chunk_count = 0;
650
651 $lines = explode("\n", $data);
652
653 foreach ($lines as $line) {
654 if (trim($line) === '') {
655 continue;
656 }
657
658 // Handle different provider formats
659 if ($provider === 'claude') {
660 // Claude uses event: and data: format
661 if (strpos($line, 'data: ') === 0) {
662 $json_str = substr($line, 6);
663 $json = json_decode($json_str, true);
664
665 if (isset($json['type']) && $json['type'] === 'content_block_delta') {
666 if (isset($json['delta']['text'])) {
667 $chunk_count++;
668 echo "data: " . json_encode([
669 'content' => $json['delta']['text'],
670 'test_chunk' => $chunk_count
671 ]) . "\n\n";
672 flush();
673 }
674 }
675 }
676 } else {
677 // OpenAI, X.AI, DeepSeek format
678 if (strpos($line, 'data: ') === 0) {
679 $json_str = substr($line, 6);
680
681 if ($json_str === '[DONE]') {
682 // Don't echo [DONE] here, let the main function handle it
683 continue;
684 }
685
686 $json = json_decode($json_str, true);
687 if (isset($json['choices'][0]['delta']['content'])) {
688 $chunk_count++;
689 echo "data: " . json_encode([
690 'content' => $json['choices'][0]['delta']['content'],
691 'test_chunk' => $chunk_count
692 ]) . "\n\n";
693 flush();
694 }
695 }
696 }
697 }
698
699 return strlen($data);
700 }
701
702 /**
703 * Updated version of your existing test method (keep this as a fallback)
704 */
705 public function mxchat_handle_test_streaming() {
706 check_ajax_referer('mxchat_test_streaming_nonce', 'nonce');
707
708 // Use the actual streaming test instead
709 $this->mxchat_handle_test_streaming_actual();
710 }
711
712
713 public function register_pinecone_settings() {
714 register_setting(
715 'mxchat_pinecone_addon_options',
716 'mxchat_pinecone_addon_options',
717 array(
718 'type' => 'array',
719 'sanitize_callback' => array($this, 'sanitize_pinecone_settings'),
720 'default' => array(
721 'mxchat_use_pinecone' => '0',
722 'mxchat_pinecone_api_key' => '',
723 'mxchat_pinecone_host' => '',
724 'mxchat_pinecone_index' => '',
725 'mxchat_pinecone_environment' => ''
726 )
727 )
728 );
729 }
730
731 public function sanitize_pinecone_settings($input) {
732 $sanitized = array();
733
734 $sanitized['mxchat_use_pinecone'] = isset($input['mxchat_use_pinecone']) ? '1' : '0';
735 $sanitized['mxchat_pinecone_api_key'] = sanitize_text_field($input['mxchat_pinecone_api_key'] ?? '');
736 $sanitized['mxchat_pinecone_host'] = sanitize_text_field($input['mxchat_pinecone_host'] ?? '');
737 $sanitized['mxchat_pinecone_index'] = sanitize_text_field($input['mxchat_pinecone_index'] ?? '');
738 $sanitized['mxchat_pinecone_environment'] = sanitize_text_field($input['mxchat_pinecone_environment'] ?? '');
739
740 // Remove https:// from host if present
741 $sanitized['mxchat_pinecone_host'] = str_replace(['https://', 'http://'], '', $sanitized['mxchat_pinecone_host']);
742
743 return $sanitized;
744 }
745
746 public function register_openai_vectorstore_settings() {
747 register_setting(
748 'mxchat_openai_vectorstore_options',
749 'mxchat_openai_vectorstore_options',
750 array(
751 'type' => 'array',
752 'sanitize_callback' => array($this, 'sanitize_openai_vectorstore_settings'),
753 'default' => array(
754 'mxchat_use_openai_vectorstore' => '0',
755 'mxchat_vectorstore_ids' => '',
756 'mxchat_vectorstore_max_results' => 5
757 )
758 )
759 );
760 }
761
762 public function sanitize_openai_vectorstore_settings($input) {
763 $sanitized = array();
764
765 $sanitized['mxchat_use_openai_vectorstore'] = isset($input['mxchat_use_openai_vectorstore']) ? '1' : '0';
766 $sanitized['mxchat_vectorstore_ids'] = sanitize_text_field($input['mxchat_vectorstore_ids'] ?? '');
767 $sanitized['mxchat_vectorstore_max_results'] = absint($input['mxchat_vectorstore_max_results'] ?? 5);
768
769 // Ensure max results is within reasonable range
770 if ($sanitized['mxchat_vectorstore_max_results'] < 1) {
771 $sanitized['mxchat_vectorstore_max_results'] = 1;
772 }
773 if ($sanitized['mxchat_vectorstore_max_results'] > 20) {
774 $sanitized['mxchat_vectorstore_max_results'] = 20;
775 }
776
777 return $sanitized;
778 }
779
780 /**
781 * AJAX handler to test OpenAI Vector Store connection
782 */
783 public function mxchat_test_vectorstore_connection() {
784 check_ajax_referer('mxchat_admin_nonce', 'nonce');
785
786 if (!current_user_can('manage_options')) {
787 wp_send_json_error(array('message' => __('Permission denied.', 'mxchat')));
788 return;
789 }
790
791 $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
792 $vectorstore_ids = $vectorstore_options['mxchat_vectorstore_ids'] ?? '';
793 $mxchat_options = get_option('mxchat_options', array());
794 $api_key = $mxchat_options['api_key'] ?? '';
795
796 if (empty($api_key)) {
797 wp_send_json_error(array('message' => __('OpenAI API key is not configured.', 'mxchat')));
798 return;
799 }
800
801 if (empty($vectorstore_ids)) {
802 wp_send_json_error(array('message' => __('No Vector Store ID configured.', 'mxchat')));
803 return;
804 }
805
806 // Get the first Vector Store ID for testing
807 $ids_array = array_map('trim', explode(',', $vectorstore_ids));
808 $test_id = $ids_array[0];
809
810 // Test by retrieving the Vector Store info
811 $response = wp_remote_get(
812 'https://api.openai.com/v1/vector_stores/' . $test_id,
813 array(
814 'headers' => array(
815 'Authorization' => 'Bearer ' . $api_key,
816 'Content-Type' => 'application/json',
817 'OpenAI-Beta' => 'assistants=v2'
818 ),
819 'timeout' => 30
820 )
821 );
822
823 if (is_wp_error($response)) {
824 wp_send_json_error(array('message' => __('Connection failed: ', 'mxchat') . $response->get_error_message()));
825 return;
826 }
827
828 $status_code = wp_remote_retrieve_response_code($response);
829 $body = json_decode(wp_remote_retrieve_body($response), true);
830
831 if ($status_code === 200 && isset($body['id'])) {
832 $file_count = $body['file_counts']['completed'] ?? 0;
833 $name = $body['name'] ?? $test_id;
834 wp_send_json_success(array(
835 'message' => sprintf(
836 __('Connected successfully! Vector Store: %s (%d files)', 'mxchat'),
837 esc_html($name),
838 $file_count
839 )
840 ));
841 } elseif ($status_code === 404) {
842 wp_send_json_error(array('message' => __('Vector Store not found. Please check the ID.', 'mxchat')));
843 } elseif ($status_code === 401) {
844 wp_send_json_error(array('message' => __('Invalid API key.', 'mxchat')));
845 } else {
846 $error_message = $body['error']['message'] ?? __('Unknown error occurred.', 'mxchat');
847 wp_send_json_error(array('message' => $error_message));
848 }
849 }
850
851 /**
852 * AJAX handler to test Slack connection and validate scopes
853 */
854 public function mxchat_test_slack_connection() {
855 check_ajax_referer('mxchat_admin_nonce', 'nonce');
856
857 if (!current_user_can('manage_options')) {
858 wp_send_json_error(array('message' => __('Permission denied.', 'mxchat')));
859 return;
860 }
861
862 $bot_token = $this->options['live_agent_bot_token'] ?? '';
863
864 if (empty($bot_token)) {
865 wp_send_json_error(array('message' => __('Slack Bot Token is not configured. Please enter your token and save settings first.', 'mxchat')));
866 return;
867 }
868
869 // Test 1: Validate bot token with auth.test
870 $auth_response = wp_remote_post('https://slack.com/api/auth.test', array(
871 'headers' => array(
872 'Authorization' => 'Bearer ' . $bot_token,
873 'Content-Type' => 'application/json'
874 ),
875 'timeout' => 15
876 ));
877
878 if (is_wp_error($auth_response)) {
879 wp_send_json_error(array('message' => __('Connection failed: ', 'mxchat') . $auth_response->get_error_message()));
880 return;
881 }
882
883 $auth_body = json_decode(wp_remote_retrieve_body($auth_response), true);
884
885 if (!isset($auth_body['ok']) || !$auth_body['ok']) {
886 $error = $auth_body['error'] ?? 'unknown_error';
887 $error_messages = array(
888 'invalid_auth' => __('Invalid bot token. Please check your token starts with xoxb-', 'mxchat'),
889 'not_authed' => __('No authentication token provided.', 'mxchat'),
890 'account_inactive' => __('The Slack workspace has been deactivated.', 'mxchat'),
891 'token_revoked' => __('The bot token has been revoked. Please generate a new one.', 'mxchat'),
892 );
893 $message = $error_messages[$error] ?? sprintf(__('Authentication failed: %s', 'mxchat'), $error);
894 wp_send_json_error(array('message' => $message));
895 return;
896 }
897
898 $team_name = $auth_body['team'] ?? 'Unknown Workspace';
899 $bot_name = $auth_body['user'] ?? 'Unknown Bot';
900
901 // Test 2: Check if we can list channels (tests channels:read scope)
902 $channels_response = wp_remote_post('https://slack.com/api/conversations.list', array(
903 'headers' => array(
904 'Authorization' => 'Bearer ' . $bot_token,
905 'Content-Type' => 'application/json'
906 ),
907 'body' => json_encode(array('limit' => 1)),
908 'timeout' => 15
909 ));
910
911 $channels_body = json_decode(wp_remote_retrieve_body($channels_response), true);
912 $can_read_channels = isset($channels_body['ok']) && $channels_body['ok'];
913
914 // Test 3: Check if we can create channels (tests channels:manage scope)
915 // We'll just check the error message without actually creating
916 $create_response = wp_remote_post('https://slack.com/api/conversations.create', array(
917 'headers' => array(
918 'Authorization' => 'Bearer ' . $bot_token,
919 'Content-Type' => 'application/json'
920 ),
921 'body' => json_encode(array('name' => 'mxchat-test-' . time(), 'is_private' => false)),
922 'timeout' => 15
923 ));
924
925 $create_body = json_decode(wp_remote_retrieve_body($create_response), true);
926
927 // If channel was created, delete it immediately
928 if (isset($create_body['ok']) && $create_body['ok'] && isset($create_body['channel']['id'])) {
929 wp_remote_post('https://slack.com/api/conversations.archive', array(
930 'headers' => array(
931 'Authorization' => 'Bearer ' . $bot_token,
932 'Content-Type' => 'application/json'
933 ),
934 'body' => json_encode(array('channel' => $create_body['channel']['id'])),
935 'timeout' => 15
936 ));
937 $can_create_channels = true;
938 } else {
939 $create_error = $create_body['error'] ?? '';
940 // name_taken means we have permission but channel exists
941 $can_create_channels = ($create_error === 'name_taken' || (isset($create_body['ok']) && $create_body['ok']));
942
943 // Check for missing scope errors
944 if ($create_error === 'missing_scope') {
945 $can_create_channels = false;
946 }
947 }
948
949 // Build result message
950 $results = array();
951 $results[] = sprintf(__('Workspace: %s', 'mxchat'), esc_html($team_name));
952 $results[] = sprintf(__('Bot: %s', 'mxchat'), esc_html($bot_name));
953 $results[] = '';
954 $results[] = ($can_read_channels ? '✓' : '✗') . ' ' . __('channels:read - List channels', 'mxchat');
955 $results[] = ($can_create_channels ? '✓' : '✗') . ' ' . __('channels:manage - Create channels', 'mxchat');
956
957 $missing_scopes = array();
958 if (!$can_read_channels) $missing_scopes[] = 'channels:read';
959 if (!$can_create_channels) $missing_scopes[] = 'channels:manage';
960
961 if (!empty($missing_scopes)) {
962 wp_send_json_error(array(
963 'message' => implode("\n", $results),
964 'missing_scopes' => $missing_scopes,
965 'partial' => true
966 ));
967 } else {
968 wp_send_json_success(array(
969 'message' => implode("\n", $results)
970 ));
971 }
972 }
973
974 public function mxchat_display_admin_notice() {
975 // Success notice
976 if ($message = get_transient('mxchat_admin_notice_success')) {
977 ?>
978 <div class="notice notice-success is-dismissible">
979 <p><?php echo esc_html($message); ?></p>
980 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php echo esc_html__('Dismiss this notice.', 'mxchat'); ?></span></button>
981 </div>
982 <?php
983 delete_transient('mxchat_admin_notice_success'); // Clear the transient after displaying
984 }
985
986 // Error notice
987 if ($message = get_transient('mxchat_admin_notice_error')) {
988 ?>
989 <div class="notice notice-error 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_error'); // Clear the transient after displaying
995 }
996 }
997
998 public function show_live_agent_disabled_banner() {
999 $show_disabled_notice = get_option('mxchat_show_live_agent_disabled_notice', false);
1000
1001 if ($show_disabled_notice) {
1002 ?>
1003 <div class="mxchat-live-agent-disabled-notice" id="mxchat-disabled-notice">
1004 <div class="mxchat-pro-notification">
1005 <button type="button" class="mxchat-dismiss-btn" onclick="dismissLiveAgentNotice()" aria-label="Dismiss notification">
1006 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1007 <line x1="18" y1="6" x2="6" y2="18"></line>
1008 <line x1="6" y1="6" x2="18" y2="18"></line>
1009 </svg>
1010 </button>
1011 <div class="mxchat-live-agent-content">
1012 <h3>🔧 Live Agent Integration Updated!</h3>
1013 <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>
1014 </div>
1015 </div>
1016 </div>
1017 <?php
1018 }
1019 }
1020 public function mxchat_create_admin_page() {
1021 $this->add_live_agent_nonce();
1022 $this->add_theme_migration_nonce();
1023
1024 // Include and render the new sidebar-based settings page
1025 require_once plugin_dir_path(__FILE__) . 'admin-settings-page.php';
1026 mxchat_render_settings_page($this);
1027 }
1028
1029 public function dismiss_live_agent_notice() {
1030 // Add debugging
1031 //error_log('dismiss_live_agent_notice called');
1032 //error_log('POST data: ' . print_r($_POST, true));
1033
1034 // Verify nonce
1035 if (!wp_verify_nonce($_POST['nonce'], 'dismiss_live_agent_notice')) {
1036 //error_log('Nonce verification failed');
1037 wp_die('Security check failed');
1038 }
1039
1040 // Remove the notice flag
1041 $deleted = delete_option('mxchat_show_live_agent_disabled_notice');
1042 //error_log('Option deleted: ' . ($deleted ? 'yes' : 'no'));
1043
1044 wp_send_json_success();
1045 }
1046 public function add_live_agent_nonce() {
1047 if (get_option('mxchat_show_live_agent_disabled_notice', false)) {
1048 // Make sure your admin script is enqueued and localize the data
1049 wp_localize_script('mxchat-admin-js', 'mxchatLiveAgent', array(
1050 'nonce' => wp_create_nonce('dismiss_live_agent_notice'),
1051 'ajaxurl' => admin_url('admin-ajax.php')
1052 ));
1053 }
1054 }
1055
1056 /**
1057 * Show theme migration notice for Pro users with AI-generated themes
1058 * Only shown once - dismissible and stored in options
1059 */
1060 public function show_theme_migration_banner() {
1061 // Only show if Pro is activated
1062 if (!$this->is_activated) {
1063 return;
1064 }
1065
1066 // Check if notice should be shown
1067 $show_notice = get_option('mxchat_show_theme_migration_notice', false);
1068
1069 if ($show_notice) {
1070 ?>
1071 <div class="mxchat-theme-migration-notice" id="mxchat-theme-migration-notice">
1072 <div class="mxchat-pro-notification">
1073 <button type="button" class="mxchat-dismiss-btn" onclick="dismissThemeMigrationNotice()" aria-label="Dismiss notification">
1074 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1075 <line x1="18" y1="6" x2="6" y2="18"></line>
1076 <line x1="6" y1="6" x2="18" y2="18"></line>
1077 </svg>
1078 </button>
1079 <div class="mxchat-theme-migration-content">
1080 <h3>🎨 AI Theme Migration Required</h3>
1081 <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>
1082 </div>
1083 </div>
1084 </div>
1085 <?php
1086 }
1087 }
1088
1089 /**
1090 * Dismiss theme migration notice via AJAX
1091 */
1092 public function dismiss_theme_migration_notice() {
1093 // Verify nonce
1094 if (!wp_verify_nonce($_POST['nonce'], 'dismiss_theme_migration_notice')) {
1095 wp_die('Security check failed');
1096 }
1097
1098 // Remove the notice flag
1099 delete_option('mxchat_show_theme_migration_notice');
1100
1101 wp_send_json_success();
1102 }
1103
1104 /**
1105 * Add nonce for theme migration notice dismiss
1106 */
1107 public function add_theme_migration_nonce() {
1108 if (get_option('mxchat_show_theme_migration_notice', false) && $this->is_activated) {
1109 wp_localize_script('mxchat-admin-js', 'mxchatThemeMigration', array(
1110 'nonce' => wp_create_nonce('dismiss_theme_migration_notice'),
1111 'ajaxurl' => admin_url('admin-ajax.php')
1112 ));
1113 }
1114 }
1115
1116 public function mxchat_create_transcripts_page() {
1117 global $wpdb;
1118 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1119
1120 // Get basic stats
1121 $total_chats = $wpdb->get_var("SELECT COUNT(DISTINCT session_id) FROM $table_name") ?: 0;
1122 $total_messages = $wpdb->get_var("SELECT COUNT(*) FROM $table_name") ?: 0;
1123
1124 // Count unique users with detailed breakdown
1125 $total_users = $wpdb->get_var("
1126 SELECT COUNT(DISTINCT
1127 CASE
1128 WHEN user_email != '' AND user_email IS NOT NULL THEN user_email
1129 WHEN user_id != 0 THEN CONCAT('user_', user_id)
1130 WHEN user_identifier NOT LIKE 'Tech-Savvy User'
1131 AND user_identifier NOT LIKE 'Detail-Oriented User'
1132 AND user_identifier NOT LIKE 'Language Learner'
1133 AND user_identifier NOT LIKE 'Casual Browser'
1134 AND user_identifier NOT LIKE 'Policy Enforcer'
1135 AND user_identifier NOT LIKE 'Researcher'
1136 AND user_identifier NOT LIKE 'Loyalty Member'
1137 AND user_identifier NOT LIKE 'Gift Buyer'
1138 AND user_identifier NOT LIKE 'Parent or Caregiver'
1139 THEN user_identifier
1140 ELSE session_id
1141 END
1142 )
1143 FROM $table_name
1144 WHERE role != 'assistant'
1145 ");
1146
1147 // Get user type breakdown
1148 $registered_users = $wpdb->get_var("
1149 SELECT COUNT(DISTINCT user_email)
1150 FROM $table_name
1151 WHERE user_email != '' AND user_email IS NOT NULL
1152 ");
1153
1154 $guest_users = $wpdb->get_var("
1155 SELECT COUNT(DISTINCT user_identifier)
1156 FROM $table_name
1157 WHERE (user_email = '' OR user_email IS NULL)
1158 AND role != 'assistant'
1159 AND user_identifier NOT LIKE 'Tech-Savvy User'
1160 AND user_identifier NOT LIKE 'Detail-Oriented User'
1161 AND user_identifier NOT LIKE 'Language Learner'
1162 AND user_identifier NOT LIKE 'Casual Browser'
1163 AND user_identifier NOT LIKE 'Policy Enforcer'
1164 AND user_identifier NOT LIKE 'Researcher'
1165 AND user_identifier NOT LIKE 'Loyalty Member'
1166 AND user_identifier NOT LIKE 'Gift Buyer'
1167 AND user_identifier NOT LIKE 'Parent or Caregiver'
1168 ");
1169
1170 // Get agent test messages count
1171 $agent_tests = $wpdb->get_var("
1172 SELECT COUNT(DISTINCT session_id)
1173 FROM $table_name
1174 WHERE user_identifier IN (
1175 'Tech-Savvy User',
1176 'Detail-Oriented User',
1177 'Language Learner',
1178 'Casual Browser',
1179 'Policy Enforcer',
1180 'Researcher',
1181 'Loyalty Member',
1182 'Gift Buyer',
1183 'Parent or Caregiver'
1184 )
1185 ");
1186 // Get activity metrics
1187 $today_chats = $wpdb->get_var("
1188 SELECT COUNT(DISTINCT session_id)
1189 FROM $table_name
1190 WHERE DATE(timestamp) = CURDATE()
1191 ");
1192
1193 $week_chats = $wpdb->get_var("
1194 SELECT COUNT(DISTINCT session_id)
1195 FROM $table_name
1196 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)
1197 ");
1198
1199 $month_chats = $wpdb->get_var("
1200 SELECT COUNT(DISTINCT session_id)
1201 FROM $table_name
1202 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 30 DAY)
1203 ");
1204
1205 // Get daily chat data for last 7 days
1206 $daily_stats = $wpdb->get_results("
1207 SELECT
1208 DATE(timestamp) as date,
1209 COUNT(DISTINCT session_id) as chats,
1210 COUNT(*) as messages
1211 FROM $table_name
1212 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)
1213 GROUP BY DATE(timestamp)
1214 ORDER BY date ASC
1215 ");
1216
1217 // Get average messages per chat
1218 $avg_messages = $wpdb->get_var("
1219 SELECT AVG(message_count)
1220 FROM (
1221 SELECT session_id, COUNT(*) as message_count
1222 FROM $table_name
1223 GROUP BY session_id
1224 ) as chat_counts
1225 ");
1226 $avg_messages = $avg_messages ? round($avg_messages, 1) : 0;
1227
1228 // Get busiest hour
1229 $busiest_hour = $wpdb->get_row("
1230 SELECT HOUR(timestamp) as hour, COUNT(DISTINCT session_id) as chat_count
1231 FROM $table_name
1232 GROUP BY HOUR(timestamp)
1233 ORDER BY chat_count DESC
1234 LIMIT 1
1235 ");
1236
1237 // Prepare chart data
1238 $chart_labels = array();
1239 $chart_chats = array();
1240 $chart_messages = array();
1241
1242 // Fill last 7 days with data
1243 for ($i = 6; $i >= 0; $i--) {
1244 $date = date('Y-m-d', strtotime("-$i days"));
1245 $day_name = date('D', strtotime("-$i days"));
1246 $chart_labels[] = $day_name;
1247
1248 $found = false;
1249 foreach ($daily_stats as $stat) {
1250 if ($stat->date === $date) {
1251 $chart_chats[] = (int)$stat->chats;
1252 $chart_messages[] = (int)$stat->messages;
1253 $found = true;
1254 break;
1255 }
1256 }
1257 if (!$found) {
1258 $chart_chats[] = 0;
1259 $chart_messages[] = 0;
1260 }
1261 }
1262
1263 // Prepare page data for the template
1264 $page_data = array(
1265 'total_chats' => $total_chats,
1266 'total_messages' => $total_messages,
1267 'total_users' => $total_users,
1268 'registered_users' => $registered_users,
1269 'guest_users' => $guest_users,
1270 'agent_tests' => $agent_tests,
1271 'today_chats' => $today_chats,
1272 'week_chats' => $week_chats,
1273 'month_chats' => $month_chats,
1274 'avg_messages' => $avg_messages,
1275 'busiest_hour' => $busiest_hour,
1276 'chart_labels' => $chart_labels,
1277 'chart_chats' => $chart_chats,
1278 'chart_messages' => $chart_messages,
1279 );
1280
1281 // Include and render the new template
1282 require_once plugin_dir_path(__FILE__) . 'admin-transcripts-page.php';
1283 mxchat_render_transcripts_page($this, $page_data);
1284 }
1285
1286 /**
1287 * Get chart data for transcripts page
1288 * Used by both page render and script localization
1289 *
1290 * @return array Chart data with labels, chats, and messages arrays
1291 */
1292 private function get_transcripts_chart_data() {
1293 global $wpdb;
1294 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1295
1296 // Get daily chat data for last 7 days - same query as mxchat_transcripts_page()
1297 $daily_stats = $wpdb->get_results("
1298 SELECT
1299 DATE(timestamp) as date,
1300 COUNT(DISTINCT session_id) as chats,
1301 COUNT(*) as messages
1302 FROM $table_name
1303 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)
1304 GROUP BY DATE(timestamp)
1305 ORDER BY date ASC
1306 ");
1307
1308 // Prepare chart data
1309 $chart_labels = array();
1310 $chart_chats = array();
1311 $chart_messages = array();
1312
1313 // Fill last 7 days with data - same logic as mxchat_transcripts_page()
1314 for ($i = 6; $i >= 0; $i--) {
1315 $date = date('Y-m-d', strtotime("-$i days"));
1316 $day_name = date('D', strtotime("-$i days"));
1317 $chart_labels[] = $day_name;
1318
1319 $found = false;
1320 if ($daily_stats) {
1321 foreach ($daily_stats as $stat) {
1322 if ($stat->date === $date) {
1323 $chart_chats[] = (int)$stat->chats;
1324 $chart_messages[] = (int)$stat->messages;
1325 $found = true;
1326 break;
1327 }
1328 }
1329 }
1330 if (!$found) {
1331 $chart_chats[] = 0;
1332 $chart_messages[] = 0;
1333 }
1334 }
1335
1336 return array(
1337 'labels' => $chart_labels,
1338 'chats' => $chart_chats,
1339 'messages' => $chart_messages
1340 );
1341 }
1342
1343 public function mxchat_transcripts_notification_section_callback() {
1344 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>';
1345 }
1346 public function mxchat_enable_notifications_callback() {
1347 $options = get_option('mxchat_transcripts_options', array());
1348 $enabled = isset($options['mxchat_enable_notifications']) ? $options['mxchat_enable_notifications'] : 0;
1349 ?>
1350 <label for="mxchat_enable_notifications">
1351 <input type="checkbox" id="mxchat_enable_notifications"
1352 name="mxchat_transcripts_options[mxchat_enable_notifications]"
1353 value="1" <?php checked(1, $enabled); ?>>
1354 <?php esc_html_e('Send email notification when a new chat session starts', 'mxchat'); ?>
1355 </label>
1356 <p class="description">
1357 <?php esc_html_e('Enable this option to receive email notifications for new chat sessions.', 'mxchat'); ?>
1358 </p>
1359 <?php
1360 }
1361 public function mxchat_notification_email_callback() {
1362 $options = get_option('mxchat_transcripts_options', array());
1363 $email = isset($options['mxchat_notification_email']) ? $options['mxchat_notification_email'] : get_option('admin_email');
1364 ?>
1365 <input type="email" id="mxchat_notification_email"
1366 name="mxchat_transcripts_options[mxchat_notification_email]"
1367 value="<?php echo esc_attr($email); ?>"
1368 class="regular-text">
1369 <p class="description">
1370 <?php esc_html_e('Enter the email address where notifications should be sent. Defaults to the admin email address.', 'mxchat'); ?>
1371 </p>
1372 <?php
1373 }
1374
1375 public function mxchat_auto_delete_transcripts_callback() {
1376 $options = get_option('mxchat_transcripts_options', array());
1377 $interval = isset($options['mxchat_auto_delete_transcripts']) ? $options['mxchat_auto_delete_transcripts'] : 'never';
1378 ?>
1379 <select id="mxchat_auto_delete_transcripts"
1380 name="mxchat_transcripts_options[mxchat_auto_delete_transcripts]">
1381 <option value="never" <?php selected($interval, 'never'); ?>>
1382 <?php esc_html_e('Never (Keep All Transcripts)', 'mxchat'); ?>
1383 </option>
1384 <option value="1week" <?php selected($interval, '1week'); ?>>
1385 <?php esc_html_e('After 1 Week', 'mxchat'); ?>
1386 </option>
1387 <option value="2weeks" <?php selected($interval, '2weeks'); ?>>
1388 <?php esc_html_e('After 2 Weeks', 'mxchat'); ?>
1389 </option>
1390 <option value="1month" <?php selected($interval, '1month'); ?>>
1391 <?php esc_html_e('After 1 Month', 'mxchat'); ?>
1392 </option>
1393 </select>
1394 <p class="description">
1395 <?php esc_html_e('Automatically delete old chat transcripts after the selected time period. This helps manage database size and privacy.', 'mxchat'); ?>
1396 </p>
1397 <?php
1398 }
1399
1400 public function mxchat_auto_email_transcript_callback() {
1401 $options = get_option('mxchat_transcripts_options', array());
1402 $enabled = isset($options['mxchat_auto_email_transcript_enabled']) ? $options['mxchat_auto_email_transcript_enabled'] : 0;
1403 $delay = isset($options['mxchat_auto_email_transcript_delay']) ? $options['mxchat_auto_email_transcript_delay'] : '30';
1404 $require_contact = isset($options['mxchat_auto_email_transcript_require_contact']) ? $options['mxchat_auto_email_transcript_require_contact'] : 0;
1405 ?>
1406 <label>
1407 <input type="checkbox"
1408 id="mxchat_auto_email_transcript_enabled"
1409 name="mxchat_transcripts_options[mxchat_auto_email_transcript_enabled]"
1410 value="1"
1411 <?php checked($enabled, 1); ?>>
1412 <?php esc_html_e('Enable Auto-Email of Full Transcript', 'mxchat'); ?>
1413 </label>
1414 <br><br>
1415 <label for="mxchat_auto_email_transcript_delay">
1416 <?php esc_html_e('Send transcript after:', 'mxchat'); ?>
1417 </label>
1418 <select id="mxchat_auto_email_transcript_delay"
1419 name="mxchat_transcripts_options[mxchat_auto_email_transcript_delay]">
1420 <option value="15" <?php selected($delay, '15'); ?>>
1421 <?php esc_html_e('15 minutes', 'mxchat'); ?>
1422 </option>
1423 <option value="30" <?php selected($delay, '30'); ?>>
1424 <?php esc_html_e('30 minutes', 'mxchat'); ?>
1425 </option>
1426 <option value="60" <?php selected($delay, '60'); ?>>
1427 <?php esc_html_e('1 hour', 'mxchat'); ?>
1428 </option>
1429 </select>
1430 <br><br>
1431 <label>
1432 <input type="checkbox"
1433 id="mxchat_auto_email_transcript_require_contact"
1434 name="mxchat_transcripts_options[mxchat_auto_email_transcript_require_contact]"
1435 value="1"
1436 <?php checked($require_contact, 1); ?>>
1437 <?php esc_html_e('Only send if visitor provided contact info', 'mxchat'); ?>
1438 </label>
1439 <p class="description">
1440 <?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'); ?>
1441 <br>
1442 <?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'); ?>
1443 </p>
1444 <?php
1445 }
1446
1447
1448
1449 public function sanitize_transcripts_options($input) {
1450 $sanitized = array();
1451
1452 $sanitized['mxchat_enable_notifications'] = isset($input['mxchat_enable_notifications']) ? 1 : 0;
1453
1454 if (isset($input['mxchat_notification_email'])) {
1455 $sanitized['mxchat_notification_email'] = sanitize_email($input['mxchat_notification_email']);
1456 if (!is_email($sanitized['mxchat_notification_email'])) {
1457 add_settings_error(
1458 'mxchat_transcripts_options',
1459 'invalid_email',
1460 __('Please enter a valid email address for notifications.', 'mxchat'),
1461 'error'
1462 );
1463 $sanitized['mxchat_notification_email'] = get_option('admin_email');
1464 }
1465 }
1466
1467 // Sanitize auto-delete setting
1468 $valid_intervals = array('never', '1week', '2weeks', '1month');
1469 if (isset($input['mxchat_auto_delete_transcripts'])) {
1470 $sanitized['mxchat_auto_delete_transcripts'] = in_array($input['mxchat_auto_delete_transcripts'], $valid_intervals)
1471 ? $input['mxchat_auto_delete_transcripts']
1472 : 'never';
1473 } else {
1474 $sanitized['mxchat_auto_delete_transcripts'] = 'never';
1475 }
1476
1477 // Get old value to check if auto-delete setting changed
1478 $old_options = get_option('mxchat_transcripts_options');
1479 $old_interval = isset($old_options['mxchat_auto_delete_transcripts']) ? $old_options['mxchat_auto_delete_transcripts'] : 'never';
1480
1481 // If the auto-delete setting changed, reschedule the cron job
1482 if ($old_interval !== $sanitized['mxchat_auto_delete_transcripts']) {
1483 $this->schedule_transcript_cleanup($sanitized['mxchat_auto_delete_transcripts']);
1484 }
1485
1486 // Sanitize auto-email transcript settings
1487 $sanitized['mxchat_auto_email_transcript_enabled'] = isset($input['mxchat_auto_email_transcript_enabled']) ? 1 : 0;
1488
1489 $valid_delays = array('15', '30', '60');
1490 if (isset($input['mxchat_auto_email_transcript_delay'])) {
1491 $sanitized['mxchat_auto_email_transcript_delay'] = in_array($input['mxchat_auto_email_transcript_delay'], $valid_delays)
1492 ? $input['mxchat_auto_email_transcript_delay']
1493 : '30';
1494 } else {
1495 $sanitized['mxchat_auto_email_transcript_delay'] = '30';
1496 }
1497
1498 // Sanitize require contact info setting
1499 $sanitized['mxchat_auto_email_transcript_require_contact'] = isset($input['mxchat_auto_email_transcript_require_contact']) ? 1 : 0;
1500
1501 return $sanitized;
1502 }
1503
1504 /**
1505 * Schedule or unschedule the transcript cleanup cron job
1506 */
1507 public function schedule_transcript_cleanup($interval) {
1508 // Clear any existing scheduled event
1509 $timestamp = wp_next_scheduled('mxchat_cleanup_old_transcripts');
1510 if ($timestamp) {
1511 wp_unschedule_event($timestamp, 'mxchat_cleanup_old_transcripts');
1512 }
1513
1514 // Schedule new event if not set to "never"
1515 if ($interval !== 'never') {
1516 // Schedule to run daily at 3 AM
1517 $next_run = strtotime('tomorrow 3:00 AM');
1518 wp_schedule_event($next_run, 'daily', 'mxchat_cleanup_old_transcripts');
1519 }
1520 }
1521
1522 /**
1523 * Delete old transcripts based on the configured interval
1524 */
1525 public function cleanup_old_transcripts() {
1526 $options = get_option('mxchat_transcripts_options', array());
1527 $interval = isset($options['mxchat_auto_delete_transcripts']) ? $options['mxchat_auto_delete_transcripts'] : 'never';
1528
1529 // If set to never, don't delete anything
1530 if ($interval === 'never') {
1531 return;
1532 }
1533
1534 // Calculate the cutoff date
1535 $days = 0;
1536 switch ($interval) {
1537 case '1week':
1538 $days = 7;
1539 break;
1540 case '2weeks':
1541 $days = 14;
1542 break;
1543 case '1month':
1544 $days = 30;
1545 break;
1546 default:
1547 return; // Invalid interval, don't delete anything
1548 }
1549
1550 global $wpdb;
1551 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1552 $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
1553
1554 // Calculate the cutoff timestamp
1555 $cutoff_date = date('Y-m-d H:i:s', strtotime("-{$days} days"));
1556
1557 // First, get the session IDs that will be deleted (to clean up translations too)
1558 $sessions_to_delete = $wpdb->get_col(
1559 $wpdb->prepare(
1560 "SELECT DISTINCT session_id FROM {$table_name} WHERE timestamp < %s",
1561 $cutoff_date
1562 )
1563 );
1564
1565 // Delete transcripts older than the cutoff date
1566 $deleted = $wpdb->query(
1567 $wpdb->prepare(
1568 "DELETE FROM {$table_name} WHERE timestamp < %s",
1569 $cutoff_date
1570 )
1571 );
1572
1573 // Also delete any translations for the deleted sessions
1574 if (!empty($sessions_to_delete) && $wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table) {
1575 $placeholders = implode(',', array_fill(0, count($sessions_to_delete), '%s'));
1576 $wpdb->query(
1577 $wpdb->prepare(
1578 "DELETE FROM {$translations_table} WHERE session_id IN ($placeholders)",
1579 $sessions_to_delete
1580 )
1581 );
1582 }
1583
1584 // Log the cleanup action
1585 if ($deleted !== false && $deleted > 0) {
1586 //error_log(sprintf('MXChat: Auto-deleted %d transcripts older than %d days', $deleted, $days));
1587 }
1588 }
1589
1590 public function export_chat_transcripts() {
1591 if (!current_user_can('manage_options')) {
1592 wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'mxchat'));
1593 }
1594
1595 check_ajax_referer('mxchat_export_transcripts', 'security');
1596
1597 global $wpdb;
1598 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1599
1600 // Get all transcripts ordered by session and timestamp
1601 $results = $wpdb->get_results(
1602 "SELECT session_id, user_email, user_identifier, role, message, timestamp
1603 FROM {$table_name}
1604 ORDER BY session_id, timestamp ASC"
1605 );
1606
1607 if (empty($results)) {
1608 wp_send_json_error(array('message' => 'No transcripts found.'));
1609 wp_die();
1610 }
1611
1612 // Set headers for CSV download
1613 header('Content-Type: text/csv');
1614 header('Content-Disposition: attachment; filename="chat-transcripts-' . date('Y-m-d') . '.csv"');
1615 header('Pragma: no-cache');
1616 header('Expires: 0');
1617
1618 // Create output stream
1619 $output = fopen('php://output', 'w');
1620
1621 // Add UTF-8 BOM for proper Excel encoding
1622 fputs($output, "\xEF\xBB\xBF");
1623
1624 // Add CSV headers
1625 fputcsv($output, array(
1626 'Session ID',
1627 'Email',
1628 'User Identifier',
1629 'Role',
1630 'Message',
1631 'Timestamp'
1632 ));
1633
1634 // Add data rows
1635 foreach ($results as $row) {
1636 fputcsv($output, array(
1637 $row->session_id,
1638 $row->user_email,
1639 $row->user_identifier,
1640 $row->role,
1641 $row->message,
1642 $row->timestamp
1643 ));
1644 }
1645
1646 fclose($output);
1647 wp_die();
1648 }
1649
1650 // ============================================================================
1651 // Leads tab (inside Transcripts)
1652 //
1653 // Leads are derived from existing data — no dedicated table. Primary source:
1654 // wp_mxchat_chat_transcripts rows where user_email is populated. Secondary
1655 // source: wp_options entries `mxchat_email_{session_id}` / `mxchat_name_{sid}`
1656 // for "orphan" leads who submitted the pre-chat form but never chatted.
1657 // ============================================================================
1658
1659 /**
1660 * Fetch leads: dedup-by-email rows, stats strip, and top pages in one call.
1661 */
1662 public function mxchat_fetch_leads() {
1663 if (!current_user_can('manage_options')) {
1664 wp_send_json_error(['message' => 'Insufficient permissions']);
1665 wp_die();
1666 }
1667
1668 global $wpdb;
1669 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
1670
1671 $page = isset($_POST['page']) ? max(1, absint($_POST['page'])) : 1;
1672 $per_page = isset($_POST['per_page']) ? min(100, max(10, absint($_POST['per_page']))) : 25;
1673 $offset = ($page - 1) * $per_page;
1674 $search = isset($_POST['search']) ? sanitize_text_field(wp_unslash($_POST['search'])) : '';
1675 $date_range = isset($_POST['date_range']) ? sanitize_key($_POST['date_range']) : 'all';
1676 $status = isset($_POST['status']) ? sanitize_key($_POST['status']) : 'all';
1677 $page_filter = isset($_POST['page_url']) ? esc_url_raw(wp_unslash($_POST['page_url'])) : '';
1678 $sort = isset($_POST['sort']) ? sanitize_key($_POST['sort']) : 'last_seen';
1679 $sort_dir = (isset($_POST['sort_dir']) && $_POST['sort_dir'] === 'asc') ? 'ASC' : 'DESC';
1680
1681 $date_cutoff = self::mxchat_leads_date_cutoff($date_range);
1682 $has_page_url_column = !empty($wpdb->get_results("SHOW COLUMNS FROM $table LIKE 'originating_page_url'"));
1683
1684 // Base WHERE for transcripts leads.
1685 $where_clauses = ["user_email IS NOT NULL", "user_email != ''"];
1686 $where_params = [];
1687
1688 if ($date_cutoff) {
1689 $where_clauses[] = 'timestamp >= %s';
1690 $where_params[] = $date_cutoff;
1691 }
1692 if ($page_filter && $has_page_url_column) {
1693 $where_clauses[] = 'originating_page_url = %s';
1694 $where_params[] = $page_filter;
1695 }
1696 if ($search !== '') {
1697 $like = '%' . $wpdb->esc_like($search) . '%';
1698 $where_clauses[] = '(user_email LIKE %s OR user_name LIKE %s)';
1699 $where_params[] = $like;
1700 $where_params[] = $like;
1701 }
1702 $where_sql = 'WHERE ' . implode(' AND ', $where_clauses);
1703
1704 // Aggregate query grouped by email.
1705 $select_sql = $has_page_url_column
1706 ? "SELECT user_email, MAX(timestamp) AS last_seen, MIN(timestamp) AS first_seen,
1707 COUNT(DISTINCT session_id) AS conversation_count"
1708 : "SELECT user_email, MAX(timestamp) AS last_seen, MIN(timestamp) AS first_seen,
1709 COUNT(DISTINCT session_id) AS conversation_count";
1710
1711 $order_column = in_array($sort, ['last_seen', 'conversation_count', 'first_seen'], true) ? $sort : 'last_seen';
1712 $group_order_limit = " GROUP BY user_email ORDER BY {$order_column} {$sort_dir} LIMIT %d OFFSET %d";
1713
1714 $transcripts_sql = $wpdb->prepare(
1715 "{$select_sql} FROM {$table} {$where_sql}{$group_order_limit}",
1716 array_merge($where_params, [$per_page, $offset])
1717 );
1718 $transcript_rows = $wpdb->get_results($transcripts_sql);
1719
1720 // Count of unique transcript-based leads under the same filters.
1721 $count_sql = $wpdb->prepare(
1722 "SELECT COUNT(DISTINCT user_email) FROM {$table} {$where_sql}",
1723 $where_params
1724 );
1725 $transcripts_lead_count = (int) $wpdb->get_var($count_sql);
1726
1727 // Hydrate each row: name, latest_session_id, top page.
1728 $leads = [];
1729 foreach ($transcript_rows as $row) {
1730 $detail = $has_page_url_column
1731 ? $wpdb->get_row($wpdb->prepare(
1732 "SELECT session_id, user_name, originating_page_url, originating_page_title
1733 FROM {$table} WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
1734 $row->user_email
1735 ))
1736 : $wpdb->get_row($wpdb->prepare(
1737 "SELECT session_id, user_name FROM {$table}
1738 WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
1739 $row->user_email
1740 ));
1741
1742 $leads[] = [
1743 'email' => $row->user_email,
1744 'name' => isset($detail->user_name) ? (string) $detail->user_name : '',
1745 'conversation_count' => (int) $row->conversation_count,
1746 'last_seen' => $row->last_seen,
1747 'last_seen_display' => self::mxchat_leads_format_relative($row->last_seen),
1748 'first_seen' => $row->first_seen,
1749 'latest_session_id' => isset($detail->session_id) ? $detail->session_id : '',
1750 'top_page_url' => isset($detail->originating_page_url) ? $detail->originating_page_url : '',
1751 'top_page_title' => isset($detail->originating_page_title) ? $detail->originating_page_title : '',
1752 'is_orphan' => false,
1753 'status' => 'active',
1754 ];
1755 }
1756
1757 // Non-transcript lead sources. Built once here, then filtered/merged based on the
1758 // status filter below. Deduplication priority when the same email appears in multiple
1759 // sources: transcripts > chat_deleted > orphan.
1760 $transcripts_emails_seen = array_flip(array_map(
1761 function ($r) { return strtolower($r['email']); },
1762 $leads
1763 ));
1764
1765 // Chat-deleted leads: had a conversation that an admin removed. Preserved via
1766 // mxchat_lead_del_* options, with timestamps so they respect date filters.
1767 $chat_deleted_leads_all = [];
1768 if ($status === 'all' || $status === 'chat_deleted') {
1769 $chat_deleted_leads_all = self::mxchat_collect_chat_deleted_leads($search, $date_cutoff);
1770 // Dedup: drop any chat_deleted row whose email is already in the transcripts set.
1771 $chat_deleted_leads_all = array_values(array_filter(
1772 $chat_deleted_leads_all,
1773 function ($row) use ($transcripts_emails_seen) {
1774 return !isset($transcripts_emails_seen[strtolower($row['email'])]);
1775 }
1776 ));
1777 foreach ($chat_deleted_leads_all as $row) {
1778 $transcripts_emails_seen[strtolower($row['email'])] = true;
1779 }
1780 }
1781
1782 // Orphans: pre-chat form captures with no conversation. No timestamp, so skipped
1783 // when a date filter is active.
1784 $orphan_leads_all = [];
1785 if (($status === 'all' || $status === 'orphan') && !$date_cutoff && !$page_filter) {
1786 $orphan_leads_all = self::mxchat_collect_orphan_leads($search);
1787 $orphan_leads_all = array_values(array_filter(
1788 $orphan_leads_all,
1789 function ($row) use ($transcripts_emails_seen) {
1790 return !isset($transcripts_emails_seen[strtolower($row['email'])]);
1791 }
1792 ));
1793 }
1794
1795 // Apply status filter to the transcripts-derived list.
1796 if ($status === 'orphan' || $status === 'chat_deleted') {
1797 $leads = [];
1798 $transcripts_lead_count = 0;
1799 }
1800
1801 // Stitch the current page from the three buckets in priority order.
1802 $total_count = $transcripts_lead_count + count($chat_deleted_leads_all) + count($orphan_leads_all);
1803 $remaining_slots = $per_page - count($leads);
1804
1805 if ($remaining_slots > 0 && !empty($chat_deleted_leads_all)) {
1806 $start = max(0, ($page - 1) * $per_page - $transcripts_lead_count);
1807 if ($start < count($chat_deleted_leads_all)) {
1808 $leads = array_merge($leads, array_slice($chat_deleted_leads_all, $start, $remaining_slots));
1809 $remaining_slots = $per_page - count($leads);
1810 }
1811 }
1812
1813 if ($remaining_slots > 0 && !empty($orphan_leads_all)) {
1814 $before = $transcripts_lead_count + count($chat_deleted_leads_all);
1815 $start = max(0, ($page - 1) * $per_page - $before);
1816 if ($start < count($orphan_leads_all)) {
1817 $leads = array_merge($leads, array_slice($orphan_leads_all, $start, $remaining_slots));
1818 }
1819 }
1820
1821 $total_pages = $per_page > 0 ? (int) ceil($total_count / $per_page) : 1;
1822
1823 // Stats strip: always computed over full dataset, unaffected by filters.
1824 $stats = self::mxchat_leads_stats($table, $has_page_url_column);
1825
1826 // Top pages: top 5 by distinct emails captured.
1827 $top_pages = [];
1828 if ($has_page_url_column) {
1829 $top_pages_rows = $wpdb->get_results(
1830 "SELECT originating_page_url AS url,
1831 MAX(originating_page_title) AS title,
1832 COUNT(DISTINCT user_email) AS lead_count
1833 FROM {$table}
1834 WHERE user_email IS NOT NULL AND user_email != ''
1835 AND originating_page_url IS NOT NULL AND originating_page_url != ''
1836 GROUP BY originating_page_url
1837 ORDER BY lead_count DESC, url ASC
1838 LIMIT 5"
1839 );
1840 foreach ($top_pages_rows as $p) {
1841 $top_pages[] = [
1842 'url' => $p->url,
1843 'title' => $p->title ?: $p->url,
1844 'lead_count' => (int) $p->lead_count,
1845 ];
1846 }
1847 }
1848
1849 wp_send_json([
1850 'success' => true,
1851 'leads' => $leads,
1852 'page' => $page,
1853 'per_page' => $per_page,
1854 'total_count' => $total_count,
1855 'total_pages' => $total_pages,
1856 'showing_start' => $total_count === 0 ? 0 : ($offset + 1),
1857 'showing_end' => min($offset + $per_page, $total_count),
1858 'stats' => $stats,
1859 'top_pages' => $top_pages,
1860 ]);
1861 wp_die();
1862 }
1863
1864 /**
1865 * Delete one or more leads by email. Removes every transcripts row for that
1866 * email and cleans up related wp_options (mxchat_email_{sid}, mxchat_name_{sid},
1867 * mxchat_history_{sid}) and any orphan option entries matching the email.
1868 */
1869 public function mxchat_delete_leads() {
1870 if (!current_user_can('manage_options')) {
1871 wp_send_json_error(['message' => 'Insufficient permissions']);
1872 wp_die();
1873 }
1874 check_ajax_referer('mxchat_delete_leads', 'security');
1875
1876 $emails_raw = isset($_POST['emails']) ? (array) wp_unslash($_POST['emails']) : [];
1877 $emails = [];
1878 foreach ($emails_raw as $e) {
1879 $clean = sanitize_email((string) $e);
1880 if ($clean) {
1881 $emails[] = $clean;
1882 }
1883 }
1884 if (empty($emails)) {
1885 wp_send_json_error(['message' => 'No emails provided']);
1886 wp_die();
1887 }
1888
1889 $summary = self::mxchat_wipe_leads_by_email($emails);
1890
1891 wp_send_json([
1892 'success' => true,
1893 'deleted_leads' => count($emails),
1894 'deleted_sessions' => $summary['deleted_sessions'],
1895 'deleted_rows' => $summary['deleted_rows'],
1896 ]);
1897 wp_die();
1898 }
1899
1900 /**
1901 * Fully wipe one or more leads by email: every transcripts row, every related wp_options
1902 * entry (history, pre-chat capture, chat_deleted preservation, agent name, translations).
1903 *
1904 * Shared between the Leads-tab Delete button and the transcript-delete opt-in checkbox.
1905 * Input emails must already be sanitized with sanitize_email().
1906 */
1907 private static function mxchat_wipe_leads_by_email(array $emails) {
1908 global $wpdb;
1909 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
1910 $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
1911 $has_translations = $wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table;
1912
1913 $deleted_sessions = 0;
1914 $deleted_rows = 0;
1915 $emails_lc = array_map('strtolower', $emails);
1916
1917 foreach ($emails as $email) {
1918 $session_ids = $wpdb->get_col($wpdb->prepare(
1919 "SELECT DISTINCT session_id FROM {$table} WHERE user_email = %s",
1920 $email
1921 ));
1922
1923 $rows_removed = $wpdb->delete($table, ['user_email' => $email], ['%s']);
1924 if ($rows_removed !== false) {
1925 $deleted_rows += (int) $rows_removed;
1926 }
1927
1928 foreach ($session_ids as $sid) {
1929 $deleted_sessions++;
1930 wp_cache_delete('chat_session_' . $sid, 'mxchat_chat_sessions');
1931 delete_option('mxchat_history_' . $sid);
1932 delete_option('mxchat_email_' . $sid);
1933 delete_option('mxchat_name_' . $sid);
1934 delete_option('mxchat_agent_name_' . $sid);
1935 delete_option('mxchat_lead_del_email_' . $sid);
1936 delete_option('mxchat_lead_del_name_' . $sid);
1937 delete_option('mxchat_lead_del_ts_' . $sid);
1938 if ($has_translations) {
1939 $wpdb->delete($translations_table, ['session_id' => $sid], ['%s']);
1940 }
1941 }
1942 }
1943
1944 // Clean up any lingering option entries (orphan pre-chat captures + chat_deleted
1945 // preservations) whose stored value matches one of the emails being wiped.
1946 $lingering = $wpdb->get_results(
1947 "SELECT option_name, option_value FROM {$wpdb->options}
1948 WHERE option_name LIKE 'mxchat_email_%' OR option_name LIKE 'mxchat_lead_del_email_%'"
1949 );
1950 foreach ($lingering as $opt) {
1951 if (!in_array(strtolower(trim($opt->option_value)), $emails_lc, true)) {
1952 continue;
1953 }
1954 if (strpos($opt->option_name, 'mxchat_lead_del_email_') === 0) {
1955 $sid = substr($opt->option_name, strlen('mxchat_lead_del_email_'));
1956 delete_option('mxchat_lead_del_email_' . $sid);
1957 delete_option('mxchat_lead_del_name_' . $sid);
1958 delete_option('mxchat_lead_del_ts_' . $sid);
1959 } else {
1960 $sid = substr($opt->option_name, strlen('mxchat_email_'));
1961 delete_option('mxchat_email_' . $sid);
1962 delete_option('mxchat_name_' . $sid);
1963 }
1964 }
1965
1966 wp_cache_delete('all_chat_sessions', 'mxchat_chat_sessions');
1967
1968 return [
1969 'deleted_sessions' => $deleted_sessions,
1970 'deleted_rows' => $deleted_rows,
1971 ];
1972 }
1973
1974 /**
1975 * Stream a leads CSV. scope=all exports every lead under current filters is not
1976 * supported to keep semantics simple; caller either exports all leads or a
1977 * specific set of selected emails.
1978 */
1979 public function mxchat_export_leads() {
1980 if (!current_user_can('manage_options')) {
1981 wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'mxchat'));
1982 }
1983 check_ajax_referer('mxchat_export_leads', 'security');
1984
1985 $scope = isset($_POST['scope']) ? sanitize_key($_POST['scope']) : 'all';
1986 $fields_mode = isset($_POST['fields']) ? sanitize_key($_POST['fields']) : 'email_and_name';
1987 $emails_in = isset($_POST['emails']) ? (array) wp_unslash($_POST['emails']) : [];
1988
1989 $emails_in_clean = [];
1990 foreach ($emails_in as $e) {
1991 $clean = sanitize_email((string) $e);
1992 if ($clean) {
1993 $emails_in_clean[] = $clean;
1994 }
1995 }
1996
1997 global $wpdb;
1998 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
1999 $has_page_url_column = !empty($wpdb->get_results("SHOW COLUMNS FROM $table LIKE 'originating_page_url'"));
2000
2001 // Collect leads from transcripts.
2002 $transcripts_sql = "SELECT user_email AS email,
2003 MAX(timestamp) AS last_seen,
2004 COUNT(DISTINCT session_id) AS conversation_count
2005 FROM {$table}
2006 WHERE user_email IS NOT NULL AND user_email != ''";
2007 $params = [];
2008 if ($scope === 'selected' && !empty($emails_in_clean)) {
2009 $placeholders = implode(',', array_fill(0, count($emails_in_clean), '%s'));
2010 $transcripts_sql .= " AND user_email IN ({$placeholders})";
2011 $params = $emails_in_clean;
2012 }
2013 $transcripts_sql .= " GROUP BY user_email ORDER BY last_seen DESC";
2014
2015 $rows = !empty($params)
2016 ? $wpdb->get_results($wpdb->prepare($transcripts_sql, $params))
2017 : $wpdb->get_results($transcripts_sql);
2018
2019 // Hydrate each row with name + top page.
2020 $export_rows = [];
2021 foreach ($rows as $row) {
2022 $detail = $has_page_url_column
2023 ? $wpdb->get_row($wpdb->prepare(
2024 "SELECT user_name, originating_page_url FROM {$table}
2025 WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
2026 $row->email
2027 ))
2028 : $wpdb->get_row($wpdb->prepare(
2029 "SELECT user_name FROM {$table}
2030 WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
2031 $row->email
2032 ));
2033 $export_rows[] = [
2034 'email' => $row->email,
2035 'name' => isset($detail->user_name) ? (string) $detail->user_name : '',
2036 'conversation_count' => (int) $row->conversation_count,
2037 'last_seen' => $row->last_seen,
2038 'top_page_url' => isset($detail->originating_page_url) ? $detail->originating_page_url : '',
2039 ];
2040 }
2041
2042 // Include orphan + chat_deleted leads when exporting all.
2043 if ($scope === 'all') {
2044 $transcripts_emails_lc = array_flip(array_map(
2045 function ($r) { return strtolower($r['email']); },
2046 $export_rows
2047 ));
2048 foreach (self::mxchat_collect_chat_deleted_leads('') as $cd) {
2049 if (isset($transcripts_emails_lc[strtolower($cd['email'])])) continue;
2050 $transcripts_emails_lc[strtolower($cd['email'])] = true;
2051 $export_rows[] = [
2052 'email' => $cd['email'],
2053 'name' => $cd['name'],
2054 'conversation_count' => 0,
2055 'last_seen' => $cd['last_seen'],
2056 'top_page_url' => '',
2057 ];
2058 }
2059 foreach (self::mxchat_collect_orphan_leads('') as $orphan) {
2060 if (isset($transcripts_emails_lc[strtolower($orphan['email'])])) continue;
2061 $transcripts_emails_lc[strtolower($orphan['email'])] = true;
2062 $export_rows[] = [
2063 'email' => $orphan['email'],
2064 'name' => $orphan['name'],
2065 'conversation_count' => 0,
2066 'last_seen' => '',
2067 'top_page_url' => '',
2068 ];
2069 }
2070 }
2071
2072 if (empty($export_rows)) {
2073 wp_send_json_error(['message' => 'No leads to export.']);
2074 wp_die();
2075 }
2076
2077 $filename = 'mxchat-leads-' . date('Y-m-d') . '.csv';
2078 header('Content-Type: text/csv');
2079 header('Content-Disposition: attachment; filename="' . $filename . '"');
2080 header('Pragma: no-cache');
2081 header('Expires: 0');
2082
2083 $output = fopen('php://output', 'w');
2084 fputs($output, "\xEF\xBB\xBF"); // UTF-8 BOM for Excel
2085
2086 if ($fields_mode === 'email_only') {
2087 fputcsv($output, ['Email']);
2088 foreach ($export_rows as $r) {
2089 fputcsv($output, [$r['email']]);
2090 }
2091 } else {
2092 fputcsv($output, ['Email', 'Name', 'Conversations', 'Last seen', 'Top page']);
2093 foreach ($export_rows as $r) {
2094 fputcsv($output, [
2095 $r['email'],
2096 $r['name'],
2097 $r['conversation_count'],
2098 $r['last_seen'],
2099 $r['top_page_url'],
2100 ]);
2101 }
2102 }
2103
2104 fclose($output);
2105 wp_die();
2106 }
2107
2108 /**
2109 * Stats strip payload (independent of filters).
2110 */
2111 private static function mxchat_leads_stats($table, $has_page_url_column) {
2112 global $wpdb;
2113
2114 $total_transcripts_emails = (int) $wpdb->get_var(
2115 "SELECT COUNT(DISTINCT user_email) FROM {$table}
2116 WHERE user_email IS NOT NULL AND user_email != ''"
2117 );
2118
2119 $new_this_week = (int) $wpdb->get_var($wpdb->prepare(
2120 "SELECT COUNT(*) FROM (
2121 SELECT user_email FROM {$table}
2122 WHERE user_email IS NOT NULL AND user_email != ''
2123 GROUP BY user_email
2124 HAVING MIN(timestamp) >= %s
2125 ) AS new_leads",
2126 gmdate('Y-m-d H:i:s', strtotime('-7 days'))
2127 ));
2128
2129 $total_convos = (int) $wpdb->get_var(
2130 "SELECT COUNT(DISTINCT session_id) FROM {$table}
2131 WHERE user_email IS NOT NULL AND user_email != ''"
2132 );
2133
2134 $orphan_count = count(self::mxchat_collect_orphan_leads(''));
2135 $chat_deleted_count = self::mxchat_count_chat_deleted_leads();
2136
2137 // Total leads = unique emails across all three sources (dedup priority: transcripts > chat_deleted > orphan
2138 // is already enforced at collection time in mxchat_fetch_leads; stats re-apply it here).
2139 $total_leads = $total_transcripts_emails + $chat_deleted_count + $orphan_count;
2140
2141 $avg = $total_transcripts_emails > 0
2142 ? round($total_convos / $total_transcripts_emails, 1)
2143 : 0;
2144
2145 // Orphan % reflects *true* orphans only (pre-chat dropoffs). Chat-deleted leads are
2146 // excluded so the metric stays meaningful — admins shouldn't see their cleanups
2147 // inflate this number.
2148 $orphan_pct = $total_leads > 0
2149 ? (int) round(($orphan_count / $total_leads) * 100)
2150 : 0;
2151
2152 return [
2153 'total_leads' => $total_leads,
2154 'new_this_week' => $new_this_week,
2155 'avg_convos' => $avg,
2156 'orphan_pct' => $orphan_pct,
2157 'orphan_count' => $orphan_count,
2158 'chat_deleted_count' => $chat_deleted_count,
2159 ];
2160 }
2161
2162 /**
2163 * Collect leads who had a conversation that an admin later deleted (preserved via
2164 * mxchat_lead_del_* options). Returns rows tagged status='chat_deleted' with the
2165 * original last-seen timestamp so they still sort and filter sensibly.
2166 *
2167 * @param string $search Optional email/name substring filter.
2168 * @param string $date_cutoff Optional 'Y-m-d H:i:s' cutoff — only rows with last_ts >= cutoff.
2169 * @return array
2170 */
2171 private static function mxchat_collect_chat_deleted_leads($search = '', $date_cutoff = '') {
2172 global $wpdb;
2173 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2174
2175 $rows = $wpdb->get_results(
2176 "SELECT option_name, option_value FROM {$wpdb->options}
2177 WHERE option_name LIKE 'mxchat_lead_del_email_%'"
2178 );
2179 if (empty($rows)) {
2180 return [];
2181 }
2182
2183 // Emails that currently have transcripts rows should not appear as chat_deleted —
2184 // they've come back and chatted, so they're active leads again.
2185 $emails_in_transcripts = array_map(
2186 'strtolower',
2187 (array) $wpdb->get_col(
2188 "SELECT DISTINCT user_email FROM {$table}
2189 WHERE user_email IS NOT NULL AND user_email != ''"
2190 )
2191 );
2192 $emails_in_transcripts = array_flip($emails_in_transcripts);
2193
2194 $needle = strtolower(trim((string) $search));
2195 $by_email = [];
2196
2197 foreach ($rows as $opt) {
2198 $email = sanitize_email(trim((string) $opt->option_value));
2199 if (!$email) {
2200 continue;
2201 }
2202 if (isset($emails_in_transcripts[strtolower($email)])) {
2203 continue;
2204 }
2205 $sid = substr($opt->option_name, strlen('mxchat_lead_del_email_'));
2206 if (!$sid) {
2207 continue;
2208 }
2209 $name = (string) get_option('mxchat_lead_del_name_' . $sid, '');
2210 $ts = (string) get_option('mxchat_lead_del_ts_' . $sid, '');
2211
2212 if ($date_cutoff !== '' && ($ts === '' || $ts < $date_cutoff)) {
2213 continue;
2214 }
2215 if ($needle !== '') {
2216 $hay = strtolower($email . ' ' . $name);
2217 if (strpos($hay, $needle) === false) {
2218 continue;
2219 }
2220 }
2221
2222 $key = strtolower($email);
2223 if (!isset($by_email[$key]) || (isset($by_email[$key]['last_seen']) && $ts > $by_email[$key]['last_seen'])) {
2224 $by_email[$key] = [
2225 'email' => $email,
2226 'name' => $name,
2227 'conversation_count' => 0,
2228 'last_seen' => $ts,
2229 'last_seen_display' => $ts ? self::mxchat_leads_format_relative($ts) : __('Chat deleted', 'mxchat'),
2230 'first_seen' => $ts,
2231 'latest_session_id' => '',
2232 'top_page_url' => '',
2233 'top_page_title' => '',
2234 'is_orphan' => false,
2235 'status' => 'chat_deleted',
2236 ];
2237 }
2238 }
2239
2240 // Newest chat_deleted first.
2241 usort($by_email, function ($a, $b) {
2242 return strcmp((string) $b['last_seen'], (string) $a['last_seen']);
2243 });
2244 return array_values($by_email);
2245 }
2246
2247 /**
2248 * Count unique emails preserved as "chat deleted" (for the stats strip).
2249 */
2250 private static function mxchat_count_chat_deleted_leads() {
2251 global $wpdb;
2252 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2253
2254 $emails = $wpdb->get_col(
2255 "SELECT DISTINCT option_value FROM {$wpdb->options}
2256 WHERE option_name LIKE 'mxchat_lead_del_email_%'"
2257 );
2258 if (empty($emails)) {
2259 return 0;
2260 }
2261
2262 $transcripts_emails = array_map(
2263 'strtolower',
2264 (array) $wpdb->get_col(
2265 "SELECT DISTINCT user_email FROM {$table}
2266 WHERE user_email IS NOT NULL AND user_email != ''"
2267 )
2268 );
2269 $transcripts_emails = array_flip($transcripts_emails);
2270
2271 $count = 0;
2272 $seen = [];
2273 foreach ($emails as $raw) {
2274 $email = strtolower(trim((string) $raw));
2275 if (!$email || isset($seen[$email]) || isset($transcripts_emails[$email])) {
2276 continue;
2277 }
2278 $seen[$email] = true;
2279 $count++;
2280 }
2281 return $count;
2282 }
2283
2284 /**
2285 * Find leads who submitted the pre-chat form but never produced a transcripts row.
2286 * Returned rows have no conversation_count, no timestamp.
2287 *
2288 * @param string $search Optional email/name substring filter.
2289 * @return array
2290 */
2291 private static function mxchat_collect_orphan_leads($search = '') {
2292 global $wpdb;
2293 $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2294
2295 $option_rows = $wpdb->get_results(
2296 "SELECT option_name, option_value FROM {$wpdb->options}
2297 WHERE option_name LIKE 'mxchat_email_%'"
2298 );
2299 if (empty($option_rows)) {
2300 return [];
2301 }
2302
2303 // Collect all session_ids that have real transcripts rows so we can exclude them.
2304 $session_ids_with_rows = $wpdb->get_col(
2305 "SELECT DISTINCT session_id FROM {$table}
2306 WHERE user_email IS NOT NULL AND user_email != ''"
2307 );
2308 $session_ids_with_rows = array_flip($session_ids_with_rows);
2309
2310 // Seen emails in transcripts (so orphans only include truly never-chatted leads).
2311 $emails_in_transcripts = array_map(
2312 'strtolower',
2313 (array) $wpdb->get_col(
2314 "SELECT DISTINCT user_email FROM {$table}
2315 WHERE user_email IS NOT NULL AND user_email != ''"
2316 )
2317 );
2318 $emails_in_transcripts = array_flip($emails_in_transcripts);
2319
2320 $orphans_by_email = [];
2321 $needle = strtolower(trim((string) $search));
2322
2323 foreach ($option_rows as $opt) {
2324 $email = sanitize_email(trim((string) $opt->option_value));
2325 if (!$email) {
2326 continue;
2327 }
2328 $sid = substr($opt->option_name, strlen('mxchat_email_'));
2329 if (!$sid) {
2330 continue;
2331 }
2332 // Exclude leads who have any transcripts rows (they appear in the main list).
2333 if (isset($emails_in_transcripts[strtolower($email)])) {
2334 continue;
2335 }
2336 if (isset($session_ids_with_rows[$sid])) {
2337 continue;
2338 }
2339
2340 $name_option = get_option('mxchat_name_' . $sid, '');
2341 $name = is_string($name_option) ? trim($name_option) : '';
2342
2343 if ($needle !== '') {
2344 $hay = strtolower($email . ' ' . $name);
2345 if (strpos($hay, $needle) === false) {
2346 continue;
2347 }
2348 }
2349
2350 $key = strtolower($email);
2351 if (!isset($orphans_by_email[$key])) {
2352 $orphans_by_email[$key] = [
2353 'email' => $email,
2354 'name' => $name,
2355 'conversation_count' => 0,
2356 'last_seen' => '',
2357 'last_seen_display' => __('No conversation yet', 'mxchat'),
2358 'first_seen' => '',
2359 'latest_session_id' => '',
2360 'top_page_url' => '',
2361 'top_page_title' => '',
2362 'is_orphan' => true,
2363 'status' => 'orphan',
2364 ];
2365 }
2366 }
2367
2368 return array_values($orphans_by_email);
2369 }
2370
2371 /**
2372 * Map a date_range key to a SQL-comparable cutoff string, or '' for all-time.
2373 */
2374 private static function mxchat_leads_date_cutoff($date_range) {
2375 switch ($date_range) {
2376 case 'today': return gmdate('Y-m-d H:i:s', strtotime('-24 hours'));
2377 case '7d': return gmdate('Y-m-d H:i:s', strtotime('-7 days'));
2378 case '30d': return gmdate('Y-m-d H:i:s', strtotime('-30 days'));
2379 case '90d': return gmdate('Y-m-d H:i:s', strtotime('-90 days'));
2380 case 'all':
2381 default: return '';
2382 }
2383 }
2384
2385 /**
2386 * Turn a UTC timestamp into a short relative display like "2h ago" or "Apr 12".
2387 */
2388 private static function mxchat_leads_format_relative($timestamp) {
2389 if (!$timestamp) {
2390 return '';
2391 }
2392 $ts = strtotime($timestamp . ' UTC');
2393 if (!$ts) {
2394 return '';
2395 }
2396 $diff = time() - $ts;
2397 if ($diff < 60) return __('just now', 'mxchat');
2398 if ($diff < 3600) return floor($diff / 60) . __('m ago', 'mxchat');
2399 if ($diff < 86400) return floor($diff / 3600) . __('h ago', 'mxchat');
2400 if ($diff < 604800) return floor($diff / 86400) . __('d ago', 'mxchat');
2401 return wp_date('M j', $ts);
2402 }
2403
2404 /**
2405 * Handle translation of chat messages via AJAX
2406 */
2407 public function mxchat_translate_messages() {
2408 if (!current_user_can('manage_options')) {
2409 wp_send_json_error(['error' => 'Insufficient permissions']);
2410 wp_die();
2411 }
2412
2413 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
2414 $target_lang = isset($_POST['target_lang']) ? sanitize_text_field($_POST['target_lang']) : 'en';
2415 $messages_json = isset($_POST['messages']) ? wp_unslash($_POST['messages']) : '[]';
2416 $messages = json_decode($messages_json, true);
2417
2418 if (empty($session_id)) {
2419 wp_send_json_error(['error' => 'No session ID provided']);
2420 wp_die();
2421 }
2422
2423 if (empty($messages) || !is_array($messages)) {
2424 wp_send_json_error(['error' => 'No messages to translate']);
2425 wp_die();
2426 }
2427
2428 // Language names for prompting
2429 $languages = [
2430 'en' => 'English',
2431 'es' => 'Spanish',
2432 'fr' => 'French',
2433 'de' => 'German',
2434 'it' => 'Italian',
2435 'pt' => 'Portuguese',
2436 'nl' => 'Dutch',
2437 'ru' => 'Russian',
2438 'zh' => 'Chinese',
2439 'ja' => 'Japanese',
2440 'ko' => 'Korean',
2441 'ar' => 'Arabic',
2442 'hi' => 'Hindi',
2443 'tr' => 'Turkish',
2444 'pl' => 'Polish',
2445 'vi' => 'Vietnamese',
2446 'th' => 'Thai',
2447 'id' => 'Indonesian',
2448 'sv' => 'Swedish',
2449 'da' => 'Danish'
2450 ];
2451
2452 $target_lang_name = isset($languages[$target_lang]) ? $languages[$target_lang] : 'English';
2453
2454 // Build combined text for translation (numbered for parsing)
2455 $numbered_messages = [];
2456 foreach ($messages as $i => $msg) {
2457 $content = isset($msg['content']) ? trim($msg['content']) : '';
2458 if (!empty($content)) {
2459 $numbered_messages[] = "[MSG" . $i . "]" . $content . "[/MSG" . $i . "]";
2460 }
2461 }
2462
2463 if (empty($numbered_messages)) {
2464 wp_send_json_error(['error' => 'No valid messages to translate']);
2465 wp_die();
2466 }
2467
2468 $combined_text = implode("\n\n", $numbered_messages);
2469
2470 // Prepare the translation prompt
2471 $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.";
2472
2473 // Get user's selected model and determine provider
2474 $options = get_option('mxchat_options', []);
2475 $selected_model = $options['model'] ?? 'gpt-5.1-chat-latest';
2476
2477 // Check if using OpenRouter
2478 if ($selected_model === 'openrouter') {
2479 $provider = 'openrouter';
2480 $selected_model = $options['openrouter_selected_model'] ?? '';
2481 $api_key = $options['openrouter_api_key'] ?? '';
2482
2483 if (empty($selected_model)) {
2484 wp_send_json_error(['error' => 'No OpenRouter model selected']);
2485 wp_die();
2486 }
2487 } else {
2488 // Determine provider from model name
2489 $model_parts = explode('-', $selected_model);
2490 $provider = strtolower($model_parts[0]);
2491
2492 // Get the appropriate API key based on provider
2493 $api_key = '';
2494 switch ($provider) {
2495 case 'gpt':
2496 case 'o1':
2497 $api_key = $options['api_key'] ?? '';
2498 break;
2499 case 'claude':
2500 $api_key = $options['claude_api_key'] ?? '';
2501 break;
2502 case 'grok':
2503 $api_key = $options['xai_api_key'] ?? '';
2504 break;
2505 case 'deepseek':
2506 $api_key = $options['deepseek_api_key'] ?? '';
2507 break;
2508 case 'gemini':
2509 $api_key = $options['gemini_api_key'] ?? '';
2510 break;
2511 default:
2512 // Default to OpenAI
2513 $api_key = $options['api_key'] ?? '';
2514 $provider = 'gpt';
2515 break;
2516 }
2517 }
2518
2519 if (empty($api_key)) {
2520 wp_send_json_error(['error' => 'No API key configured for ' . $provider]);
2521 wp_die();
2522 }
2523
2524 // Make API request based on provider
2525 $response = $this->translate_with_provider($provider, $selected_model, $api_key, $system_prompt, $combined_text);
2526
2527 if (is_wp_error($response)) {
2528 wp_send_json_error(['error' => $response->get_error_message()]);
2529 wp_die();
2530 }
2531
2532 // Parse the response to extract translated messages
2533 $translations = [];
2534 foreach ($messages as $msg) {
2535 $index = $msg['index'];
2536 $pattern = '/\[MSG' . $index . '\](.*?)\[\/MSG' . $index . '\]/s';
2537 if (preg_match($pattern, $response, $matches)) {
2538 $translations[] = [
2539 'index' => $index,
2540 'translated' => trim($matches[1])
2541 ];
2542 }
2543 }
2544
2545 // Save translations to database
2546 if (!empty($translations)) {
2547 $this->save_transcript_translation($session_id, $target_lang, $translations);
2548 }
2549
2550 wp_send_json(['success' => true, 'translations' => $translations, 'language' => $target_lang]);
2551 wp_die();
2552 }
2553
2554 /**
2555 * Save transcript translation to database
2556 */
2557 private function save_transcript_translation($session_id, $language_code, $translations) {
2558 global $wpdb;
2559 $table_name = $wpdb->prefix . 'mxchat_transcript_translations';
2560
2561 // Check if table exists, create if not
2562 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") !== $table_name) {
2563 mxchat_create_translations_table();
2564 }
2565
2566 $now = current_time('mysql');
2567 $translations_json = wp_json_encode($translations);
2568
2569 // Use REPLACE to insert or update
2570 $wpdb->query($wpdb->prepare(
2571 "REPLACE INTO $table_name (session_id, language_code, translations, created_at, updated_at)
2572 VALUES (%s, %s, %s, %s, %s)",
2573 $session_id,
2574 $language_code,
2575 $translations_json,
2576 $now,
2577 $now
2578 ));
2579 }
2580
2581 /**
2582 * Get saved translation for a session
2583 */
2584 public function mxchat_get_transcript_translation() {
2585 if (!current_user_can('manage_options')) {
2586 wp_send_json_error(['error' => 'Insufficient permissions']);
2587 wp_die();
2588 }
2589
2590 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
2591
2592 if (empty($session_id)) {
2593 wp_send_json_error(['error' => 'No session ID provided']);
2594 wp_die();
2595 }
2596
2597 global $wpdb;
2598 $table_name = $wpdb->prefix . 'mxchat_transcript_translations';
2599
2600 // Check if table exists
2601 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") !== $table_name) {
2602 wp_send_json(['success' => true, 'has_translation' => false]);
2603 wp_die();
2604 }
2605
2606 // Get the most recent translation for this session
2607 $result = $wpdb->get_row($wpdb->prepare(
2608 "SELECT language_code, translations FROM $table_name WHERE session_id = %s ORDER BY updated_at DESC LIMIT 1",
2609 $session_id
2610 ));
2611
2612 if ($result) {
2613 $translations = json_decode($result->translations, true);
2614 wp_send_json([
2615 'success' => true,
2616 'has_translation' => true,
2617 'language' => $result->language_code,
2618 'translations' => $translations
2619 ]);
2620 } else {
2621 wp_send_json(['success' => true, 'has_translation' => false]);
2622 }
2623 wp_die();
2624 }
2625
2626 /**
2627 * Translate text using the user's selected provider and model
2628 */
2629 private function translate_with_provider($provider, $model, $api_key, $system_prompt, $text) {
2630 switch ($provider) {
2631 case 'claude':
2632 return $this->translate_with_claude($api_key, $model, $system_prompt, $text);
2633 case 'grok':
2634 return $this->translate_with_xai($api_key, $model, $system_prompt, $text);
2635 case 'deepseek':
2636 return $this->translate_with_deepseek($api_key, $model, $system_prompt, $text);
2637 case 'gemini':
2638 return $this->translate_with_gemini($api_key, $model, $system_prompt, $text);
2639 case 'openrouter':
2640 return $this->translate_with_openrouter($api_key, $model, $system_prompt, $text);
2641 case 'gpt':
2642 case 'o1':
2643 default:
2644 return $this->translate_with_openai($api_key, $model, $system_prompt, $text);
2645 }
2646 }
2647
2648 /**
2649 * Translate text using OpenAI API
2650 */
2651 private function translate_with_openai($api_key, $model, $system_prompt, $text) {
2652 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', [
2653 'timeout' => 60,
2654 'headers' => [
2655 'Authorization' => 'Bearer ' . $api_key,
2656 'Content-Type' => 'application/json'
2657 ],
2658 'body' => wp_json_encode([
2659 'model' => $model,
2660 'messages' => [
2661 ['role' => 'system', 'content' => $system_prompt],
2662 ['role' => 'user', 'content' => $text]
2663 ],
2664 'temperature' => 0.3
2665 ])
2666 ]);
2667
2668 if (is_wp_error($response)) {
2669 return $response;
2670 }
2671
2672 $body = json_decode(wp_remote_retrieve_body($response), true);
2673
2674 if (isset($body['error'])) {
2675 return new WP_Error('api_error', $body['error']['message']);
2676 }
2677
2678 if (isset($body['choices'][0]['message']['content'])) {
2679 return $body['choices'][0]['message']['content'];
2680 }
2681
2682 return new WP_Error('api_error', 'Invalid API response');
2683 }
2684
2685 /**
2686 * Translate text using Claude API
2687 */
2688 private function translate_with_claude($api_key, $model, $system_prompt, $text) {
2689 $response = wp_remote_post('https://api.anthropic.com/v1/messages', [
2690 'timeout' => 60,
2691 'headers' => [
2692 'x-api-key' => $api_key,
2693 'anthropic-version' => '2023-06-01',
2694 'Content-Type' => 'application/json'
2695 ],
2696 'body' => wp_json_encode([
2697 'model' => $model,
2698 'max_tokens' => 4096,
2699 'system' => $system_prompt,
2700 'messages' => [
2701 ['role' => 'user', 'content' => $text]
2702 ]
2703 ])
2704 ]);
2705
2706 if (is_wp_error($response)) {
2707 return $response;
2708 }
2709
2710 $body = json_decode(wp_remote_retrieve_body($response), true);
2711
2712 if (isset($body['error'])) {
2713 return new WP_Error('api_error', $body['error']['message']);
2714 }
2715
2716 if (isset($body['content'][0]['text'])) {
2717 return $body['content'][0]['text'];
2718 }
2719
2720 return new WP_Error('api_error', 'Invalid API response');
2721 }
2722
2723 /**
2724 * Translate text using xAI (Grok) API
2725 */
2726 private function translate_with_xai($api_key, $model, $system_prompt, $text) {
2727 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', [
2728 'timeout' => 60,
2729 'headers' => [
2730 'Authorization' => 'Bearer ' . $api_key,
2731 'Content-Type' => 'application/json'
2732 ],
2733 'body' => wp_json_encode([
2734 'model' => $model,
2735 'messages' => [
2736 ['role' => 'system', 'content' => $system_prompt],
2737 ['role' => 'user', 'content' => $text]
2738 ],
2739 'temperature' => 0.3
2740 ])
2741 ]);
2742
2743 if (is_wp_error($response)) {
2744 return $response;
2745 }
2746
2747 $body = json_decode(wp_remote_retrieve_body($response), true);
2748
2749 if (isset($body['error'])) {
2750 return new WP_Error('api_error', $body['error']['message']);
2751 }
2752
2753 if (isset($body['choices'][0]['message']['content'])) {
2754 return $body['choices'][0]['message']['content'];
2755 }
2756
2757 return new WP_Error('api_error', 'Invalid API response');
2758 }
2759
2760 /**
2761 * Translate text using DeepSeek API
2762 */
2763 private function translate_with_deepseek($api_key, $model, $system_prompt, $text) {
2764 $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', [
2765 'timeout' => 60,
2766 'headers' => [
2767 'Authorization' => 'Bearer ' . $api_key,
2768 'Content-Type' => 'application/json'
2769 ],
2770 'body' => wp_json_encode([
2771 'model' => $model,
2772 'messages' => [
2773 ['role' => 'system', 'content' => $system_prompt],
2774 ['role' => 'user', 'content' => $text]
2775 ],
2776 'temperature' => 0.3
2777 ])
2778 ]);
2779
2780 if (is_wp_error($response)) {
2781 return $response;
2782 }
2783
2784 $body = json_decode(wp_remote_retrieve_body($response), true);
2785
2786 if (isset($body['error'])) {
2787 return new WP_Error('api_error', $body['error']['message']);
2788 }
2789
2790 if (isset($body['choices'][0]['message']['content'])) {
2791 return $body['choices'][0]['message']['content'];
2792 }
2793
2794 return new WP_Error('api_error', 'Invalid API response');
2795 }
2796
2797 /**
2798 * Translate text using Google Gemini API
2799 */
2800 private function translate_with_gemini($api_key, $model, $system_prompt, $text) {
2801 $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':generateContent?key=' . $api_key;
2802
2803 $response = wp_remote_post($url, [
2804 'timeout' => 60,
2805 'headers' => [
2806 'Content-Type' => 'application/json'
2807 ],
2808 'body' => wp_json_encode([
2809 'contents' => [
2810 [
2811 'parts' => [
2812 ['text' => $system_prompt . "\n\n" . $text]
2813 ]
2814 ]
2815 ],
2816 'generationConfig' => [
2817 'temperature' => 0.3
2818 ]
2819 ])
2820 ]);
2821
2822 if (is_wp_error($response)) {
2823 return $response;
2824 }
2825
2826 $body = json_decode(wp_remote_retrieve_body($response), true);
2827
2828 if (isset($body['error'])) {
2829 return new WP_Error('api_error', $body['error']['message']);
2830 }
2831
2832 if (isset($body['candidates'][0]['content']['parts'][0]['text'])) {
2833 return $body['candidates'][0]['content']['parts'][0]['text'];
2834 }
2835
2836 return new WP_Error('api_error', 'Invalid API response');
2837 }
2838
2839 /**
2840 * Translate text using OpenRouter API
2841 */
2842 private function translate_with_openrouter($api_key, $model, $system_prompt, $text) {
2843 $response = wp_remote_post('https://openrouter.ai/api/v1/chat/completions', [
2844 'timeout' => 60,
2845 'headers' => [
2846 'Authorization' => 'Bearer ' . $api_key,
2847 'Content-Type' => 'application/json',
2848 'HTTP-Referer' => home_url(),
2849 'X-Title' => 'MxChat Translation'
2850 ],
2851 'body' => wp_json_encode([
2852 'model' => $model,
2853 'messages' => [
2854 ['role' => 'system', 'content' => $system_prompt],
2855 ['role' => 'user', 'content' => $text]
2856 ],
2857 'temperature' => 0.3
2858 ])
2859 ]);
2860
2861 if (is_wp_error($response)) {
2862 return $response;
2863 }
2864
2865 $body = json_decode(wp_remote_retrieve_body($response), true);
2866
2867 if (isset($body['error'])) {
2868 return new WP_Error('api_error', $body['error']['message']);
2869 }
2870
2871 if (isset($body['choices'][0]['message']['content'])) {
2872 return $body['choices'][0]['message']['content'];
2873 }
2874
2875 return new WP_Error('api_error', 'Invalid API response');
2876 }
2877
2878 public function mxchat_fetch_chat_history() {
2879 global $wpdb;
2880 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
2881 $url_clicks_table = $wpdb->prefix . 'mxchat_url_clicks';
2882
2883 if (!current_user_can('manage_options')) {
2884 wp_send_json_error(['message' => 'Insufficient permissions']);
2885 wp_die();
2886 }
2887
2888 $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
2889 $per_page = isset($_POST['per_page']) ? absint($_POST['per_page']) : 50;
2890 $offset = ($page - 1) * $per_page;
2891 $search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
2892 $sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] === 'asc' ? 'ASC' : 'DESC';
2893
2894 // Build search condition
2895 $search_condition = '';
2896 $search_params = [];
2897 if (!empty($search)) {
2898 $search_condition = "WHERE (
2899 session_id LIKE %s
2900 OR user_email LIKE %s
2901 OR user_name LIKE %s
2902 OR user_identifier LIKE %s
2903 OR message LIKE %s
2904 )";
2905 $search_params = array_fill(0, 5, '%' . $wpdb->esc_like($search) . '%');
2906 }
2907
2908 // Get total count
2909 $count_query = !empty($search)
2910 ? $wpdb->prepare("SELECT COUNT(DISTINCT session_id) FROM {$table_name} {$search_condition}", $search_params)
2911 : "SELECT COUNT(DISTINCT session_id) FROM {$table_name}";
2912 $total_sessions = (int) $wpdb->get_var($count_query);
2913
2914 // Get session IDs for current page (sorted by newest or oldest)
2915 $session_query = !empty($search)
2916 ? $wpdb->prepare(
2917 "SELECT DISTINCT t.session_id FROM {$table_name} t {$search_condition}
2918 GROUP BY t.session_id ORDER BY MAX(t.timestamp) {$sort_order} LIMIT %d OFFSET %d",
2919 array_merge($search_params, [$per_page, $offset])
2920 )
2921 : $wpdb->prepare(
2922 "SELECT DISTINCT session_id FROM {$table_name}
2923 GROUP BY session_id ORDER BY MAX(timestamp) {$sort_order} LIMIT %d OFFSET %d",
2924 $per_page, $offset
2925 );
2926 $session_ids = $wpdb->get_col($session_query);
2927
2928 $total_pages = ceil($total_sessions / $per_page);
2929
2930 if (empty($session_ids)) {
2931 wp_send_json([
2932 'success' => true,
2933 'sessions' => [],
2934 'page' => $page,
2935 'total_pages' => 0,
2936 'total_sessions' => 0,
2937 'showing_start' => 0,
2938 'showing_end' => 0
2939 ]);
2940 wp_die();
2941 }
2942
2943 // Check for optional columns/tables
2944 $url_table_exists = $wpdb->get_var("SHOW TABLES LIKE '$url_clicks_table'") === $url_clicks_table;
2945 $originating_columns_exist = !empty($wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'"));
2946
2947 // Build session list data
2948 $sessions = [];
2949 foreach ($session_ids as $session_id) {
2950 // Get session metadata
2951 $session_data = $originating_columns_exist
2952 ? $wpdb->get_row($wpdb->prepare(
2953 "SELECT user_email, user_name, user_identifier, originating_page_url, originating_page_title, timestamp
2954 FROM {$table_name} WHERE session_id = %s ORDER BY timestamp ASC LIMIT 1",
2955 $session_id
2956 ))
2957 : $wpdb->get_row($wpdb->prepare(
2958 "SELECT user_email, user_name, user_identifier, timestamp FROM {$table_name}
2959 WHERE session_id = %s LIMIT 1",
2960 $session_id
2961 ));
2962
2963 // Get message count and latest timestamp
2964 $message_stats = $wpdb->get_row($wpdb->prepare(
2965 "SELECT COUNT(*) as count, MAX(timestamp) as latest FROM {$table_name} WHERE session_id = %s",
2966 $session_id
2967 ));
2968
2969 // Get first user message as preview
2970 $first_user_msg = $wpdb->get_var($wpdb->prepare(
2971 "SELECT message FROM {$table_name} WHERE session_id = %s AND role = 'user' ORDER BY timestamp ASC LIMIT 1",
2972 $session_id
2973 ));
2974 $preview = $first_user_msg ? wp_trim_words(wp_strip_all_tags(stripslashes($first_user_msg)), 12, '...') : 'No messages';
2975
2976 // Build display name
2977 $user_email = !empty($session_data->user_email) ? $session_data->user_email : '';
2978 $user_name = !empty($session_data->user_name) ? $session_data->user_name : '';
2979 $user_identifier = !empty($session_data->user_identifier) ? $session_data->user_identifier : 'Guest';
2980
2981 $display_name = $user_name ?: ($user_email ? explode('@', $user_email)[0] : $user_identifier);
2982 $display_sub = $user_email ?: ('ID: ' . $user_identifier);
2983
2984 // Format time - show relative for recent, date for older
2985 $timestamp = strtotime($message_stats->latest . ' UTC');
2986 $now = time();
2987 $diff = $now - $timestamp;
2988 if ($diff < 3600) {
2989 $time_display = floor($diff / 60) . 'm ago';
2990 } elseif ($diff < 86400) {
2991 $time_display = floor($diff / 3600) . 'h ago';
2992 } elseif ($diff < 604800) {
2993 $time_display = floor($diff / 86400) . 'd ago';
2994 } else {
2995 $time_display = wp_date('M j', $timestamp);
2996 }
2997
2998 // Get initials for avatar
2999 $initials = strtoupper(substr($display_name, 0, 2));
3000 if (strlen($display_name) > 2 && strpos($display_name, ' ') !== false) {
3001 $parts = explode(' ', $display_name);
3002 $initials = strtoupper(substr($parts[0], 0, 1) . substr(end($parts), 0, 1));
3003 }
3004
3005 $sessions[] = [
3006 'session_id' => $session_id,
3007 'display_name' => $display_name,
3008 'display_sub' => $display_sub,
3009 'initials' => $initials,
3010 'preview' => $preview,
3011 'message_count' => (int) $message_stats->count,
3012 'time_display' => $time_display,
3013 'timestamp' => $message_stats->latest
3014 ];
3015 }
3016
3017 wp_send_json([
3018 'success' => true,
3019 'sessions' => $sessions,
3020 'page' => $page,
3021 'total_pages' => $total_pages,
3022 'total_sessions' => $total_sessions,
3023 'showing_start' => $offset + 1,
3024 'showing_end' => min($offset + $per_page, $total_sessions)
3025 ]);
3026 wp_die();
3027 }
3028
3029 /**
3030 * Fetch single conversation details for split-panel view
3031 */
3032 public function mxchat_fetch_conversation() {
3033 global $wpdb;
3034 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
3035 $url_clicks_table = $wpdb->prefix . 'mxchat_url_clicks';
3036
3037 if (!current_user_can('manage_options')) {
3038 wp_send_json_error(['message' => 'Insufficient permissions']);
3039 wp_die();
3040 }
3041
3042 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
3043 if (empty($session_id)) {
3044 wp_send_json_error(['message' => 'No session ID provided']);
3045 wp_die();
3046 }
3047
3048 // Check for optional columns/tables
3049 $url_table_exists = $wpdb->get_var("SHOW TABLES LIKE '$url_clicks_table'") === $url_clicks_table;
3050 $originating_columns_exist = !empty($wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'"));
3051
3052 // Get session metadata
3053 $session_data = $originating_columns_exist
3054 ? $wpdb->get_row($wpdb->prepare(
3055 "SELECT user_email, user_name, user_identifier, originating_page_url, originating_page_title, timestamp
3056 FROM {$table_name} WHERE session_id = %s ORDER BY timestamp ASC LIMIT 1",
3057 $session_id
3058 ))
3059 : $wpdb->get_row($wpdb->prepare(
3060 "SELECT user_email, user_name, user_identifier, timestamp FROM {$table_name}
3061 WHERE session_id = %s LIMIT 1",
3062 $session_id
3063 ));
3064
3065 if (!$session_data) {
3066 wp_send_json_error(['message' => 'Session not found']);
3067 wp_die();
3068 }
3069
3070 // Get clicked URLs
3071 $clicked_urls = [];
3072 if ($url_table_exists) {
3073 $url_clicks = $wpdb->get_results($wpdb->prepare(
3074 "SELECT DISTINCT clicked_url FROM {$url_clicks_table}
3075 WHERE session_id = %s ORDER BY click_timestamp ASC",
3076 $session_id
3077 ));
3078 foreach ($url_clicks as $click) {
3079 $clicked_urls[] = $click->clicked_url;
3080 }
3081 }
3082
3083 // Get all messages
3084 $messages = $wpdb->get_results($wpdb->prepare(
3085 "SELECT * FROM {$table_name} WHERE session_id = %s ORDER BY timestamp ASC",
3086 $session_id
3087 ));
3088
3089 // Build user info
3090 $user_email = !empty($session_data->user_email) ? $session_data->user_email : '';
3091 $user_name = !empty($session_data->user_name) ? $session_data->user_name : '';
3092 $user_identifier = !empty($session_data->user_identifier) ? $session_data->user_identifier : 'Guest';
3093
3094 $display_name = $user_name ?: ($user_email ? explode('@', $user_email)[0] : $user_identifier);
3095 $display_sub = $user_email ?: ('ID: ' . $user_identifier);
3096
3097 // Get initials
3098 $initials = strtoupper(substr($display_name, 0, 2));
3099 if (strlen($display_name) > 2 && strpos($display_name, ' ') !== false) {
3100 $parts = explode(' ', $display_name);
3101 $initials = strtoupper(substr($parts[0], 0, 1) . substr(end($parts), 0, 1));
3102 }
3103
3104 // Page info
3105 $page_url = $originating_columns_exist && !empty($session_data->originating_page_url) ? $session_data->originating_page_url : '';
3106 $page_title = '';
3107 if ($page_url) {
3108 $page_title = !empty($session_data->originating_page_title) ? $session_data->originating_page_title : parse_url($page_url, PHP_URL_PATH);
3109 }
3110
3111 // Format messages for output
3112 $formatted_messages = [];
3113 foreach ($messages as $msg) {
3114 $is_user = ($msg->role === 'user');
3115 $is_bot = ($msg->role === 'bot' || $msg->role === 'assistant');
3116
3117 $content = wp_kses(
3118 stripslashes($msg->message),
3119 [
3120 'b' => [], 'strong' => [], 'i' => [], 'em' => [], 'u' => [],
3121 'br' => [], 'p' => [], 'ul' => [], 'ol' => [], 'li' => [],
3122 'a' => ['href' => [], 'title' => [], 'target' => [], 'class' => []],
3123 'div' => ['class' => [], 'id' => [], 'data-nonce' => []],
3124 'img' => ['src' => [], 'alt' => [], 'class' => []],
3125 'h3' => ['class' => []],
3126 'h4' => ['class' => []],
3127 'button' => ['type' => [], 'class' => [], 'data-product-id' => [], 'data-nonce' => [], 'data-product-type' => [], 'data-original-text' => [], 'data-mxchat-action' => []],
3128 'select' => ['class' => [], 'data-attribute' => []],
3129 'option' => ['value' => []],
3130 'span' => ['class' => []],
3131 'del' => [], 'ins' => [],
3132 ]
3133 );
3134 $formatted_content = $this->format_transcript_message($content);
3135
3136 $has_rag = $is_bot && !empty($msg->rag_context);
3137
3138 $formatted_messages[] = [
3139 'id' => $msg->id,
3140 'role' => $msg->role,
3141 'is_user' => $is_user,
3142 'is_bot' => $is_bot,
3143 'content' => $formatted_content,
3144 'timestamp' => wp_date('g:i A', strtotime($msg->timestamp . ' UTC')),
3145 'full_timestamp' => wp_date('F j, Y g:i A', strtotime($msg->timestamp . ' UTC')),
3146 'has_rag' => $has_rag
3147 ];
3148 }
3149
3150 // First message timestamp for "started" display
3151 $started = !empty($messages) ? wp_date('M j, Y g:i A', strtotime($messages[0]->timestamp . ' UTC')) : '-';
3152
3153 wp_send_json([
3154 'success' => true,
3155 'session_id' => $session_id,
3156 'user' => [
3157 'name' => $display_name,
3158 'sub' => $display_sub,
3159 'initials' => $initials,
3160 'email' => $user_email,
3161 'identifier' => $user_identifier
3162 ],
3163 'page' => [
3164 'url' => $page_url,
3165 'title' => $page_title
3166 ],
3167 'clicked_urls' => $clicked_urls,
3168 'messages' => $formatted_messages,
3169 'message_count' => count($messages),
3170 'started' => $started
3171 ]);
3172 wp_die();
3173 }
3174
3175
3176 /**
3177 * ALTERNATIVE: Simpler helper method using string replacement
3178 */
3179 private function highlight_clicked_links($message_content, $clicked_urls) {
3180 if (empty($clicked_urls)) {
3181 return wp_kses(
3182 $message_content,
3183 [
3184 'b' => [], 'strong' => [], 'i' => [], 'em' => [], 'u' => [],
3185 'br' => [], 'p' => [], 'ul' => [], 'ol' => [], 'li' => [],
3186 'a' => ['href' => [], 'title' => [], 'target' => [], 'class' => []]
3187 ]
3188 );
3189 }
3190
3191 // First apply standard sanitization
3192 $message_content = wp_kses(
3193 $message_content,
3194 [
3195 'b' => [], 'strong' => [], 'i' => [], 'em' => [], 'u' => [],
3196 'br' => [], 'p' => [], 'ul' => [], 'ol' => [], 'li' => [],
3197 'a' => ['href' => [], 'title' => [], 'target' => [], 'class' => []]
3198 ]
3199 );
3200
3201 // Process each clicked URL
3202 foreach ($clicked_urls as $clicked_url) {
3203 // Try multiple patterns to catch different link formats
3204 $patterns = [
3205 // Standard link format
3206 '/<a([^>]*href=["\']' . preg_quote($clicked_url, '/') . '["\'][^>]*)>/i',
3207 // Link with trailing slash
3208 '/<a([^>]*href=["\']' . preg_quote(rtrim($clicked_url, '/'), '/') . '\/?["\'][^>]*)>/i',
3209 // Encoded entities version
3210 '/<a([^>]*href=["\']' . preg_quote(htmlentities($clicked_url), '/') . '["\'][^>]*)>/i',
3211 ];
3212
3213 foreach ($patterns as $pattern) {
3214 if (preg_match($pattern, $message_content)) {
3215 $message_content = preg_replace_callback(
3216 $pattern,
3217 function($matches) {
3218 $full_match = $matches[0];
3219 $attributes = $matches[1];
3220
3221 // Check if it already has the class
3222 if (strpos($full_match, 'mxchat-clicked-link') !== false) {
3223 return $full_match;
3224 }
3225
3226 // Check if class attribute exists
3227 if (preg_match('/class=["\']([^"\']*)["\']/', $attributes, $class_matches)) {
3228 // Add to existing class
3229 $new_attributes = preg_replace(
3230 '/class=["\']([^"\']*)["\']/',
3231 'class="$1 mxchat-clicked-link"',
3232 $attributes
3233 );
3234 } else {
3235 // Add new class attribute
3236 $new_attributes = $attributes . ' class="mxchat-clicked-link"';
3237 }
3238
3239 // Add title if not present
3240 if (strpos($new_attributes, 'title=') === false) {
3241 $new_attributes .= ' title="User clicked this link"';
3242 }
3243
3244 return '<a' . $new_attributes . '>';
3245 },
3246 $message_content
3247 );
3248
3249 // If we found and replaced, break out of the patterns loop
3250 break;
3251 }
3252 }
3253 }
3254
3255 return $message_content;
3256 }
3257
3258 public function mxchat_create_prompts_page() {
3259 //error_log('=== DEBUG: mxchat_create_prompts_page started ===');
3260
3261 global $wpdb;
3262 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
3263
3264 $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
3265 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
3266
3267 // Display success message if all prompts were deleted
3268 if (isset($_GET['all_deleted']) && $_GET['all_deleted'] === 'true') {
3269 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>';
3270 }
3271
3272 // Set up pagination, search query, and content type filter
3273 $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field($_GET['_wpnonce']) : '';
3274 $search_query = (!empty($nonce) && wp_verify_nonce($nonce, 'mxchat_prompts_search_nonce') && isset($_GET['search'])) ? sanitize_text_field($_GET['search']) : '';
3275 $content_type_filter = isset($_GET['content_type']) ? sanitize_key($_GET['content_type']) : ''; // ADDED 2.5.6
3276 $current_page = isset($_GET['paged']) ? absint($_GET['paged']) : 1;
3277 $per_page = 25;
3278
3279 //error_log('DEBUG: Search query: ' . $search_query);
3280 //error_log('DEBUG: Content type filter: ' . $content_type_filter);
3281 //error_log('DEBUG: Current page: ' . $current_page);
3282 //error_log('DEBUG: Per page: ' . $per_page);
3283
3284 // ================================
3285 // MULTI-BOT CONFIGURATION
3286 // ================================
3287
3288 // Check for multi-bot and set up bot selection
3289 if (class_exists('MxChat_Multi_Bot_Manager')) {
3290 $multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
3291 $available_bots = $multi_bot_manager->get_available_bots();
3292
3293 // Get saved bot selection (user-specific first, then site-wide default)
3294 $user_id = get_current_user_id();
3295 $saved_bot_id = get_user_meta($user_id, 'mxchat_selected_knowledge_bot', true);
3296 if (empty($saved_bot_id)) {
3297 $saved_bot_id = get_option('mxchat_current_knowledge_bot', 'default');
3298 }
3299
3300 // Allow URL override but default to saved selection
3301 $current_bot_id = isset($_GET['bot_id']) ? sanitize_key($_GET['bot_id']) : $saved_bot_id;
3302 $multibot_active = true;
3303 } else {
3304 $current_bot_id = 'default';
3305 $multibot_active = false;
3306 }
3307
3308 // ================================
3309 // DATA SOURCE CONFIGURATION
3310 // ================================
3311
3312 // Get bot-specific Pinecone settings
3313 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($current_bot_id);
3314 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
3315 $pinecone_api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
3316
3317 // Get OpenAI Vector Store settings
3318 $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
3319 $use_vectorstore = ($vectorstore_options['mxchat_use_openai_vectorstore'] ?? '0') === '1';
3320
3321 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Use Pinecone: ' . ($use_pinecone ? 'YES' : 'NO'));
3322 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Has API Key: ' . (!empty($pinecone_api_key) ? 'YES' : 'NO'));
3323 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Host: ' . ($pinecone_options['mxchat_pinecone_host'] ?? 'NOT SET'));
3324 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Namespace: ' . ($pinecone_options['mxchat_pinecone_namespace'] ?? 'NOT SET'));
3325
3326 if ($use_pinecone && !empty($pinecone_api_key)) {
3327 //error_log('DEBUG: Using PINECONE data source with bot-specific config');
3328 // PINECONE DATA SOURCE
3329 $data_source = 'pinecone';
3330
3331 // IMPORTANT: Pass the bot-specific $pinecone_options, not default options!
3332 // UPDATED 2.5.6: Added content_type_filter parameter
3333 $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, $search_query, $current_page, $per_page, $current_bot_id, $content_type_filter);
3334
3335 // TEMPORARY DEBUG - Add this right after the fetch call
3336 //error_log('=== DEBUG: Bot switching issue ===');
3337 //error_log('Current bot ID: ' . $current_bot_id);
3338 //error_log('Use Pinecone: ' . ($use_pinecone ? 'YES' : 'NO'));
3339 //error_log('Records returned: ' . count($records['data'] ?? []));
3340 //error_log('Total records: ' . ($records['total'] ?? 0));
3341
3342 // Check first few records to see their bot_id
3343 if (!empty($records['data'])) {
3344 foreach (array_slice($records['data'], 0, 3) as $i => $record) {
3345 $record_bot_id = $record->bot_id ?? 'NOT_SET';
3346 //error_log('Record ' . ($i+1) . ' bot_id: ' . $record_bot_id . ', content preview: ' . substr($record->article_content ?? '', 0, 30) . '...');
3347 }
3348 }
3349 //error_log('=== END DEBUG ===');
3350
3351 $total_records = $records['total'] ?? 0;
3352 $prompts = $records['data'] ?? array();
3353 $total_in_database = $records['total_in_database'] ?? 0;
3354 $showing_recent_only = $records['showing_recent_only'] ?? false;
3355
3356 $total_pages = ceil($total_records / $per_page);
3357
3358 } else {
3359 //error_log('DEBUG: Using WORDPRESS DB data source');
3360 // WORDPRESS DB DATA SOURCE (your existing logic)
3361 $data_source = 'wordpress';
3362
3363 // Initialize these variables for WordPress DB
3364 $total_in_database = 0;
3365 $showing_recent_only = false;
3366
3367 $offset = ($current_page - 1) * $per_page;
3368
3369 // UPDATED 2.5.6: Build WHERE clause for search and content type filtering
3370 $where_clauses = array();
3371 $where_values = array();
3372
3373 if ($search_query) {
3374 $where_clauses[] = "article_content LIKE %s";
3375 $where_values[] = '%' . $wpdb->esc_like($search_query) . '%';
3376 }
3377
3378 if ($content_type_filter) {
3379 $where_clauses[] = "content_type = %s";
3380 $where_values[] = $content_type_filter;
3381 }
3382
3383 $sql_where = "";
3384 if (!empty($where_clauses)) {
3385 $sql_where = "WHERE " . implode(" AND ", $where_clauses);
3386 }
3387
3388 // UPDATED 2.6.3: Count unique entries (by source_url) instead of individual rows
3389 // This ensures pagination shows X entries per page, not X chunks
3390 // Entries with empty source_url are counted individually
3391 if (!empty($where_values)) {
3392 // Count unique source_urls + count of rows with empty source_url
3393 $count_query = $wpdb->prepare(
3394 "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$sql_where} AND source_url != '') +
3395 (SELECT COUNT(*) FROM {$table_name} {$sql_where} AND (source_url = '' OR source_url IS NULL))",
3396 array_merge($where_values, $where_values)
3397 );
3398 } else {
3399 $count_query = "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '') +
3400 (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL)";
3401 }
3402 $total_records = $wpdb->get_var($count_query);
3403 $total_pages = ceil($total_records / $per_page);
3404
3405 // UPDATED 2.6.3: Get unique source_urls for pagination, then fetch all their rows
3406 // Step 1: Get the source_urls for this page (distinct URLs ordered by latest timestamp)
3407 if (!empty($where_values)) {
3408 $urls_query = $wpdb->prepare(
3409 "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name} {$sql_where}
3410 GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
3411 array_merge($where_values, array($per_page, $offset))
3412 );
3413 } else {
3414 $urls_query = $wpdb->prepare(
3415 "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
3416 GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
3417 $per_page, $offset
3418 );
3419 }
3420 $page_urls = $wpdb->get_results($urls_query);
3421
3422 // Step 2: Fetch all rows for these source_urls
3423 $prompts = array();
3424 if (!empty($page_urls)) {
3425 // Build URL order map to preserve newest-first ordering from step 1
3426 $url_order_map = array();
3427 $url_list = array();
3428 $has_empty_url = false;
3429 $order_index = 0;
3430 foreach ($page_urls as $url_row) {
3431 if (empty($url_row->source_url)) {
3432 $has_empty_url = true;
3433 $url_order_map['__empty__'] = $order_index++;
3434 } else {
3435 $url_list[] = $url_row->source_url;
3436 $url_order_map[$url_row->source_url] = $order_index++;
3437 }
3438 }
3439
3440 // Build query to fetch all rows for these URLs
3441 $url_conditions = array();
3442 $url_values = array();
3443
3444 if (!empty($url_list)) {
3445 $placeholders = implode(',', array_fill(0, count($url_list), '%s'));
3446 $url_conditions[] = "source_url IN ($placeholders)";
3447 $url_values = array_merge($url_values, $url_list);
3448 }
3449
3450 if ($has_empty_url) {
3451 $url_conditions[] = "(source_url = '' OR source_url IS NULL)";
3452 }
3453
3454 if (!empty($url_conditions)) {
3455 $url_where = "WHERE (" . implode(" OR ", $url_conditions) . ")";
3456
3457 // Add original filters back
3458 if (!empty($where_clauses)) {
3459 $url_where .= " AND " . implode(" AND ", $where_clauses);
3460 $url_values = array_merge($url_values, $where_values);
3461 }
3462
3463 if (!empty($url_values)) {
3464 $prompts_query = $wpdb->prepare(
3465 "SELECT * FROM {$table_name} {$url_where} ORDER BY timestamp DESC",
3466 $url_values
3467 );
3468 } else {
3469 $prompts_query = "SELECT * FROM {$table_name} {$url_where} ORDER BY timestamp DESC";
3470 }
3471
3472 $prompts = $wpdb->get_results($prompts_query);
3473
3474 // Sort prompts by the original URL order (newest first), then by timestamp within each URL group
3475 usort($prompts, function($a, $b) use ($url_order_map) {
3476 $url_a = empty($a->source_url) ? '__empty__' : $a->source_url;
3477 $url_b = empty($b->source_url) ? '__empty__' : $b->source_url;
3478 $order_a = $url_order_map[$url_a] ?? PHP_INT_MAX;
3479 $order_b = $url_order_map[$url_b] ?? PHP_INT_MAX;
3480
3481 // First sort by URL order (newest URLs first)
3482 if ($order_a !== $order_b) {
3483 return $order_a - $order_b;
3484 }
3485
3486 // Within same URL, sort by timestamp DESC (newest chunks first)
3487 return strtotime($b->timestamp) - strtotime($a->timestamp);
3488 });
3489 }
3490 }
3491 }
3492
3493 // ================================
3494 // PAGINATION GENERATION
3495 // ================================
3496
3497 // Generate pagination links
3498 if ($total_pages > 1) {
3499 // Build a clean base URL with only necessary parameters
3500 $pagination_args = array('page' => 'mxchat-prompts');
3501
3502 // Preserve bot_id parameter if multi-bot is active
3503 if ($multibot_active && !empty($current_bot_id) && $current_bot_id !== 'default') {
3504 $pagination_args['bot_id'] = $current_bot_id;
3505 }
3506
3507 // Preserve search query and nonce if present
3508 if ($search_query) {
3509 $pagination_args['search'] = $search_query;
3510 if (!empty($nonce)) {
3511 $pagination_args['_wpnonce'] = $nonce;
3512 }
3513 }
3514
3515 // Preserve content type filter if present
3516 if ($content_type_filter) {
3517 $pagination_args['content_type'] = $content_type_filter;
3518 }
3519
3520 // Build clean base URL
3521 $base_url = add_query_arg($pagination_args, admin_url('admin.php'));
3522
3523 $page_links = paginate_links(array(
3524 'base' => $base_url . '%_%',
3525 'format' => '&paged=%#%',
3526 'prev_text' => __('&laquo; Previous', 'mxchat'),
3527 'next_text' => __('Next &raquo;', 'mxchat'),
3528 'total' => $total_pages,
3529 'current' => $current_page,
3530 'type' => 'plain',
3531 ));
3532 } else {
3533 $page_links = '';
3534 }
3535
3536 // ================================
3537 // PROCESSING STATUS RETRIEVAL
3538 // ================================
3539
3540 // Retrieve processing statuses using queue-based method
3541 $processing_statuses = $knowledge_manager->mxchat_get_processing_statuses();
3542 $pdf_status = $processing_statuses['pdf_status'];
3543 $sitemap_status = $processing_statuses['sitemap_status'];
3544 $is_processing = $processing_statuses['is_processing'];
3545
3546 //error_log('=== DEBUG: mxchat_create_prompts_page data preparation completed ===');
3547
3548 // ================================
3549 // RENDER PAGE WITH NEW SIDEBAR LAYOUT
3550 // ================================
3551
3552 // Include the new knowledge page template
3553 require_once plugin_dir_path(__FILE__) . 'admin-knowledge-page.php';
3554
3555 // Package all page data for the render function
3556 $page_data = array(
3557 'prompts' => $prompts,
3558 'total_records' => $total_records,
3559 'total_pages' => $total_pages,
3560 'current_page' => $current_page,
3561 'per_page' => $per_page,
3562 'page_links' => $page_links,
3563 'search_query' => $search_query,
3564 'content_type_filter' => $content_type_filter,
3565 'data_source' => $data_source,
3566 'use_pinecone' => $use_pinecone,
3567 'use_vectorstore' => $use_vectorstore,
3568 'multibot_active' => $multibot_active,
3569 'current_bot_id' => $current_bot_id,
3570 'pdf_status' => $pdf_status,
3571 'sitemap_status' => $sitemap_status,
3572 'is_processing' => $is_processing,
3573 'total_in_database' => $total_in_database ?? 0,
3574 'showing_recent_only' => $showing_recent_only ?? false,
3575 );
3576
3577 // Render the new sidebar-based page
3578 mxchat_render_knowledge_page($this, $knowledge_manager, $page_data);
3579 }
3580
3581 /**
3582 * Get bot-specific Pinecone configuration
3583 * Used in the knowledge retrieval functions
3584 */
3585 private function get_bot_pinecone_config($bot_id = 'default') {
3586 //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
3587
3588 // If default bot or multi-bot add-on not active, use default Pinecone config
3589 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
3590 //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
3591 $addon_options = get_option('mxchat_pinecone_addon_options', array());
3592 $config = array(
3593 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
3594 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
3595 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
3596 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
3597 );
3598 //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
3599 return $config;
3600 }
3601
3602 //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
3603
3604 // Hook for multi-bot add-on to provide bot-specific Pinecone config
3605 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
3606
3607 if (!empty($bot_pinecone_config)) {
3608 //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
3609 //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
3610 //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
3611 //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
3612 } else {
3613 //error_log("MXCHAT DEBUG: Filter returned empty config!");
3614 }
3615
3616 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
3617 }
3618
3619 public function mxchat_delete_chat_history() {
3620 if (!current_user_can('manage_options')) {
3621 echo wp_json_encode(['error' => esc_html__('You do not have sufficient permissions.', 'mxchat')]);
3622 wp_die();
3623 }
3624 check_ajax_referer('mxchat_delete_chat_history', 'security');
3625
3626 if (!isset($_POST['delete_session_ids']) || !is_array($_POST['delete_session_ids'])) {
3627 echo wp_json_encode(['error' => esc_html__('No chat sessions selected for deletion.', 'mxchat')]);
3628 wp_die();
3629 }
3630
3631 global $wpdb;
3632 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
3633 $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
3634 $has_translations = $wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table;
3635
3636 // When true, any lead attached to these sessions is fully wiped (all their sessions,
3637 // across the whole table). Default false: the chat rows go away but the lead is
3638 // preserved as a separate "chat deleted" lead in the Leads tab.
3639 $also_delete_lead = !empty($_POST['also_delete_lead']) && $_POST['also_delete_lead'] !== 'false';
3640
3641 $deleted_count = 0;
3642 $preserved_as_deleted_leads = 0;
3643 $emails_to_fully_wipe = [];
3644
3645 foreach ((array) $_POST['delete_session_ids'] as $session_id) {
3646 $session_id_sanitized = sanitize_text_field($session_id);
3647 if ($session_id_sanitized === '') {
3648 continue;
3649 }
3650
3651 // Capture the lead info attached to this session *before* we delete the rows.
3652 $lead_row = $wpdb->get_row($wpdb->prepare(
3653 "SELECT user_email, user_name, MAX(timestamp) AS last_ts
3654 FROM {$table_name}
3655 WHERE session_id = %s AND user_email IS NOT NULL AND user_email != ''
3656 GROUP BY user_email, user_name
3657 ORDER BY last_ts DESC LIMIT 1",
3658 $session_id_sanitized
3659 ));
3660
3661 wp_cache_delete('chat_session_' . $session_id_sanitized, 'mxchat_chat_sessions');
3662 $wpdb->delete($table_name, ['session_id' => $session_id_sanitized]);
3663
3664 if ($has_translations) {
3665 $wpdb->delete($translations_table, ['session_id' => $session_id_sanitized]);
3666 }
3667
3668 delete_option('mxchat_history_' . $session_id_sanitized);
3669 delete_option('mxchat_agent_name_' . $session_id_sanitized);
3670
3671 if ($lead_row && !empty($lead_row->user_email)) {
3672 if ($also_delete_lead) {
3673 // Full-wipe requested — queue the email so that all their sessions and
3674 // related options get swept below. Also clear this session's pre-chat
3675 // capture options (they're no longer meaningful).
3676 $emails_to_fully_wipe[strtolower($lead_row->user_email)] = $lead_row->user_email;
3677 delete_option('mxchat_email_' . $session_id_sanitized);
3678 delete_option('mxchat_name_' . $session_id_sanitized);
3679 } else {
3680 // Preserve the lead in a "Chat deleted" state via distinct option keys so
3681 // they stay out of the orphan bucket (orphan = pre-chat form dropoff).
3682 update_option('mxchat_lead_del_email_' . $session_id_sanitized, $lead_row->user_email, false);
3683 if (!empty($lead_row->user_name)) {
3684 update_option('mxchat_lead_del_name_' . $session_id_sanitized, $lead_row->user_name, false);
3685 }
3686 if (!empty($lead_row->last_ts)) {
3687 update_option('mxchat_lead_del_ts_' . $session_id_sanitized, $lead_row->last_ts, false);
3688 }
3689 // Clean up pre-chat capture options for this session — chat_deleted supersedes.
3690 delete_option('mxchat_email_' . $session_id_sanitized);
3691 delete_option('mxchat_name_' . $session_id_sanitized);
3692 $preserved_as_deleted_leads++;
3693 }
3694 } else {
3695 // No lead attached — nothing to preserve. Clean up any orphan options anyway.
3696 delete_option('mxchat_email_' . $session_id_sanitized);
3697 delete_option('mxchat_name_' . $session_id_sanitized);
3698 }
3699
3700 $deleted_count++;
3701 }
3702
3703 // Opt-in full-lead wipe: sweep every remaining row + every option key (including
3704 // chat_deleted preservation) for each affected email. Reuses the same internal
3705 // helper as the Leads-tab Delete button for consistency.
3706 if (!empty($emails_to_fully_wipe)) {
3707 self::mxchat_wipe_leads_by_email(array_values($emails_to_fully_wipe));
3708 }
3709
3710 wp_cache_delete('all_chat_sessions', 'mxchat_chat_sessions');
3711
3712 echo wp_json_encode([
3713 'success' => sprintf(
3714 esc_html__('%d chat session(s) have been deleted.', 'mxchat'),
3715 $deleted_count
3716 ),
3717 'preserved_as_deleted_leads' => $preserved_as_deleted_leads,
3718 'leads_fully_wiped' => count($emails_to_fully_wipe),
3719 ]);
3720 wp_die();
3721 }
3722
3723 /**
3724 * Format transcript message content with markdown processing
3725 * Converts markdown links, bold, italic, code blocks, and plain URLs to HTML
3726 */
3727 private function format_transcript_message($text) {
3728 if (empty($text)) {
3729 return '';
3730 }
3731
3732 // Normalize line endings and clean up excessive whitespace
3733 $text = str_replace("\r\n", "\n", $text);
3734 $text = str_replace("\r", "\n", $text);
3735
3736 // Clean up existing <br> tags that may have been saved (legacy data)
3737 // Convert <br>, <br/>, <br /> back to newlines for consistent processing
3738 $text = preg_replace('/<br\s*\/?>\s*/i', "\n", $text);
3739
3740 // Collapse 3+ consecutive newlines to just 2 (paragraph break)
3741 $text = preg_replace('/\n{3,}/', "\n\n", $text);
3742
3743 // Process markdown headers (# Header)
3744 $text = preg_replace_callback('/^(#{1,6})\s+(.+)$/m', function($matches) {
3745 $level = strlen($matches[1]);
3746 $content = esc_html(trim($matches[2]));
3747 return "<h{$level}>{$content}</h{$level}>";
3748 }, $text);
3749
3750 // Process code blocks with triple backticks
3751 $text = preg_replace_callback('/```(\w+)?\n?([\s\S]*?)```/', function($matches) {
3752 $language = !empty($matches[1]) ? ' class="language-' . esc_attr($matches[1]) . '"' : '';
3753 $code = esc_html($matches[2]);
3754 return "<pre><code{$language}>{$code}</code></pre>";
3755 }, $text);
3756
3757 // Process inline code with single backticks
3758 $text = preg_replace('/`([^`]+)`/', '<code>$1</code>', $text);
3759
3760 // Process bold text **text** or __text__
3761 $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
3762 $text = preg_replace('/__(.+?)__/', '<strong>$1</strong>', $text);
3763
3764 // Process italic text *text* or _text_ (but not if part of URL)
3765 $text = preg_replace('/(?<![*_\w])\*([^*]+)\*(?![*\w])/', '<em>$1</em>', $text);
3766 $text = preg_replace('/(?<![*_\w])_([^_]+)_(?![*\w])/', '<em>$1</em>', $text);
3767
3768 // Process markdown links [text](url)
3769 $text = preg_replace_callback('/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/', function($matches) {
3770 $link_text = esc_html($matches[1]);
3771 $url = esc_url($matches[2]);
3772 return "<a href=\"{$url}\" target=\"_blank\" rel=\"noopener\">{$link_text}</a>";
3773 }, $text);
3774
3775 // Process citation-style brackets [URL]
3776 $text = preg_replace_callback('/\[(https?:\/\/[^\]]+)\]/', function($matches) {
3777 $url = esc_url($matches[1]);
3778 return "<a href=\"{$url}\" target=\"_blank\" rel=\"noopener\">{$url}</a>";
3779 }, $text);
3780
3781 // Process standalone URLs (not already in links or img src)
3782 $text = preg_replace_callback(
3783 '/(?<!href="|src="|">)(https?:\/\/[^\s<>"]+)(?![^<]*<\/a>)/',
3784 function($matches) {
3785 $url = esc_url($matches[1]);
3786 // Truncate display URL if too long
3787 $display = strlen($matches[1]) > 50 ? substr($matches[1], 0, 47) . '...' : $matches[1];
3788 return "<a href=\"{$url}\" target=\"_blank\" rel=\"noopener\">{$display}</a>";
3789 },
3790 $text
3791 );
3792
3793 // Process mailto links
3794 $text = preg_replace_callback('/\[([^\]]+)\]\((mailto:[^)]+)\)/', function($matches) {
3795 $link_text = esc_html($matches[1]);
3796 $mailto = esc_url($matches[2]);
3797 return "<a href=\"{$mailto}\">{$link_text}</a>";
3798 }, $text);
3799
3800 // Convert paragraphs: split by double newlines, wrap in <p> tags
3801 // This creates proper paragraph structure instead of excessive <br> tags
3802 $paragraphs = preg_split('/\n\n+/', $text);
3803
3804 // Filter out empty paragraphs but preserve content like "0"
3805 $paragraphs = array_values(array_filter(array_map('trim', $paragraphs), function($p) {
3806 return $p !== '';
3807 }));
3808
3809 if (empty($paragraphs)) {
3810 // No content after filtering
3811 return '';
3812 } elseif (count($paragraphs) > 1) {
3813 // Multiple paragraphs - wrap each in <p> tags, convert single newlines to <br>
3814 $formatted_paragraphs = array_map(function($p) {
3815 return nl2br($p);
3816 }, $paragraphs);
3817 $text = '<p>' . implode('</p><p>', $formatted_paragraphs) . '</p>';
3818 } else {
3819 // Single paragraph - just convert newlines to <br>
3820 $text = nl2br($paragraphs[0]);
3821 }
3822
3823 return $text;
3824 }
3825
3826 /**
3827 * AJAX handler to fetch RAG context for a specific message
3828 * Used by the transcript viewer to show retrieved documents
3829 */
3830 public function mxchat_get_rag_context() {
3831 // Check permissions
3832 if (!current_user_can('manage_options')) {
3833 wp_send_json_error(['message' => esc_html__('You do not have sufficient permissions.', 'mxchat')]);
3834 wp_die();
3835 }
3836
3837 // Validate message ID
3838 if (!isset($_POST['message_id']) || empty($_POST['message_id'])) {
3839 wp_send_json_error(['message' => esc_html__('Message ID is required.', 'mxchat')]);
3840 wp_die();
3841 }
3842
3843 $message_id = absint($_POST['message_id']);
3844
3845 global $wpdb;
3846 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
3847
3848 // Fetch the RAG context for this message
3849 $result = $wpdb->get_row($wpdb->prepare(
3850 "SELECT rag_context FROM {$table_name} WHERE id = %d",
3851 $message_id
3852 ));
3853
3854 if (!$result || empty($result->rag_context)) {
3855 wp_send_json_error(['message' => esc_html__('No RAG context found for this message.', 'mxchat')]);
3856 wp_die();
3857 }
3858
3859 // Decode the JSON data
3860 $rag_context = json_decode($result->rag_context, true);
3861
3862 if (json_last_error() !== JSON_ERROR_NONE) {
3863 wp_send_json_error(['message' => esc_html__('Invalid RAG context data.', 'mxchat')]);
3864 wp_die();
3865 }
3866
3867 wp_send_json_success($rag_context);
3868 wp_die();
3869 }
3870
3871 public function display_admin_notices() {
3872 // Check if we're on a MXChat admin page
3873 $screen = get_current_screen();
3874 if (!$screen || strpos($screen->base, 'mxchat') === false) {
3875 return;
3876 }
3877
3878 $dismiss_button = '<button type="button" class="notice-dismiss"><span class="screen-reader-text">' . esc_html__('Dismiss this notice.', 'mxchat') . '</span></button>';
3879
3880 // Check for error notices
3881 $error_notice = get_transient('mxchat_admin_notice_error');
3882 if ($error_notice) {
3883 echo '<div class="notice notice-error is-dismissible"><p>' . wp_kses_post($error_notice) . '</p>' . $dismiss_button . '</div>';
3884 delete_transient('mxchat_admin_notice_error');
3885 }
3886
3887 // Check for success notices
3888 $success_notice = get_transient('mxchat_admin_notice_success');
3889 if ($success_notice) {
3890 echo '<div class="notice notice-success is-dismissible"><p>' . wp_kses_post($success_notice) . '</p>' . $dismiss_button . '</div>';
3891 delete_transient('mxchat_admin_notice_success');
3892 }
3893
3894 // Check for info notices
3895 $info_notice = get_transient('mxchat_admin_notice_info');
3896 if ($info_notice) {
3897 echo '<div class="notice notice-info is-dismissible"><p>' . wp_kses_post($info_notice) . '</p>' . $dismiss_button . '</div>';
3898 delete_transient('mxchat_admin_notice_info');
3899 }
3900 }
3901
3902
3903 public function mxchat_create_activation_page() {
3904 // Include the new Pro & Extensions page template
3905 require_once plugin_dir_path(__FILE__) . 'admin-pro-page.php';
3906
3907 // Get addons configuration from the MxChat_Addons class
3908 require_once plugin_dir_path(__FILE__) . 'class-mxchat-addons.php';
3909 $addons_instance = new MxChat_Addons();
3910 $addons_config = $addons_instance->get_addons_config();
3911
3912 // Render the consolidated Pro & Extensions page
3913 mxchat_render_pro_page($this, $addons_config);
3914 }
3915
3916 /**
3917 * Check if current activation is linked to a domain
3918 * This checks YOUR website's database, not the user's local database
3919 */
3920 public function is_current_activation_linked($domain) {
3921 $license_key = get_option('mxchat_activation_key');
3922 $email = get_option('mxchat_pro_email');
3923
3924 if (empty($license_key) || empty($email)) {
3925 return false;
3926 }
3927
3928 // Check with YOUR website's API
3929 $response = wp_remote_post('https://mxchat.ai/mxchat-api/check-domain', array(
3930 'body' => array(
3931 'license_key' => $license_key,
3932 'email' => $email,
3933 'domain' => $domain
3934 ),
3935 'timeout' => 10,
3936 'sslverify' => false
3937 ));
3938
3939 if (is_wp_error($response)) {
3940 return false;
3941 }
3942
3943 $body = json_decode(wp_remote_retrieve_body($response), true);
3944 return isset($body['success']) && $body['success'] && isset($body['data']['linked']) && $body['data']['linked'];
3945 }
3946
3947
3948 public function mxchat_actions_page_html() {
3949 if (!current_user_can('manage_options')) {
3950 return;
3951 }
3952
3953 global $wpdb;
3954 $table_name = $wpdb->prefix . 'mxchat_intents';
3955
3956 // Get stats for dashboard
3957 $total_actions = $wpdb->get_var("SELECT COUNT(*) FROM $table_name");
3958 $enabled_actions = $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE enabled = 1");
3959 $disabled_actions = $total_actions - $enabled_actions;
3960
3961 // Get unique action types count
3962 $action_types_count = $wpdb->get_var("SELECT COUNT(DISTINCT callback_function) FROM $table_name");
3963
3964 // Get action type distribution
3965 $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");
3966 $available_callbacks = $this->mxchat_get_available_callbacks();
3967 $callback_groups = $this->mxchat_get_available_callbacks(true, true);
3968
3969 $action_type_distribution = array();
3970 foreach ($type_distribution_raw as $row) {
3971 $label = isset($available_callbacks[$row->callback_function]['label'])
3972 ? $available_callbacks[$row->callback_function]['label']
3973 : $row->callback_function;
3974 $action_type_distribution[$label] = $row->count;
3975 }
3976
3977 // Prepare page data
3978 $page_data = array(
3979 'total_actions' => $total_actions,
3980 'enabled_actions' => $enabled_actions,
3981 'disabled_actions' => $disabled_actions,
3982 'action_types_count' => $action_types_count,
3983 'action_type_distribution' => $action_type_distribution,
3984 'available_callbacks' => $available_callbacks,
3985 'callback_groups' => $callback_groups,
3986 );
3987
3988 // Include and render the new template
3989 require_once plugin_dir_path(__FILE__) . 'admin-actions-page.php';
3990 mxchat_render_actions_page($this, $page_data);
3991 }
3992
3993 /**
3994 * LEGACY HTML - Preserved below for reference, to be removed in future update
3995 */
3996 function mxchat_actions_page_legacy_html() {
3997 // This function is deprecated and no longer used
3998 // The new template is in includes/admin-actions-page.php
3999 ?>
4000 <div class="wrap mxchat-wrapper">
4001 <!-- Hero Section -->
4002 <div class="mxchat-hero">
4003 <h1 class="mxchat-main-title">
4004 <span class="mxchat-gradient-text">Actions</span> Manager
4005 </h1>
4006 <p class="mxchat-hero-subtitle">
4007 <?php esc_html_e('Create and manage custom actions to enhance your chatbot\'s capabilities.', 'mxchat'); ?>
4008 </p>
4009 </div>
4010
4011 <!-- Actions Header with Search and Filter -->
4012 <div class="mxchat-actions-header">
4013 <div class="mxchat-actions-filters">
4014 <form method="get" class="mxchat-search-form">
4015 <input type="hidden" name="page" value="mxchat-actions">
4016 <div class="mxchat-search-group">
4017 <span class="dashicons dashicons-search"></span>
4018 <input type="text" name="s" class="mxchat-search-input"
4019 placeholder="<?php esc_attr_e('Search Actions', 'mxchat'); ?>"
4020 value="<?php echo esc_attr($search_term); ?>">
4021 </div>
4022 <select name="callback_filter" class="mxchat-action-filter">
4023 <option value=""><?php esc_html_e('All Action Types', 'mxchat'); ?></option>
4024 <?php foreach ($available_callbacks as $function => $callback_data) :
4025 $label = $callback_data['label']; ?>
4026 <option value="<?php echo esc_attr($function); ?>"
4027 <?php selected($callback_filter, $function); ?>>
4028 <?php echo esc_html($label); ?>
4029 </option>
4030 <?php endforeach; ?>
4031 </select>
4032 <button type="submit" class="mxchat-button-secondary">
4033 <?php esc_html_e('Filter', 'mxchat'); ?>
4034 </button>
4035 </form>
4036 </div>
4037 <div class="mxchat-actions-controls">
4038 <button type="button" id="mxchat-add-action-btn" class="mxchat-button-primary">
4039 <span class="dashicons dashicons-plus-alt"></span>
4040 <?php esc_html_e('Add New Action', 'mxchat'); ?>
4041 </button>
4042 </div>
4043 </div>
4044
4045 <!-- Actions Grid Layout - All actions in a single grid -->
4046 <div class="mxchat-actions-grid">
4047 <div class="mxchat-cards-container">
4048 <?php if (!empty($actions)) : ?>
4049 <?php foreach ($actions as $action) :
4050 $callback_function = $action->callback_function;
4051 $callback_label = isset($available_callbacks[$callback_function]['label'])
4052 ? $available_callbacks[$callback_function]['label']
4053 : $callback_function;
4054 $threshold_value = isset($action->similarity_threshold)
4055 ? round($action->similarity_threshold * 100)
4056 : 85;
4057
4058 // Check if this is a form action
4059 $is_form_action = strpos($action->intent_label, 'Form ') === 0;
4060
4061 // Get action status (enabled/disabled) - default to true if column doesn't exist
4062 $is_enabled = isset($action->enabled) ? (bool)$action->enabled : true;
4063
4064 // Get enabled bots for display
4065 $enabled_bots = [];
4066 if (isset($action->enabled_bots) && !empty($action->enabled_bots)) {
4067 $enabled_bots = json_decode($action->enabled_bots, true);
4068 if (!is_array($enabled_bots)) {
4069 $enabled_bots = ['default'];
4070 }
4071 } else {
4072 $enabled_bots = ['default']; // Backward compatibility
4073 }
4074 ?>
4075 <div class="mxchat-action-card <?php echo $is_form_action ? 'mxchat-form-action' : ''; ?>">
4076 <div class="mxchat-card-header">
4077 <div class="mxchat-card-title"><?php echo esc_html($action->intent_label); ?></div>
4078 <div class="mxchat-card-toggle">
4079 <label class="mxchat-switch">
4080 <input type="checkbox" class="mxchat-action-toggle"
4081 data-action-id="<?php echo esc_attr($action->id); ?>"
4082 <?php checked($is_enabled); ?>>
4083 <span class="mxchat-slider round"></span>
4084 </label>
4085 </div>
4086 </div>
4087
4088 <div class="mxchat-card-body">
4089 <div class="mxchat-card-description">
4090 <strong><?php esc_html_e('Type:', 'mxchat'); ?></strong>
4091 <?php echo esc_html($callback_label); ?>
4092 </div>
4093
4094 <div class="mxchat-card-phrases">
4095 <strong><?php esc_html_e('Trigger phrases:', 'mxchat'); ?></strong>
4096 <div class="mxchat-phrases-preview">
4097 <?php
4098 // Check if the helper function exists, otherwise use a simple substring
4099 if (method_exists($this, 'get_trimmed_phrases')) {
4100 echo esc_html($this->get_trimmed_phrases($action->phrases));
4101 } else {
4102 echo esc_html(strlen($action->phrases) > 100 ?
4103 substr($action->phrases, 0, 97) . '...' :
4104 $action->phrases);
4105 }
4106 ?>
4107 </div>
4108 </div>
4109
4110 <div class="mxchat-threshold-control">
4111 <div class="mxchat-threshold-label">
4112 <?php esc_html_e('Similarity Threshold:', 'mxchat'); ?>
4113 <span class="mxchat-threshold-value"><?php echo esc_html($threshold_value); ?>%</span>
4114 </div>
4115 </div>
4116
4117 <!-- Bot Availability Display (Read-only) -->
4118 <div class="mxchat-card-bots">
4119 <strong><?php esc_html_e('Assigned bots:', 'mxchat'); ?></strong>
4120 <div class="mxchat-bot-badges">
4121 <?php
4122 foreach ($enabled_bots as $bot_id) {
4123 if ($bot_id === 'default') {
4124 echo '<span class="mxchat-bot-badge">' . esc_html__('Default Bot', 'mxchat') . '</span>';
4125 } else {
4126 // Try to get bot name from multi-bot manager
4127 if (class_exists('MxChat_Multi_Bot_Core_Manager')) {
4128 $multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
4129 $available_bots = $multi_bot_manager->get_available_bots();
4130 $bot_name = isset($available_bots[$bot_id]) ? $available_bots[$bot_id] : $bot_id;
4131 echo '<span class="mxchat-bot-badge">' . esc_html($bot_name) . '</span>';
4132 } else {
4133 echo '<span class="mxchat-bot-badge">' . esc_html($bot_id) . '</span>';
4134 }
4135 }
4136 }
4137 ?>
4138 </div>
4139 </div>
4140 </div>
4141
4142 <div class="mxchat-card-footer">
4143 <?php
4144 // Check if it's a form action
4145 $is_form_action = preg_match('/Form (\d+)/', $action->intent_label, $form_matches);
4146
4147 // Check if it's a recommendation flow action
4148 $is_flow_action = preg_match('/Recommendation Flow (\d+)/', $action->intent_label, $flow_matches);
4149
4150 if ($is_form_action) {
4151 $form_id = isset($form_matches[1]) ? $form_matches[1] : '';
4152 ?>
4153 <a href="<?php echo esc_url(admin_url('admin.php?page=mxchat-forms&action=edit&form_id=' . $form_id)); ?>"
4154 class="mxchat-button-primary">
4155 <span class="dashicons dashicons-feedback"></span>
4156 <?php esc_html_e('Edit Form', 'mxchat'); ?>
4157 </a>
4158 <?php } elseif ($is_flow_action) {
4159 ?>
4160 <a href="<?php echo esc_url(admin_url('admin.php?page=mxchat-smart-recommender')); ?>"
4161 class="mxchat-button-primary">
4162 <span class="dashicons dashicons-list-view"></span>
4163 <?php esc_html_e('Manage Flows', 'mxchat'); ?>
4164 </a>
4165 <?php } else { ?>
4166 <button type="button"
4167 class="mxchat-button-secondary mxchat-edit-button"
4168 data-action-id="<?php echo esc_attr($action->id); ?>"
4169 data-phrases="<?php echo esc_attr($action->phrases); ?>"
4170 data-label="<?php echo esc_attr($action->intent_label); ?>"
4171 data-threshold="<?php echo esc_attr(round($action->similarity_threshold * 100)); ?>"
4172 data-callback-function="<?php echo esc_attr($action->callback_function); ?>"
4173 data-enabled-bots="<?php echo esc_attr(json_encode($enabled_bots)); ?>">
4174 <span class="dashicons dashicons-edit"></span>
4175 <?php esc_html_e('Edit', 'mxchat'); ?>
4176 </button>
4177 <form method="post"
4178 action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
4179 class="mxchat-delete-form"
4180 onsubmit="return confirm('<?php esc_attr_e('Are you sure you want to delete this action?', 'mxchat'); ?>');">
4181 <?php wp_nonce_field('mxchat_delete_intent_nonce'); ?>
4182 <input type="hidden" name="action" value="mxchat_delete_intent">
4183 <input type="hidden" name="intent_id" value="<?php echo esc_attr($action->id); ?>">
4184 <button type="submit" class="mxchat-button-text mxchat-delete-button">
4185 <span class="dashicons dashicons-trash"></span>
4186 <?php esc_html_e('Delete', 'mxchat'); ?>
4187 </button>
4188 </form>
4189 <?php } ?>
4190 </div>
4191 </div>
4192 <?php endforeach; ?>
4193 <?php else : ?>
4194 <!-- If no actions found -->
4195 <div class="mxchat-no-actions">
4196 <div class="mxchat-empty-state">
4197 <span class="dashicons dashicons-format-chat"></span>
4198 <h2><?php esc_html_e('No actions found', 'mxchat'); ?></h2>
4199 <p><?php esc_html_e('Get started by creating your first action to enhance your chatbot.', 'mxchat'); ?></p>
4200 <button type="button" id="mxchat-create-first-action" class="mxchat-button-primary">
4201 <?php esc_html_e('Create Your First Action', 'mxchat'); ?>
4202 </button>
4203 </div>
4204 </div>
4205 <?php endif; ?>
4206 </div>
4207 </div>
4208
4209 <?php if ($total_pages > 1) : ?>
4210 <div class="mxchat-pagination">
4211 <?php
4212 echo paginate_links(array(
4213 'base' => add_query_arg('paged', '%#%'),
4214 'format' => '',
4215 'prev_text' => __('&laquo; Previous', 'mxchat'),
4216 'next_text' => __('Next &raquo;', 'mxchat'),
4217 'total' => $total_pages,
4218 'current' => $page
4219 ));
4220 ?>
4221 </div>
4222 <?php endif; ?>
4223
4224 <!-- Add/Edit Action Modal with Step-Based Approach -->
4225 <!-- Complete Modal HTML with Defined Groups Variable -->
4226 <div id="mxchat-action-modal" class="mxchat-modal" style="display: none;">
4227 <div class="mxchat-modal-content">
4228 <span class="mxchat-modal-close">&times;</span>
4229
4230 <form id="mxchat-action-form" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
4231 <!-- Dynamic nonce field -->
4232 <div id="action-nonce-container">
4233 <?php wp_nonce_field('mxchat_add_intent_nonce', 'add_intent_nonce'); ?>
4234 </div>
4235 <input type="hidden" name="action" id="form_action_type" value="mxchat_add_intent">
4236 <input type="hidden" name="intent_id" id="edit_action_id" value="">
4237 <input type="hidden" name="callback_function" id="callback_function" value="">
4238
4239 <!-- Step 1: Action Type Selection -->
4240 <div id="mxchat-action-step-1" class="mxchat-action-step active">
4241 <div class="mxchat-step-indicator">
4242 <div class="mxchat-step-number">1</div>
4243 <div class="mxchat-step-title"><?php esc_html_e('Select Action Type', 'mxchat'); ?></div>
4244 </div>
4245
4246 <div id="mxchat-action-type-selector" class="mxchat-action-type-selector">
4247 <div class="mxchat-action-type-search">
4248 <span class="dashicons dashicons-search"></span>
4249 <input type="text" id="action-type-search" placeholder="<?php esc_attr_e('Search action types...', 'mxchat'); ?>" class="mxchat-action-type-search-input">
4250 </div>
4251
4252 <?php
4253 // Get the callbacks - IMPORTANT: Define the $groups variable here
4254 $groups = $this->mxchat_get_available_callbacks(true, true);
4255 ?>
4256
4257 <div class="mxchat-action-type-categories">
4258 <button type="button" class="mxchat-category-button active" data-category="all"><?php esc_html_e('All', 'mxchat'); ?></button>
4259 <?php
4260 // Get unique categories from the defined groups
4261 foreach ($groups as $group_label => $group_callbacks) :
4262 $category_slug = sanitize_title($group_label);
4263 ?>
4264 <button type="button" class="mxchat-category-button" data-category="<?php echo esc_attr($category_slug); ?>"><?php echo esc_html($group_label); ?></button>
4265 <?php endforeach; ?>
4266 </div>
4267
4268 <div class="mxchat-action-types-grid">
4269 <?php
4270 // Generate action cards from available callbacks
4271 foreach ($groups as $group_label => $group_callbacks) :
4272 $category_slug = sanitize_title($group_label);
4273
4274 foreach ($group_callbacks as $function => $data) :
4275 $label = $data['label'];
4276 $icon = isset($data['icon']) ? $data['icon'] : 'admin-generic';
4277 $description = isset($data['description']) ? $data['description'] : '';
4278 $is_addon = isset($data['addon']) && $data['addon'] !== false;
4279 $addon_name = isset($data['addon_name']) ? $data['addon_name'] : '';
4280 $is_installed = isset($data['installed']) ? $data['installed'] : true;
4281 $is_promo = !empty($data['addon_promo']) && !$is_installed;
4282
4283 // Determine card status and styling
4284 $card_class = 'mxchat-action-type-card';
4285 $icon_class = 'mxchat-action-type-icon';
4286 $status_badge = '';
4287
4288 if ($is_promo) {
4289 // Promotional card — not selectable, just informational
4290 $card_class .= ' not-installed mxchat-promo-card';
4291 $status_badge = '<span class="mxchat-addon-badge">' . esc_html__('Add-on Required', 'mxchat') . '</span>';
4292 } elseif ($is_addon && !$is_installed) {
4293 // Add-on not installed
4294 $card_class .= ' not-installed';
4295 $status_badge .= '<span class="mxchat-addon-badge">' . esc_html__('Add-on Required', 'mxchat') . '</span>';
4296 }
4297
4298 // Default description if none provided
4299 if (empty($description)) {
4300 $description = sprintf(
4301 esc_html__('Use the %s action in your chatbot', 'mxchat'),
4302 $label
4303 );
4304 }
4305 ?>
4306 <div class="<?php echo esc_attr($card_class); ?>"
4307 data-category="<?php echo esc_attr($category_slug); ?>"
4308 <?php if (!$is_promo) : ?>
4309 data-value="<?php echo esc_attr($function); ?>"
4310 data-label="<?php echo esc_attr($label); ?>"
4311 <?php endif; ?>
4312 data-pro="false"
4313 data-addon="<?php echo esc_attr($is_addon ? $data['addon'] : ''); ?>"
4314 data-installed="<?php echo $is_installed ? 'true' : 'false'; ?>"
4315 <?php if ($is_promo) : ?>data-promo="true"<?php endif; ?>>
4316 <div class="<?php echo esc_attr($icon_class); ?>">
4317 <span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span>
4318 </div>
4319 <div class="mxchat-action-type-info">
4320 <h4><?php echo esc_html($label); ?></h4>
4321 <p><?php echo esc_html($description); ?></p>
4322 <?php if (!empty($status_badge)) : ?>
4323 <?php echo $status_badge; ?>
4324 <?php endif; ?>
4325
4326 <?php if ($is_promo || ($is_addon && !$is_installed)) : ?>
4327 <div class="mxchat-addon-info">
4328 <?php echo esc_html(sprintf(
4329 __('Requires %s', 'mxchat'),
4330 $addon_name
4331 )); ?>
4332 — <a href="https://mxchat.ai/" target="_blank"><?php esc_html_e('Get Add-on', 'mxchat'); ?></a>
4333 </div>
4334 <?php endif; ?>
4335 </div>
4336 </div>
4337 <?php endforeach;
4338 endforeach; ?>
4339 </div>
4340 </div>
4341
4342 <div class="mxchat-modal-actions">
4343 <button type="button" class="mxchat-button-secondary mxchat-modal-cancel">
4344 <?php esc_html_e('Cancel', 'mxchat'); ?>
4345 </button>
4346 </div>
4347 </div>
4348
4349 <!-- Step 2: Action Configuration -->
4350 <div id="mxchat-action-step-2" class="mxchat-action-step">
4351 <div class="mxchat-step-indicator">
4352 <div class="mxchat-step-number">2</div>
4353 <div class="mxchat-step-title"><?php esc_html_e('Configure Action', 'mxchat'); ?></div>
4354 </div>
4355
4356 <div class="mxchat-selected-action">
4357 <button type="button" class="mxchat-back-button" id="mxchat-back-to-step-1">
4358 <span class="dashicons dashicons-arrow-left-alt"></span>
4359 <?php esc_html_e('Back to Action Types', 'mxchat'); ?>
4360 </button>
4361 <div class="mxchat-selected-action-info">
4362 <div id="selected-action-icon" class="mxchat-action-type-icon">
4363 <span class="dashicons dashicons-admin-generic"></span>
4364 </div>
4365 <div class="mxchat-selected-action-details">
4366 <h3 id="selected-action-title"><?php esc_html_e('Selected Action', 'mxchat'); ?></h3>
4367 <p id="selected-action-description"><?php esc_html_e('Configure this action for your chatbot', 'mxchat'); ?></p>
4368 </div>
4369 </div>
4370 </div>
4371
4372 <div class="mxchat-form-group">
4373 <label for="intent_label">
4374 <?php esc_html_e('Action Label (For your reference only)', 'mxchat'); ?>
4375 </label>
4376 <input name="intent_label" type="text" id="intent_label" required
4377 class="mxchat-intent-input"
4378 placeholder="<?php esc_attr_e('Example: Newsletter Signup', 'mxchat'); ?>">
4379 </div>
4380
4381 <div class="mxchat-form-group">
4382 <label for="phrases">
4383 <?php esc_html_e('Trigger Phrases (comma-separated)', 'mxchat'); ?>
4384 </label>
4385 <textarea name="phrases" id="action_phrases" rows="5" required
4386 class="mxchat-intent-textarea"
4387 placeholder="<?php esc_attr_e('Example: sign me up, subscribe me, I want to join, add me to the newsletter', 'mxchat'); ?>"></textarea>
4388 </div>
4389
4390 <div class="mxchat-form-group">
4391 <label for="similarity_threshold">
4392 <?php esc_html_e('Similarity Threshold', 'mxchat'); ?>
4393 <span class="mxchat-threshold-value-display">85%</span>
4394 </label>
4395 <div class="mxchat-slider-group modal-slider">
4396 <input type="range"
4397 name="similarity_threshold"
4398 id="similarity_threshold"
4399 min="10"
4400 max="95"
4401 value="85"
4402 class="mxchat-intent-slider"
4403 oninput="document.querySelector('.mxchat-threshold-value-display').textContent = this.value + '%'">
4404 </div>
4405 <div class="mxchat-threshold-hint">
4406 <?php esc_html_e('Lower values (10-30) make the action trigger more easily. Higher values (70-95) require more exact matches.', 'mxchat'); ?>
4407 </div>
4408 </div>
4409
4410 <!-- Bot Selection Section -->
4411 <div class="mxchat-form-group">
4412 <label for="enabled_bots">
4413 <?php esc_html_e('Which bots should we enable this action for?', 'mxchat'); ?>
4414 </label>
4415 <div class="mxchat-bot-selector">
4416 <div class="mxchat-bot-option">
4417 <label class="mxchat-checkbox-label">
4418 <input type="checkbox"
4419 name="enabled_bots[]"
4420 value="default"
4421 id="bot_default"
4422 checked="checked">
4423 <span class="mxchat-checkmark"></span>
4424 <?php esc_html_e('Default Bot', 'mxchat'); ?>
4425 </label>
4426 </div>
4427
4428 <?php if (class_exists('MxChat_Multi_Bot_Core_Manager')) :
4429 $multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
4430 $available_bots = $multi_bot_manager->get_available_bots();
4431
4432 foreach ($available_bots as $bot_id => $bot_name) :
4433 if ($bot_id === 'default') continue; // Skip default, already shown above
4434 ?>
4435 <div class="mxchat-bot-option">
4436 <label class="mxchat-checkbox-label">
4437 <input type="checkbox"
4438 name="enabled_bots[]"
4439 value="<?php echo esc_attr($bot_id); ?>"
4440 id="bot_<?php echo esc_attr($bot_id); ?>">
4441 <span class="mxchat-checkmark"></span>
4442 <?php echo esc_html($bot_name); ?>
4443 </label>
4444 </div>
4445 <?php
4446 endforeach;
4447 endif; ?>
4448 </div>
4449 </div>
4450
4451 <div class="mxchat-modal-actions">
4452 <button type="button" class="mxchat-button-secondary mxchat-modal-cancel">
4453 <?php esc_html_e('Cancel', 'mxchat'); ?>
4454 </button>
4455 <button type="submit" class="mxchat-button-primary" id="mxchat-save-action-btn">
4456 <?php esc_html_e('Save Action', 'mxchat'); ?>
4457 </button>
4458 </div>
4459 </div>
4460 </form>
4461 </div>
4462 </div>
4463
4464 <div id="mxchat-action-loading" class="mxchat-action-loading" style="display: none;">
4465 <div class="mxchat-action-loading-spinner"></div>
4466 <div class="mxchat-action-loading-text">
4467 <?php esc_html_e('Saving action, please wait...', 'mxchat'); ?>
4468 </div>
4469 </div>
4470 </div><!-- .mxchat-wrapper -->
4471 <?php
4472 }
4473 private function get_trimmed_phrases($phrases, $max_length = 100) {
4474 if (strlen($phrases) <= $max_length) {
4475 return $phrases;
4476 }
4477
4478 $trimmed = substr($phrases, 0, $max_length);
4479 $last_comma = strrpos($trimmed, ',');
4480
4481 if ($last_comma !== false) {
4482 $trimmed = substr($trimmed, 0, $last_comma);
4483 }
4484
4485 return $trimmed . '...';
4486 }
4487 public function mxchat_add_enabled_column_to_intents() {
4488 global $wpdb;
4489 $table_name = $wpdb->prefix . 'mxchat_intents';
4490
4491 // Check if the column already exists
4492 $columns = $wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'enabled'");
4493
4494 if (empty($columns)) {
4495 // Add the column with default value of 1 (enabled)
4496 $wpdb->query("ALTER TABLE $table_name ADD COLUMN enabled TINYINT(1) NOT NULL DEFAULT 1");
4497 }
4498 }
4499
4500 public function mxchat_handle_delete_intent() {
4501 if ( ! current_user_can( 'manage_options' ) ) {
4502 wp_die( esc_html__('Unauthorized user', 'mxchat') );
4503 }
4504
4505 check_admin_referer('mxchat_delete_intent_nonce');
4506
4507 if (isset($_POST['intent_id'])) {
4508 global $wpdb;
4509 $table_name = $wpdb->prefix . 'mxchat_intents';
4510 $intent_id = intval($_POST['intent_id']);
4511
4512 $wpdb->delete($table_name, ['id' => $intent_id], ['%d']);
4513 }
4514
4515 wp_safe_redirect(admin_url('admin.php?page=mxchat-actions'));
4516 exit;
4517 }
4518 public function mxchat_handle_edit_intent() {
4519 // Security checks (nonce and permissions)
4520 if (!current_user_can('manage_options')) {
4521 wp_die(esc_html__('Unauthorized user', 'mxchat'));
4522 }
4523 check_admin_referer('mxchat_edit_intent');
4524
4525 // Get POST data
4526 $intent_id = isset($_POST['intent_id']) ? absint($_POST['intent_id']) : 0;
4527 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
4528 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
4529 $threshold_percentage = isset($_POST['similarity_threshold']) ? intval($_POST['similarity_threshold']) : 85;
4530 $similarity_threshold = min(95, max(10, $threshold_percentage)) / 100; // Convert to 0.10–0.95
4531
4532 // Handle enabled_bots
4533 $enabled_bots = isset($_POST['enabled_bots']) ? $_POST['enabled_bots'] : array('default');
4534 $enabled_bots = array_map('sanitize_text_field', $enabled_bots);
4535
4536 // Ensure default is always included for backward compatibility
4537 if (!in_array('default', $enabled_bots)) {
4538 $enabled_bots[] = 'default';
4539 }
4540
4541 $enabled_bots_json = json_encode($enabled_bots);
4542
4543 // Validate inputs
4544 if (!$intent_id || empty($intent_label) || empty($phrases_input)) {
4545 $this->handle_embedding_error(__('Invalid input. Please ensure all fields are filled out.', 'mxchat'));
4546 return;
4547 }
4548
4549 $phrases_array = array_map('sanitize_text_field', array_filter(array_map('trim', explode(',', $phrases_input))));
4550 if (empty($phrases_array)) {
4551 $this->handle_embedding_error(__('Please enter at least one valid phrase.', 'mxchat'));
4552 return;
4553 }
4554
4555 // Generate embeddings with improved error handling
4556 $vectors = [];
4557 $failed_phrases = [];
4558
4559 foreach ($phrases_array as $phrase) {
4560 $embedding_vector = $this->mxchat_generate_embedding($phrase, $this->options['api_key']);
4561 if (is_array($embedding_vector)) {
4562 $vectors[] = $embedding_vector;
4563 } else {
4564 $failed_phrases[] = $phrase;
4565 }
4566 }
4567
4568 if (!empty($failed_phrases)) {
4569 $this->handle_embedding_error(
4570 sprintf(
4571 __('Error generating embeddings for phrases: %s. Check your embedding API.', 'mxchat'),
4572 implode(', ', $failed_phrases)
4573 )
4574 );
4575 return;
4576 }
4577
4578 if (empty($vectors)) {
4579 $this->handle_embedding_error(__('No valid embeddings generated. Please check your phrases.', 'mxchat'));
4580 return;
4581 }
4582
4583 $combined_vector = $this->mxchat_average_vectors($vectors);
4584 $serialized_vector = maybe_serialize($combined_vector);
4585
4586 // Update the database
4587 global $wpdb;
4588 $table_name = $wpdb->prefix . 'mxchat_intents';
4589
4590 $result = $wpdb->update(
4591 $table_name,
4592 array(
4593 'intent_label' => $intent_label,
4594 'phrases' => implode(', ', $phrases_array),
4595 'embedding_vector' => $serialized_vector,
4596 'similarity_threshold' => $similarity_threshold,
4597 'enabled_bots' => $enabled_bots_json, // Include enabled_bots in update
4598 ),
4599 array('id' => $intent_id),
4600 array('%s', '%s', '%s', '%f', '%s'), // Format: string, string, string, float, string
4601 array('%d') // Where format: integer
4602 );
4603
4604 if (false === $result) {
4605 $this->handle_embedding_error(__('Failed to update action in database.', 'mxchat'));
4606 return;
4607 }
4608
4609 // Set success message and redirect
4610 set_transient('mxchat_admin_notice_success', __('Intent updated successfully!', 'mxchat'), 60);
4611
4612 $redirect_url = add_query_arg(
4613 array(
4614 'page' => 'mxchat-actions'
4615 ),
4616 admin_url('admin.php')
4617 );
4618 wp_safe_redirect($redirect_url);
4619 exit;
4620 }
4621 public function mxchat_handle_add_intent() {
4622 if (!current_user_can('manage_options')) {
4623 wp_die(esc_html__('Unauthorized user', 'mxchat'));
4624 }
4625 check_admin_referer('mxchat_add_intent_nonce');
4626 global $wpdb;
4627 $table_name = $wpdb->prefix . 'mxchat_intents';
4628
4629 // Sanitize and get form data
4630 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
4631 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
4632 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
4633
4634 // Get similarity threshold from form (convert percentage to decimal)
4635 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
4636
4637 // Handle enabled_bots
4638 $enabled_bots = isset($_POST['enabled_bots']) ? $_POST['enabled_bots'] : array('default');
4639 $enabled_bots = array_map('sanitize_text_field', $enabled_bots);
4640
4641 // Ensure default is always included for backward compatibility with existing actions
4642 if (!in_array('default', $enabled_bots)) {
4643 $enabled_bots[] = 'default';
4644 }
4645
4646 $enabled_bots_json = json_encode($enabled_bots);
4647
4648 // Validate required fields
4649 if (empty($intent_label) || empty($callback_function) || empty($phrases_input)) {
4650 $this->handle_embedding_error(__('Invalid input. Please ensure all fields are filled out.', 'mxchat'));
4651 return;
4652 }
4653
4654 // Validate callback function
4655 $available_callbacks = $this->mxchat_get_available_callbacks();
4656 if (!array_key_exists($callback_function, $available_callbacks)) {
4657 $this->handle_embedding_error(__('Invalid callback function selected.', 'mxchat'));
4658 return;
4659 }
4660
4661 // Check if this is an add-on promotional placeholder (not a real action)
4662 if (!empty($available_callbacks[$callback_function]['addon_promo'])) {
4663 $addon_name = isset($available_callbacks[$callback_function]['addon_name']) ? $available_callbacks[$callback_function]['addon_name'] : __('an add-on', 'mxchat');
4664 $this->handle_embedding_error(sprintf(
4665 __('This action requires the %s to be installed and activated.', 'mxchat'),
4666 $addon_name
4667 ));
4668 return;
4669 }
4670
4671 // Process phrases
4672 $phrases_array = array_map('sanitize_text_field', array_filter(array_map('trim', explode(',', $phrases_input))));
4673 if (empty($phrases_array)) {
4674 $this->handle_embedding_error(__('Please enter at least one valid phrase.', 'mxchat'));
4675 return;
4676 }
4677
4678 // Generate embeddings with improved error handling
4679 $vectors = [];
4680 $failed_phrases = [];
4681 foreach ($phrases_array as $phrase) {
4682 $embedding_vector = $this->mxchat_generate_embedding($phrase, $this->options['api_key']);
4683 if (is_array($embedding_vector)) {
4684 $vectors[] = $embedding_vector;
4685 } else {
4686 $failed_phrases[] = $phrase;
4687 }
4688 }
4689
4690 // Check for embedding failures
4691 if (!empty($failed_phrases)) {
4692 $this->handle_embedding_error(
4693 sprintf(
4694 __('Error generating embeddings for phrases: %s. Check your embedding API.', 'mxchat'),
4695 implode(', ', $failed_phrases)
4696 )
4697 );
4698 return;
4699 }
4700
4701 if (empty($vectors)) {
4702 $this->handle_embedding_error(__('No valid embeddings generated. Please check your phrases.', 'mxchat'));
4703 return;
4704 }
4705
4706 // Create combined vector and insert into database
4707 $combined_vector = $this->mxchat_average_vectors($vectors);
4708 $serialized_vector = maybe_serialize($combined_vector);
4709
4710 $result = $wpdb->insert($table_name, [
4711 'intent_label' => $intent_label,
4712 'phrases' => implode(', ', $phrases_array),
4713 'embedding_vector' => $serialized_vector,
4714 'callback_function' => $callback_function,
4715 'similarity_threshold' => $similarity_threshold,
4716 'enabled_bots' => $enabled_bots_json, // NEW field
4717 ]);
4718
4719 if ($result === false) {
4720 $this->handle_embedding_error(__('Database error: ', 'mxchat') . $wpdb->last_error);
4721 return;
4722 }
4723
4724 // Set success message and redirect
4725 set_transient('mxchat_admin_notice_success', __('New intent added successfully!', 'mxchat'), 60);
4726 wp_safe_redirect(admin_url('admin.php?page=mxchat-actions'));
4727 exit;
4728 }
4729
4730
4731
4732
4733 private function handle_embedding_error($message, $redirect = true) {
4734 // Store the error message in the existing transient
4735 set_transient('mxchat_admin_notice_error', $message, 60);
4736
4737 if ($redirect) {
4738 // Redirect back to the actions page
4739 $redirect_url = add_query_arg(
4740 array(
4741 'page' => 'mxchat-actions'
4742 ),
4743 admin_url('admin.php')
4744 );
4745 wp_safe_redirect($redirect_url);
4746 exit;
4747 }
4748 }
4749
4750 /**
4751 * AJAX handler to fetch actions list for the new split-panel UI
4752 */
4753 public function mxchat_fetch_actions_list() {
4754 check_ajax_referer('mxchat_actions_nonce', 'security');
4755
4756 if (!current_user_can('manage_options')) {
4757 wp_send_json_error(__('Unauthorized', 'mxchat'));
4758 }
4759
4760 global $wpdb;
4761 $table_name = $wpdb->prefix . 'mxchat_intents';
4762
4763 $page = isset($_POST['page']) ? max(1, intval($_POST['page'])) : 1;
4764 $per_page = isset($_POST['per_page']) ? min(100, max(1, intval($_POST['per_page']))) : 50;
4765 $offset = ($page - 1) * $per_page;
4766 $search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
4767 $callback_filter = isset($_POST['callback_filter']) ? sanitize_text_field($_POST['callback_filter']) : '';
4768 $sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] === 'asc' ? 'ASC' : 'DESC';
4769
4770 // Build WHERE clause
4771 $where = '1=1';
4772 $params = array();
4773
4774 if ($search) {
4775 $search_like = '%' . $wpdb->esc_like($search) . '%';
4776 $where .= ' AND (intent_label LIKE %s OR phrases LIKE %s)';
4777 $params[] = $search_like;
4778 $params[] = $search_like;
4779 }
4780
4781 if ($callback_filter) {
4782 $where .= ' AND callback_function = %s';
4783 $params[] = $callback_filter;
4784 }
4785
4786 // Get total count
4787 $count_query = "SELECT COUNT(*) FROM $table_name WHERE $where";
4788 if (!empty($params)) {
4789 $count_query = $wpdb->prepare($count_query, $params);
4790 }
4791 $total_actions = $wpdb->get_var($count_query);
4792
4793 // Get actions
4794 $query = "SELECT * FROM $table_name WHERE $where ORDER BY id $sort_order LIMIT %d OFFSET %d";
4795 $all_params = array_merge($params, array($per_page, $offset));
4796 $actions = $wpdb->get_results($wpdb->prepare($query, $all_params));
4797
4798 // Get available callbacks for labels/icons
4799 $available_callbacks = $this->mxchat_get_available_callbacks();
4800
4801 // Prefetch individual phrase counts for all fetched actions
4802 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
4803 $phrase_counts = array();
4804 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
4805 $action_ids = wp_list_pluck($actions, 'id');
4806 if (!empty($action_ids)) {
4807 $id_placeholders = implode(',', array_fill(0, count($action_ids), '%d'));
4808 $count_results = $wpdb->get_results($wpdb->prepare(
4809 "SELECT intent_id, COUNT(*) as cnt FROM $phrases_table WHERE intent_id IN ($id_placeholders) GROUP BY intent_id",
4810 $action_ids
4811 ));
4812 foreach ($count_results as $row) {
4813 $phrase_counts[$row->intent_id] = intval($row->cnt);
4814 }
4815 }
4816 }
4817
4818 // Format actions for response
4819 $formatted_actions = array();
4820 foreach ($actions as $action) {
4821 $callback_data = isset($available_callbacks[$action->callback_function])
4822 ? $available_callbacks[$action->callback_function]
4823 : array('label' => $action->callback_function, 'icon' => 'admin-generic');
4824
4825 $enabled_bots = json_decode($action->enabled_bots, true);
4826 if (!is_array($enabled_bots)) {
4827 $enabled_bots = array('default');
4828 }
4829
4830 $formatted_actions[] = array(
4831 'id' => intval($action->id),
4832 'label' => $action->intent_label,
4833 'phrases' => $action->phrases,
4834 'callback_function' => $action->callback_function,
4835 'callback_label' => $callback_data['label'],
4836 'icon' => isset($callback_data['icon']) ? $callback_data['icon'] : 'admin-generic',
4837 'threshold' => round($action->similarity_threshold * 100),
4838 'enabled' => (bool) $action->enabled,
4839 'enabled_bots' => $enabled_bots,
4840 'has_legacy_vector' => !empty($action->embedding_vector),
4841 'individual_phrase_count' => isset($phrase_counts[$action->id]) ? $phrase_counts[$action->id] : 0,
4842 );
4843 }
4844
4845 $total_pages = ceil($total_actions / $per_page);
4846 $showing_start = $total_actions > 0 ? $offset + 1 : 0;
4847 $showing_end = min($offset + $per_page, $total_actions);
4848
4849 wp_send_json_success(array(
4850 'actions' => $formatted_actions,
4851 'page' => $page,
4852 'per_page' => $per_page,
4853 'total_actions' => intval($total_actions),
4854 'total_pages' => $total_pages,
4855 'showing_start' => $showing_start,
4856 'showing_end' => $showing_end,
4857 ));
4858 }
4859
4860 /**
4861 * AJAX handler to toggle action enabled status
4862 */
4863 public function mxchat_toggle_action_status() {
4864 check_ajax_referer('mxchat_actions_nonce', 'security');
4865
4866 if (!current_user_can('manage_options')) {
4867 wp_send_json_error(__('Unauthorized', 'mxchat'));
4868 }
4869
4870 $action_id = isset($_POST['action_id']) ? intval($_POST['action_id']) : 0;
4871 $enabled = isset($_POST['enabled']) ? intval($_POST['enabled']) : 0;
4872
4873 if (!$action_id) {
4874 wp_send_json_error(__('Invalid action ID', 'mxchat'));
4875 }
4876
4877 global $wpdb;
4878 $table_name = $wpdb->prefix . 'mxchat_intents';
4879
4880 $result = $wpdb->update(
4881 $table_name,
4882 array('enabled' => $enabled ? 1 : 0),
4883 array('id' => $action_id),
4884 array('%d'),
4885 array('%d')
4886 );
4887
4888 if ($result === false) {
4889 wp_send_json_error(__('Failed to update action status', 'mxchat'));
4890 }
4891
4892 wp_send_json_success(array('enabled' => (bool) $enabled));
4893 }
4894
4895 /**
4896 * AJAX handler to bulk delete actions
4897 */
4898 public function mxchat_bulk_delete_actions() {
4899 check_ajax_referer('mxchat_delete_intent_nonce', 'security');
4900
4901 if (!current_user_can('manage_options')) {
4902 wp_send_json_error(__('Unauthorized', 'mxchat'));
4903 }
4904
4905 $action_ids = isset($_POST['action_ids']) ? array_map('intval', (array) $_POST['action_ids']) : array();
4906
4907 if (empty($action_ids)) {
4908 wp_send_json_error(__('No actions selected', 'mxchat'));
4909 }
4910
4911 global $wpdb;
4912 $table_name = $wpdb->prefix . 'mxchat_intents';
4913
4914 $placeholders = implode(',', array_fill(0, count($action_ids), '%d'));
4915 $query = $wpdb->prepare("DELETE FROM $table_name WHERE id IN ($placeholders)", $action_ids);
4916 $result = $wpdb->query($query);
4917
4918 if ($result === false) {
4919 wp_send_json_error(__('Failed to delete actions', 'mxchat'));
4920 }
4921
4922 // Also delete individual phrases for these intents
4923 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
4924 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
4925 $wpdb->query($wpdb->prepare("DELETE FROM $phrases_table WHERE intent_id IN ($placeholders)", $action_ids));
4926 }
4927
4928 wp_send_json_success(array('deleted' => $result));
4929 }
4930
4931 /**
4932 * AJAX handler to add a new intent/action
4933 */
4934 public function mxchat_add_intent_ajax() {
4935 check_ajax_referer('mxchat_add_intent_nonce', 'security');
4936
4937 if (!current_user_can('manage_options')) {
4938 wp_send_json_error(__('Unauthorized', 'mxchat'));
4939 }
4940
4941 global $wpdb;
4942 $table_name = $wpdb->prefix . 'mxchat_intents';
4943
4944 // Sanitize input
4945 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
4946 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
4947 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
4948 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
4949 $enabled_bots = isset($_POST['enabled_bots']) ? array_map('sanitize_text_field', (array) $_POST['enabled_bots']) : array('default');
4950
4951 // Validate
4952 // Check if using new individual phrases mode or legacy mode
4953 $individual_phrases = isset($_POST['individual_phrases']) ? array_filter(array_map('sanitize_text_field', (array) $_POST['individual_phrases'])) : array();
4954 $use_individual = !empty($individual_phrases);
4955
4956 // Validate required fields (phrases not required when using individual mode)
4957 if (empty($intent_label) || empty($callback_function)) {
4958 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
4959 }
4960 if (!$use_individual && empty($phrases_input)) {
4961 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
4962 }
4963
4964 // Ensure default bot is included
4965 if (!in_array('default', $enabled_bots)) {
4966 $enabled_bots[] = 'default';
4967 }
4968 $enabled_bots_json = json_encode($enabled_bots);
4969
4970 if ($use_individual) {
4971 // New mode: individual phrases each get their own vector
4972 // Insert the intent row with empty legacy fields
4973 $result = $wpdb->insert(
4974 $table_name,
4975 array(
4976 'intent_label' => $intent_label,
4977 'phrases' => '',
4978 'embedding_vector' => '',
4979 'similarity_threshold' => $similarity_threshold,
4980 'callback_function' => $callback_function,
4981 'enabled' => 1,
4982 'enabled_bots' => $enabled_bots_json,
4983 )
4984 );
4985
4986 if ($result === false) {
4987 wp_send_json_error(__('Failed to add action to database.', 'mxchat'));
4988 }
4989
4990 $intent_id = $wpdb->insert_id;
4991
4992 // Insert each phrase individually with its own embedding
4993 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
4994 $failed_phrases = array();
4995 foreach ($individual_phrases as $phrase) {
4996 $phrase = trim($phrase);
4997 if (empty($phrase)) continue;
4998
4999 $embedding_vector = $this->mxchat_generate_embedding($phrase);
5000 if (is_wp_error($embedding_vector)) {
5001 $failed_phrases[] = $phrase;
5002 continue;
5003 }
5004
5005 $wpdb->insert(
5006 $phrases_table,
5007 array(
5008 'intent_id' => $intent_id,
5009 'phrase' => $phrase,
5010 'embedding_vector' => maybe_serialize($embedding_vector),
5011 )
5012 );
5013 }
5014
5015 $response = array('id' => $intent_id);
5016 if (!empty($failed_phrases)) {
5017 $response['failed_phrases'] = $failed_phrases;
5018 }
5019 wp_send_json_success($response);
5020
5021 } else {
5022 // Legacy mode: combine all phrases into one embedding (backwards compatible)
5023 $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
5024 if (empty($phrases_array)) {
5025 wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
5026 }
5027
5028 // Generate embedding (combine phrases into single string for embedding)
5029 $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
5030 if (is_wp_error($embedding_vector)) {
5031 // Fallback: store without embedding
5032 $serialized_vector = null;
5033 } else {
5034 $serialized_vector = maybe_serialize($embedding_vector);
5035 }
5036
5037 // Insert
5038 $result = $wpdb->insert(
5039 $table_name,
5040 array(
5041 'intent_label' => $intent_label,
5042 'phrases' => implode(', ', $phrases_array),
5043 'embedding_vector' => $serialized_vector,
5044 'similarity_threshold' => $similarity_threshold,
5045 'callback_function' => $callback_function,
5046 'enabled' => 1,
5047 'enabled_bots' => $enabled_bots_json,
5048 )
5049 );
5050
5051 if ($result === false) {
5052 wp_send_json_error(__('Failed to add action to database.', 'mxchat'));
5053 }
5054
5055 wp_send_json_success(array('id' => $wpdb->insert_id));
5056 }
5057 }
5058
5059 /**
5060 * AJAX handler to edit an existing intent/action
5061 */
5062 public function mxchat_edit_intent_ajax() {
5063 check_ajax_referer('mxchat_edit_intent', 'security');
5064
5065 if (!current_user_can('manage_options')) {
5066 wp_send_json_error(__('Unauthorized', 'mxchat'));
5067 }
5068
5069 global $wpdb;
5070 $table_name = $wpdb->prefix . 'mxchat_intents';
5071
5072 // Sanitize input
5073 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5074 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
5075 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
5076 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
5077 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
5078 $enabled_bots = isset($_POST['enabled_bots']) ? array_map('sanitize_text_field', (array) $_POST['enabled_bots']) : array('default');
5079
5080 // Validate
5081 if (!$intent_id || empty($intent_label)) {
5082 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
5083 }
5084
5085 // Check if phrases are managed individually (empty phrases_input means individual mode)
5086 $uses_individual_phrases = empty($phrases_input);
5087
5088 if ($uses_individual_phrases) {
5089 // Individual phrase mode: only update non-phrase fields, skip embedding regeneration
5090 $update_data = array(
5091 'intent_label' => $intent_label,
5092 'similarity_threshold' => $similarity_threshold,
5093 'callback_function' => $callback_function,
5094 'enabled_bots' => json_encode($enabled_bots),
5095 );
5096 } else {
5097 // Legacy mode: process phrases and regenerate embedding (backwards compatible for add-ons)
5098 $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
5099 if (empty($phrases_array)) {
5100 wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
5101 }
5102
5103 // Generate new embedding (combine phrases into single string for embedding)
5104 $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
5105 if (is_wp_error($embedding_vector)) {
5106 // Keep existing embedding
5107 $update_data = array(
5108 'intent_label' => $intent_label,
5109 'phrases' => implode(', ', $phrases_array),
5110 'similarity_threshold' => $similarity_threshold,
5111 'callback_function' => $callback_function,
5112 'enabled_bots' => json_encode($enabled_bots),
5113 );
5114 } else {
5115 $serialized_vector = maybe_serialize($embedding_vector);
5116 $update_data = array(
5117 'intent_label' => $intent_label,
5118 'phrases' => implode(', ', $phrases_array),
5119 'embedding_vector' => $serialized_vector,
5120 'similarity_threshold' => $similarity_threshold,
5121 'callback_function' => $callback_function,
5122 'enabled_bots' => json_encode($enabled_bots),
5123 );
5124 }
5125 }
5126
5127 // Ensure default bot is included
5128 if (!in_array('default', $enabled_bots)) {
5129 $enabled_bots[] = 'default';
5130 }
5131 $update_data['enabled_bots'] = json_encode($enabled_bots);
5132
5133 $result = $wpdb->update(
5134 $table_name,
5135 $update_data,
5136 array('id' => $intent_id),
5137 null,
5138 array('%d')
5139 );
5140
5141 if ($result === false) {
5142 wp_send_json_error(__('Failed to update action.', 'mxchat'));
5143 }
5144
5145 wp_send_json_success(array('updated' => true));
5146 }
5147
5148 /**
5149 * AJAX handler to delete a single intent/action
5150 */
5151 public function mxchat_delete_intent_ajax() {
5152 check_ajax_referer('mxchat_delete_intent_nonce', 'security');
5153
5154 if (!current_user_can('manage_options')) {
5155 wp_send_json_error(__('Unauthorized', 'mxchat'));
5156 }
5157
5158 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5159
5160 if (!$intent_id) {
5161 wp_send_json_error(__('Invalid action ID', 'mxchat'));
5162 }
5163
5164 global $wpdb;
5165 $table_name = $wpdb->prefix . 'mxchat_intents';
5166
5167 $result = $wpdb->delete($table_name, array('id' => $intent_id), array('%d'));
5168
5169 if ($result === false) {
5170 wp_send_json_error(__('Failed to delete action', 'mxchat'));
5171 }
5172
5173 // Also delete individual phrases for this intent
5174 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5175 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5176 $wpdb->delete($phrases_table, array('intent_id' => $intent_id), array('%d'));
5177 }
5178
5179 wp_send_json_success(array('deleted' => true));
5180 }
5181
5182 /**
5183 * AJAX handler to add a single phrase with its own embedding to an intent
5184 */
5185 public function mxchat_add_phrase_ajax() {
5186 check_ajax_referer('mxchat_add_phrase_nonce', 'security');
5187
5188 if (!current_user_can('manage_options')) {
5189 wp_send_json_error(__('Unauthorized', 'mxchat'));
5190 }
5191
5192 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5193 $phrase = isset($_POST['phrase']) ? sanitize_text_field($_POST['phrase']) : '';
5194
5195 if (!$intent_id || empty($phrase)) {
5196 wp_send_json_error(__('Please provide an intent ID and phrase.', 'mxchat'));
5197 }
5198
5199 // Verify the intent exists
5200 global $wpdb;
5201 $intents_table = $wpdb->prefix . 'mxchat_intents';
5202 $intent = $wpdb->get_row($wpdb->prepare("SELECT id FROM $intents_table WHERE id = %d", $intent_id));
5203 if (!$intent) {
5204 wp_send_json_error(__('Action not found.', 'mxchat'));
5205 }
5206
5207 // Generate embedding for this single phrase
5208 $embedding_vector = $this->mxchat_generate_embedding($phrase);
5209 if (is_wp_error($embedding_vector)) {
5210 wp_send_json_error(__('Failed to generate embedding: ', 'mxchat') . $embedding_vector->get_error_message());
5211 }
5212
5213 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5214 $result = $wpdb->insert(
5215 $phrases_table,
5216 array(
5217 'intent_id' => $intent_id,
5218 'phrase' => $phrase,
5219 'embedding_vector' => maybe_serialize($embedding_vector),
5220 )
5221 );
5222
5223 if ($result === false) {
5224 wp_send_json_error(__('Failed to add phrase.', 'mxchat'));
5225 }
5226
5227 wp_send_json_success(array('id' => $wpdb->insert_id, 'phrase' => $phrase));
5228 }
5229
5230 /**
5231 * AJAX handler to delete a single phrase from wp_mxchat_intent_phrases
5232 */
5233 public function mxchat_delete_phrase_ajax() {
5234 check_ajax_referer('mxchat_delete_phrase_nonce', 'security');
5235
5236 if (!current_user_can('manage_options')) {
5237 wp_send_json_error(__('Unauthorized', 'mxchat'));
5238 }
5239
5240 $phrase_id = isset($_POST['phrase_id']) ? intval($_POST['phrase_id']) : 0;
5241 if (!$phrase_id) {
5242 wp_send_json_error(__('Invalid phrase ID.', 'mxchat'));
5243 }
5244
5245 global $wpdb;
5246 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5247 $result = $wpdb->delete($phrases_table, array('id' => $phrase_id), array('%d'));
5248
5249 if ($result === false) {
5250 wp_send_json_error(__('Failed to delete phrase.', 'mxchat'));
5251 }
5252
5253 wp_send_json_success(array('deleted' => true));
5254 }
5255
5256 /**
5257 * AJAX handler to fetch individual phrases for an intent
5258 */
5259 public function mxchat_get_phrases_ajax() {
5260 check_ajax_referer('mxchat_get_phrases_nonce', 'security');
5261
5262 if (!current_user_can('manage_options')) {
5263 wp_send_json_error(__('Unauthorized', 'mxchat'));
5264 }
5265
5266 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5267 if (!$intent_id) {
5268 wp_send_json_error(__('Invalid intent ID.', 'mxchat'));
5269 }
5270
5271 global $wpdb;
5272 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5273
5274 $phrases = array();
5275 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5276 $phrases = $wpdb->get_results($wpdb->prepare(
5277 "SELECT id, phrase, created_at FROM $phrases_table WHERE intent_id = %d ORDER BY created_at ASC",
5278 $intent_id
5279 ));
5280 }
5281
5282 wp_send_json_success(array('phrases' => $phrases));
5283 }
5284
5285 /**
5286 * AJAX handler to clear legacy phrases and embedding from the main intents table
5287 */
5288 public function mxchat_delete_legacy_phrases_ajax() {
5289 check_ajax_referer('mxchat_delete_legacy_nonce', 'security');
5290
5291 if (!current_user_can('manage_options')) {
5292 wp_send_json_error(__('Unauthorized', 'mxchat'));
5293 }
5294
5295 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5296 if (!$intent_id) {
5297 wp_send_json_error(__('Invalid intent ID.', 'mxchat'));
5298 }
5299
5300 global $wpdb;
5301 $table_name = $wpdb->prefix . 'mxchat_intents';
5302
5303 $result = $wpdb->update(
5304 $table_name,
5305 array('phrases' => '', 'embedding_vector' => ''),
5306 array('id' => $intent_id),
5307 array('%s', '%s'),
5308 array('%d')
5309 );
5310
5311 if ($result === false) {
5312 wp_send_json_error(__('Failed to clear legacy phrases.', 'mxchat'));
5313 }
5314
5315 wp_send_json_success(array('cleared' => true));
5316 }
5317
5318 /**
5319 * Enhanced get_available_callbacks function with form action exclusion
5320 *
5321 * @param bool $grouped Whether to return callbacks grouped by category
5322 * @param bool $include_all Whether to include all potential actions (even if add-on not installed)
5323 * @return array Callbacks data with icons, descriptions and availability status
5324 */
5325 private function mxchat_get_available_callbacks($grouped = false, $include_all = true) {
5326 // Load WordPress plugin functions if needed
5327 if (!function_exists('get_plugins')) {
5328 require_once ABSPATH . 'wp-admin/includes/plugin.php';
5329 }
5330
5331 // Get active plugins
5332 $active_plugins = get_option('active_plugins', array());
5333
5334 // Functions to exclude from the action selector only if Pro is activated
5335 // If user doesn't have Pro, show these so they can see what they're missing
5336 $excluded_when_pro_active_functions = array(
5337 'mxchat_handle_form_collection' // Forms add-on action
5338 );
5339
5340 // Always excluded functions (regardless of Pro status)
5341 $always_excluded_functions = array();
5342
5343 // Combine exclusion lists based on Pro activation status
5344 $excluded_functions = $always_excluded_functions;
5345 if ($this->is_activated) {
5346 // Only exclude add-on managed functions if Pro is active
5347 $excluded_functions = array_merge($excluded_functions, $excluded_when_pro_active_functions);
5348 }
5349
5350 // Define add-on plugin files and their corresponding action functions
5351 $addon_plugins = array(
5352 'mxchat-woo/mxchat-woo.php' => array(
5353 'functions' => array(
5354 'mxchat_handle_product_recommendations',
5355 'mxchat_handle_order_history',
5356 'mxchat_show_product_card',
5357 'mxchat_add_to_cart',
5358 'mxchat_checkout_redirect',
5359 'mxchat_handle_featured_products'
5360 ),
5361 'name' => __('WooCommerce Add-on', 'mxchat'),
5362 'pro_required' => true
5363 ),
5364 'mxchat-perplexity/mxchat-perplexity.php' => array(
5365 'functions' => array('mxchat_perplexity_research'),
5366 'name' => __('Perplexity Add-on', 'mxchat'),
5367 'pro_required' => true
5368 ),
5369 'mxchat-forms/mxchat-forms.php' => array(
5370 'functions' => array('mxchat_handle_form_collection'),
5371 'name' => __('Forms Add-on', 'mxchat'),
5372 'pro_required' => true
5373 ),
5374 // Add other add-ons and their functions here
5375 );
5376
5377 // Get the functions that are provided by active add-ons
5378 $addon_provided_functions = array();
5379 $addon_function_mapping = array(); // Maps functions to their add-on info
5380
5381 // Check which add-ons are active
5382 foreach ($addon_plugins as $plugin_file => $addon_info) {
5383 $is_active = in_array($plugin_file, $active_plugins);
5384
5385 // For each function in this addon
5386 foreach ($addon_info['functions'] as $function) {
5387 // Consider a function installed only if:
5388 // 1. The add-on is active AND
5389 // 2. Either it doesn't require Pro OR Pro is activated
5390 $is_installed = $is_active && (!$addon_info['pro_required'] || $this->is_activated);
5391
5392 // If the add-on is installed, mark this function as provided by an add-on
5393 if ($is_installed) {
5394 $addon_provided_functions[] = $function;
5395 }
5396
5397 // Store addon info for this function regardless of installation status
5398 $addon_function_mapping[$function] = array(
5399 'addon' => basename(dirname($plugin_file)),
5400 'addon_name' => $addon_info['name'],
5401 'pro_required' => $addon_info['pro_required'],
5402 'is_active' => $is_active,
5403 'is_installed' => $is_installed
5404 );
5405 }
5406 }
5407
5408 // Core callbacks - always available in the base plugin
5409 $core_callbacks = array(
5410 'mxchat_handle_email_capture' => array(
5411 'label' => __('Loops Email Capture', 'mxchat'),
5412 'pro_only' => false,
5413 'group' => __('Customer Engagement', 'mxchat'),
5414 'icon' => 'email-alt',
5415 'description' => __('Collect visitor emails for your mailing list in Loops', 'mxchat'),
5416 'addon' => false, // Not from an add-on
5417 'installed' => true // Always installed with base plugin
5418 ),
5419 'mxchat_handle_search_request' => array(
5420 'label' => __('Brave Web Search', 'mxchat'),
5421 'pro_only' => false,
5422 'group' => __('Search Features', 'mxchat'),
5423 'icon' => 'search',
5424 'description' => __('Let users search the web directly from the chat', 'mxchat'),
5425 'addon' => false,
5426 'installed' => true
5427 ),
5428 'mxchat_handle_image_search_request' => array(
5429 'label' => __('Brave Image Search', 'mxchat'),
5430 'pro_only' => false,
5431 'group' => __('Search Features', 'mxchat'),
5432 'icon' => 'format-image',
5433 'description' => __('Search and display images in the chat conversation', 'mxchat'),
5434 'addon' => false,
5435 'installed' => true
5436 ),
5437 // Pro core features - check is_activated property
5438 'mxchat_generate_image' => array(
5439 'label' => __('Generate Image (OpenAI)', 'mxchat'),
5440 'pro_only' => false,
5441 'group' => __('Other Features', 'mxchat'),
5442 'icon' => 'art',
5443 'description' => __('Create images with GPT Image from OpenAI (requires OpenAI API key)', 'mxchat'),
5444 'addon' => false,
5445 'installed' => true
5446 ),
5447 'mxchat_generate_gemini_image' => array(
5448 'label' => __('Generate Image (Gemini)', 'mxchat'),
5449 'pro_only' => false,
5450 'group' => __('Other Features', 'mxchat'),
5451 'icon' => 'art',
5452 'description' => __('Create images with Imagen from Google (requires Gemini API key)', 'mxchat'),
5453 'addon' => false,
5454 'installed' => true
5455 ),
5456 'mxchat_handle_pdf_discussion' => array(
5457 'label' => __('Chat with PDF', 'mxchat'),
5458 'pro_only' => false,
5459 'group' => __('Other Features', 'mxchat'),
5460 'icon' => 'media-document',
5461 'description' => __('Answer questions about uploaded PDF documents', 'mxchat'),
5462 'addon' => false,
5463 'installed' => true
5464 ),
5465 'mxchat_live_agent_handover' => array(
5466 'label' => __('Slack Live Agent', 'mxchat'),
5467 'pro_only' => false,
5468 'group' => __('Customer Engagement', 'mxchat'),
5469 'icon' => 'admin-users',
5470 'description' => __('Transfer conversation to a human support agent on Slack', 'mxchat'),
5471 'addon' => false,
5472 'installed' => true
5473 ),
5474 'mxchat_telegram_live_agent_handover' => array(
5475 'label' => __('Telegram Live Agent', 'mxchat'),
5476 'pro_only' => false,
5477 'group' => __('Customer Engagement', 'mxchat'),
5478 'icon' => 'format-chat',
5479 'description' => __('Transfer conversation to a human support agent on Telegram', 'mxchat'),
5480 'addon' => false,
5481 'installed' => true
5482 ),
5483 'mxchat_handle_switch_to_chatbot_intent' => array(
5484 'label' => __('Back to Chatbot', 'mxchat'),
5485 'pro_only' => false,
5486 'group' => __('Customer Engagement', 'mxchat'),
5487 'icon' => 'backup',
5488 'description' => __('Return from live agent mode to AI chatbot', 'mxchat'),
5489 'addon' => false,
5490 'installed' => true
5491 ),
5492 );
5493
5494 // Add-on callbacks with placeholders - only include if the add-on is NOT active
5495 // These are promotional/informational only — not selectable as real actions
5496 $addon_callbacks = array(
5497 // WooCommerce Add-on
5498 'mxchat_handle_product_recommendations' => array(
5499 'label' => __('Product Recommendations', 'mxchat'),
5500 'pro_only' => false,
5501 'addon_promo' => true,
5502 'group' => __('WooCommerce Features', 'mxchat'),
5503 'icon' => 'cart',
5504 'description' => __('Suggest products based on customer preferences', 'mxchat'),
5505 ),
5506 'mxchat_handle_order_history' => array(
5507 'label' => __('Order History', 'mxchat'),
5508 'pro_only' => false,
5509 'addon_promo' => true,
5510 'group' => __('WooCommerce Features', 'mxchat'),
5511 'icon' => 'clipboard',
5512 'description' => __('Allow customers to check their order status', 'mxchat'),
5513 ),
5514 'mxchat_show_product_card' => array(
5515 'label' => __('Show Product Card', 'mxchat'),
5516 'pro_only' => false,
5517 'addon_promo' => true,
5518 'group' => __('WooCommerce Features', 'mxchat'),
5519 'icon' => 'products',
5520 'description' => __('Display product information in the chat', 'mxchat'),
5521 ),
5522 'mxchat_add_to_cart' => array(
5523 'label' => __('Add to Cart', 'mxchat'),
5524 'pro_only' => false,
5525 'addon_promo' => true,
5526 'group' => __('WooCommerce Features', 'mxchat'),
5527 'icon' => 'plus-alt',
5528 'description' => __('Add products to cart directly from chat', 'mxchat'),
5529 ),
5530 'mxchat_checkout_redirect' => array(
5531 'label' => __('Proceed to Checkout', 'mxchat'),
5532 'pro_only' => false,
5533 'addon_promo' => true,
5534 'group' => __('WooCommerce Features', 'mxchat'),
5535 'icon' => 'arrow-right-alt',
5536 'description' => __('Redirect customer to checkout page', 'mxchat'),
5537 ),
5538 'mxchat_handle_featured_products' => array(
5539 'label' => __('Featured Products Showcase', 'mxchat'),
5540 'pro_only' => false,
5541 'addon_promo' => true,
5542 'group' => __('WooCommerce Features', 'mxchat'),
5543 'icon' => 'star-filled',
5544 'description' => __('Display a curated selection of products with an AI-generated message', 'mxchat'),
5545 ),
5546
5547 // Perplexity Add-on
5548 'mxchat_perplexity_research' => array(
5549 'label' => __('Perplexity Research', 'mxchat'),
5550 'pro_only' => false,
5551 'addon_promo' => true,
5552 'group' => __('Search Features', 'mxchat'),
5553 'icon' => 'book-alt',
5554 'description' => __('Allows the chatbot to search the web for accurate, up-to-date answers', 'mxchat'),
5555 ),
5556
5557 // Forms Add-on
5558 'mxchat_handle_form_collection' => array(
5559 'label' => __('Form Collection', 'mxchat'),
5560 'pro_only' => false,
5561 'addon_promo' => true,
5562 'group' => __('Form Features', 'mxchat'),
5563 'icon' => 'feedback',
5564 'description' => __('Collect user information through custom forms in chat', 'mxchat'),
5565 ),
5566 );
5567
5568 // Enhance add-on callbacks with installation status and addon info
5569 foreach ($addon_callbacks as $function => $data) {
5570 if (isset($addon_function_mapping[$function])) {
5571 $addon_info = $addon_function_mapping[$function];
5572
5573 $addon_callbacks[$function]['addon'] = $addon_info['addon'];
5574 $addon_callbacks[$function]['addon_name'] = $addon_info['addon_name'];
5575 $addon_callbacks[$function]['installed'] = $addon_info['is_installed'];
5576
5577 // Set pro_only based on add-on configuration
5578 $addon_callbacks[$function]['pro_only'] = $addon_info['pro_required'];
5579 } else {
5580 $addon_callbacks[$function]['addon'] = 'unknown';
5581 $addon_callbacks[$function]['addon_name'] = __('Unknown Add-on', 'mxchat');
5582 $addon_callbacks[$function]['installed'] = false;
5583 }
5584 }
5585
5586 // Initialize callbacks with core features
5587 $callbacks = $core_callbacks;
5588
5589 // Get callbacks from active add-ons
5590 $active_addon_callbacks = apply_filters('mxchat_available_callbacks', array());
5591
5592 // Add placeholder callbacks only for add-ons that aren't active
5593 if ($include_all) {
5594 foreach ($addon_callbacks as $function => $data) {
5595 // Skip placeholders for functions provided by active add-ons
5596 if (in_array($function, $addon_provided_functions)) {
5597 continue;
5598 }
5599
5600 // Skip excluded functions
5601 if (in_array($function, $excluded_functions)) {
5602 continue;
5603 }
5604
5605 // Add the placeholder
5606 $callbacks[$function] = $data;
5607 }
5608 }
5609
5610 // Add callbacks from active add-ons (will override placeholders)
5611 foreach ($active_addon_callbacks as $function => $data) {
5612 // Skip excluded functions
5613 if (in_array($function, $excluded_functions)) {
5614 continue;
5615 }
5616
5617 // Always include callbacks from add-ons
5618 $callbacks[$function] = $data;
5619
5620 // Ensure they have the proper add-on info
5621 if (isset($addon_function_mapping[$function])) {
5622 $addon_info = $addon_function_mapping[$function];
5623 $callbacks[$function]['addon'] = $addon_info['addon'];
5624 $callbacks[$function]['addon_name'] = $addon_info['addon_name'];
5625 $callbacks[$function]['installed'] = $addon_info['is_installed'];
5626 $callbacks[$function]['pro_only'] = $addon_info['pro_required'];
5627 }
5628 }
5629
5630 // Just before returning callbacks, sort them to prioritize free features
5631 if (!$grouped) {
5632 // Create temporary arrays for sorting
5633 $free_callbacks = array();
5634 $pro_callbacks = array();
5635
5636 // Split callbacks into free and pro
5637 foreach ($callbacks as $key => $data) {
5638 if (isset($data['pro_only']) && $data['pro_only']) {
5639 $pro_callbacks[$key] = $data;
5640 } else {
5641 $free_callbacks[$key] = $data;
5642 }
5643 }
5644
5645 // Merge with free callbacks first
5646 $callbacks = array_merge($free_callbacks, $pro_callbacks);
5647 }
5648
5649 // Return grouped structure if requested
5650 if ($grouped) {
5651 $grouped_callbacks = array();
5652 foreach ($callbacks as $key => $data) {
5653 $group_label = isset($data['group']) ? $data['group'] : __('Other Features', 'mxchat');
5654
5655 // Ensure we carry forward all the new fields in grouped mode
5656 $callback_data = array(
5657 'label' => $data['label'],
5658 'pro_only' => isset($data['pro_only']) ? $data['pro_only'] : false,
5659 'icon' => isset($data['icon']) ? $data['icon'] : 'admin-generic',
5660 'description' => isset($data['description']) ? $data['description'] : __('Custom action for your chatbot', 'mxchat'),
5661 'addon' => isset($data['addon']) ? $data['addon'] : false,
5662 'addon_name' => isset($data['addon_name']) ? $data['addon_name'] : '',
5663 'installed' => isset($data['installed']) ? $data['installed'] : true
5664 );
5665
5666 $grouped_callbacks[$group_label][$key] = $callback_data;
5667 }
5668
5669 // Sort within each group to prioritize free features
5670 foreach ($grouped_callbacks as $group => $items) {
5671 $free_items = array();
5672 $pro_items = array();
5673
5674 foreach ($items as $key => $data) {
5675 if (isset($data['pro_only']) && $data['pro_only']) {
5676 $pro_items[$key] = $data;
5677 } else {
5678 $free_items[$key] = $data;
5679 }
5680 }
5681
5682 $grouped_callbacks[$group] = array_merge($free_items, $pro_items);
5683 }
5684
5685 return $grouped_callbacks;
5686 }
5687
5688 return $callbacks;
5689 }
5690
5691 public function mxchat_page_init() {
5692 register_setting(
5693 'mxchat_option_group',
5694 'mxchat_options',
5695 array($this, 'mxchat_sanitize')
5696 );
5697
5698 register_setting(
5699 'mxchat_option_group',
5700 'mxchat_similarity_threshold',
5701 array(
5702 'type' => 'number',
5703 'sanitize_callback' => function($value) {
5704 $value = absint($value);
5705 return min(max($value, 20), 95);
5706 },
5707 'default' => 80,
5708 )
5709 );
5710
5711 // Chatbot Settings Section
5712 add_settings_section(
5713 'mxchat_chatbot_section',
5714 esc_html__('Chatbot Settings', 'mxchat'),
5715 null,
5716 'mxchat-chatbot'
5717 );
5718
5719 // API Keys Settings Section
5720 add_settings_section(
5721 'mxchat_api_keys_section',
5722 esc_html__('API Keys', 'mxchat'),
5723 array($this, 'mxchat_api_keys_section_callback'),
5724 'mxchat-api-keys'
5725 );
5726
5727 // OpenAI API Key
5728 add_settings_field(
5729 'api_key',
5730 esc_html__('OpenAI API Key', 'mxchat'),
5731 array($this, 'api_key_callback'),
5732 'mxchat-api-keys',
5733 'mxchat_api_keys_section'
5734 );
5735
5736 // X.AI API Key
5737 add_settings_field(
5738 'xai_api_key',
5739 esc_html__('X.AI API Key', 'mxchat'),
5740 array($this, 'xai_api_key_callback'),
5741 'mxchat-api-keys',
5742 'mxchat_api_keys_section'
5743 );
5744
5745 // Claude API Key
5746 add_settings_field(
5747 'claude_api_key',
5748 esc_html__('Claude API Key', 'mxchat'),
5749 array($this, 'claude_api_key_callback'),
5750 'mxchat-api-keys',
5751 'mxchat_api_keys_section'
5752 );
5753
5754 // DeepSeek API Key
5755 add_settings_field(
5756 'deepseek_api_key',
5757 esc_html__('DeepSeek API Key', 'mxchat'),
5758 array($this, 'deepseek_api_key_callback'),
5759 'mxchat-api-keys',
5760 'mxchat_api_keys_section'
5761 );
5762
5763 // Google Gemini API Key
5764 add_settings_field(
5765 'gemini_api_key',
5766 esc_html__('Google Gemini API Key', 'mxchat'),
5767 array($this, 'gemini_api_key_callback'),
5768 'mxchat-api-keys',
5769 'mxchat_api_keys_section'
5770 );
5771
5772 // Voyage AI API Key
5773 add_settings_field(
5774 'voyage_api_key',
5775 esc_html__('Voyage AI API Key', 'mxchat'),
5776 array($this, 'voyage_api_key_callback'),
5777 'mxchat-api-keys',
5778 'mxchat_api_keys_section'
5779 );
5780
5781 // OpenRouter API Key
5782 add_settings_field(
5783 'openrouter_api_key',
5784 esc_html__('OpenRouter API Key', 'mxchat'),
5785 array($this, 'openrouter_api_key_callback'),
5786 'mxchat-api-keys',
5787 'mxchat_api_keys_section'
5788 );
5789
5790 // Loops API Key
5791 add_settings_field(
5792 'loops_api_key',
5793 esc_html__('Loops API Key', 'mxchat'),
5794 array($this, 'mxchat_loops_api_key_callback'),
5795 'mxchat-api-keys',
5796 'mxchat_api_keys_section'
5797 );
5798
5799 // Brave Search API Key
5800 add_settings_field(
5801 'brave_api_key',
5802 __('Brave API Key', 'mxchat'),
5803 array($this, 'mxchat_brave_api_key_callback'),
5804 'mxchat-api-keys',
5805 'mxchat_api_keys_section'
5806 );
5807
5808 // Similarity Threshold Slider
5809 add_settings_field(
5810 'similarity_threshold', // Field ID
5811 esc_html__('Similarity Threshold', 'mxchat'), // Field title
5812 array($this, 'mxchat_similarity_threshold_callback'), // Callback function
5813 'mxchat-chatbot', // Page
5814 'mxchat_chatbot_section' // Section
5815 );
5816
5817 // RAG Sources Limit Slider
5818 add_settings_field(
5819 'rag_sources_limit', // Field ID
5820 esc_html__('RAG Sources Limit', 'mxchat'), // Field title
5821 array($this, 'mxchat_rag_sources_limit_callback'), // Callback function
5822 'mxchat-chatbot', // Page
5823 'mxchat_chatbot_section' // Section
5824 );
5825
5826 // RAG Chunks Limit Slider
5827 add_settings_field(
5828 'rag_chunks_limit', // Field ID
5829 esc_html__('RAG Chunks Limit', 'mxchat'), // Field title
5830 array($this, 'mxchat_rag_chunks_limit_callback'), // Callback function
5831 'mxchat-chatbot', // Page
5832 'mxchat_chatbot_section' // Section
5833 );
5834
5835 add_settings_field(
5836 'append_to_body',
5837 esc_html__('Auto-Display Chatbot', 'mxchat'),
5838 array($this, 'mxchat_append_to_body_callback'),
5839 'mxchat-chatbot',
5840 'mxchat_chatbot_section'
5841 );
5842
5843 add_settings_field(
5844 'contextual_awareness_toggle',
5845 esc_html__('Contextual Awareness', 'mxchat'),
5846 array($this, 'mxchat_contextual_awareness_callback'),
5847 'mxchat-chatbot',
5848 'mxchat_chatbot_section'
5849 );
5850
5851 add_settings_field(
5852 'citation_links_toggle',
5853 esc_html__('Citation Links', 'mxchat'),
5854 array($this, 'mxchat_citation_links_toggle_callback'),
5855 'mxchat-chatbot',
5856 'mxchat_chatbot_section'
5857 );
5858
5859 add_settings_field(
5860 'enable_streaming_toggle',
5861 esc_html__('Enable Streaming', 'mxchat'),
5862 array($this, 'enable_streaming_toggle_callback'),
5863 'mxchat-chatbot',
5864 'mxchat_chatbot_section', // Same section as your working toggle
5865 array(
5866 'class' => 'mxchat-setting-row streaming-setting',
5867 'style' => 'display: none;' // Hidden by default, shown when OpenAI/Claude selected
5868 )
5869 );
5870
5871
5872 add_settings_field(
5873 'model',
5874 esc_html__('Chat Model', 'mxchat'),
5875 array($this, 'mxchat_model_callback'),
5876 'mxchat-chatbot',
5877 'mxchat_chatbot_section'
5878 );
5879
5880 add_settings_field(
5881 'embedding_model',
5882 esc_html__('Embedding Model', 'mxchat'),
5883 array($this, 'embedding_model_callback'),
5884 'mxchat-chatbot',
5885 'mxchat_chatbot_section'
5886 );
5887
5888 add_settings_field(
5889 'system_prompt_instructions',
5890 esc_html__('AI Instructions (Behavior)', 'mxchat'),
5891 array($this, 'system_prompt_instructions_callback'),
5892 'mxchat-chatbot',
5893 'mxchat_chatbot_section'
5894 );
5895
5896
5897 add_settings_field(
5898 'top_bar_title',
5899 esc_html__('Top Bar Title', 'mxchat'),
5900 array($this, 'mxchat_top_bar_title_callback'),
5901 'mxchat-chatbot',
5902 'mxchat_chatbot_section'
5903 );
5904
5905 add_settings_field(
5906 'ai_agent_text',
5907 esc_html__('AI Agent Text', 'mxchat'),
5908 array($this, 'mxchat_ai_agent_text_callback'),
5909 'mxchat-chatbot',
5910 'mxchat_chatbot_section'
5911 );
5912
5913 add_settings_field(
5914 'enable_email_block',
5915 esc_html__('Require Email To Chat', 'mxchat'),
5916 array($this, 'enable_email_block_callback'),
5917 'mxchat-chatbot',
5918 'mxchat_chatbot_section'
5919 );
5920
5921 add_settings_field(
5922 'email_blocker_header_content',
5923 esc_html__('Require Email Chat Content', 'mxchat'),
5924 array($this, 'email_blocker_header_content_callback'),
5925 'mxchat-chatbot',
5926 'mxchat_chatbot_section'
5927 );
5928
5929 add_settings_field(
5930 'email_blocker_button_text',
5931 esc_html__('Require Email Chat Button Text', 'mxchat'),
5932 [$this, 'email_blocker_button_text_callback'],
5933 'mxchat-chatbot',
5934 'mxchat_chatbot_section'
5935 );
5936
5937 add_settings_field(
5938 'enable_name_field',
5939 esc_html__('Require Name Field', 'mxchat'),
5940 array($this, 'enable_name_field_callback'),
5941 'mxchat-chatbot',
5942 'mxchat_chatbot_section'
5943 );
5944
5945 add_settings_field(
5946 'name_field_placeholder',
5947 esc_html__('Name Field Placeholder', 'mxchat'),
5948 array($this, 'name_field_placeholder_callback'),
5949 'mxchat-chatbot',
5950 'mxchat_chatbot_section'
5951 );
5952
5953 add_settings_field(
5954 'intro_message',
5955 esc_html__('Introductory Message', 'mxchat'),
5956 array($this, 'mxchat_intro_message_callback'),
5957 'mxchat-chatbot',
5958 'mxchat_chatbot_section'
5959 );
5960
5961 add_settings_field(
5962 'input_copy',
5963 esc_html__('Input Copy', 'mxchat'),
5964 array($this, 'mxchat_input_copy_callback'),
5965 'mxchat-chatbot',
5966 'mxchat_chatbot_section'
5967 );
5968
5969 add_settings_field(
5970 'pre_chat_message',
5971 esc_html__('Chat Teaser Pop-up', 'mxchat'),
5972 array($this, 'mxchat_pre_chat_message_callback'),
5973 'mxchat-chatbot',
5974 'mxchat_chatbot_section'
5975 );
5976
5977 add_settings_field(
5978 'privacy_toggle',
5979 esc_html__('Toggle Privacy Notice', 'mxchat'),
5980 array($this, 'mxchat_privacy_toggle_callback'),
5981 'mxchat-chatbot',
5982 'mxchat_chatbot_section'
5983 );
5984
5985 add_settings_field(
5986 'complianz_toggle',
5987 esc_html__('Enable Complianz', 'mxchat'),
5988 array($this, 'mxchat_complianz_toggle_callback'),
5989 'mxchat-chatbot',
5990 'mxchat_chatbot_section'
5991 );
5992
5993 add_settings_field(
5994 'link_target_toggle',
5995 esc_html__('Open Links in a New Tab', 'mxchat'),
5996 array($this, 'mxchat_link_target_toggle_callback'),
5997 'mxchat-chatbot',
5998 'mxchat_chatbot_section'
5999 );
6000
6001 add_settings_field(
6002 'chat_persistence_toggle',
6003 esc_html__('Enable Chat Persistence', 'mxchat'),
6004 array($this, 'mxchat_chat_persistence_toggle_callback'),
6005 'mxchat-chatbot',
6006 'mxchat_chatbot_section'
6007 );
6008
6009 add_settings_field(
6010 'popular_question_1',
6011 esc_html__('Quick Question 1', 'mxchat'),
6012 array($this, 'mxchat_popular_question_1_callback'),
6013 'mxchat-chatbot',
6014 'mxchat_chatbot_section'
6015 );
6016
6017 add_settings_field(
6018 'popular_question_2',
6019 esc_html__('Quick Question 2', 'mxchat'),
6020 array($this, 'mxchat_popular_question_2_callback'),
6021 'mxchat-chatbot',
6022 'mxchat_chatbot_section'
6023 );
6024
6025 add_settings_field(
6026 'popular_question_3',
6027 esc_html__('Quick Question 3', 'mxchat'),
6028 array($this, 'mxchat_popular_question_3_callback'),
6029 'mxchat-chatbot',
6030 'mxchat_chatbot_section'
6031 );
6032
6033 add_settings_field(
6034 'additional_popular_questions',
6035 esc_html__('Additional Quick Questions', 'mxchat'),
6036 array($this, 'mxchat_additional_popular_questions_callback'),
6037 'mxchat-chatbot',
6038 'mxchat_chatbot_section'
6039 );
6040
6041
6042 add_settings_field(
6043 'rate_limits',
6044 __('Rate Limits Settings', 'mxchat'),
6045 array($this, 'mxchat_rate_limits_callback'),
6046 'mxchat-chatbot',
6047 'mxchat_chatbot_section'
6048 );
6049
6050 // Loops Settings Section
6051 add_settings_section(
6052 'mxchat_loops_section',
6053 esc_html__('Loops Settings', 'mxchat'),
6054 null,
6055 'mxchat-embed'
6056 );
6057
6058 // Loops Settings Fields (API Key moved to API Keys tab)
6059 add_settings_field(
6060 'loops_mailing_list',
6061 esc_html__('Loops Mailing List', 'mxchat'),
6062 array($this, 'mxchat_loops_mailing_list_callback'),
6063 'mxchat-embed',
6064 'mxchat_loops_section'
6065 );
6066
6067 add_settings_field(
6068 'triggered_phrase_response',
6069 esc_html__('Triggered Phrase Response', 'mxchat'),
6070 array($this, 'mxchat_triggered_phrase_response_callback'),
6071 'mxchat-embed',
6072 'mxchat_loops_section'
6073 );
6074
6075 add_settings_field(
6076 'email_capture_response',
6077 esc_html__('Email Capture Response', 'mxchat'),
6078 array($this, 'mxchat_email_capture_response_callback'),
6079 'mxchat-embed',
6080 'mxchat_loops_section'
6081 );
6082
6083 // Brave Search Settings Fields
6084 add_settings_section(
6085 'mxchat_brave_section',
6086 __('Brave Search Settings', 'mxchat'),
6087 array($this, 'mxchat_brave_section_callback'),
6088 'mxchat-embed'
6089 );
6090
6091 // Brave API Key moved to API Keys tab
6092 add_settings_field(
6093 'brave_image_count',
6094 __('Number of Images to Return', 'mxchat'),
6095 array($this, 'mxchat_brave_image_count_callback'),
6096 'mxchat-embed',
6097 'mxchat_brave_section'
6098 );
6099
6100 add_settings_field(
6101 'brave_safe_search',
6102 __('Safe Search', 'mxchat'),
6103 array($this, 'mxchat_brave_safe_search_callback'),
6104 'mxchat-embed',
6105 'mxchat_brave_section'
6106 );
6107
6108 add_settings_field(
6109 'brave_news_count',
6110 __('Number of News Articles', 'mxchat'),
6111 array($this, 'mxchat_brave_news_count_callback'),
6112 'mxchat-embed',
6113 'mxchat_brave_section'
6114 );
6115
6116 add_settings_field(
6117 'brave_country',
6118 __('Country', 'mxchat'),
6119 array($this, 'mxchat_brave_country_callback'),
6120 'mxchat-embed',
6121 'mxchat_brave_section'
6122 );
6123
6124 add_settings_field(
6125 'brave_language',
6126 __('Language', 'mxchat'),
6127 array($this, 'mxchat_brave_language_callback'),
6128 'mxchat-embed',
6129 'mxchat_brave_section'
6130 );
6131
6132 // Chat with PDF Intent Settings Fields
6133 add_settings_section(
6134 'mxchat_pdf_intent_section',
6135 __('Toolbar Settings & Intents', 'mxchat'),
6136 array($this, 'mxchat_pdf_intent_section_callback'),
6137 'mxchat-embed'
6138 );
6139
6140 add_settings_field(
6141 'chat_toolbar_toggle',
6142 __('Show Chat Toolbar', 'mxchat'),
6143 array($this, 'mxchat_chat_toolbar_toggle_callback'),
6144 'mxchat-embed',
6145 'mxchat_pdf_intent_section'
6146 );
6147
6148 // PDF Upload Button Toggle
6149 add_settings_field(
6150 'show_pdf_upload_button',
6151 __('Show PDF Upload Button', 'mxchat'),
6152 array($this, 'mxchat_show_pdf_upload_button_callback'),
6153 'mxchat-embed',
6154 'mxchat_pdf_intent_section'
6155 );
6156
6157 // Word Upload Button Toggle
6158 add_settings_field(
6159 'show_word_upload_button',
6160 __('Show Word Upload Button', 'mxchat'),
6161 array($this, 'mxchat_show_word_upload_button_callback'),
6162 'mxchat-embed',
6163 'mxchat_pdf_intent_section'
6164 );
6165
6166 add_settings_field(
6167 'pdf_intent_trigger_text',
6168 __('Intent Trigger Text', 'mxchat'),
6169 array($this, 'mxchat_pdf_intent_trigger_text_callback'),
6170 'mxchat-embed',
6171 'mxchat_pdf_intent_section'
6172 );
6173
6174 add_settings_field(
6175 'pdf_intent_success_text',
6176 __('Success Text', 'mxchat'),
6177 array($this, 'mxchat_pdf_intent_success_text_callback'),
6178 'mxchat-embed',
6179 'mxchat_pdf_intent_section'
6180 );
6181
6182 add_settings_field(
6183 'pdf_intent_error_text',
6184 __('Error Text', 'mxchat'),
6185 array($this, 'mxchat_pdf_intent_error_text_callback'),
6186 'mxchat-embed',
6187 'mxchat_pdf_intent_section'
6188 );
6189
6190 // Add PDF Maximum Pages Field
6191 add_settings_field(
6192 'pdf_max_pages',
6193 __('Maximum Document Pages', 'mxchat'),
6194 array($this, 'mxchat_pdf_max_pages_callback'),
6195 'mxchat-embed',
6196 'mxchat_pdf_intent_section'
6197 );
6198
6199 // Live Agent Settings Fields
6200 add_settings_section(
6201 'mxchat_live_agent_section',
6202 __('Live Agent Settings', 'mxchat'),
6203 array($this, 'mxchat_live_agent_section_callback'),
6204 'mxchat-embed'
6205 );
6206
6207 // Live Agent Status Fields (add at top of live agent settings)
6208 add_settings_field(
6209 'live_agent_status',
6210 __('Live Agent Status', 'mxchat'),
6211 array($this, 'mxchat_live_agent_status_callback'),
6212 'mxchat-embed',
6213 'mxchat_live_agent_section'
6214 );
6215
6216 add_settings_field(
6217 'live_agent_notification_message',
6218 __('Notification Message', 'mxchat'),
6219 array($this, 'mxchat_live_agent_notification_message_callback'),
6220 'mxchat-embed',
6221 'mxchat_live_agent_section'
6222 );
6223
6224 add_settings_field(
6225 'live_agent_away_message',
6226 __('Away Message', 'mxchat'),
6227 array($this, 'mxchat_live_agent_away_message_callback'),
6228 'mxchat-embed',
6229 'mxchat_live_agent_section'
6230 );
6231
6232 add_settings_field(
6233 'live_agent_user_ids',
6234 __('Slack Agent User IDs', 'mxchat'),
6235 array($this, 'mxchat_live_agent_user_ids_callback'),
6236 'mxchat-embed',
6237 'mxchat_live_agent_section'
6238 );
6239
6240 add_settings_field(
6241 'live_agent_webhook_url',
6242 __('Slack Webhook URL', 'mxchat'),
6243 array($this, 'mxchat_live_agent_webhook_url_callback'),
6244 'mxchat-embed',
6245 'mxchat_live_agent_section'
6246 );
6247
6248 add_settings_field(
6249 'live_agent_secret_key',
6250 __('Slack Secret Key', 'mxchat'),
6251 array($this, 'mxchat_live_agent_secret_key_callback'),
6252 'mxchat-embed',
6253 'mxchat_live_agent_section'
6254 );
6255
6256 // Live Agent Integration Fields
6257 add_settings_field(
6258 'live_agent_bot_token',
6259 __('Slack Bot OAuth Token', 'mxchat'),
6260 array($this, 'mxchat_live_agent_bot_token_callback'),
6261 'mxchat-embed',
6262 'mxchat_live_agent_section'
6263 );
6264
6265 // Telegram Integration Section
6266 add_settings_section(
6267 'mxchat_telegram_section',
6268 __('Telegram Settings', 'mxchat'),
6269 array($this, 'mxchat_telegram_section_callback'),
6270 'mxchat-embed'
6271 );
6272
6273 add_settings_field(
6274 'telegram_status',
6275 __('Live Agent Status', 'mxchat'),
6276 array($this, 'mxchat_telegram_status_callback'),
6277 'mxchat-embed',
6278 'mxchat_telegram_section'
6279 );
6280
6281 add_settings_field(
6282 'telegram_notification_message',
6283 __('Notification Message', 'mxchat'),
6284 array($this, 'mxchat_telegram_notification_message_callback'),
6285 'mxchat-embed',
6286 'mxchat_telegram_section'
6287 );
6288
6289 add_settings_field(
6290 'telegram_away_message',
6291 __('Away Message', 'mxchat'),
6292 array($this, 'mxchat_telegram_away_message_callback'),
6293 'mxchat-embed',
6294 'mxchat_telegram_section'
6295 );
6296
6297 add_settings_field(
6298 'telegram_bot_token',
6299 __('Telegram Bot Token', 'mxchat'),
6300 array($this, 'mxchat_telegram_bot_token_callback'),
6301 'mxchat-embed',
6302 'mxchat_telegram_section'
6303 );
6304
6305 add_settings_field(
6306 'telegram_group_id',
6307 __('Telegram Group ID', 'mxchat'),
6308 array($this, 'mxchat_telegram_group_id_callback'),
6309 'mxchat-embed',
6310 'mxchat_telegram_section'
6311 );
6312
6313 add_settings_field(
6314 'telegram_webhook_secret',
6315 __('Webhook Secret Token', 'mxchat'),
6316 array($this, 'mxchat_telegram_webhook_secret_callback'),
6317 'mxchat-embed',
6318 'mxchat_telegram_section'
6319 );
6320
6321 // General Settings Section
6322 add_settings_section(
6323 'mxchat_general_section',
6324 esc_html__('YouTube Tutorials', 'mxchat'),
6325 null,
6326 'mxchat-general'
6327 );
6328 }
6329
6330 public function mxchat_prompts_page_init() {
6331 register_setting(
6332 'mxchat_prompts_options',
6333 'mxchat_prompts_options',
6334 array(
6335 'type' => 'array',
6336 'description' => __('MXChat Knowledge Base Settings', 'mxchat'),
6337 'default' => array(
6338 'mxchat_auto_sync_posts' => 0,
6339 'mxchat_auto_sync_pages' => 0,
6340 'mxchat_use_pinecone' => 0,
6341 'mxchat_pinecone_api_key' => '',
6342 'mxchat_pinecone_environment' => '',
6343 'mxchat_pinecone_index' => '',
6344 'mxchat_pinecone_host' => '',
6345 ),
6346 'sanitize_callback' => array($this, 'sanitize_prompts_options'),
6347 )
6348 );
6349
6350 add_action('admin_notices', array($this, 'sync_settings_notice'));
6351 }
6352
6353 public function mxchat_transcripts_page_init() {
6354 register_setting(
6355 'mxchat_transcripts_options',
6356 'mxchat_transcripts_options',
6357 array(
6358 'type' => 'array',
6359 'description' => __('MXChat Transcripts Notification Settings', 'mxchat'),
6360 'default' => array(
6361 'mxchat_enable_notifications' => 0,
6362 'mxchat_notification_email' => get_option('admin_email'),
6363 'mxchat_auto_delete_transcripts' => 'never',
6364 ),
6365 'sanitize_callback' => array($this, 'sanitize_transcripts_options'),
6366 )
6367 );
6368
6369 add_settings_section(
6370 'mxchat_transcripts_notification_section',
6371 esc_html__('Chat Notification Settings', 'mxchat'),
6372 array($this, 'mxchat_transcripts_notification_section_callback'),
6373 'mxchat-transcripts'
6374 );
6375
6376 add_settings_field(
6377 'mxchat_enable_notifications',
6378 esc_html__('Enable Chat Notifications', 'mxchat'),
6379 array($this, 'mxchat_enable_notifications_callback'),
6380 'mxchat-transcripts',
6381 'mxchat_transcripts_notification_section'
6382 );
6383
6384 add_settings_field(
6385 'mxchat_notification_email',
6386 esc_html__('Notification Email Address', 'mxchat'),
6387 array($this, 'mxchat_notification_email_callback'),
6388 'mxchat-transcripts',
6389 'mxchat_transcripts_notification_section'
6390 );
6391
6392 add_settings_field(
6393 'mxchat_auto_delete_transcripts',
6394 esc_html__('Auto-Delete Old Transcripts', 'mxchat'),
6395 array($this, 'mxchat_auto_delete_transcripts_callback'),
6396 'mxchat-transcripts',
6397 'mxchat_transcripts_notification_section'
6398 );
6399
6400 add_settings_field(
6401 'mxchat_auto_email_transcript',
6402 esc_html__('Auto-Email Full Transcript', 'mxchat'),
6403 array($this, 'mxchat_auto_email_transcript_callback'),
6404 'mxchat-transcripts',
6405 'mxchat_transcripts_notification_section'
6406 );
6407 }
6408
6409
6410 /**
6411 * Sanitize all prompts options
6412 *
6413 * @param array $input The unsanitized options array
6414 * @return array The sanitized options array
6415 */
6416 public function sanitize_prompts_options($input) {
6417 // Log the incoming input.
6418 //error_log('Sanitizing inputs: ' . print_r($input, true));
6419
6420 $sanitized = array();
6421
6422 // Boolean options
6423 $sanitized['mxchat_auto_sync_posts'] = isset($input['mxchat_auto_sync_posts']) ? 1 : 0;
6424 $sanitized['mxchat_auto_sync_pages'] = isset($input['mxchat_auto_sync_pages']) ? 1 : 0;
6425 $sanitized['mxchat_use_pinecone'] = !empty($input['mxchat_use_pinecone']) ? 1 : 0;
6426
6427 // API Key: if less than 32 characters, flag as invalid.
6428 $api_key = sanitize_text_field($input['mxchat_pinecone_api_key'] ?? '');
6429 if (!empty($api_key) && strlen($api_key) < 32) {
6430 add_settings_error(
6431 'mxchat_prompts_options',
6432 'invalid_api_key',
6433 __('The Pinecone API key appears to be invalid. Please check your API key.', 'mxchat')
6434 );
6435 $existing_options = get_option('mxchat_prompts_options', array());
6436 $sanitized['mxchat_pinecone_api_key'] = $existing_options['mxchat_pinecone_api_key'] ?? '';
6437 } else {
6438 $sanitized['mxchat_pinecone_api_key'] = $api_key;
6439 }
6440
6441 // Environment and Index Name
6442 $sanitized['mxchat_pinecone_environment'] = sanitize_text_field($input['mxchat_pinecone_environment'] ?? '');
6443 $sanitized['mxchat_pinecone_index'] = sanitize_text_field($input['mxchat_pinecone_index'] ?? '');
6444
6445 // Host: Remove protocol and validate format.
6446 $host = sanitize_text_field($input['mxchat_pinecone_host'] ?? '');
6447 $host = preg_replace('#^https?://#', '', $host);
6448 //error_log('Host after removing protocol: ' . $host);
6449 if (!empty($host)) {
6450 if (!preg_match('/^[\w-]+\.svc\.[\w-]+\.pinecone\.io$/', $host)) {
6451 add_settings_error(
6452 'mxchat_prompts_options',
6453 'invalid_host',
6454 __('The Pinecone host appears to be invalid. It should look like "mxchat-vectors-zrmsquq.svc.aped-4627-b74a.pinecone.io"', 'mxchat')
6455 );
6456 $existing_options = get_option('mxchat_prompts_options', array());
6457 $sanitized['mxchat_pinecone_host'] = $existing_options['mxchat_pinecone_host'] ?? '';
6458 } else {
6459 $sanitized['mxchat_pinecone_host'] = $host;
6460 }
6461 } else {
6462 $sanitized['mxchat_pinecone_host'] = '';
6463 }
6464
6465 //error_log('Final sanitized array: ' . print_r($sanitized, true));
6466
6467 return $sanitized;
6468 }
6469
6470 public function sync_settings_notice() {
6471 // Only show notice on our plugin page
6472 if (!isset($_GET['page']) || $_GET['page'] !== 'mxchat-prompts') {
6473 return;
6474 }
6475
6476 // Check if settings were updated
6477 if (isset($_GET['settings-updated'])) {
6478
6479 ?>
6480 <div class="notice notice-success is-dismissible">
6481 <p><?php esc_html_e('Sync settings updated successfully.', 'mxchat'); ?></p>
6482 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php esc_html_e('Dismiss this notice.', 'mxchat'); ?></span></button>
6483 </div>
6484 <?php
6485
6486 }
6487 }
6488 // Add this sanitization function to your class
6489 public function sanitize_sync_setting($input) {
6490 return (bool)$input ? __('1', 'mxchat') : __('', 'mxchat');
6491 }
6492
6493 public function mxchat_rate_limits_callback() {
6494 $all_options = get_option('mxchat_options', []);
6495
6496 // Define available rate limits
6497 $rate_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited');
6498
6499 // Define available timeframes
6500 $timeframes = array(
6501 'hourly' => __('Per Hour', 'mxchat'),
6502 'daily' => __('Per Day', 'mxchat'),
6503 'weekly' => __('Per Week', 'mxchat'),
6504 'monthly' => __('Per Month', 'mxchat')
6505 );
6506
6507 // Get all roles plus a "logged_out" pseudo-role
6508 $roles = wp_roles()->get_names();
6509 $roles['logged_out'] = __('Logged Out Users', 'mxchat');
6510
6511 // Start the wrapper
6512 echo '<div class="pro-feature-wrapper active">';
6513 echo '<div class="mxchat-rate-limits-container">';
6514
6515 echo '<p class="description" style="margin-bottom: 20px;">' .
6516 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') .
6517 '</p>';
6518
6519 // Add markdown link documentation
6520 echo '<div class="notice notice-info inline" style="margin-bottom: 20px; padding: 10px;">';
6521 echo '<p><strong>' . esc_html__('Markdown Links Supported:', 'mxchat') . '</strong></p>';
6522 echo '<p>' . esc_html__('You can include clickable links in your custom messages using markdown syntax:', 'mxchat') . '</p>';
6523 echo '<ul style="margin-left: 20px;">';
6524 echo '<li><code>[Link text](https://example.com)</code> - Creates a clickable link</li>';
6525 echo '<li><code>[Visit our pricing](https://example.com/pricing)</code> - Link with custom text</li>';
6526 echo '<li><code>Plain URLs like https://example.com will also become clickable</code></li>';
6527 echo '</ul>';
6528 echo '</div>';
6529
6530 // Output the controls for each role
6531 foreach ($roles as $role_id => $role_name) {
6532 // Get saved options or defaults
6533 $default_limit = ($role_id === 'logged_out') ? '10' : '100';
6534 $default_timeframe = 'daily';
6535 $default_message = __('Rate limit exceeded. Please try again later.', 'mxchat');
6536
6537 $selected_limit = isset($all_options['rate_limits'][$role_id]['limit'])
6538 ? $all_options['rate_limits'][$role_id]['limit']
6539 : $default_limit;
6540
6541 $selected_timeframe = isset($all_options['rate_limits'][$role_id]['timeframe'])
6542 ? $all_options['rate_limits'][$role_id]['timeframe']
6543 : $default_timeframe;
6544
6545 $custom_message = isset($all_options['rate_limits'][$role_id]['message'])
6546 ? $all_options['rate_limits'][$role_id]['message']
6547 : $default_message;
6548
6549 // Output the row
6550 echo '<div class="mxchat-rate-limit-row mxchat-autosave-section">';
6551
6552 // Role label
6553 echo '<div class="mxchat-rate-limit-role">' . esc_html($role_name) . '</div>';
6554
6555 // Controls section
6556 echo '<div class="mxchat-rate-limit-controls-wrapper">';
6557
6558 // Rate limit and timeframe controls
6559 echo '<div class="mxchat-rate-limit-controls">';
6560
6561 // Limit dropdown
6562 echo '<div>';
6563 echo '<label for="rate_limits_' . esc_attr($role_id) . '_limit">' . esc_html__('Limit:', 'mxchat') . '</label>';
6564 echo '<select
6565 id="rate_limits_' . esc_attr($role_id) . '_limit"
6566 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][limit]"
6567 class="mxchat-autosave-field">';
6568 foreach ($rate_limits as $limit) {
6569 echo '<option value="' . esc_attr($limit) . '" ' . selected($selected_limit, $limit, false) . '>' . esc_html($limit) . '</option>';
6570 }
6571 echo '</select>';
6572 echo '</div>';
6573
6574 // Timeframe dropdown
6575 echo '<div>';
6576 echo '<label for="rate_limits_' . esc_attr($role_id) . '_timeframe">' . esc_html__('Timeframe:', 'mxchat') . '</label>';
6577 echo '<select
6578 id="rate_limits_' . esc_attr($role_id) . '_timeframe"
6579 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][timeframe]"
6580 class="mxchat-autosave-field">';
6581 foreach ($timeframes as $value => $label) {
6582 echo '<option value="' . esc_attr($value) . '" ' . selected($selected_timeframe, $value, false) . '>' . esc_html($label) . '</option>';
6583 }
6584 echo '</select>';
6585 echo '</div>';
6586
6587 echo '</div>'; // End controls
6588
6589 // Custom message textarea
6590 echo '<div class="mxchat-rate-limit-message">';
6591 echo '<label for="rate_limits_' . esc_attr($role_id) . '_message">' . esc_html__('Custom Message:', 'mxchat') . '</label>';
6592 echo '<textarea
6593 id="rate_limits_' . esc_attr($role_id) . '_message"
6594 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][message]"
6595 class="mxchat-autosave-field"
6596 placeholder="' . esc_attr__('Enter custom message when rate limit is exceeded', 'mxchat') . '">' .
6597 esc_textarea($custom_message) .
6598 '</textarea>';
6599 echo '<p class="description">' .
6600 esc_html__('Example: Rate limit reached! [Visit our pricing page](https://example.com/pricing) to upgrade.', 'mxchat') .
6601 '</p>';
6602 echo '</div>'; // End message
6603
6604 echo '</div>'; // End controls wrapper
6605
6606 echo '</div>'; // End row
6607 }
6608
6609 echo '</div>'; // End container
6610
6611 echo '</div>'; // End pro-feature-wrapper
6612 }
6613
6614 private function mxchat_add_option_field($id, $title, $callback = '') {
6615 add_settings_field(
6616 $id,
6617 __($title, 'mxchat'),
6618 $callback ? array($this, $callback) : array($this, $id . '_callback'),
6619 'mxchat-max',
6620 'mxchat_setting_section_id',
6621 $id === 'model' ? ['label_for' => 'model'] : []
6622 );
6623 }
6624
6625 // API Keys Section Callback
6626 public function mxchat_api_keys_section_callback() {
6627 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>';
6628 }
6629
6630 // OpenAI API Key
6631 public function api_key_callback() {
6632 $apiKey = isset($this->options['api_key']) ? esc_attr($this->options['api_key']) : '';
6633 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6634
6635 echo '<div class="api-key-wrapper">';
6636 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 . '" />';
6637 echo '<button type="button" id="toggleApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6638 echo '<p class="description">' . esc_html__('Required for OpenAI GPT models and OpenAI embeddings. Get your API key from OpenAI Platform.', 'mxchat') . '</p>';
6639 echo '</div>';
6640 }
6641
6642 // X.AI API Key
6643 public function xai_api_key_callback() {
6644 $xaiApiKey = isset($this->options['xai_api_key']) ? esc_attr($this->options['xai_api_key']) : '';
6645 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6646
6647 echo '<div class="api-key-wrapper">';
6648 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 . '" />';
6649 echo '<button type="button" id="toggleXaiApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6650 echo '<p class="description">' . esc_html__('Required for X.AI Grok models. Get your API key from X.AI Console.', 'mxchat') . '</p>';
6651 echo '</div>';
6652 }
6653 // Claude API Key
6654 public function claude_api_key_callback() {
6655 $claudeApiKey = isset($this->options['claude_api_key']) ? esc_attr($this->options['claude_api_key']) : '';
6656 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6657
6658 echo '<div class="api-key-wrapper">';
6659 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 . '" />';
6660 echo '<button type="button" id="toggleClaudeApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6661 echo '<p class="description">' . esc_html__('Required for Anthropic Claude models. Get your API key from Anthropic Console.', 'mxchat') . '</p>';
6662 echo '</div>';
6663 }
6664
6665 // DeepSeek API Key
6666 public function deepseek_api_key_callback() {
6667 $apiKey = isset($this->options['deepseek_api_key']) ? esc_attr($this->options['deepseek_api_key']) : '';
6668 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6669
6670 echo '<div class="api-key-wrapper">';
6671 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 . '" />';
6672 echo '<button type="button" id="toggleDeepSeekApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6673 echo '<p class="description">' . esc_html__('Required for DeepSeek models. Get your API key from DeepSeek Platform.', 'mxchat') . '</p>';
6674 echo '</div>';
6675 }
6676
6677 // Gemini API Key
6678 public function gemini_api_key_callback() {
6679 $geminiApiKey = isset($this->options['gemini_api_key']) ? esc_attr($this->options['gemini_api_key']) : '';
6680 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6681
6682 echo '<div class="api-key-wrapper">';
6683 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 . '" />';
6684 echo '<button type="button" id="toggleGeminiApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6685 echo '<p class="description">' . esc_html__('Required for Google Gemini models and embeddings. Get your API key from Google AI Studio.', 'mxchat') . '</p>';
6686 echo '</div>';
6687 }
6688
6689
6690 // OpenRouter API Key
6691 public function openrouter_api_key_callback() {
6692 $openrouterApiKey = isset($this->options['openrouter_api_key']) ? esc_attr($this->options['openrouter_api_key']) : '';
6693 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6694
6695 echo '<div class="api-key-wrapper">';
6696 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 . '" />';
6697 echo '<button type="button" id="toggleOpenRouterApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6698 echo '<p class="description">' . esc_html__('Required for OpenRouter models. Get your API key from OpenRouter.ai', 'mxchat') . '</p>';
6699 echo '</div>';
6700 }
6701
6702 // Voyage API Key
6703 public function voyage_api_key_callback() {
6704 $apiKey = isset($this->options['voyage_api_key']) ? esc_attr($this->options['voyage_api_key']) : '';
6705 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6706
6707 echo '<div class="api-key-wrapper">';
6708 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 . '" />';
6709 echo '<button type="button" id="toggleVoyageAPIKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6710 echo '<p class="description">' . esc_html__('Required for Voyage AI embedding models. Get your API key from Voyage AI.', 'mxchat') . '</p>';
6711 echo '</div>';
6712 }
6713
6714 public function mxchat_loops_api_key_callback() {
6715 $loops_api_key = isset($this->options['loops_api_key']) ? esc_attr($this->options['loops_api_key']) : '';
6716 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6717
6718 echo '<div class="api-key-wrapper">';
6719 echo sprintf(
6720 '<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" />',
6721 $loops_api_key,
6722 $nonce
6723 );
6724 echo '<button type="button" id="toggleLoopsApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6725 echo '</div>';
6726 echo '<p class="description">' . esc_html__('Required for Loops email integration. Get your API key from Loops.so', 'mxchat') . '</p>';
6727 }
6728 public function mxchat_loops_mailing_list_callback() {
6729 // Add error handling and type checking
6730 $loops_api_key = '';
6731 $selected_list = '';
6732 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6733
6734 // Safely get the API key
6735 if (isset($this->options['loops_api_key']) && is_string($this->options['loops_api_key'])) {
6736 $loops_api_key = $this->options['loops_api_key'];
6737 }
6738
6739 // Safely get the selected list
6740 if (isset($this->options['loops_mailing_list']) && is_string($this->options['loops_mailing_list'])) {
6741 $selected_list = $this->options['loops_mailing_list'];
6742 }
6743
6744 if (!empty($loops_api_key)) {
6745 $lists = $this->mxchat_fetch_loops_mailing_lists($loops_api_key);
6746 if (is_array($lists) && !empty($lists)) {
6747 echo '<div class="mxchat-field-wrapper">';
6748 echo '<select id="loops_mailing_list" name="loops_mailing_list" class="mxchat-autosave-field" data-nonce="' . $nonce . '">';
6749
6750 // Add a default "Select a list" option
6751 echo '<option value="" ' . selected($selected_list, '', false) . '>' . esc_html__('Select a list', 'mxchat') . '</option>';
6752
6753 foreach ($lists as $list) {
6754 if (is_array($list) && isset($list['id']) && isset($list['name'])) {
6755 echo sprintf(
6756 '<option value="%s" %s>%s</option>',
6757 esc_attr($list['id']),
6758 selected($selected_list, $list['id'], false),
6759 esc_html($list['name'])
6760 );
6761 }
6762 }
6763 echo '</select>';
6764 echo '</div>';
6765 echo '<p class="description">' . esc_html__('Please select a mailing list to use with Loops.', 'mxchat') . '</p>';
6766 } else {
6767 echo '<p class="description">' . esc_html__('No lists found. Please verify your API Key.', 'mxchat') . '</p>';
6768 }
6769 } else {
6770 echo '<p class="description">' . esc_html__('Enter a valid Loops API Key to load mailing lists.', 'mxchat') . '</p>';
6771 }
6772 }
6773 public function mxchat_triggered_phrase_response_callback() {
6774 $default_response = __('Would you like to join our mailing list? Please provide your email below.', 'mxchat');
6775 $triggered_response = isset($this->options['triggered_phrase_response'])
6776 ? $this->options['triggered_phrase_response']
6777 : $default_response;
6778 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6779
6780 echo '<div class="mxchat-field-wrapper">';
6781 echo sprintf(
6782 '<textarea id="triggered_phrase_response" name="triggered_phrase_response" rows="3" cols="50" class="mxchat-autosave-field" data-nonce="%s">%s</textarea>',
6783 $nonce,
6784 esc_textarea($triggered_response)
6785 );
6786 echo '</div>';
6787 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>';
6788 }
6789
6790 public function mxchat_email_capture_response_callback() {
6791 $default_response = __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
6792 $email_capture_response = isset($this->options['email_capture_response'])
6793 ? $this->options['email_capture_response']
6794 : $default_response;
6795 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6796
6797 echo '<div class="mxchat-field-wrapper">';
6798 echo sprintf(
6799 '<textarea id="email_capture_response" name="email_capture_response" rows="3" cols="50" class="mxchat-autosave-field" data-nonce="%s">%s</textarea>',
6800 $nonce,
6801 esc_textarea($email_capture_response)
6802 );
6803 echo '</div>';
6804 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>';
6805 }
6806 public function mxchat_pre_chat_message_callback() {
6807 // Load the entire 'mxchat_options' array
6808 $all_options = get_option('mxchat_options', []);
6809
6810 // Retrieve the saved message or use the default value
6811 $default_message = __('Hey there! Ask me anything!', 'mxchat');
6812 $pre_chat_message = isset($all_options['pre_chat_message']) ? $all_options['pre_chat_message'] : $default_message;
6813
6814 // Output the textarea
6815 printf(
6816 '<textarea id="pre_chat_message" name="pre_chat_message" rows="5" cols="50">%s</textarea>',
6817 esc_textarea($pre_chat_message)
6818 );
6819 }
6820
6821 // Callback for AI Instructions textarea
6822 public function system_prompt_instructions_callback() {
6823 // Retrieve the current value of the system prompt instructions
6824 $instructions = isset($this->options['system_prompt_instructions']) ? esc_textarea($this->options['system_prompt_instructions']) : '';
6825 // Render the textarea field
6826 printf(
6827 '<textarea id="system_prompt_instructions" name="system_prompt_instructions" rows="5" cols="50">%s</textarea>',
6828 $instructions
6829 );
6830 // Personalization hint
6831 echo '<p class="description" style="margin-top: 8px;">';
6832 echo esc_html__('Use {visitor_name} to personalize AI responses when lead capture is enabled.', 'mxchat') . '<br>';
6833 echo '<code style="font-size: 12px;">' . esc_html__('Example: The visitor\'s name is {visitor_name}. Address them by name.', 'mxchat') . '</code>';
6834 echo '</p>';
6835 // Sample instructions button
6836 echo '<div class="mxchat-instructions-container">';
6837 echo '<button type="button" class="mxchat-instructions-btn" id="mxchatViewSampleBtn">';
6838 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">';
6839 echo '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>';
6840 echo '<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>';
6841 echo '</svg>';
6842 echo esc_html__('View Sample Instructions', 'mxchat');
6843 echo '</button>';
6844 echo '</div>';
6845
6846 // Add modal to WordPress admin footer instead of inline
6847 add_action('admin_footer', array($this, 'render_sample_instructions_modal'));
6848 }
6849
6850 // New method to render modal in admin footer
6851 public function render_sample_instructions_modal() {
6852 static $modal_rendered = false;
6853 if ($modal_rendered) return; // Prevent duplicate modals
6854 $modal_rendered = true;
6855
6856 echo '<div class="mxchat-instructions-modal-overlay" id="mxchatSampleModal">';
6857 echo '<div class="mxchat-instructions-modal-content">';
6858 echo '<div class="mxchat-instructions-modal-header">';
6859 echo '<h3 class="mxchat-instructions-modal-title">';
6860 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">';
6861 echo '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>';
6862 echo '<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>';
6863 echo '</svg>';
6864 echo esc_html__('Sample AI Instructions', 'mxchat');
6865 echo '</h3>';
6866 echo '<button type="button" class="mxchat-instructions-modal-close" id="mxchatModalClose">';
6867 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">';
6868 echo '<line x1="18" y1="6" x2="6" y2="18"/>';
6869 echo '<line x1="6" y1="6" x2="18" y2="18"/>';
6870 echo '</svg>';
6871 echo '</button>';
6872 echo '</div>';
6873 echo '<div class="mxchat-instructions-modal-body">';
6874 echo '<div class="mxchat-instructions-content">';
6875 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:
6876
6877 # Response Style - CRITICALLY IMPORTANT
6878 - MAXIMUM LENGTH: 1-3 short sentences per response
6879 - Ultra-concise: Get straight to the answer with no filler
6880 - No introductions like "Sure!" or "I\'d be happy to help"
6881 - No phrases like "based on my knowledge" or "according to information"
6882 - No explanatory text before giving the answer
6883 - No summaries or repetition
6884 - Hyperlink all URLs
6885 - Respond in user\'s language
6886 - Minor chit chat or conversation is okay, but try to keep it focused on [insert topic]
6887
6888 # Knowledge Base Requirements - PREVENT HALLUCINATIONS
6889 - ONLY answer using information explicitly provided in OFFICIAL KNOWLEDGE DATABASE CONTENT sections marked with ===== delimiters
6890 - If required information is NOT in the knowledge database: "I don\'t have enough information in my knowledge base to answer that question accurately."
6891 - NEVER invent or hallucinate URLs, links, product specs, procedures, dates, statistics, names, contacts, or company information
6892 - When knowledge base information is unclear or contradictory, acknowledge the limitation rather than guessing
6893 - Better to admit insufficient information than provide inaccurate answers');
6894 echo '</div>';
6895 echo '<button type="button" class="mxchat-instructions-copy-btn" id="mxchatCopyBtn">';
6896 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">';
6897 echo '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>';
6898 echo '<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>';
6899 echo '</svg>';
6900 echo esc_html__('Copy Instructions', 'mxchat');
6901 echo '</button>';
6902 echo '</div>';
6903 echo '<div class="mxchat-instructions-modal-footer">';
6904 echo '<button type="button" class="mxchat-instructions-btn-secondary" id="mxchatCloseBtn">' . esc_html__('Close', 'mxchat') . '</button>';
6905 echo '</div>';
6906 echo '</div>';
6907 echo '</div>';
6908 }
6909
6910
6911 public function mxchat_model_callback() {
6912 // Define available models grouped by provider
6913 $models = array(
6914 esc_html__('OpenRouter (100+ Models)', 'mxchat') => array(
6915 'openrouter' => esc_html__('OpenRouter - Select after entering API key', 'mxchat'),
6916 ),
6917 esc_html__('Google Gemini Models', 'mxchat') => array(
6918 'gemini-3-pro-preview' => esc_html__('Gemini 3 Pro (Most Intelligent, Multimodal)', 'mxchat'),
6919 'gemini-3-flash-preview' => esc_html__('Gemini 3 Flash (Balanced Speed & Scale)', 'mxchat'),
6920 'gemini-2.5-pro' => esc_html__('Gemini 2.5 Pro (Advanced Thinking)', 'mxchat'),
6921 'gemini-2.5-flash' => esc_html__('Gemini 2.5 Flash (Best Price-Performance)', 'mxchat'),
6922 'gemini-2.5-flash-lite' => esc_html__('Gemini 2.5 Flash-Lite (Ultra Fast)', 'mxchat'),
6923 'gemini-2.0-flash' => esc_html__('Gemini 2.0 Flash (Deprecated Mar 2026)', 'mxchat'),
6924 'gemini-2.0-flash-lite' => esc_html__('Gemini 2.0 Flash-Lite (Deprecated Mar 2026)', 'mxchat'),
6925 'gemini-1.5-pro' => esc_html__('Gemini 1.5 Pro (Deprecated Sep 2025)', 'mxchat'),
6926 'gemini-1.5-flash' => esc_html__('Gemini 1.5 Flash (Deprecated Sep 2025)', 'mxchat'),
6927 ),
6928 esc_html__('X.AI Models', 'mxchat') => array(
6929 'grok-3-beta' => esc_html__('Grok-3 (Powerful)', 'mxchat'),
6930 'grok-3-fast-beta' => esc_html__('Grok-3 Fast (High Performance)', 'mxchat'),
6931 'grok-3-mini-beta' => esc_html__('Grok-3 Mini (Affordable)', 'mxchat'),
6932 'grok-3-mini-fast-beta' => esc_html__('Grok-3 Mini Fast (Quick Response)', 'mxchat'),
6933 'grok-2' => esc_html__('Grok 2', 'mxchat'),
6934 'grok-4-0709' => esc_html__('Grok 4 (Latest Flagship)', 'mxchat'),
6935 'grok-4-1-fast-reasoning' => esc_html__('Grok 4.1 Fast (Reasoning)', 'mxchat'),
6936 'grok-4-1-fast-non-reasoning' => esc_html__('Grok 4.1 Fast (Non-Reasoning)', 'mxchat'),
6937 ),
6938 esc_html__('DeepSeek Models', 'mxchat') => array(
6939 'deepseek-chat' => esc_html__('DeepSeek-V3', 'mxchat'),
6940 ),
6941 esc_html__('Claude Models', 'mxchat') => array(
6942 'claude-opus-4-6' => esc_html__('Claude Opus 4.6 (Most Capable - Recommended)', 'mxchat'),
6943 'claude-sonnet-4-6' => esc_html__('Claude Sonnet 4.6 (Latest Sonnet - Fast & Capable)', 'mxchat'),
6944 'claude-opus-4-5' => esc_html__('Claude Opus 4.5 (Highly Capable)', 'mxchat'),
6945 'claude-sonnet-4-5-20250929' => esc_html__('Claude Sonnet 4.5 (Best for Agents & Coding)', 'mxchat'),
6946 'claude-opus-4-1-20250805' => esc_html__('Claude Opus 4.1 (Exceptional for Complex Tasks)', 'mxchat'),
6947 'claude-haiku-4-5-20251001' => esc_html__('Claude Haiku 4.5 (Fastest & Most Intelligent)', 'mxchat'),
6948 'claude-opus-4-20250514' => esc_html__('Claude 4 Opus (Complex Tasks)', 'mxchat'),
6949 'claude-sonnet-4-20250514' => esc_html__('Claude 4 Sonnet (High Performance)', 'mxchat'),
6950 ),
6951 esc_html__('OpenAI Models', 'mxchat') => array(
6952 'gpt-5.4' => esc_html__('GPT-5.4 (Flagship Reasoning & Coding)', 'mxchat'),
6953 'gpt-5.4-mini' => esc_html__('GPT-5.4 Mini (Fast, 400K Context)', 'mxchat'),
6954 'gpt-5.4-nano' => esc_html__('GPT-5.4 Nano (Fastest & Cheapest)', 'mxchat'),
6955 'gpt-5.3-chat-latest' => esc_html__('GPT-5.3 Chat (Conversational)', 'mxchat'),
6956 'gpt-5.2' => esc_html__('GPT-5.2 (Best General-Purpose & Agentic Model)', 'mxchat'),
6957 'gpt-5.1-chat-latest' => esc_html__('GPT-5.1 Chat Latest (Recommended)', 'mxchat'),
6958 'gpt-5.1-2025-11-13' => esc_html__('GPT-5.1 (Flagship for Coding & Agentic Tasks)', 'mxchat'),
6959 'gpt-5' => esc_html__('GPT-5 (Flagship for Coding, Reasoning & Agents)', 'mxchat'),
6960 'gpt-5-mini' => esc_html__('GPT-5 Mini (Fast and Lightweight)', 'mxchat'),
6961 'gpt-5-nano' => esc_html__('GPT-5 Nano (Fastest & Cheapest for Summarization/Classification)', 'mxchat'),
6962 ),
6963 );
6964
6965 // Retrieve the currently selected model from saved options
6966 $selected_model = isset($this->options['model']) ? esc_attr($this->options['model']) : 'gpt-5.1-chat-latest';
6967
6968 // Begin the select dropdown
6969 echo '<select id="model" name="model">';
6970
6971 // Iterate over groups of models
6972 foreach ($models as $group_label => $group_models) {
6973 echo '<optgroup label="' . esc_attr($group_label) . '">';
6974
6975 foreach ($group_models as $model_value => $model_label) {
6976 echo '<option value="' . esc_attr($model_value) . '" ' . selected($selected_model, $model_value, false) . '>' . esc_html($model_label) . '</option>';
6977 }
6978
6979 echo '</optgroup>';
6980 }
6981
6982 echo '</select>';
6983
6984 // Add a note for OpenRouter
6985 echo '<p class="description" id="openrouter-model-note" style="display:none; color: #d63638; font-weight: 500;">';
6986 echo '<span class="dashicons dashicons-info" style="font-size: 16px; vertical-align: middle;"></span> ';
6987 echo esc_html__('After entering your OpenRouter API key above, click the button below to load available models.', 'mxchat');
6988 echo '</p>';
6989
6990 // API Key Status Messages (hidden by default, shown by JS based on selected model)
6991 $has_openai_key = !empty($this->options['api_key']);
6992 $has_claude_key = !empty($this->options['claude_api_key']);
6993 $has_xai_key = !empty($this->options['xai_api_key']);
6994 $has_deepseek_key = !empty($this->options['deepseek_api_key']);
6995 $has_gemini_key = !empty($this->options['gemini_api_key']);
6996 $has_openrouter_key = !empty($this->options['openrouter_api_key']);
6997
6998 // OpenAI/GPT models
6999 echo '<p class="mxchat-api-status" data-provider="openai" style="display:none;">';
7000 if ($has_openai_key) {
7001 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenAI detected', 'mxchat') . '</span>';
7002 } else {
7003 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenAI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7004 }
7005 echo '</p>';
7006
7007 // Claude models
7008 echo '<p class="mxchat-api-status" data-provider="claude" style="display:none;">';
7009 if ($has_claude_key) {
7010 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Anthropic (Claude) detected', 'mxchat') . '</span>';
7011 } else {
7012 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Anthropic (Claude) detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7013 }
7014 echo '</p>';
7015
7016 // X.AI models
7017 echo '<p class="mxchat-api-status" data-provider="xai" style="display:none;">';
7018 if ($has_xai_key) {
7019 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for X.AI (Grok) detected', 'mxchat') . '</span>';
7020 } else {
7021 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>';
7022 }
7023 echo '</p>';
7024
7025 // DeepSeek models
7026 echo '<p class="mxchat-api-status" data-provider="deepseek" style="display:none;">';
7027 if ($has_deepseek_key) {
7028 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for DeepSeek detected', 'mxchat') . '</span>';
7029 } else {
7030 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for DeepSeek detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7031 }
7032 echo '</p>';
7033
7034 // Gemini models
7035 echo '<p class="mxchat-api-status" data-provider="gemini" style="display:none;">';
7036 if ($has_gemini_key) {
7037 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Google Gemini detected', 'mxchat') . '</span>';
7038 } else {
7039 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Google Gemini detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7040 }
7041 echo '</p>';
7042
7043 // OpenRouter models
7044 echo '<p class="mxchat-api-status" data-provider="openrouter" style="display:none;">';
7045 if ($has_openrouter_key) {
7046 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenRouter detected', 'mxchat') . '</span>';
7047 } else {
7048 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenRouter detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7049 }
7050 echo '</p>';
7051
7052 // ADD THESE HIDDEN FIELDS RIGHT HERE:
7053 $openrouter_model = isset($this->options['openrouter_selected_model']) ? esc_attr($this->options['openrouter_selected_model']) : '';
7054 $openrouter_model_name = isset($this->options['openrouter_selected_model_name']) ? esc_attr($this->options['openrouter_selected_model_name']) : '';
7055
7056 echo '<input type="hidden" id="openrouter_selected_model" name="openrouter_selected_model" value="' . $openrouter_model . '" />';
7057 echo '<input type="hidden" id="openrouter_selected_model_name" name="openrouter_selected_model_name" value="' . $openrouter_model_name . '" />';
7058 }
7059
7060 // Update your existing callback method
7061 public function enable_streaming_toggle_callback() {
7062 // Get value from options array, default to 'on'
7063 $enabled = isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on';
7064 $checked = ($enabled === 'on') ? 'checked' : '';
7065
7066 echo '<label class="toggle-switch">';
7067 echo sprintf(
7068 '<input type="checkbox" id="enable_streaming_toggle" name="enable_streaming_toggle" value="on" %s />',
7069 esc_attr($checked)
7070 );
7071 echo '<span class="slider"></span>';
7072 echo '</label>';
7073
7074 // Test button with better styling
7075 echo '<div style="margin-top: 15px;">';
7076 echo '<button type="button" id="mxchat-test-streaming-btn" class="button button-secondary">';
7077 echo '<span class="dashicons dashicons-admin-tools" style="vertical-align: middle; margin-right: 5px;"></span>';
7078 echo esc_html__('Test Streaming Compatibility', 'mxchat');
7079 echo '</button>';
7080 echo '<p id="mxchat-test-streaming-result" style="margin-top:10px; font-weight: bold;"></p>';
7081 echo '</div>';
7082 }
7083
7084 // Web Search toggle callback
7085 public function enable_web_search_toggle_callback() {
7086 // Get value from options array, default to 'off'
7087 $enabled = isset($this->options['enable_web_search']) ? $this->options['enable_web_search'] : 'off';
7088 $checked = ($enabled === 'on') ? 'checked' : '';
7089
7090 // Get current model to determine if we should show/enable the toggle
7091 $current_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest';
7092
7093 // Models that DON'T support web search (per OpenAI docs)
7094 // gpt-5 with minimal reasoning is handled at API level, gpt-4.1-nano doesn't support it
7095 $unsupported_models = array('gpt-4.1-nano');
7096
7097 // Check if current model is an OpenAI model that supports web search
7098 $openai_models = array(
7099 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.4-nano', 'gpt-5.3-chat-latest',
7100 'gpt-5.2', 'gpt-5.1-chat-latest', 'gpt-5.1-2025-11-13', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano'
7101 );
7102
7103 $is_openai = in_array($current_model, $openai_models);
7104 $is_supported = $is_openai && !in_array($current_model, $unsupported_models);
7105
7106 // Wrapper div with data attributes for JS to show/hide
7107 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;"' : '') . '>';
7108
7109 echo '<label class="toggle-switch">';
7110 echo sprintf(
7111 '<input type="checkbox" id="enable_web_search" name="enable_web_search" value="on" %s />',
7112 esc_attr($checked)
7113 );
7114 echo '<span class="slider"></span>';
7115 echo '</label>';
7116
7117 echo '</div>';
7118
7119 // Message shown when non-OpenAI model is selected
7120 echo '<p id="web-search-unavailable-message" class="description" style="color: #666;' . ($is_supported ? ' display:none;' : '') . '">';
7121 echo '<span class="dashicons dashicons-info" style="font-size: 16px; vertical-align: middle; margin-right: 4px;"></span>';
7122 echo esc_html__('Web search is only available for OpenAI models.', 'mxchat');
7123 echo '</p>';
7124 }
7125
7126 // AJAX handler to fetch OpenRouter models
7127 public function fetch_openrouter_models() {
7128 check_ajax_referer('mxchat_fetch_openrouter_models', 'nonce');
7129
7130 $api_key = isset($_POST['api_key']) ? sanitize_text_field($_POST['api_key']) : '';
7131
7132 if (empty($api_key)) {
7133 wp_send_json_error(array('message' => 'API key is required'));
7134 }
7135
7136 $response = wp_remote_get('https://openrouter.ai/api/v1/models', array(
7137 'headers' => array(
7138 'Authorization' => 'Bearer ' . $api_key,
7139 'Content-Type' => 'application/json',
7140 ),
7141 'timeout' => 15,
7142 ));
7143
7144 if (is_wp_error($response)) {
7145 wp_send_json_error(array('message' => $response->get_error_message()));
7146 }
7147
7148 $body = wp_remote_retrieve_body($response);
7149 $data = json_decode($body, true);
7150
7151 if (isset($data['data']) && is_array($data['data'])) {
7152 // Format the models for the frontend
7153 $models = array_map(function($model) {
7154 return array(
7155 'id' => $model['id'],
7156 'name' => $model['name'] ?? $model['id'],
7157 'description' => $model['description'] ?? '',
7158 'context_length' => $model['context_length'] ?? 0,
7159 'pricing' => array(
7160 'prompt' => $model['pricing']['prompt'] ?? 0,
7161 'completion' => $model['pricing']['completion'] ?? 0,
7162 ),
7163 );
7164 }, $data['data']);
7165
7166 wp_send_json_success(array('models' => $models));
7167 } else {
7168 wp_send_json_error(array('message' => 'Invalid response from OpenRouter'));
7169 }
7170 }
7171
7172 // Callback function for embedding model selection
7173 public function embedding_model_callback() {
7174 $models = array(
7175 esc_html__('OpenAI Embeddings', 'mxchat') => array(
7176 'text-embedding-3-small' => esc_html__('TE3 Small (1536, Efficient)', 'mxchat'),
7177 'text-embedding-ada-002' => esc_html__('Ada 2 (1536, Recommended)', 'mxchat'),
7178 'text-embedding-3-large' => esc_html__('TE3 Large (3072, Powerful)', 'mxchat'),
7179 ),
7180 esc_html__('Voyage AI Embeddings', 'mxchat') => array(
7181 'voyage-3-large' => esc_html__('Voyage-3 Large (2048, Most Capable)', 'mxchat'),
7182 ),
7183 esc_html__('Google Gemini Embeddings', 'mxchat') => array(
7184 'gemini-embedding-001' => esc_html__('Gemini Embedding (1536, Stable)', 'mxchat'),
7185 )
7186 );
7187 $selected_model = isset($this->options['embedding_model']) ? esc_attr($this->options['embedding_model']) : 'text-embedding-ada-002';
7188 echo '<select id="embedding_model" name="embedding_model">';
7189 foreach ($models as $group_label => $group_models) {
7190 echo '<optgroup label="' . esc_attr($group_label) . '">';
7191 foreach ($group_models as $model_value => $model_label) {
7192 echo '<option value="' . esc_attr($model_value) . '" ' . selected($selected_model, $model_value, false) . '>' . esc_html($model_label) . '</option>';
7193 }
7194 echo '</optgroup>';
7195 }
7196 echo '</select>';
7197
7198 // API Key Status Messages for Embedding Models
7199 $has_openai_key = !empty($this->options['api_key']);
7200 $has_voyage_key = !empty($this->options['voyage_api_key']);
7201 $has_gemini_key = !empty($this->options['gemini_api_key']);
7202
7203 // OpenAI Embeddings
7204 echo '<p class="mxchat-embedding-api-status" data-provider="openai" style="display:none;">';
7205 if ($has_openai_key) {
7206 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenAI detected', 'mxchat') . '</span>';
7207 } else {
7208 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenAI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7209 }
7210 echo '</p>';
7211
7212 // Voyage AI Embeddings
7213 echo '<p class="mxchat-embedding-api-status" data-provider="voyage" style="display:none;">';
7214 if ($has_voyage_key) {
7215 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Voyage AI detected', 'mxchat') . '</span>';
7216 } else {
7217 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Voyage AI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7218 }
7219 echo '</p>';
7220
7221 // Gemini Embeddings
7222 echo '<p class="mxchat-embedding-api-status" data-provider="gemini" style="display:none;">';
7223 if ($has_gemini_key) {
7224 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Google Gemini detected', 'mxchat') . '</span>';
7225 } else {
7226 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Google Gemini detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7227 }
7228 echo '</p>';
7229
7230 }
7231
7232
7233 public function mxchat_top_bar_title_callback() {
7234 // Retrieve the current value of the top bar title from saved options
7235 $top_bar_title = isset($this->options['top_bar_title']) ? esc_attr($this->options['top_bar_title']) : '';
7236
7237 // Render the input field
7238 echo '<input type="text" id="top_bar_title" name="top_bar_title" value="' . $top_bar_title . '" />';
7239 }
7240 public function mxchat_ai_agent_text_callback() {
7241 // Retrieve the current value of the AI agent text from saved options
7242 $ai_agent_text = isset($this->options['ai_agent_text']) ? esc_attr($this->options['ai_agent_text']) : '';
7243 // Render the input field
7244 echo '<input type="text" id="ai_agent_text" name="ai_agent_text" value="' . $ai_agent_text . '" />';
7245 }
7246
7247
7248 public function enable_email_block_callback() {
7249 // Load full plugin options array
7250 $all_options = get_option('mxchat_options', []);
7251
7252 // Get the value, default to 'off'
7253 $enable_email_block = isset($all_options['enable_email_block']) ? $all_options['enable_email_block'] : 'off';
7254
7255 // Check if it's 'on'
7256 $checked = ($enable_email_block === 'on') ? 'checked' : '';
7257
7258 echo '<label class="toggle-switch">';
7259 echo sprintf(
7260 '<input type="checkbox" id="enable_email_block" name="enable_email_block" value="on" %s />',
7261 esc_attr($checked)
7262 );
7263 echo '<span class="slider"></span>';
7264 echo '</label>';
7265 }
7266
7267 public function email_blocker_header_content_callback() {
7268 // Load the entire 'mxchat_options' array
7269 $all_options = get_option('mxchat_options', []);
7270
7271 // Retrieve the saved content or default to empty
7272 $content = isset($all_options['email_blocker_header_content'])
7273 ? $all_options['email_blocker_header_content']
7274 : '';
7275
7276 // Render the textarea - IMPORTANT: name should be just "email_blocker_header_content"
7277 echo '<textarea
7278 id="email_blocker_header_content"
7279 name="email_blocker_header_content"
7280 rows="5"
7281 cols="70"
7282 data-setting="email_blocker_header_content"
7283 >' . esc_textarea($content) . '</textarea>';
7284 }
7285
7286 public function email_blocker_button_text_callback() {
7287 // Load the entire 'mxchat_options' array
7288 $all_options = get_option('mxchat_options', []);
7289
7290 // Retrieve the saved button text or default to empty
7291 $button_text = isset($all_options['email_blocker_button_text'])
7292 ? $all_options['email_blocker_button_text']
7293 : '';
7294
7295 // Use esc_attr to safely render the existing text
7296 echo '<input type="text" id="email_blocker_button_text" name="email_blocker_button_text" value="' . esc_attr($button_text) . '" style="width: 300px;" />';
7297 }
7298
7299 //Enable name field callback
7300 public function enable_name_field_callback() {
7301 // Load full plugin options array
7302 $all_options = get_option('mxchat_options', []);
7303 // Get the value, default to 'off'
7304 $enable_name_field = isset($all_options['enable_name_field']) ? $all_options['enable_name_field'] : 'off';
7305 // Check if it's 'on'
7306 $checked = ($enable_name_field === 'on') ? 'checked' : '';
7307 echo '<label class="toggle-switch">';
7308 echo sprintf(
7309 '<input type="checkbox" id="enable_name_field" name="enable_name_field" value="on" %s />',
7310 esc_attr($checked)
7311 );
7312 echo '<span class="slider"></span>';
7313 echo '</label>';
7314 }
7315 //Name field placeholder callback
7316 public function name_field_placeholder_callback() {
7317 $all_options = get_option('mxchat_options', []);
7318 $placeholder = isset($all_options['name_field_placeholder'])
7319 ? $all_options['name_field_placeholder']
7320 : esc_html__('Enter your name', 'mxchat');
7321
7322 echo '<input type="text" id="name_field_placeholder" name="name_field_placeholder" value="' . esc_attr($placeholder) . '" style="width: 300px;" />';
7323 }
7324
7325
7326
7327 public function mxchat_intro_message_callback() {
7328 // Load the entire 'mxchat_options' array
7329 $all_options = get_option('mxchat_options', []);
7330 // Retrieve the saved intro message or use the default
7331 $default_message = __('Hello! How can I assist you today?', 'mxchat');
7332 $saved_message = isset($all_options['intro_message']) ? $all_options['intro_message'] : $default_message;
7333 // Output the textarea with the saved value without escaping HTML
7334 ?>
7335 <textarea id="intro_message" name="intro_message" rows="5" cols="50"><?php echo $saved_message; ?></textarea>
7336 <p class="description" style="margin-top: 8px;">
7337 <?php esc_html_e('Use {visitor_name} to personalize greetings when lead capture is enabled.', 'mxchat'); ?><br>
7338 <code style="font-size: 12px;"><?php esc_html_e('Example: Hello {visitor_name}! How can I help you today?', 'mxchat'); ?></code>
7339 </p>
7340 <?php
7341 }
7342
7343 public function mxchat_input_copy_callback() {
7344 // Load the entire 'mxchat_options' array
7345 $all_options = get_option('mxchat_options', []);
7346
7347 // Retrieve the saved input copy or use the default value
7348 $default_copy = __('How can I assist?', 'mxchat');
7349 $input_copy = isset($all_options['input_copy']) ? $all_options['input_copy'] : $default_copy;
7350
7351 // Output the input field with the saved value
7352 printf(
7353 '<input type="text" id="input_copy" name="input_copy" value="%s" placeholder="%s" />',
7354 esc_attr($input_copy),
7355 esc_attr__('How can I assist?', 'mxchat')
7356 );
7357 }
7358
7359
7360 public function mxchat_append_to_body_callback() {
7361 // Fetch fresh options to ensure we have the latest saved values
7362 $options = get_option('mxchat_options', array());
7363
7364 // Get value from options array, default to 'off'
7365 $append_to_body = isset($options['append_to_body']) ? $options['append_to_body'] : 'off';
7366 $checked = ($append_to_body === 'on') ? 'checked' : '';
7367
7368 // Get post type visibility settings
7369 $visibility_mode = isset($options['post_type_visibility_mode']) ? $options['post_type_visibility_mode'] : 'all';
7370 $visibility_list = isset($options['post_type_visibility_list']) ? $options['post_type_visibility_list'] : array();
7371 if (!is_array($visibility_list)) {
7372 $visibility_list = array();
7373 }
7374
7375 echo '<div class="mxchat-autosave-section">';
7376
7377 // Main toggle
7378 echo '<label class="toggle-switch">';
7379 echo sprintf(
7380 '<input type="checkbox" id="append_to_body" name="append_to_body" value="on" %s />',
7381 esc_attr($checked)
7382 );
7383 echo '<span class="slider"></span>';
7384 echo '</label>';
7385
7386 // Post Type Visibility Options (only visible when auto-display is ON)
7387 $display_style = ($append_to_body === 'on') ? '' : 'display: none;';
7388 echo '<div id="post-type-visibility-options" class="mxchat-sub-options" style="' . esc_attr($display_style) . '">';
7389
7390 echo '<div class="mxchat-post-type-visibility-header">';
7391 echo '<h4>' . esc_html__('Post Type Visibility', 'mxchat') . '</h4>';
7392 echo '</div>';
7393
7394 // Mode selector (radio buttons)
7395 echo '<div class="mxchat-visibility-mode">';
7396
7397 echo '<label class="mxchat-radio-label">';
7398 echo '<input type="radio" name="post_type_visibility_mode" value="all" ' . checked($visibility_mode, 'all', false) . ' />';
7399 echo '<span>' . esc_html__('Show on all post types', 'mxchat') . '</span>';
7400 echo '</label>';
7401
7402 echo '<label class="mxchat-radio-label">';
7403 echo '<input type="radio" name="post_type_visibility_mode" value="include" ' . checked($visibility_mode, 'include', false) . ' />';
7404 echo '<span>' . esc_html__('Only show on selected post types', 'mxchat') . '</span>';
7405 echo '</label>';
7406
7407 echo '<label class="mxchat-radio-label">';
7408 echo '<input type="radio" name="post_type_visibility_mode" value="exclude" ' . checked($visibility_mode, 'exclude', false) . ' />';
7409 echo '<span>' . esc_html__('Hide on selected post types', 'mxchat') . '</span>';
7410 echo '</label>';
7411
7412 echo '</div>';
7413
7414 // Post type checkboxes (only visible when mode is include or exclude)
7415 $list_display = ($visibility_mode !== 'all') ? '' : 'display: none;';
7416 echo '<div id="post-type-list" class="mxchat-post-type-list" style="' . esc_attr($list_display) . '">';
7417
7418 // Get all public post types
7419 $post_types = get_post_types(array('public' => true), 'objects');
7420
7421 foreach ($post_types as $post_type) {
7422 // Skip attachments
7423 if ($post_type->name === 'attachment') {
7424 continue;
7425 }
7426
7427 $is_checked = in_array($post_type->name, $visibility_list) ? 'checked' : '';
7428
7429 echo '<label class="mxchat-checkbox-label">';
7430 echo '<input type="checkbox" name="post_type_visibility_list[]" value="' . esc_attr($post_type->name) . '" ' . $is_checked . ' />';
7431 echo '<span>' . esc_html($post_type->label) . '</span>';
7432 echo '</label>';
7433 }
7434
7435 echo '</div>'; // End post-type-list
7436 echo '</div>'; // End post-type-visibility-options
7437 echo '</div>'; // End mxchat-autosave-section
7438 }
7439
7440 public function mxchat_contextual_awareness_callback() {
7441 // Get value from options array, default to 'off'
7442 $contextual_awareness = isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off';
7443 $checked = ($contextual_awareness === 'on') ? 'checked' : '';
7444 echo '<label class="toggle-switch">';
7445 echo sprintf(
7446 '<input type="checkbox" id="contextual_awareness_toggle" name="contextual_awareness_toggle" value="on" %s />',
7447 esc_attr($checked)
7448 );
7449 echo '<span class="slider"></span>';
7450 echo '</label>';
7451 }
7452
7453 public function mxchat_citation_links_toggle_callback() {
7454 // Get value from options array, default to 'on' (enabled by default)
7455 $citation_links = isset($this->options['citation_links_toggle']) ? $this->options['citation_links_toggle'] : 'on';
7456 $checked = ($citation_links === 'on') ? 'checked' : '';
7457 echo '<label class="toggle-switch">';
7458 echo sprintf(
7459 '<input type="checkbox" id="citation_links_toggle" name="citation_links_toggle" value="on" %s />',
7460 esc_attr($checked)
7461 );
7462 echo '<span class="slider"></span>';
7463 echo '</label>';
7464 }
7465
7466 public function mxchat_privacy_toggle_callback() {
7467 // Load from mxchat_options array
7468 $options = get_option('mxchat_options', []);
7469
7470 // Get privacy toggle value with fallback
7471 $privacy_toggle = isset($options['privacy_toggle']) ? $options['privacy_toggle'] : 'off';
7472 $checked = ($privacy_toggle === 'on') ? 'checked' : '';
7473
7474 // Get privacy text with fallback
7475 $privacy_text = isset($options['privacy_text'])
7476 ? $options['privacy_text']
7477 : __('By chatting, you agree to our <a href="https://example.com/privacy-policy" target="_blank">privacy policy</a>.', 'mxchat');
7478
7479 // Output the toggle switch
7480 echo '<label class="toggle-switch">';
7481 echo sprintf(
7482 '<input type="checkbox" id="privacy_toggle" name="privacy_toggle" value="on" %s />',
7483 esc_attr($checked)
7484 );
7485 echo '<span class="slider"></span>';
7486 echo '</label>';
7487
7488 // Output the custom text input field
7489 echo sprintf(
7490 '<textarea id="privacy_text" name="privacy_text" rows="5" cols="50" class="regular-text">%s</textarea>',
7491 esc_textarea($privacy_text)
7492 );
7493 }
7494
7495
7496 public function mxchat_complianz_toggle_callback() {
7497 // Load from mxchat_options array
7498 $options = get_option('mxchat_options', []);
7499
7500 // Get complianz toggle value with fallback
7501 $complianz_toggle = isset($options['complianz_toggle']) ? $options['complianz_toggle'] : 'off';
7502 $checked = ($complianz_toggle === 'on') ? 'checked' : '';
7503
7504 // Output the toggle switch
7505 echo '<label class="toggle-switch">';
7506 echo sprintf(
7507 '<input type="checkbox" id="complianz_toggle" name="complianz_toggle" value="on" %s />',
7508 esc_attr($checked)
7509 );
7510 echo '<span class="slider"></span>';
7511 echo '</label>';
7512 }
7513
7514 public function mxchat_link_target_toggle_callback() {
7515 // Load from mxchat_options array
7516 $options = get_option('mxchat_options', []);
7517
7518 // Get link target toggle value with fallback
7519 $link_target_toggle = isset($options['link_target_toggle']) ? $options['link_target_toggle'] : 'off';
7520 $checked = ($link_target_toggle === 'on') ? 'checked' : '';
7521
7522 // Output the toggle switch
7523 echo '<label class="toggle-switch">';
7524 echo sprintf(
7525 '<input type="checkbox" id="link_target_toggle" name="link_target_toggle" value="on" %s />',
7526 esc_attr($checked)
7527 );
7528 echo '<span class="slider"></span>';
7529 echo '</label>';
7530 }
7531
7532 public function mxchat_chat_persistence_toggle_callback() {
7533 // Load from mxchat_options array
7534 $options = get_option('mxchat_options', []);
7535
7536 // Get chat persistence toggle value with fallback
7537 $chat_persistence_toggle = isset($options['chat_persistence_toggle']) ? $options['chat_persistence_toggle'] : 'off';
7538 $checked = ($chat_persistence_toggle === 'on') ? 'checked' : '';
7539
7540 // Output the toggle switch
7541 echo '<label class="toggle-switch">';
7542 echo sprintf(
7543 '<input type="checkbox" id="chat_persistence_toggle" name="chat_persistence_toggle" value="on" %s />',
7544 esc_attr($checked)
7545 );
7546 echo '<span class="slider"></span>';
7547 echo '</label>';
7548 }
7549
7550 public function mxchat_popular_question_1_callback() {
7551 // Load the full plugin options array
7552 $all_options = get_option('mxchat_options', []);
7553
7554 // Retrieve the specific option for popular_question_1
7555 $popular_question_1 = isset($all_options['popular_question_1']) ? $all_options['popular_question_1'] : '';
7556
7557 // Render the input field
7558 printf(
7559 '<input type="text" id="popular_question_1" name="popular_question_1" value="%s" placeholder="%s" class="regular-text" />',
7560 esc_attr($popular_question_1),
7561 esc_attr__('Enter Quick Question 1', 'mxchat')
7562 );
7563 }
7564
7565
7566 public function mxchat_popular_question_2_callback() {
7567 // Load the full plugin options array
7568 $all_options = get_option('mxchat_options', []);
7569
7570 // Retrieve the specific option for popular_question_2
7571 $popular_question_2 = isset($all_options['popular_question_2']) ? $all_options['popular_question_2'] : '';
7572
7573 // Render the input field
7574 printf(
7575 '<input type="text" id="popular_question_2" name="popular_question_2" value="%s" placeholder="%s" class="regular-text" />',
7576 esc_attr($popular_question_2),
7577 esc_attr__('Enter Quick Question 2', 'mxchat')
7578 );
7579 }
7580
7581
7582 public function mxchat_popular_question_3_callback() {
7583 // Load the full plugin options array
7584 $all_options = get_option('mxchat_options', []);
7585
7586 // Retrieve the specific option for popular_question_3
7587 $popular_question_3 = isset($all_options['popular_question_3']) ? $all_options['popular_question_3'] : '';
7588
7589 // Render the input field
7590 printf(
7591 '<input type="text" id="popular_question_3" name="popular_question_3" value="%s" placeholder="%s" class="regular-text" />',
7592 esc_attr($popular_question_3),
7593 esc_attr(__('Enter Quick Question 3', 'mxchat'))
7594 );
7595 }
7596
7597 public function mxchat_additional_popular_questions_callback() {
7598 $options = get_option('mxchat_options', []);
7599 $additional_questions = isset($options['additional_popular_questions'])
7600 ? $options['additional_popular_questions']
7601 : get_option('additional_popular_questions', array());
7602
7603 echo '<div id="mxchat-additional-questions-container">';
7604 if (!empty($additional_questions)) {
7605 foreach ($additional_questions as $index => $question) {
7606 printf(
7607 '<div class="mxchat-question-row">
7608 <input type="text" name="additional_popular_questions[]"
7609 value="%s"
7610 placeholder="%s"
7611 class="regular-text mxchat-question-input"
7612 data-question-index="%d" />
7613 <button type="button" class="button mxchat-remove-question"
7614 aria-label="%s">%s</button>
7615 </div>',
7616 esc_attr($question),
7617 esc_attr(sprintf(__('Enter Additional Quick Question %d', 'mxchat'), $index + 4)),
7618 $index,
7619 esc_attr(__('Remove question', 'mxchat')),
7620 esc_html__('Remove', 'mxchat')
7621 );
7622 }
7623 } else {
7624 printf(
7625 '<div class="mxchat-question-row">
7626 <input type="text" name="additional_popular_questions[]"
7627 value=""
7628 placeholder="%s"
7629 class="regular-text mxchat-question-input"
7630 data-question-index="0" />
7631 <button type="button" class="button mxchat-remove-question"
7632 aria-label="%s">%s</button>
7633 </div>',
7634 esc_attr(__('Enter Additional Quick Question 4', 'mxchat')),
7635 esc_attr(__('Remove question', 'mxchat')),
7636 esc_html__('Remove', 'mxchat')
7637 );
7638 }
7639 echo '</div>';
7640 printf(
7641 '<button type="button" class="button mxchat-add-question" aria-label="%s">%s</button>',
7642 esc_attr(__('Add question', 'mxchat')),
7643 esc_html__('Add Question', 'mxchat')
7644 );
7645 }
7646
7647 public function mxchat_brave_api_key_callback() {
7648 $brave_api_key = isset($this->options['brave_api_key']) ? esc_attr($this->options['brave_api_key']) : '';
7649 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7650
7651 echo '<div class="api-key-wrapper">';
7652 echo sprintf(
7653 '<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" />',
7654 $brave_api_key,
7655 $nonce
7656 );
7657 echo '<button type="button" id="toggleBraveApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7658 echo '</div>';
7659 echo '<p class="description">' . __('Required for Brave Search integration. Get your API key from Brave Search API.', 'mxchat') . '</p>';
7660 }
7661
7662 public function mxchat_brave_image_count_callback() {
7663 $brave_image_count = isset($this->options['brave_image_count'])
7664 ? intval($this->options['brave_image_count'])
7665 : 4;
7666 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7667
7668 echo '<div class="mxchat-field-wrapper">';
7669 echo sprintf(
7670 '<input type="number" id="brave_image_count" name="brave_image_count"
7671 value="%d" min="1" max="6" class="small-text mxchat-autosave-field" data-nonce="%s" />',
7672 $brave_image_count,
7673 $nonce
7674 );
7675 echo '</div>';
7676 echo '<p class="description">' . __('Select the number of images to return (1-6).', 'mxchat') . '</p>';
7677 }
7678
7679 public function mxchat_brave_safe_search_callback() {
7680 $brave_safe_search = isset($this->options['brave_safe_search'])
7681 ? esc_attr($this->options['brave_safe_search'])
7682 : 'strict';
7683 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7684
7685 echo '<div class="mxchat-field-wrapper">';
7686 echo '<select id="brave_safe_search" name="brave_safe_search" class="mxchat-autosave-field" data-nonce="' . $nonce . '">';
7687 echo sprintf(
7688 '<option value="strict" %s>%s</option>',
7689 selected($brave_safe_search, 'strict', false),
7690 __('Strict', 'mxchat')
7691 );
7692 echo sprintf(
7693 '<option value="off" %s>%s</option>',
7694 selected($brave_safe_search, 'off', false),
7695 __('Off', 'mxchat')
7696 );
7697 echo '</select>';
7698 echo '</div>';
7699 echo '<p class="description">' .
7700 esc_html__('Set the Safe Search level for image searches. Brave Search only supports "Strict" and "Off" options.', 'mxchat') .
7701 '</p>';
7702 }
7703
7704 public function mxchat_brave_news_count_callback() {
7705 $brave_news_count = isset($this->options['brave_news_count'])
7706 ? intval($this->options['brave_news_count'])
7707 : 3;
7708 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7709
7710 echo '<div class="mxchat-field-wrapper">';
7711 echo sprintf(
7712 '<input type="number" id="brave_news_count" name="brave_news_count"
7713 value="%d" min="1" max="10" class="small-text mxchat-autosave-field" data-nonce="%s" />',
7714 $brave_news_count,
7715 $nonce
7716 );
7717 echo '</div>';
7718 echo '<p class="description">' . esc_html__('Select the number of news articles to retrieve (1-10).', 'mxchat') . '</p>';
7719 }
7720
7721 public function mxchat_brave_country_callback() {
7722 $brave_country = isset($this->options['brave_country'])
7723 ? esc_attr($this->options['brave_country'])
7724 : 'us';
7725 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7726
7727 echo '<div class="mxchat-field-wrapper">';
7728 echo sprintf(
7729 '<input type="text" id="brave_country" name="brave_country"
7730 value="%s" maxlength="2" class="small-text mxchat-autosave-field" data-nonce="%s" />',
7731 $brave_country,
7732 $nonce
7733 );
7734 echo '</div>';
7735 echo '<p class="description">' . esc_html__('Enter the country code (e.g., "us" for United States).', 'mxchat') . '</p>';
7736 }
7737
7738 public function mxchat_brave_language_callback() {
7739 $brave_language = isset($this->options['brave_language'])
7740 ? esc_attr($this->options['brave_language'])
7741 : 'en';
7742 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7743
7744 echo '<div class="mxchat-field-wrapper">';
7745 echo sprintf(
7746 '<input type="text" id="brave_language" name="brave_language"
7747 value="%s" maxlength="2" class="small-text mxchat-autosave-field" data-nonce="%s" />',
7748 $brave_language,
7749 $nonce
7750 );
7751 echo '</div>';
7752 echo '<p class="description">' . esc_html__('Enter the language code (e.g., "en" for English).', 'mxchat') . '</p>';
7753 }
7754
7755
7756
7757
7758
7759 // Section Callback
7760 public function mxchat_pdf_intent_section_callback() {
7761 echo '<p>' . esc_html__('Configure the intent settings for the Chat with PDF feature.', 'mxchat') . '</p>';
7762 }
7763
7764 public function mxchat_chat_toolbar_toggle_callback() {
7765 // Get chat toolbar toggle value with fallback
7766 $chat_toolbar_toggle = isset($this->options['chat_toolbar_toggle']) ? $this->options['chat_toolbar_toggle'] : 'off';
7767 $checked = ($chat_toolbar_toggle === 'on') ? 'checked' : '';
7768
7769 // Output the toggle switch
7770 echo '<label class="toggle-switch">';
7771 echo sprintf(
7772 '<input type="checkbox" id="chat_toolbar_toggle" name="chat_toolbar_toggle" value="on" %s />',
7773 esc_attr($checked)
7774 );
7775 echo '<span class="slider"></span>';
7776 echo '</label>';
7777
7778 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>';
7779 }
7780
7781 /**
7782 * Callback for PDF upload button toggle setting
7783 */
7784 public function mxchat_show_pdf_upload_button_callback() {
7785 // Get toggle value with fallback
7786 $show_pdf_button = isset($this->options['show_pdf_upload_button']) ? $this->options['show_pdf_upload_button'] : 'on';
7787 $checked = ($show_pdf_button === 'on') ? 'checked' : '';
7788
7789 // Output the toggle switch
7790 echo '<label class="toggle-switch">';
7791 echo sprintf(
7792 '<input type="checkbox" id="show_pdf_upload_button" name="show_pdf_upload_button" value="on" %s />',
7793 esc_attr($checked)
7794 );
7795 echo '<span class="slider"></span>';
7796 echo '</label>';
7797
7798 echo '<p class="description">' . esc_html__('Enable to show the PDF upload button in the chatbot toolbar. Disable to hide it.', 'mxchat') . '</p>';
7799 }
7800
7801 /**
7802 * Callback for Word upload button toggle setting
7803 */
7804 public function mxchat_show_word_upload_button_callback() {
7805 // Get toggle value with fallback
7806 $show_word_button = isset($this->options['show_word_upload_button']) ? $this->options['show_word_upload_button'] : 'on';
7807 $checked = ($show_word_button === 'on') ? 'checked' : '';
7808
7809 // Output the toggle switch
7810 echo '<label class="toggle-switch">';
7811 echo sprintf(
7812 '<input type="checkbox" id="show_word_upload_button" name="show_word_upload_button" value="on" %s />',
7813 esc_attr($checked)
7814 );
7815 echo '<span class="slider"></span>';
7816 echo '</label>';
7817
7818 echo '<p class="description">' . esc_html__('Enable to show the Word document upload button in the chatbot toolbar. Disable to hide it.', 'mxchat') . '</p>';
7819 }
7820
7821 public function mxchat_pdf_intent_trigger_text_callback() {
7822 $default_text = __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat');
7823
7824 echo sprintf(
7825 '<textarea id="pdf_intent_trigger_text"
7826 name="pdf_intent_trigger_text"
7827 rows="3"
7828 cols="50"
7829 placeholder="%s">%s</textarea>',
7830 esc_attr__('Enter trigger text', 'mxchat'),
7831 isset($this->options['pdf_intent_trigger_text'])
7832 ? esc_textarea($this->options['pdf_intent_trigger_text'])
7833 : esc_textarea($default_text)
7834 );
7835 echo '<p class="description">' . esc_html__('Text displayed when the intent is triggered.', 'mxchat') . '</p>';
7836 }
7837
7838 public function mxchat_pdf_intent_success_text_callback() {
7839 $default_text = __("I've processed the PDF. What questions do you have about it?", 'mxchat');
7840
7841 echo sprintf(
7842 '<textarea id="pdf_intent_success_text"
7843 name="pdf_intent_success_text"
7844 rows="3"
7845 cols="50"
7846 placeholder="%s">%s</textarea>',
7847 esc_attr__('Enter success text', 'mxchat'),
7848 isset($this->options['pdf_intent_success_text'])
7849 ? esc_textarea($this->options['pdf_intent_success_text'])
7850 : esc_textarea($default_text)
7851 );
7852 echo '<p class="description">' . esc_html__('Text displayed when the intent is successful.', 'mxchat') . '</p>';
7853 }
7854
7855 public function mxchat_pdf_intent_error_text_callback() {
7856 $default_text = __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
7857
7858 echo sprintf(
7859 '<textarea id="pdf_intent_error_text"
7860 name="pdf_intent_error_text"
7861 rows="3"
7862 cols="50"
7863 placeholder="%s">%s</textarea>',
7864 esc_attr__('Enter error text', 'mxchat'),
7865 isset($this->options['pdf_intent_error_text'])
7866 ? esc_textarea($this->options['pdf_intent_error_text'])
7867 : esc_textarea($default_text)
7868 );
7869 echo '<p class="description">' . esc_html__('Text displayed when an error occurs during the intent.', 'mxchat') . '</p>';
7870 }
7871
7872 public function mxchat_pdf_max_pages_callback() {
7873 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
7874
7875 echo sprintf(
7876 '<input type="range"
7877 id="pdf_max_pages"
7878 name="pdf_max_pages"
7879 min="1"
7880 max="69"
7881 value="%d"
7882 class="range-slider" />',
7883 esc_attr($max_pages)
7884 );
7885 echo '<span id="pdf_max_pages_output">' . esc_html($max_pages) . '</span>';
7886 echo '<p class="description">' . esc_html__('Set the maximum number of document pages users can upload for processing. (1-69 pages)', 'mxchat') . '</p>';
7887 }
7888
7889 public function mxchat_live_agent_status_callback() {
7890 // Always get fresh options instead of using cached $this->options
7891 $fresh_options = get_option('mxchat_options');
7892 $status = isset($fresh_options['live_agent_status']) ? $fresh_options['live_agent_status'] : 'off';
7893
7894 echo '<label class="toggle-switch">';
7895 echo sprintf(
7896 '<input type="checkbox" id="live_agent_status" name="live_agent_status" value="on" %s />',
7897 checked($status, 'on', false)
7898 );
7899 echo '<span class="slider"></span>';
7900 echo '</label>';
7901 echo '<label for="live_agent_status" class="mxchat-status-label">';
7902 echo '<span class="status-text">' . ($status === 'on' ? esc_html__('Online', 'mxchat') : esc_html__('Offline', 'mxchat')) . '</span>';
7903 echo '</label>';
7904 }
7905
7906 public function mxchat_live_agent_away_message_callback() {
7907 $message = isset($this->options['live_agent_away_message'])
7908 ? $this->options['live_agent_away_message']
7909 : __('Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.', 'mxchat');
7910
7911 printf(
7912 '<textarea id="live_agent_away_message" name="live_agent_away_message" rows="3" cols="50">%s</textarea>',
7913 esc_textarea($message)
7914 );
7915 echo '<p class="description">' . esc_html__('Message shown when live agents are offline.', 'mxchat') . '</p>';
7916 }
7917
7918 public function mxchat_live_agent_notification_message_callback() {
7919 $message = isset($this->options['live_agent_notification_message'])
7920 ? $this->options['live_agent_notification_message']
7921 : __('Live agent has been notified.', 'mxchat');
7922
7923 printf(
7924 '<textarea id="live_agent_notification_message" name="live_agent_notification_message" rows="3" cols="50">%s</textarea>',
7925 esc_textarea($message)
7926 );
7927 echo '<p class="description">' . esc_html__('Message shown when live transfer activated.', 'mxchat') . '</p>';
7928 }
7929
7930 public function mxchat_live_agent_webhook_url_callback() {
7931 $webhook_url = isset($this->options['live_agent_webhook_url'])
7932 ? esc_url($this->options['live_agent_webhook_url'])
7933 : esc_url(get_option('live_agent_webhook_url', ''));
7934
7935 printf(
7936 '<input type="password" id="live_agent_webhook_url" name="live_agent_webhook_url" value="%s" class="regular-text" />',
7937 $webhook_url
7938 );
7939 echo '<button type="button" id="toggleWebhookUrlVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7940 echo '<p class="description">' . esc_html__('Enter your Slack webhook URL for live agent notifications.', 'mxchat') . '</p>';
7941 }
7942
7943 public function mxchat_live_agent_secret_key_callback() {
7944 printf(
7945 '<input type="password" id="live_agent_secret_key" name="live_agent_secret_key" value="%s" class="regular-text" />',
7946 isset($this->options['live_agent_secret_key']) ? esc_attr($this->options['live_agent_secret_key']) : ''
7947 );
7948 echo '<button type="button" id="toggleSecretKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7949 echo '<p class="description">' . esc_html__('Secret key for validating Slack requests. Keep this secure.', 'mxchat') . '</p>';
7950 }
7951
7952 public function mxchat_live_agent_bot_token_callback() {
7953 printf(
7954 '<input type="password" id="live_agent_bot_token" name="live_agent_bot_token" value="%s" class="regular-text" />',
7955 isset($this->options['live_agent_bot_token']) ? esc_attr($this->options['live_agent_bot_token']) : ''
7956 );
7957 echo '<button type="button" id="toggleBotTokenVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7958 echo '<p class="description">' . esc_html__('Your Slack Bot OAuth Token (starts with xoxb-). Keep this secure.', 'mxchat') . '</p>';
7959 }
7960
7961 public function mxchat_live_agent_user_ids_callback() {
7962 $user_ids = isset($this->options['live_agent_user_ids'])
7963 ? esc_textarea($this->options['live_agent_user_ids'])
7964 : '';
7965
7966 printf(
7967 '<textarea id="live_agent_user_ids" name="live_agent_user_ids" rows="4" class="large-text">%s</textarea>',
7968 $user_ids
7969 );
7970 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>';
7971 }
7972
7973 /**
7974 * Telegram Integration Callbacks
7975 */
7976 public function mxchat_telegram_section_callback() {
7977 echo '<p>' . esc_html__('Configure Telegram integration for live agent support.', 'mxchat') . '</p>';
7978 }
7979
7980 public function mxchat_telegram_status_callback() {
7981 $fresh_options = get_option('mxchat_options');
7982 $status = isset($fresh_options['telegram_status']) ? $fresh_options['telegram_status'] : 'off';
7983
7984 echo '<label class="toggle-switch">';
7985 echo sprintf(
7986 '<input type="checkbox" id="telegram_status" name="telegram_status" value="on" %s />',
7987 checked($status, 'on', false)
7988 );
7989 echo '<span class="slider"></span>';
7990 echo '</label>';
7991 echo '<label for="telegram_status" class="mxchat-status-label">';
7992 echo '<span class="status-text">' . ($status === 'on' ? esc_html__('Online', 'mxchat') : esc_html__('Offline', 'mxchat')) . '</span>';
7993 echo '</label>';
7994 }
7995
7996 public function mxchat_telegram_notification_message_callback() {
7997 $message = isset($this->options['telegram_notification_message'])
7998 ? $this->options['telegram_notification_message']
7999 : __("I've notified a support agent. Please allow a moment for them to respond.", 'mxchat');
8000
8001 printf(
8002 '<textarea id="telegram_notification_message" name="telegram_notification_message" rows="3" cols="50">%s</textarea>',
8003 esc_textarea($message)
8004 );
8005 echo '<p class="description">' . esc_html__('Message shown when live transfer activated.', 'mxchat') . '</p>';
8006 }
8007
8008 public function mxchat_telegram_away_message_callback() {
8009 $message = isset($this->options['telegram_away_message'])
8010 ? $this->options['telegram_away_message']
8011 : __('Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.', 'mxchat');
8012
8013 printf(
8014 '<textarea id="telegram_away_message" name="telegram_away_message" rows="3" cols="50">%s</textarea>',
8015 esc_textarea($message)
8016 );
8017 echo '<p class="description">' . esc_html__('Message shown when live agents are offline.', 'mxchat') . '</p>';
8018 }
8019
8020 public function mxchat_telegram_bot_token_callback() {
8021 printf(
8022 '<input type="password" id="telegram_bot_token" name="telegram_bot_token" value="%s" class="regular-text" />',
8023 isset($this->options['telegram_bot_token']) ? esc_attr($this->options['telegram_bot_token']) : ''
8024 );
8025 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>';
8026 echo '<p class="description">' . esc_html__('Your Telegram Bot Token from @BotFather (e.g., 123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ).', 'mxchat') . '</p>';
8027 }
8028
8029 public function mxchat_telegram_group_id_callback() {
8030 printf(
8031 '<input type="text" id="telegram_group_id" name="telegram_group_id" value="%s" class="regular-text" />',
8032 isset($this->options['telegram_group_id']) ? esc_attr($this->options['telegram_group_id']) : ''
8033 );
8034 echo '<p class="description">' . esc_html__('Your Telegram supergroup ID (starts with -100). The group must have forum topics enabled.', 'mxchat') . '</p>';
8035 }
8036
8037 public function mxchat_telegram_webhook_secret_callback() {
8038 $secret = isset($this->options['telegram_webhook_secret']) ? $this->options['telegram_webhook_secret'] : '';
8039
8040 // Auto-generate secret if empty
8041 if (empty($secret)) {
8042 $secret = wp_generate_password(64, false, false);
8043 $options = get_option('mxchat_options', []);
8044 $options['telegram_webhook_secret'] = $secret;
8045 update_option('mxchat_options', $options);
8046 $this->options['telegram_webhook_secret'] = $secret;
8047 }
8048
8049 printf(
8050 '<input type="password" id="telegram_webhook_secret" name="telegram_webhook_secret" value="%s" class="regular-text" />',
8051 esc_attr($secret)
8052 );
8053 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>';
8054 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>';
8055 }
8056
8057 public function mxchat_similarity_threshold_callback() {
8058 // Load from mxchat_options array
8059 $options = get_option('mxchat_options', []);
8060
8061 // Get value from options array with default of 35
8062 $threshold = isset($options['similarity_threshold']) ? $options['similarity_threshold'] : 35;
8063
8064 echo '<div class="slider-container">';
8065 echo sprintf(
8066 '<input type="range"
8067 id="similarity_threshold"
8068 name="similarity_threshold"
8069 min="20"
8070 max="85"
8071 step="1"
8072 value="%s"
8073 class="range-slider" />',
8074 esc_attr($threshold)
8075 );
8076 echo sprintf(
8077 '<span id="threshold_value" class="range-value">%s</span>',
8078 esc_html($threshold)
8079 );
8080 echo '</div>';
8081 }
8082
8083 public function mxchat_rag_sources_limit_callback() {
8084 // Load from mxchat_options array
8085 $options = get_option('mxchat_options', []);
8086
8087 // Get value from options array with default of 3
8088 $rag_sources_limit = isset($options['rag_sources_limit']) ? intval($options['rag_sources_limit']) : 3;
8089
8090 echo '<div class="slider-container">';
8091 echo sprintf(
8092 '<input type="range"
8093 id="rag_sources_limit"
8094 name="rag_sources_limit"
8095 min="3"
8096 max="10"
8097 step="1"
8098 value="%s"
8099 class="range-slider" />',
8100 esc_attr($rag_sources_limit)
8101 );
8102 echo sprintf(
8103 '<span id="rag_sources_limit_value" class="range-value">%s</span>',
8104 esc_html($rag_sources_limit)
8105 );
8106 echo '</div>';
8107 }
8108
8109 public function mxchat_rag_chunks_limit_callback() {
8110 // Load from mxchat_options array
8111 $options = get_option('mxchat_options', []);
8112
8113 // Get value from options array with default of 15
8114 $rag_chunks_limit = isset($options['rag_chunks_limit']) ? intval($options['rag_chunks_limit']) : 15;
8115
8116 echo '<div class="slider-container">';
8117 echo sprintf(
8118 '<input type="range"
8119 id="rag_chunks_limit"
8120 name="rag_chunks_limit"
8121 min="8"
8122 max="20"
8123 step="1"
8124 value="%s"
8125 class="range-slider" />',
8126 esc_attr($rag_chunks_limit)
8127 );
8128 echo sprintf(
8129 '<span id="rag_chunks_limit_value" class="range-value">%s</span>',
8130 esc_html($rag_chunks_limit)
8131 );
8132 echo '</div>';
8133 }
8134
8135 public function mxchat_enqueue_admin_assets() {
8136 // Get plugin version
8137 $version = MXCHAT_VERSION;
8138
8139 // Use file modification time for development (remove in production)
8140 if (defined('WP_DEBUG') && WP_DEBUG) {
8141 $version = filemtime(plugin_dir_path(__FILE__) . '../mxchat-basic.php');
8142 }
8143
8144 $current_page = isset($_GET['page']) ? $_GET['page'] : '';
8145 $plugin_url = plugin_dir_url(__FILE__) . '../';
8146
8147 // Only load on MxChat pages
8148 if (strpos($current_page, 'mxchat') === false) {
8149 return;
8150 }
8151
8152 // Always load these on all MxChat pages
8153 $this->enqueue_core_admin_assets($plugin_url, $version);
8154 $this->enqueue_page_specific_assets($current_page, $plugin_url, $version);
8155 $this->localize_admin_scripts($current_page);
8156 }
8157 private function enqueue_core_admin_assets($plugin_url, $version) {
8158 // Core admin styles
8159 wp_enqueue_style('mxchat-admin-css', $plugin_url . 'css/admin-style.css', array(), $version);
8160 wp_enqueue_style('mxchat-knowledge-css', $plugin_url . 'css/knowledge-style.css', array(), $version);
8161
8162 // New sidebar navigation styles (for main settings page)
8163 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8164
8165 // Core admin scripts
8166 wp_enqueue_script('mxchat-admin-js', $plugin_url . 'js/mxchat-admin.js', array('jquery'), $version, true);
8167 }
8168 private function enqueue_page_specific_assets($current_page, $plugin_url, $version) {
8169 switch ($current_page) {
8170 case 'mxchat-prompts':
8171 // Knowledge processing page assets
8172 wp_enqueue_style('mxchat-content-selector-css', $plugin_url . 'css/content-selector.css', array(), $version);
8173 wp_enqueue_script('mxchat-content-selector-js', $plugin_url . 'js/content-selector.js', array('jquery'), $version, true);
8174 // Add the knowledge processing script (common script needed for WordPress dismiss functionality)
8175 wp_enqueue_script('mxchat-knowledge-processing', $plugin_url . 'js/knowledge-processing.js', array('jquery', 'common'), $version, true);
8176 break;
8177
8178 case 'mxchat-transcripts':
8179 // Load admin sidebar CSS first (shared styles for sidebar navigation)
8180 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8181 // Load transcripts-specific styles
8182 wp_enqueue_style('mxchat-chat-transcripts-css', $plugin_url . 'css/chat-transcripts.css', array('mxchat-admin-sidebar-css'), $version);
8183 wp_enqueue_script('mxchat-transcripts-js', $plugin_url . 'js/mxchat_transcripts.js', array('jquery'), $version, true);
8184 break;
8185
8186 case 'mxchat-actions':
8187 // Load admin sidebar CSS first (shared styles for sidebar navigation)
8188 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8189 // Load actions-specific styles
8190 wp_enqueue_style('mxchat-actions-css', $plugin_url . 'css/actions.css', array('mxchat-admin-sidebar-css'), $version);
8191 wp_enqueue_script('mxchat-actions-js', $plugin_url . 'js/mxchat_actions.js', array('jquery'), $version, true);
8192
8193 // Localize script data for actions page
8194 $is_activated = get_option('mxchat_pro_license_status') === 'active';
8195 wp_localize_script('mxchat-actions-js', 'mxchActionsData', array(
8196 'ajaxUrl' => admin_url('admin-ajax.php'),
8197 'nonce' => wp_create_nonce('mxchat_actions_nonce'),
8198 'addNonce' => wp_create_nonce('mxchat_add_intent_nonce'),
8199 'editNonce' => wp_create_nonce('mxchat_edit_intent'),
8200 'deleteNonce' => wp_create_nonce('mxchat_delete_intent_nonce'),
8201 'toggleNonce' => wp_create_nonce('mxchat_actions_nonce'),
8202 'addPhraseNonce' => wp_create_nonce('mxchat_add_phrase_nonce'),
8203 'deletePhraseNonce' => wp_create_nonce('mxchat_delete_phrase_nonce'),
8204 'getPhrasesNonce' => wp_create_nonce('mxchat_get_phrases_nonce'),
8205 'deleteLegacyNonce' => wp_create_nonce('mxchat_delete_legacy_nonce'),
8206 'isActivated' => $is_activated,
8207 'i18n' => array(
8208 'confirmDelete' => __('Are you sure you want to delete this action?', 'mxchat'),
8209 'confirmBulkDelete' => __('Are you sure you want to delete the selected actions?', 'mxchat'),
8210 'saving' => __('Saving...', 'mxchat'),
8211 'saved' => __('Saved successfully!', 'mxchat'),
8212 'error' => __('An error occurred. Please try again.', 'mxchat'),
8213 'proRequired' => __('This feature requires MxChat Pro.', 'mxchat'),
8214 'addonRequired' => __('This feature requires an add-on.', 'mxchat')
8215 )
8216 ));
8217 break;
8218
8219 case 'mxchat-activation':
8220 // Load admin sidebar CSS first (shared styles for sidebar navigation)
8221 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8222 // Load pro page-specific styles
8223 wp_enqueue_style('mxchat-pro-css', $plugin_url . 'css/admin-pro.css', array('mxchat-admin-sidebar-css'), $version);
8224 // Load pro page JavaScript
8225 wp_enqueue_script('mxchat-pro-js', $plugin_url . 'js/mxchat_pro.js', array('jquery'), $version, true);
8226 // Load activation script for license activation/deactivation
8227 wp_enqueue_script('mxchat-activation-js', $plugin_url . 'js/activation-script.js', array('jquery'), $version, true);
8228 break;
8229
8230 case 'mxchat-content':
8231 // Load admin sidebar CSS (shared styles for sidebar navigation)
8232 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8233 // Load content page-specific styles
8234 wp_enqueue_style('mxchat-content-css', $plugin_url . 'css/admin-content.css', array('mxchat-admin-sidebar-css'), $version);
8235 // Load content page JavaScript
8236 wp_enqueue_script('mxchat-content-js', $plugin_url . 'js/mxchat-content.js', array('jquery'), $version, true);
8237 break;
8238 default:
8239 wp_enqueue_script(
8240 'mxchat-test-streaming-js',
8241 $plugin_url . 'js/mxchat-test-streaming.js',
8242 ['jquery'],
8243 $version,
8244 true
8245 );
8246
8247 wp_localize_script('mxchat-test-streaming-js', 'mxchatTestStreamingAjax', [
8248 'ajax_url' => admin_url('admin-ajax.php'),
8249 'nonce' => wp_create_nonce('mxchat_test_streaming_nonce'),
8250 'settings_nonce' => wp_create_nonce('mxchat_save_setting_nonce')
8251 ]);
8252
8253 // Testing Tab: Load chatbot assets for the embedded testing chatbot
8254 wp_enqueue_style('mxchat-chat-css', $plugin_url . 'css/chat-style.css', array(), $version);
8255 wp_enqueue_style('mxchat-admin-testing-css', $plugin_url . 'css/admin-testing-tab.css', array('mxchat-admin-sidebar-css', 'mxchat-chat-css'), $version);
8256
8257 wp_enqueue_script('mxchat-chat-js', $plugin_url . 'js/chat-script.js', array('jquery'), $version, true);
8258 wp_enqueue_script('mxchat-admin-testing-js', $plugin_url . 'js/admin-testing-tab.js', array('jquery', 'mxchat-chat-js'), $version, true);
8259
8260 // Allow add-ons to enqueue their public CSS/JS for the testing chatbot
8261 do_action('mxchat_enqueue_testing_tab_assets');
8262
8263 // Localize mxchatChat for the chatbot JS (same settings as frontend)
8264 $options = get_option('mxchat_options', array());
8265 $prompts_options = get_option('mxchat_prompts_options', array());
8266 $theme_options = get_option('mxchat_theme_options', array());
8267 $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
8268 $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
8269 $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
8270
8271 wp_localize_script('mxchat-chat-js', 'mxchatChat', array(
8272 'ajax_url' => admin_url('admin-ajax.php'),
8273 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
8274 'model' => isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest',
8275 'enable_streaming_toggle' => isset($options['enable_streaming_toggle']) ? $options['enable_streaming_toggle'] : 'on',
8276 'contextual_awareness_toggle' => isset($options['contextual_awareness_toggle']) ? $options['contextual_awareness_toggle'] : 'off',
8277 'link_target_toggle' => $options['link_target_toggle'] ?? 'off',
8278 'rate_limit_message' => $options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
8279 'complianz_toggle' => isset($options['complianz_toggle']) && $options['complianz_toggle'] === 'on',
8280 'user_message_bg_color' => $options['user_message_bg_color'] ?? '#fff',
8281 'user_message_font_color' => $options['user_message_font_color'] ?? '#212121',
8282 'bot_message_bg_color' => $options['bot_message_bg_color'] ?? '#212121',
8283 'bot_message_font_color' => $options['bot_message_font_color'] ?? '#fff',
8284 'top_bar_bg_color' => $options['top_bar_bg_color'] ?? '#212121',
8285 'send_button_font_color' => $options['send_button_font_color'] ?? '#212121',
8286 'close_button_color' => $options['close_button_color'] ?? '#fff',
8287 'chatbot_background_color' => $options['chatbot_background_color'] ?? '#212121',
8288 'chatbot_bg_color' => $options['chatbot_bg_color'] ?? '#fff',
8289 'icon_color' => $options['icon_color'] ?? '#fff',
8290 'chat_input_font_color' => $options['chat_input_font_color'] ?? '#212121',
8291 'chat_persistence_toggle' => 'off', // Always off for testing chatbot
8292 'appendWidgetToBody' => 'off',
8293 'live_agent_message_bg_color' => $options['live_agent_message_bg_color'] ?? '#ffffff',
8294 'live_agent_message_font_color' => $options['live_agent_message_font_color'] ?? '#333333',
8295 'chat_toolbar_toggle' => $options['chat_toolbar_toggle'] ?? 'off',
8296 'mode_indicator_bg_color' => $options['mode_indicator_bg_color'] ?? '#767676',
8297 'mode_indicator_font_color' => $options['mode_indicator_font_color'] ?? '#ffffff',
8298 'toolbar_icon_color' => $options['toolbar_icon_color'] ?? '#212121',
8299 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
8300 'email_collection_enabled' => 'off', // No email collection in testing
8301 'initial_email_state' => null,
8302 'skip_email_check' => true,
8303 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
8304 'skip_inline_colors' => $skip_inline_colors,
8305 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array()
8306 ));
8307
8308 // Localize testing tab script data
8309 wp_localize_script('mxchat-admin-testing-js', 'mxchatAdminTestData', array(
8310 'ajaxUrl' => admin_url('admin-ajax.php'),
8311 'nonce' => wp_create_nonce('mxchat_test_nonce'),
8312 'isAdmin' => true,
8313 'testingEnabled' => true
8314 ));
8315
8316 // Add testing enabled flag for the chatbot to return debug data
8317 add_action('admin_footer', function() {
8318 echo '<script>window.mxchatTestingEnabled = true;</script>';
8319 });
8320
8321 break;
8322 }
8323 }
8324 private function localize_admin_scripts($current_page) {
8325 // Base localization data for main admin script
8326 $base_data = array(
8327 'ajax_url' => admin_url('admin-ajax.php'),
8328 'nonce' => wp_create_nonce('mxchat_admin_nonce'),
8329 'admin_url' => admin_url(),
8330 'license_nonce' => wp_create_nonce('mxchat_activate_license_nonce'),
8331 'inline_edit_nonce' => wp_create_nonce('mxchat_save_inline_nonce'),
8332 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
8333 'export_nonce' => wp_create_nonce('mxchat_export_transcripts'),
8334 'actions_nonce' => wp_create_nonce('mxchat_actions_nonce'),
8335 'add_intent_nonce' => wp_create_nonce('mxchat_add_intent_nonce'),
8336 'edit_intent_nonce' => wp_create_nonce('mxchat_edit_intent'),
8337 'add_phrase_nonce' => wp_create_nonce('mxchat_add_phrase_nonce'),
8338 'delete_phrase_nonce' => wp_create_nonce('mxchat_delete_phrase_nonce'),
8339 'get_phrases_nonce' => wp_create_nonce('mxchat_get_phrases_nonce'),
8340 'delete_legacy_nonce' => wp_create_nonce('mxchat_delete_legacy_nonce'),
8341 'toggle_action_nonce' => wp_create_nonce('mxchat_actions_nonce'),
8342 'fetch_openrouter_models_nonce' => wp_create_nonce('mxchat_fetch_openrouter_models'),
8343 'is_activated' => $this->is_activated ? '1' : '0',
8344 'status_refresh_interval' => 5000,
8345 'prompts_setting_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
8346 'ajaxurl' => admin_url('admin-ajax.php')
8347 );
8348
8349 // Localize main admin script with base data
8350 wp_localize_script('mxchat-admin-js', 'mxchatAdmin', $base_data);
8351
8352 // Page-specific localizations
8353 $this->localize_page_specific_scripts($current_page);
8354 }
8355 private function localize_page_specific_scripts($current_page) {
8356 switch ($current_page) {
8357 case 'mxchat-prompts':
8358 // Status updater localization
8359 wp_localize_script('mxchat-status-updater', 'mxchat_status_data', array(
8360 'ajax_url' => admin_url('admin-ajax.php'),
8361 'nonce' => wp_create_nonce('mxchat_status_nonce')
8362 ));
8363
8364 // Content selector localization
8365 wp_localize_script('mxchat-content-selector-js', 'mxchatSelector', array(
8366 'ajaxurl' => admin_url('admin-ajax.php'),
8367 'nonce' => wp_create_nonce('mxchat_content_selector_nonce'),
8368 'i18n' => array(
8369 'searchPlaceholder' => __('Search posts and pages...', 'mxchat'),
8370 'selectAll' => __('Select All', 'mxchat'),
8371 'process' => __('Process Selected', 'mxchat'),
8372 'cancel' => __('Cancel', 'mxchat'),
8373 'noResults' => __('No content found.', 'mxchat')
8374 )
8375 ));
8376
8377 wp_localize_script('mxchat-knowledge-processing', 'mxchatAdmin', array(
8378 'ajax_url' => admin_url('admin-ajax.php'),
8379 'status_nonce' => wp_create_nonce('mxchat_status_nonce'),
8380 'queue_nonce' => wp_create_nonce('mxchat_queue_nonce'),
8381 'stop_nonce' => wp_create_nonce('mxchat_stop_processing_action'),
8382 'settings_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
8383 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
8384 'entries_nonce' => wp_create_nonce('mxchat_entries_nonce'),
8385 'admin_url' => admin_url(),
8386 'ajaxurl' => admin_url('admin-ajax.php'),
8387 'status_refresh_interval' => 2000,
8388 'bot_id' => isset($_GET['bot_id']) ? sanitize_text_field($_GET['bot_id']) : 'default'
8389 ));
8390 break;
8391
8392 case 'mxchat-activation':
8393 wp_localize_script('mxchat-activation-js', 'mxchatAdmin', array(
8394 'ajax_url' => admin_url('admin-ajax.php'),
8395 'license_nonce' => wp_create_nonce('mxchat_activate_license_nonce')
8396 ));
8397 break;
8398
8399 case 'mxchat-content':
8400 wp_localize_script('mxchat-content-js', 'mxchatContent', array(
8401 'ajaxUrl' => admin_url('admin-ajax.php'),
8402 'nonce' => wp_create_nonce('mxchat_content_nonce'),
8403 'settingNonce' => wp_create_nonce('mxchat_save_setting_nonce'),
8404 'previewUrl' => home_url('/?p='),
8405 'isActivated' => $this->is_activated(),
8406 'hasAdvancedContent' => apply_filters('mxchat_content_pro_feature', false, 'seo_readability'),
8407 'hasGSC' => apply_filters('mxchat_content_pro_feature', false, 'gsc_integration'),
8408 'seoOptimize' => array(
8409 'meta_description' => ($options['seo_optimize_meta_desc'] ?? 'on') === 'on',
8410 'seo_title' => ($options['seo_optimize_seo_title'] ?? 'on') === 'on',
8411 'slug' => ($options['seo_optimize_slug'] ?? 'on') === 'on',
8412 'readability' => ($options['seo_optimize_readability'] ?? 'on') === 'on',
8413 'internal_links' => ($options['seo_optimize_internal_links'] ?? 'on') === 'on',
8414 'img_alt' => ($options['seo_optimize_img_alt'] ?? 'on') === 'on',
8415 'featured_img' => ($options['seo_optimize_featured_img'] ?? 'on') === 'on',
8416 ),
8417 'i18n' => array(
8418 'generating' => __('Generating...', 'mxchat'),
8419 'planning' => __('Planning content structure...', 'mxchat'),
8420 'images' => __('Generating images...', 'mxchat'),
8421 'writing' => __('Writing full content...', 'mxchat'),
8422 'creating' => __('Creating WordPress post...', 'mxchat'),
8423 'done' => __('Content generated successfully!', 'mxchat'),
8424 'error' => __('An error occurred. Please try again.', 'mxchat'),
8425 'editSuccess' => __('Content updated successfully.', 'mxchat'),
8426 'promptEmpty' => __('Please enter a prompt.', 'mxchat'),
8427 )
8428 ));
8429 break;
8430
8431 case 'mxchat-transcripts':
8432 // Get chart data for localization
8433 $chart_data = $this->get_transcripts_chart_data();
8434
8435 wp_localize_script('mxchat-transcripts-js', 'mxchatAdmin', array(
8436 'ajax_url' => admin_url('admin-ajax.php'),
8437 'export_nonce' => wp_create_nonce('mxchat_export_transcripts'),
8438 'delete_nonce' => wp_create_nonce('mxchat_delete_chat_history'),
8439 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
8440 'translate_nonce' => wp_create_nonce('mxchat_translate_messages')
8441 ));
8442
8443 // Localize chart data separately - use array_values to ensure proper JSON array encoding
8444 wp_localize_script('mxchat-transcripts-js', 'mxchatChartData', array(
8445 'labels' => array_values($chart_data['labels']),
8446 'chats' => array_values($chart_data['chats']),
8447 'messages' => array_values($chart_data['messages'])
8448 ));
8449 break;
8450
8451 case 'mxchat-settings':
8452 default:
8453 // Color picker and settings localization
8454 wp_localize_script('mxchat-color-picker', 'mxchatStyleSettings', array(
8455 'ajax_url' => admin_url('admin-ajax.php'),
8456 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
8457 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
8458 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
8459 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
8460 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
8461 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
8462 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
8463 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
8464 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
8465 'icon_color' => $this->options['icon_color'] ?? '#fff',
8466 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
8467 'pre_chat_message' => $this->options['pre_chat_message'] ?? esc_html__('Hey there! Ask me anything!', 'mxchat'),
8468 'rate_limit_message' => $this->options['rate_limit_message'] ?? esc_html__('Rate limit exceeded. Please try again later.', 'mxchat'),
8469 'loops_api_key' => $this->options['loops_api_key'] ?? '',
8470 'loops_mailing_list' => $this->options['loops_mailing_list'] ?? '',
8471 'triggered_phrase_response' => $this->options['triggered_phrase_response'] ?? esc_html__('Would you like to join our mailing list? Please provide your email below.', 'mxchat'),
8472 'email_capture_response' => $this->options['email_capture_response'] ?? esc_html__('Thank you for providing your email! You\'ve been added to our list.', 'mxchat'),
8473 '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'),
8474 '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'),
8475 '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'),
8476 'pdf_max_pages' => $this->options['pdf_max_pages'] ?? 69,
8477 'live_agent_webhook_url' => $this->options['live_agent_webhook_url'] ?? '',
8478 'live_agent_secret_key' => $this->options['live_agent_secret_key'] ?? '',
8479 'live_agent_bot_token' => $this->options['live_agent_bot_token'] ?? '',
8480 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
8481 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
8482 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
8483 'show_pdf_upload_button' => $this->options['show_pdf_upload_button'] ?? 'on',
8484 'show_word_upload_button' => $this->options['show_word_upload_button'] ?? 'on',
8485 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
8486 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
8487 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
8488 ));
8489 break;
8490 }
8491
8492 // Additional localization that was in the original code
8493 wp_localize_script('mxchat-admin-js', 'mxchatPromptsAdmin', array(
8494 'ajax_url' => admin_url('admin-ajax.php'),
8495 'prompts_setting_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
8496 ));
8497 }
8498
8499 public function mxchat_sanitize($input) {
8500 $new_input = array();
8501
8502 if (isset($input['api_key'])) {
8503 $new_input['api_key'] = sanitize_text_field($input['api_key']);
8504 }
8505
8506 if (isset($input['similarity_threshold'])) {
8507 $new_input['similarity_threshold'] = absint($input['similarity_threshold']); // Ensure it's an integer
8508 $new_input['similarity_threshold'] = min(max($new_input['similarity_threshold'], 20), 85); // Enforce range
8509 }
8510
8511 if (isset($input['rag_sources_limit'])) {
8512 $new_input['rag_sources_limit'] = absint($input['rag_sources_limit']); // Ensure it's an integer
8513 $new_input['rag_sources_limit'] = min(max($new_input['rag_sources_limit'], 3), 10); // Enforce range 3-10
8514 }
8515
8516 if (isset($input['rag_chunks_limit'])) {
8517 $new_input['rag_chunks_limit'] = absint($input['rag_chunks_limit']); // Ensure it's an integer
8518 $new_input['rag_chunks_limit'] = min(max($new_input['rag_chunks_limit'], 8), 20); // Enforce range 8-20
8519 }
8520
8521 if (isset($input['xai_api_key'])) {
8522 $new_input['xai_api_key'] = sanitize_text_field($input['xai_api_key']);
8523 }
8524
8525 if (isset($input['claude_api_key'])) {
8526 $new_input['claude_api_key'] = sanitize_text_field($input['claude_api_key']);
8527 }
8528
8529 if (isset($input['enable_streaming_toggle'])) {
8530 $new_input['enable_streaming_toggle'] = ($input['enable_streaming_toggle'] === 'on') ? 'on' : 'off';
8531 } else {
8532 // If checkbox not checked, it won't be in $input, so set to 'off'
8533 $new_input['enable_streaming_toggle'] = 'off';
8534 }
8535
8536 if (isset($input['enable_web_search'])) {
8537 $new_input['enable_web_search'] = ($input['enable_web_search'] === 'on') ? 'on' : 'off';
8538 } else {
8539 // If checkbox not checked, it won't be in $input, so set to 'off'
8540 $new_input['enable_web_search'] = 'off';
8541 }
8542
8543 if (isset($input['deepseek_api_key'])) {
8544 $new_input['deepseek_api_key'] = sanitize_text_field($input['deepseek_api_key']);
8545 }
8546
8547 if (isset($input['gemini_api_key'])) {
8548 $new_input['gemini_api_key'] = sanitize_text_field($input['gemini_api_key']);
8549 }
8550
8551 if (isset($input['enable_woocommerce_integration'])) {
8552 $new_input['enable_woocommerce_integration'] = $input['enable_woocommerce_integration'] === 'on' ? 'on' : 'off';
8553 }
8554
8555 if (isset($input['privacy_toggle'])) {
8556 $new_input['privacy_toggle'] = $input['privacy_toggle'];
8557 }
8558
8559 if (isset($input['complianz_toggle'])) {
8560 $new_input['complianz_toggle'] = $input['complianz_toggle'];
8561 }
8562
8563 // Handle custom privacy text input
8564 if (isset($input['privacy_text'])) {
8565 // Allow basic HTML for links
8566 $new_input['privacy_text'] = wp_kses_post($input['privacy_text']);
8567 }
8568
8569 if (isset($input['system_prompt_instructions'])) {
8570 $new_input['system_prompt_instructions'] = sanitize_textarea_field($input['system_prompt_instructions']);
8571 }
8572
8573 if (isset($input['mxchat_pro_email'])) {
8574 $new_input['mxchat_pro_email'] = sanitize_email($input['mxchat_pro_email']);
8575 }
8576
8577 if (isset($input['mxchat_activation_key'])) {
8578 $new_input['mxchat_activation_key'] = sanitize_text_field($input['mxchat_activation_key']);
8579 }
8580
8581 if (isset($input['append_to_body'])) {
8582 $new_input['append_to_body'] = $input['append_to_body'] === 'on' ? 'on' : 'off';
8583 }
8584
8585 // Post type visibility settings
8586 if (isset($input['post_type_visibility_mode'])) {
8587 $allowed_modes = array('all', 'include', 'exclude');
8588 $new_input['post_type_visibility_mode'] = in_array($input['post_type_visibility_mode'], $allowed_modes)
8589 ? $input['post_type_visibility_mode']
8590 : 'all';
8591 }
8592
8593 if (isset($input['post_type_visibility_list'])) {
8594 if (is_array($input['post_type_visibility_list'])) {
8595 $new_input['post_type_visibility_list'] = array_map('sanitize_key', $input['post_type_visibility_list']);
8596 } else {
8597 $new_input['post_type_visibility_list'] = array();
8598 }
8599 }
8600
8601 if (isset($input['contextual_awareness_toggle'])) {
8602 $new_input['contextual_awareness_toggle'] = $input['contextual_awareness_toggle'] === 'on' ? 'on' : 'off';
8603 }
8604
8605 if (isset($input['citation_links_toggle'])) {
8606 $new_input['citation_links_toggle'] = $input['citation_links_toggle'] === 'on' ? 'on' : 'off';
8607 }
8608
8609 if (isset($input['top_bar_title'])) {
8610 $new_input['top_bar_title'] = sanitize_text_field($input['top_bar_title']);
8611 }
8612
8613 if (isset($input['ai_agent_text'])) {
8614 $new_input['ai_agent_text'] = sanitize_text_field($input['ai_agent_text']);
8615 }
8616
8617 if (isset($input['enable_email_block'])) {
8618 $new_input['enable_email_block'] = sanitize_text_field($input['enable_email_block']);
8619 }
8620
8621 if (isset($input['email_blocker_header_content'])) {
8622 // wp_kses_post() allows standard HTML tags permitted by WordPress
8623 $new_input['email_blocker_header_content'] = wp_kses_post($input['email_blocker_header_content']);
8624 }
8625 if (isset($input['email_blocker_button_text'])) {
8626 $new_input['email_blocker_button_text'] = sanitize_text_field($input['email_blocker_button_text']);
8627 }
8628 // Sanitize name field toggle
8629 if (isset($input['enable_name_field'])) {
8630 $new_input['enable_name_field'] = ($input['enable_name_field'] === 'on') ? 'on' : 'off';
8631 } else {
8632 $new_input['enable_name_field'] = 'off';
8633 }
8634 // Sanitize name field placeholder
8635 if (isset($input['name_field_placeholder'])) {
8636 $new_input['name_field_placeholder'] = sanitize_text_field($input['name_field_placeholder']);
8637 }
8638 if (isset($input['intro_message'])) {
8639 $new_input['intro_message'] = wp_kses_post($input['intro_message']); // Use wp_kses_post instead
8640 }
8641
8642 if (isset($input['input_copy'])) {
8643 $new_input['input_copy'] = sanitize_text_field($input['input_copy']);
8644 }
8645
8646 if (isset($input['rate_limit_message'])) {
8647 $new_input['rate_limit_message'] = sanitize_text_field($input['rate_limit_message']);
8648 }
8649
8650 // Handle the new rate limits format
8651 if (isset($input['rate_limits']) && is_array($input['rate_limits'])) {
8652 $new_input['rate_limits'] = array();
8653 $allowed_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited');
8654 $allowed_timeframes = array('hourly', 'daily', 'weekly', 'monthly');
8655
8656 foreach ($input['rate_limits'] as $role_id => $settings) {
8657 $new_input['rate_limits'][$role_id] = array();
8658
8659 // Sanitize limit
8660 if (isset($settings['limit'])) {
8661 $limit = sanitize_text_field($settings['limit']);
8662 if (in_array($limit, $allowed_limits, true)) {
8663 $new_input['rate_limits'][$role_id]['limit'] = $limit;
8664 } else {
8665 $new_input['rate_limits'][$role_id]['limit'] = ($role_id === 'logged_out') ? '10' : '100'; // Default
8666 }
8667 }
8668
8669 // Sanitize timeframe
8670 if (isset($settings['timeframe'])) {
8671 $timeframe = sanitize_text_field($settings['timeframe']);
8672 if (in_array($timeframe, $allowed_timeframes, true)) {
8673 $new_input['rate_limits'][$role_id]['timeframe'] = $timeframe;
8674 } else {
8675 $new_input['rate_limits'][$role_id]['timeframe'] = 'daily'; // Default
8676 }
8677 }
8678
8679 // Sanitize message
8680 if (isset($settings['message'])) {
8681 $new_input['rate_limits'][$role_id]['message'] = sanitize_textarea_field($settings['message']);
8682 }
8683 }
8684 }
8685
8686 if (isset($input['pre_chat_message'])) {
8687 $new_input['pre_chat_message'] = sanitize_textarea_field($input['pre_chat_message']);
8688 }
8689
8690 if (isset($input['voyage_api_key'])) {
8691 $new_input['voyage_api_key'] = sanitize_text_field($input['voyage_api_key']);
8692 }
8693
8694 // Add to your sanitize function
8695 if (isset($input['embedding_model'])) {
8696 $allowed_models = array(
8697 'text-embedding-ada-002',
8698 'text-embedding-3-small',
8699 'text-embedding-3-large',
8700 'voyage-3-large',
8701 'gemini-embedding-001'
8702 );
8703 if (in_array($input['embedding_model'], $allowed_models)) {
8704 $new_input['embedding_model'] = sanitize_text_field($input['embedding_model']);
8705 }
8706 }
8707
8708 if (isset($input['model'])) {
8709 if ($input['model'] === 'openrouter') {
8710 $new_input['model'] = 'openrouter';
8711 } else {
8712 $allowed_models = array(
8713 'gemini-3-pro-preview',
8714 'gemini-3-flash-preview',
8715 'gemini-2.5-pro',
8716 'gemini-2.5-flash',
8717 'gemini-2.5-flash-lite',
8718 'gemini-2.0-flash',
8719 'gemini-2.0-flash-lite',
8720 'gemini-1.5-pro',
8721 'gemini-1.5-flash',
8722 'grok-4-0709',
8723 'grok-4-1-fast-reasoning',
8724 'grok-4-1-fast-non-reasoning',
8725 'grok-3-beta',
8726 'grok-3-fast-beta',
8727 'grok-3-mini-beta',
8728 'grok-3-mini-fast-beta',
8729 'grok-2',
8730 'deepseek-chat',
8731 'claude-opus-4-6',
8732 'claude-sonnet-4-6',
8733 'claude-opus-4-5',
8734 'claude-sonnet-4-5-20250929',
8735 'claude-opus-4-1-20250805',
8736 'claude-haiku-4-5-20251001',
8737 'claude-opus-4-20250514',
8738 'claude-sonnet-4-20250514',
8739 'gpt-5.2',
8740 'gpt-5.1-chat-latest',
8741 'gpt-5.1-2025-11-13',
8742 'gpt-5',
8743 'gpt-5-mini',
8744 'gpt-5-nano',
8745 'gpt-5.4',
8746 'gpt-5.4-mini',
8747 'gpt-5.4-nano',
8748 'gpt-5.3-chat-latest',
8749 );
8750
8751 if (in_array($input['model'], $allowed_models)) {
8752 $new_input['model'] = sanitize_text_field($input['model']);
8753 } else {
8754 // Fallback for any deprecated model
8755 $new_input['model'] = 'gpt-5.1-chat-latest';
8756 }
8757 }
8758 }
8759
8760 if (isset($input['openrouter_selected_model'])) {
8761 // Just sanitize it, don't validate against a whitelist
8762 $new_input['openrouter_selected_model'] = sanitize_text_field($input['openrouter_selected_model']);
8763 }
8764
8765 // ADD THIS:
8766 if (isset($input['openrouter_selected_model_name'])) {
8767 $new_input['openrouter_selected_model_name'] = sanitize_text_field($input['openrouter_selected_model_name']);
8768 }
8769
8770 if (isset($input['openrouter_api_key'])) {
8771 $new_input['openrouter_api_key'] = sanitize_text_field($input['openrouter_api_key']);
8772 }
8773
8774
8775
8776 if (isset($input['woocommerce_consumer_key'])) {
8777 $new_input['woocommerce_consumer_key'] = sanitize_text_field($input['woocommerce_consumer_key']);
8778 }
8779
8780 if (isset($input['woocommerce_consumer_secret'])) {
8781 $new_input['woocommerce_consumer_secret'] = sanitize_text_field($input['woocommerce_consumer_secret']);
8782 }
8783
8784
8785 // Sanitize link_target_toggle
8786 if (isset($input['link_target_toggle'])) {
8787 $new_input['link_target_toggle'] = $input['link_target_toggle'] === 'on' ? 'on' : 'off';
8788 }
8789
8790 // Sanitize Loops API Key
8791 if (isset($input['loops_api_key'])) {
8792 $new_input['loops_api_key'] = sanitize_text_field($input['loops_api_key']);
8793 }
8794
8795 if (isset($input['chat_persistence_toggle'])) {
8796 $new_input['chat_persistence_toggle'] = $input['chat_persistence_toggle'] === 'on' ? 'on' : 'off';
8797 }
8798
8799 if (isset($input['popular_question_1'])) {
8800 $new_input['popular_question_1'] = sanitize_text_field($input['popular_question_1']);
8801 }
8802
8803 if (isset($input['popular_question_2'])) {
8804 $new_input['popular_question_2'] = sanitize_text_field($input['popular_question_2']);
8805 }
8806
8807 if (isset($input['popular_question_3'])) {
8808 $new_input['popular_question_3'] = sanitize_text_field($input['popular_question_3']);
8809 }
8810
8811 if (isset($input['additional_popular_questions']) && is_array($input['additional_popular_questions'])) {
8812 $new_input['additional_popular_questions'] = array_map('sanitize_text_field', $input['additional_popular_questions']);
8813 }
8814
8815 // Sanitize Loops Mailing List
8816 if (isset($input['loops_mailing_list'])) {
8817 $new_input['loops_mailing_list'] = sanitize_text_field($input['loops_mailing_list']);
8818 }
8819
8820 // Sanitize Triggered Phrase Response
8821 if (isset($input['triggered_phrase_response'])) {
8822 $new_input['triggered_phrase_response'] = wp_kses_post($input['triggered_phrase_response']);
8823 }
8824 if (isset($input['email_capture_response'])) {
8825 $new_input['email_capture_response'] = wp_kses_post($input['email_capture_response']);
8826 }
8827
8828 // Sanitize Brave Search Settings
8829 if (isset($input['brave_api_key'])) {
8830 $new_input['brave_api_key'] = sanitize_text_field($input['brave_api_key']);
8831 }
8832
8833 if (isset($input['brave_image_count'])) {
8834 $image_count = intval($input['brave_image_count']);
8835 $new_input['brave_image_count'] = ($image_count >=1 && $image_count <=6) ? $image_count : 4;
8836 }
8837
8838 if (isset($input['brave_safe_search'])) {
8839 $allowed = array('strict', 'off');
8840 $new_input['brave_safe_search'] = in_array($input['brave_safe_search'], $allowed, true) ? $input['brave_safe_search'] : 'strict';
8841 }
8842
8843 if (isset($input['brave_news_count'])) {
8844 $news_count = intval($input['brave_news_count']);
8845 $new_input['brave_news_count'] = ($news_count >=1 && $news_count <=10) ? $news_count : 3;
8846 }
8847
8848 if (isset($input['brave_country'])) {
8849 $new_input['brave_country'] = sanitize_text_field($input['brave_country']);
8850 }
8851
8852 if (isset($input['brave_language'])) {
8853 $new_input['brave_language'] = sanitize_text_field($input['brave_language']);
8854 }
8855
8856 if (isset($input['chat_toolbar_toggle'])) {
8857 $new_input['chat_toolbar_toggle'] = $input['chat_toolbar_toggle'] === 'on' ? 'on' : 'off';
8858 }
8859
8860 // Sanitize PDF upload button toggle
8861 if (isset($input['show_pdf_upload_button'])) {
8862 $new_input['show_pdf_upload_button'] = $input['show_pdf_upload_button'] === 'on' ? 'on' : 'off';
8863 } else {
8864 $new_input['show_pdf_upload_button'] = 'off'; // If checkbox is unchecked
8865 }
8866
8867 // Sanitize Word upload button toggle
8868 if (isset($input['show_word_upload_button'])) {
8869 $new_input['show_word_upload_button'] = $input['show_word_upload_button'] === 'on' ? 'on' : 'off';
8870 } else {
8871 $new_input['show_word_upload_button'] = 'off'; // If checkbox is unchecked
8872 }
8873
8874 if (isset($input['pdf_intent_trigger_text'])) {
8875 $new_input['pdf_intent_trigger_text'] = sanitize_text_field($input['pdf_intent_trigger_text']);
8876 }
8877
8878 if (isset($input['pdf_intent_success_text'])) {
8879 $new_input['pdf_intent_success_text'] = sanitize_text_field($input['pdf_intent_success_text']);
8880 }
8881
8882 if (isset($input['pdf_intent_error_text'])) {
8883 $new_input['pdf_intent_error_text'] = sanitize_text_field($input['pdf_intent_error_text']);
8884 }
8885
8886 if (isset($input['pdf_max_pages'])) {
8887 $new_input['pdf_max_pages'] = intval($input['pdf_max_pages']);
8888 if ($new_input['pdf_max_pages'] < 1 || $new_input['pdf_max_pages'] > 69) {
8889 $new_input['pdf_max_pages'] = 69; // Default to 69 if out of range
8890 }
8891 }
8892
8893 if (isset($input['live_agent_webhook_url'])) {
8894 $new_input['live_agent_webhook_url'] = esc_url_raw($input['live_agent_webhook_url']);
8895 }
8896 if (isset($input['live_agent_secret_key'])) {
8897 $new_input['live_agent_secret_key'] = sanitize_text_field($input['live_agent_secret_key']);
8898 }
8899
8900 // Live Agent Integration
8901 if (isset($input['live_agent_bot_token'])) {
8902 $new_input['live_agent_bot_token'] = sanitize_text_field($input['live_agent_bot_token']);
8903 }
8904
8905 if (isset($input['live_agent_user_ids'])) {
8906 $new_input['live_agent_user_ids'] = sanitize_textarea_field($input['live_agent_user_ids']);
8907 }
8908
8909 if (isset($input['live_agent_status'])) {
8910 $new_input['live_agent_status'] = ($input['live_agent_status'] === 'on') ? 'on' : 'off';
8911 }
8912 if (isset($input['live_agent_away_message'])) {
8913 $new_input['live_agent_away_message'] = sanitize_textarea_field($input['live_agent_away_message']);
8914 }
8915 if (isset($input['live_agent_notification_message'])) {
8916 $new_input['live_agent_notification_message'] = sanitize_textarea_field($input['live_agent_notification_message']);
8917 }
8918
8919 // Telegram Integration
8920 if (isset($input['telegram_status'])) {
8921 $new_input['telegram_status'] = ($input['telegram_status'] === 'on') ? 'on' : 'off';
8922 }
8923 if (isset($input['telegram_bot_token'])) {
8924 $new_input['telegram_bot_token'] = sanitize_text_field($input['telegram_bot_token']);
8925 }
8926 if (isset($input['telegram_group_id'])) {
8927 $new_input['telegram_group_id'] = sanitize_text_field($input['telegram_group_id']);
8928 }
8929 if (isset($input['telegram_webhook_secret'])) {
8930 $new_input['telegram_webhook_secret'] = sanitize_text_field($input['telegram_webhook_secret']);
8931 }
8932 if (isset($input['telegram_notification_message'])) {
8933 $new_input['telegram_notification_message'] = sanitize_textarea_field($input['telegram_notification_message']);
8934 }
8935 if (isset($input['telegram_away_message'])) {
8936 $new_input['telegram_away_message'] = sanitize_textarea_field($input['telegram_away_message']);
8937 }
8938
8939 // Sanitize script loading strategy
8940 if (isset($input['script_loading_strategy'])) {
8941 $allowed_strategies = array('default', 'defer', 'delay_1s', 'delay_3s', 'delay_5s', 'on_interaction');
8942 $new_input['script_loading_strategy'] = in_array($input['script_loading_strategy'], $allowed_strategies)
8943 ? $input['script_loading_strategy']
8944 : 'default';
8945 }
8946
8947 // Sanitize debug mode
8948 if (isset($input['debug_mode'])) {
8949 $new_input['debug_mode'] = ($input['debug_mode'] === 'on') ? 'on' : 'off';
8950 }
8951
8952 // Content Generator Settings
8953 if (isset($input['content_model'])) {
8954 $new_input['content_model'] = sanitize_text_field($input['content_model']);
8955 }
8956 if (isset($input['content_image_model'])) {
8957 $new_input['content_image_model'] = sanitize_text_field($input['content_image_model']);
8958 }
8959 if (isset($input['content_enable_images'])) {
8960 $new_input['content_enable_images'] = ($input['content_enable_images'] === 'on') ? 'on' : 'off';
8961 }
8962 if (isset($input['content_use_placeholders'])) {
8963 $new_input['content_use_placeholders'] = ($input['content_use_placeholders'] === 'on') ? 'on' : 'off';
8964 }
8965 if (isset($input['content_internal_linking'])) {
8966 $new_input['content_internal_linking'] = ($input['content_internal_linking'] === 'on') ? 'on' : 'off';
8967 }
8968 if (isset($input['content_tool_use'])) {
8969 $new_input['content_tool_use'] = ($input['content_tool_use'] === 'on') ? 'on' : 'off';
8970 }
8971
8972 // SEO Optimize toggle fields
8973 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) {
8974 if (isset($input[$seo_key])) {
8975 $new_input[$seo_key] = ($input[$seo_key] === 'on') ? 'on' : 'off';
8976 }
8977 }
8978
8979 // Preserve content generator settings when saving from main settings page
8980 // (where content fields are not in the form submission)
8981 $existing = get_option('mxchat_options', array());
8982 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) {
8983 if (!isset($new_input[$key]) && isset($existing[$key])) {
8984 $new_input[$key] = $existing[$key];
8985 }
8986 }
8987
8988 return $new_input;
8989 }
8990
8991 /**
8992 * Log a debug message if debug mode is enabled
8993 *
8994 * @param string $type The type of log entry (settings_save, api_error, activation, etc.)
8995 * @param string $message The log message
8996 * @param array $data Optional additional data to log
8997 * @return bool Whether the message was logged
8998 */
8999 public static function mxchat_log_debug( $type, $message, $data = array() ) {
9000 $options = get_option( 'mxchat_options', array() );
9001
9002 // Check if debug mode is enabled
9003 if ( ! isset( $options['debug_mode'] ) || $options['debug_mode'] !== 'on' ) {
9004 return false;
9005 }
9006
9007 // Get current log
9008 $log = get_option( 'mxchat_debug_log', array() );
9009 if ( ! is_array( $log ) ) {
9010 $log = array();
9011 }
9012
9013 // Add new entry
9014 $entry = array(
9015 'time' => current_time( 'Y-m-d H:i:s' ),
9016 'type' => sanitize_key( $type ),
9017 'message' => sanitize_text_field( $message ),
9018 );
9019
9020 if ( ! empty( $data ) ) {
9021 $entry['data'] = $data;
9022 }
9023
9024 // Add to beginning of array (newest first)
9025 array_unshift( $log, $entry );
9026
9027 // Keep only last 100 entries
9028 if ( count( $log ) > 100 ) {
9029 $log = array_slice( $log, 0, 100 );
9030 }
9031
9032 // Save log
9033 update_option( 'mxchat_debug_log', $log, false );
9034
9035 return true;
9036 }
9037
9038 /**
9039 * Get the debug log entries
9040 *
9041 * @return array The debug log entries
9042 */
9043 public static function mxchat_get_debug_log() {
9044 $log = get_option( 'mxchat_debug_log', array() );
9045 return is_array( $log ) ? $log : array();
9046 }
9047
9048 /**
9049 * Clear the debug log
9050 *
9051 * @return bool Whether the log was cleared
9052 */
9053 public static function mxchat_clear_debug_log() {
9054 return delete_option( 'mxchat_debug_log' );
9055 }
9056
9057 /**
9058 * Export settings as JSON with masked API keys
9059 *
9060 * @return array The sanitized settings array
9061 */
9062 public static function mxchat_export_settings() {
9063 $options = get_option( 'mxchat_options', array() );
9064
9065 if ( ! is_array( $options ) ) {
9066 return array();
9067 }
9068
9069 // List of API key fields to mask
9070 $api_key_fields = array(
9071 'api_key',
9072 'xai_api_key',
9073 'claude_api_key',
9074 'deepseek_api_key',
9075 'voyage_api_key',
9076 'gemini_api_key',
9077 'openrouter_api_key',
9078 'loops_api_key',
9079 'brave_api_key',
9080 'live_agent_secret_key',
9081 'live_agent_bot_token',
9082 'telegram_bot_token',
9083 'telegram_webhook_secret',
9084 'woocommerce_consumer_key',
9085 'woocommerce_consumer_secret',
9086 );
9087
9088 // Mask API keys (show only last 4 characters)
9089 foreach ( $api_key_fields as $field ) {
9090 if ( isset( $options[ $field ] ) && ! empty( $options[ $field ] ) ) {
9091 $value = $options[ $field ];
9092 if ( strlen( $value ) > 4 ) {
9093 $options[ $field ] = str_repeat( '*', strlen( $value ) - 4 ) . substr( $value, -4 );
9094 } else {
9095 $options[ $field ] = '****';
9096 }
9097 }
9098 }
9099
9100 // Add metadata
9101 $export = array(
9102 'plugin_version' => defined( 'MXCHAT_VERSION' ) ? MXCHAT_VERSION : 'unknown',
9103 'export_date' => current_time( 'Y-m-d H:i:s' ),
9104 'wordpress_version' => get_bloginfo( 'version' ),
9105 'php_version' => phpversion(),
9106 'settings' => $options,
9107 );
9108
9109 return $export;
9110 }
9111
9112 /**
9113 * Reset all settings to defaults
9114 *
9115 * @return bool Whether the reset was successful
9116 */
9117 public static function mxchat_reset_all_settings() {
9118 // Delete main options
9119 $deleted = delete_option( 'mxchat_options' );
9120
9121 // Also clear the debug log
9122 delete_option( 'mxchat_debug_log' );
9123
9124 // Log the reset (will create new log since we just cleared it)
9125 // We need to temporarily enable debug mode to log this
9126 $temp_options = array( 'debug_mode' => 'on' );
9127 update_option( 'mxchat_options', $temp_options );
9128
9129 self::mxchat_log_debug( 'reset', 'All settings have been reset to defaults' );
9130
9131 // Now delete again to trigger re-initialization
9132 delete_option( 'mxchat_options' );
9133
9134 return $deleted;
9135 }
9136
9137 // Method to append the chatbot to the body
9138 public function mxchat_append_chatbot_to_body() {
9139 $options = get_option('mxchat_options');
9140 if (isset($options['append_to_body']) && $options['append_to_body'] === 'on') {
9141 echo do_shortcode('[mxchat_chatbot floating="yes"]');
9142 }
9143 }
9144
9145
9146
9147
9148
9149 private function mxchat_fetch_loops_mailing_lists($api_key) {
9150 $url = 'https://app.loops.so/api/v1/lists';
9151 $response = wp_remote_get($url, array(
9152 'headers' => array(
9153 'Authorization' => 'Bearer ' . $api_key,
9154 'Content-Type' => 'application/json'
9155 )
9156 ));
9157
9158 if (is_wp_error($response)) {
9159 return array();
9160 }
9161
9162 $body = wp_remote_retrieve_body($response);
9163 $lists = json_decode($body, true);
9164
9165 return isset($lists) && is_array($lists) ? $lists : array();
9166 }
9167
9168 function mxchat_calculate_cosine_similarity($vec1, $vec2) {
9169 if (empty($vec1) || empty($vec2)) {
9170 return 0.0;
9171 }
9172
9173 $dot_product = 0.0;
9174 $norm_a = 0.0;
9175 $norm_b = 0.0;
9176
9177 for ($i = 0; $i < count($vec1); $i++) {
9178 $dot_product += $vec1[$i] * $vec2[$i];
9179 $norm_a += pow($vec1[$i], 2);
9180 $norm_b += pow($vec2[$i], 2);
9181 }
9182
9183 if ($norm_a == 0.0 || $norm_b == 0.0) {
9184 return 0.0;
9185 } else {
9186 return $dot_product / (sqrt($norm_a) * sqrt($norm_b));
9187 }
9188 }
9189
9190 /**
9191 * Validates nonce and deletes all chat prompts
9192 */
9193 public function mxchat_handle_delete_all_prompts() {
9194 //error_log('=== DELETE ALL DEBUG START ===');
9195
9196 // Verify nonce
9197 if (!isset($_POST['mxchat_delete_all_prompts_nonce']) || !wp_verify_nonce($_POST['mxchat_delete_all_prompts_nonce'], 'mxchat_delete_all_prompts_action')) {
9198 //error_log('DEBUG: Nonce verification failed');
9199 wp_die(__('Nonce verification failed.', 'mxchat'));
9200 }
9201
9202 // Check permissions
9203 if (!current_user_can('manage_options')) {
9204 //error_log('DEBUG: Permission check failed');
9205 wp_die(__('You do not have sufficient permissions to delete all prompts.', 'mxchat'));
9206 }
9207
9208 // Get bot_id and content type filter from POST data
9209 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
9210 $content_type_filter = isset($_POST['content_type_filter']) ? sanitize_text_field($_POST['content_type_filter']) : '';
9211
9212 $success = true;
9213 $error_messages = array();
9214
9215 // Get bot-specific Pinecone configuration
9216 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
9217 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
9218
9219 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
9220
9221 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
9222 // Delete from Pinecone (with optional content type filter)
9223 $result = $pinecone_manager->mxchat_delete_all_from_pinecone($pinecone_options, $content_type_filter);
9224
9225 if (!$result['success']) {
9226 $success = false;
9227 $error_messages[] = $result['message'];
9228 }
9229
9230 } else {
9231 // Delete from WordPress database
9232 global $wpdb;
9233 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
9234
9235 // Build WHERE conditions
9236 $where_clauses = array();
9237 $where_values = array();
9238
9239 // Bot filter
9240 if ($bot_id !== 'default' && class_exists('MxChat_Multi_Bot_Manager')) {
9241 $where_clauses[] = 'bot_id = %s';
9242 $where_values[] = $bot_id;
9243 }
9244
9245 // Content type filter
9246 if (!empty($content_type_filter)) {
9247 $where_clauses[] = 'content_type = %s';
9248 $where_values[] = $content_type_filter;
9249 }
9250
9251 if (!empty($where_clauses)) {
9252 $where_sql = implode(' AND ', $where_clauses);
9253 $result = $wpdb->query($wpdb->prepare("DELETE FROM {$table_name} WHERE {$where_sql}", $where_values));
9254 } else {
9255 // No filters — delete all
9256 $result = $wpdb->query("DELETE FROM {$table_name}");
9257 }
9258
9259 if ($result === false) {
9260 $success = false;
9261 $error_messages[] = 'Failed to delete from WordPress database';
9262 }
9263 }
9264
9265 // Redirect back with a success message and bot_id
9266 $redirect_url = add_query_arg(array(
9267 'page' => 'mxchat-prompts',
9268 'bot_id' => $bot_id,
9269 'all_deleted' => $success ? 'true' : 'false'
9270 ), admin_url('admin.php'));
9271
9272 wp_safe_redirect($redirect_url);
9273 exit;
9274 }
9275
9276
9277
9278 /**
9279 * Handles deletion prompt with nonce validation
9280 */
9281 public function mxchat_handle_delete_prompt() {
9282 // Sanitize and validate nonce
9283 $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : '';
9284 if (empty($nonce) || !wp_verify_nonce($nonce, 'mxchat_delete_prompt_nonce')) {
9285 wp_die(esc_html__('Nonce verification failed.', 'mxchat'));
9286 }
9287
9288 // Check permissions
9289 if (!current_user_can('manage_options')) {
9290 wp_die(esc_html__('You do not have sufficient permissions to delete prompts.', 'mxchat'));
9291 }
9292
9293 // Get ID and source parameters
9294 $id = isset($_GET['id']) ? sanitize_text_field($_GET['id']) : '';
9295 $source = isset($_GET['source']) ? sanitize_text_field($_GET['source']) : '';
9296
9297 if (empty($id)) {
9298 wp_die(esc_html__('Invalid prompt ID.', 'mxchat'));
9299 }
9300
9301 $success = false;
9302 $error_message = '';
9303
9304 // Check if Pinecone is enabled and determine source automatically if not specified
9305 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
9306 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
9307
9308 // If source is not specified, determine based on Pinecone configuration
9309 if (empty($source)) {
9310 $source = $use_pinecone ? 'pinecone' : 'wordpress';
9311 }
9312
9313 if ($source === 'pinecone' || $use_pinecone) {
9314 //error_log('[MXCHAT-DELETE] Deleting from Pinecone, ID: ' . $id);
9315
9316 // Handle Pinecone deletion
9317 if (empty($pinecone_options['mxchat_pinecone_host']) ||
9318 empty($pinecone_options['mxchat_pinecone_api_key'])) {
9319 wp_die(esc_html__('Pinecone configuration is missing.', 'mxchat'));
9320 }
9321
9322 // Delete from Pinecone using the vector ID directly
9323 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
9324 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
9325 $id,
9326 $pinecone_options['mxchat_pinecone_api_key'],
9327 $pinecone_options['mxchat_pinecone_host']
9328 );
9329 if ($result['success']) {
9330 $success = true;
9331
9332 // Remove from vector cache
9333 $pinecone_manager->mxchat_remove_from_pinecone_vector_cache($id);
9334 $pinecone_manager->mxchat_remove_from_processed_content_caches($id);
9335
9336 set_transient('mxchat_admin_notice_success',
9337 esc_html__('Vector deleted successfully from Pinecone.', 'mxchat'), 30);
9338 } else {
9339 $error_message = $result['message'];
9340 set_transient('mxchat_admin_notice_error',
9341 esc_html__('Failed to delete from Pinecone: ', 'mxchat') . esc_html($error_message), 30);
9342 }
9343 } else {
9344 //error_log('[MXCHAT-DELETE] Deleting from WordPress database, ID: ' . $id);
9345
9346 // Handle WordPress database deletion
9347 global $wpdb;
9348 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
9349
9350 // Clear cache and delete prompt
9351 wp_cache_delete('prompt_' . $id, 'mxchat_prompts');
9352
9353 $result = $wpdb->delete(
9354 $table_name,
9355 array('id' => intval($id)),
9356 array('%d')
9357 );
9358
9359 if ($result !== false) {
9360 $success = true;
9361 set_transient('mxchat_admin_notice_success',
9362 esc_html__('Entry deleted successfully.', 'mxchat'), 30);
9363 } else {
9364 set_transient('mxchat_admin_notice_error',
9365 esc_html__('Failed to delete entry from database.', 'mxchat'), 30);
9366 }
9367 }
9368
9369 // Redirect back to the prompts page
9370 wp_safe_redirect(add_query_arg(
9371 array(
9372 'page' => 'mxchat-prompts',
9373 'deleted' => $success ? 'true' : 'false'
9374 ),
9375 admin_url('admin.php')
9376 ));
9377 exit;
9378 }
9379
9380
9381
9382
9383 /**
9384 * Averages multiple vectors into a single vector
9385 */
9386 private function mxchat_average_vectors($vectors) {
9387 $vector_length = count($vectors[0]);
9388 $sum_vector = array_fill(0, $vector_length, 0);
9389
9390 foreach ($vectors as $vector) {
9391 for ($i = 0; $i < $vector_length; $i++) {
9392 $sum_vector[$i] += $vector[$i];
9393 }
9394 }
9395
9396 // Divide each component by the number of vectors to get the average
9397 $num_vectors = count($vectors);
9398 for ($i = 0; $i < $vector_length; $i++) {
9399 $sum_vector[$i] /= $num_vectors;
9400 }
9401
9402 return $sum_vector;
9403 }
9404
9405
9406
9407
9408 /**
9409 * Generates embeddings from input text for MXChat
9410 */
9411 public function mxchat_generate_embedding($text) {
9412 // Enable detailed logging for debugging
9413 //error_log('[MXCHAT-EMBED] Starting embedding generation. Text length: ' . strlen($text) . ' bytes');
9414 //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
9415
9416 $options = get_option('mxchat_options');
9417 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
9418 //error_log('[MXCHAT-EMBED] Selected embedding model: ' . $selected_model);
9419
9420 // Determine provider and endpoint
9421 if (strpos($selected_model, 'voyage') === 0) {
9422 $api_key = $options['voyage_api_key'] ?? '';
9423 $endpoint = 'https://api.voyageai.com/v1/embeddings';
9424 $provider_name = 'Voyage AI';
9425 //error_log('[MXCHAT-EMBED] Using Voyage AI API');
9426 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
9427 $api_key = $options['gemini_api_key'] ?? '';
9428 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
9429 $provider_name = 'Google Gemini';
9430 //error_log('[MXCHAT-EMBED] Using Google Gemini API');
9431 } else {
9432 $api_key = $options['api_key'] ?? '';
9433 $endpoint = 'https://api.openai.com/v1/embeddings';
9434 $provider_name = 'OpenAI';
9435 //error_log('[MXCHAT-EMBED] Using OpenAI API');
9436 }
9437
9438 //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
9439
9440 if (empty($api_key)) {
9441 $error_message = sprintf('Missing %s API key. Please configure your API key in the MxChat settings.', $provider_name);
9442 //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
9443 return $error_message;
9444 }
9445
9446 // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
9447 $estimated_tokens = ceil(str_word_count($text) / 0.75);
9448 //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
9449
9450 if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
9451 //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
9452 // Consider truncating text here
9453 }
9454
9455 // Prepare request body based on provider
9456 if (strpos($selected_model, 'gemini-embedding') === 0) {
9457 // Gemini API format
9458 $request_body = array(
9459 'model' => 'models/' . $selected_model,
9460 'content' => array(
9461 'parts' => array(
9462 array('text' => $text)
9463 )
9464 )
9465 );
9466
9467 // Set output dimensionality to 1536 for consistency with other models
9468 $request_body['outputDimensionality'] = 1536;
9469 } else {
9470 // OpenAI/Voyage API format
9471 $request_body = array(
9472 'model' => $selected_model,
9473 'input' => $text
9474 );
9475
9476 // Add output_dimension for voyage-3-large model
9477 if ($selected_model === 'voyage-3-large') {
9478 $request_body['output_dimension'] = 2048;
9479 }
9480 }
9481
9482 //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
9483
9484 // Prepare headers based on provider
9485 if (strpos($selected_model, 'gemini-embedding') === 0) {
9486 // Gemini uses API key as query parameter
9487 $endpoint .= '?key=' . $api_key;
9488 $headers = array(
9489 'Content-Type' => 'application/json'
9490 );
9491 } else {
9492 // OpenAI/Voyage use Bearer token
9493 $headers = array(
9494 'Authorization' => 'Bearer ' . $api_key,
9495 'Content-Type' => 'application/json'
9496 );
9497 }
9498
9499 // Make API request
9500 //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
9501 $response = wp_remote_post($endpoint, array(
9502 'body' => wp_json_encode($request_body),
9503 'headers' => $headers,
9504 'timeout' => 60 // Increased timeout for large inputs
9505 ));
9506
9507 // Handle wp_remote_post errors
9508 if (is_wp_error($response)) {
9509 $error_message = $response->get_error_message();
9510 //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
9511 return 'Connection error: ' . $error_message;
9512 }
9513
9514 // Get and check HTTP response code
9515 $http_code = wp_remote_retrieve_response_code($response);
9516 //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
9517
9518 if ($http_code !== 200) {
9519 $error_body = wp_remote_retrieve_body($response);
9520 //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
9521
9522 // Try to parse error for more details
9523 $error_json = json_decode($error_body, true);
9524 if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
9525 $error_type = $error_json['error']['type'] ?? 'unknown';
9526 $error_message = $error_json['error']['message'] ?? 'No message';
9527 //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
9528 //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
9529
9530 // Customize error message for common API errors
9531 if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
9532 $error_message = sprintf('Invalid %s API key. Please check your API key in the MxChat settings.', $provider_name);
9533 } elseif ($error_type === 'authentication_error') {
9534 $error_message = sprintf('%s authentication failed. Please verify your API key in the MxChat settings.', $provider_name);
9535 }
9536
9537 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
9538 return $error_message;
9539 }
9540
9541 $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding", $http_code);
9542 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
9543 return $error_message;
9544 }
9545
9546 // Parse response body
9547 $response_body = wp_remote_retrieve_body($response);
9548 //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
9549
9550 $response_data = json_decode($response_body, true);
9551
9552 if (json_last_error() !== JSON_ERROR_NONE) {
9553 $error = json_last_error_msg();
9554 //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
9555 //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
9556 return "Failed to parse API response: $error";
9557 }
9558
9559 // Handle different response formats based on provider
9560 if (strpos($selected_model, 'gemini-embedding') === 0) {
9561 // Gemini API response format
9562 if (isset($response_data['embedding']['values'])) {
9563 $embedding_dimensions = count($response_data['embedding']['values']);
9564 //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
9565
9566 // Check if embedding dimensions are as expected (should be 1536)
9567 if ($embedding_dimensions !== 1536) {
9568 //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
9569 }
9570
9571 MxChat_Utils::stamp_active_embedding_model($selected_model);
9572 return $response_data['embedding']['values'];
9573 } else {
9574 //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
9575 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
9576
9577 if (isset($response_data['error'])) {
9578 $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
9579 //error_log('[MXCHAT-EMBED] ' . $error_message);
9580 return $error_message;
9581 }
9582
9583 $error_message = "Invalid Gemini API response format: No embedding found";
9584 //error_log('[MXCHAT-EMBED] ' . $error_message);
9585 return $error_message;
9586 }
9587 } else {
9588 // OpenAI/Voyage API response format
9589 if (isset($response_data['data'][0]['embedding'])) {
9590 $embedding_dimensions = count($response_data['data'][0]['embedding']);
9591 //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
9592
9593 // Check if embedding dimensions are as expected
9594 if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
9595 ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
9596 //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
9597 }
9598
9599 MxChat_Utils::stamp_active_embedding_model($selected_model);
9600 return $response_data['data'][0]['embedding'];
9601 } else {
9602 //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
9603 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
9604
9605 if (isset($response_data['error'])) {
9606 $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
9607 //error_log('[MXCHAT-EMBED] ' . $error_message);
9608 return $error_message;
9609 }
9610
9611 $error_message = "Invalid API response format: No embedding found";
9612 //error_log('[MXCHAT-EMBED] ' . $error_message);
9613 return $error_message;
9614 }
9615 }
9616 }
9617
9618
9619
9620 }
9621 ?>
9622