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

9,628 lines 393.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit; // Exit if accessed directly
4 }
5
6 class MxChat_Admin {
7 private $options;
8 private $chat_count;
9 private $is_activated;
10 private $knowledge_manager;
11
12 /**
13 * Get whether the license is activated
14 * @return bool
15 */
16 public function is_activated() {
17 return $this->is_activated;
18 }
19
20 /**
21 * Get plugin options
22 * @return array
23 */
24 public function get_options() {
25 return $this->options;
26 }
27
28 public function __construct($knowledge_manager = null) {
29 $this->options = get_option('mxchat_options');
30 $this->chat_count = get_option('mxchat_chat_count', 0);
31 $this->is_activated = $this->is_license_active();
32 $this->knowledge_manager = $knowledge_manager;
33
34 // Initialize default options if they are not set
35 if (!$this->options) {
36 $this->initialize_default_options();
37 }
38
39 // Add admin menu and initialize settings
40 add_action('admin_menu', array($this, 'mxchat_add_plugin_page'));
41 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 'embedding_model' => MxChat_Utils::get_active_embedding_model()
4599 ),
4600 array('id' => $intent_id),
4601 array('%s', '%s', '%s', '%f', '%s', '%s'), // Format: string, string, string, float, string, string
4602 array('%d') // Where format: integer
4603 );
4604
4605 if (false === $result) {
4606 $this->handle_embedding_error(__('Failed to update action in database.', 'mxchat'));
4607 return;
4608 }
4609
4610 // Set success message and redirect
4611 set_transient('mxchat_admin_notice_success', __('Intent updated successfully!', 'mxchat'), 60);
4612
4613 $redirect_url = add_query_arg(
4614 array(
4615 'page' => 'mxchat-actions'
4616 ),
4617 admin_url('admin.php')
4618 );
4619 wp_safe_redirect($redirect_url);
4620 exit;
4621 }
4622 public function mxchat_handle_add_intent() {
4623 if (!current_user_can('manage_options')) {
4624 wp_die(esc_html__('Unauthorized user', 'mxchat'));
4625 }
4626 check_admin_referer('mxchat_add_intent_nonce');
4627 global $wpdb;
4628 $table_name = $wpdb->prefix . 'mxchat_intents';
4629
4630 // Sanitize and get form data
4631 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
4632 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
4633 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
4634
4635 // Get similarity threshold from form (convert percentage to decimal)
4636 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
4637
4638 // Handle enabled_bots
4639 $enabled_bots = isset($_POST['enabled_bots']) ? $_POST['enabled_bots'] : array('default');
4640 $enabled_bots = array_map('sanitize_text_field', $enabled_bots);
4641
4642 // Ensure default is always included for backward compatibility with existing actions
4643 if (!in_array('default', $enabled_bots)) {
4644 $enabled_bots[] = 'default';
4645 }
4646
4647 $enabled_bots_json = json_encode($enabled_bots);
4648
4649 // Validate required fields
4650 if (empty($intent_label) || empty($callback_function) || empty($phrases_input)) {
4651 $this->handle_embedding_error(__('Invalid input. Please ensure all fields are filled out.', 'mxchat'));
4652 return;
4653 }
4654
4655 // Validate callback function
4656 $available_callbacks = $this->mxchat_get_available_callbacks();
4657 if (!array_key_exists($callback_function, $available_callbacks)) {
4658 $this->handle_embedding_error(__('Invalid callback function selected.', 'mxchat'));
4659 return;
4660 }
4661
4662 // Check if this is an add-on promotional placeholder (not a real action)
4663 if (!empty($available_callbacks[$callback_function]['addon_promo'])) {
4664 $addon_name = isset($available_callbacks[$callback_function]['addon_name']) ? $available_callbacks[$callback_function]['addon_name'] : __('an add-on', 'mxchat');
4665 $this->handle_embedding_error(sprintf(
4666 __('This action requires the %s to be installed and activated.', 'mxchat'),
4667 $addon_name
4668 ));
4669 return;
4670 }
4671
4672 // Process phrases
4673 $phrases_array = array_map('sanitize_text_field', array_filter(array_map('trim', explode(',', $phrases_input))));
4674 if (empty($phrases_array)) {
4675 $this->handle_embedding_error(__('Please enter at least one valid phrase.', 'mxchat'));
4676 return;
4677 }
4678
4679 // Generate embeddings with improved error handling
4680 $vectors = [];
4681 $failed_phrases = [];
4682 foreach ($phrases_array as $phrase) {
4683 $embedding_vector = $this->mxchat_generate_embedding($phrase, $this->options['api_key']);
4684 if (is_array($embedding_vector)) {
4685 $vectors[] = $embedding_vector;
4686 } else {
4687 $failed_phrases[] = $phrase;
4688 }
4689 }
4690
4691 // Check for embedding failures
4692 if (!empty($failed_phrases)) {
4693 $this->handle_embedding_error(
4694 sprintf(
4695 __('Error generating embeddings for phrases: %s. Check your embedding API.', 'mxchat'),
4696 implode(', ', $failed_phrases)
4697 )
4698 );
4699 return;
4700 }
4701
4702 if (empty($vectors)) {
4703 $this->handle_embedding_error(__('No valid embeddings generated. Please check your phrases.', 'mxchat'));
4704 return;
4705 }
4706
4707 // Create combined vector and insert into database
4708 $combined_vector = $this->mxchat_average_vectors($vectors);
4709 $serialized_vector = maybe_serialize($combined_vector);
4710
4711 $result = $wpdb->insert($table_name, [
4712 'intent_label' => $intent_label,
4713 'phrases' => implode(', ', $phrases_array),
4714 'embedding_vector' => $serialized_vector,
4715 'callback_function' => $callback_function,
4716 'similarity_threshold' => $similarity_threshold,
4717 'enabled_bots' => $enabled_bots_json, // NEW field
4718 'embedding_model' => MxChat_Utils::get_active_embedding_model()
4719 ]);
4720
4721 if ($result === false) {
4722 $this->handle_embedding_error(__('Database error: ', 'mxchat') . $wpdb->last_error);
4723 return;
4724 }
4725
4726 // Set success message and redirect
4727 set_transient('mxchat_admin_notice_success', __('New intent added successfully!', 'mxchat'), 60);
4728 wp_safe_redirect(admin_url('admin.php?page=mxchat-actions'));
4729 exit;
4730 }
4731
4732
4733
4734
4735 private function handle_embedding_error($message, $redirect = true) {
4736 // Store the error message in the existing transient
4737 set_transient('mxchat_admin_notice_error', $message, 60);
4738
4739 if ($redirect) {
4740 // Redirect back to the actions page
4741 $redirect_url = add_query_arg(
4742 array(
4743 'page' => 'mxchat-actions'
4744 ),
4745 admin_url('admin.php')
4746 );
4747 wp_safe_redirect($redirect_url);
4748 exit;
4749 }
4750 }
4751
4752 /**
4753 * AJAX handler to fetch actions list for the new split-panel UI
4754 */
4755 public function mxchat_fetch_actions_list() {
4756 check_ajax_referer('mxchat_actions_nonce', 'security');
4757
4758 if (!current_user_can('manage_options')) {
4759 wp_send_json_error(__('Unauthorized', 'mxchat'));
4760 }
4761
4762 global $wpdb;
4763 $table_name = $wpdb->prefix . 'mxchat_intents';
4764
4765 $page = isset($_POST['page']) ? max(1, intval($_POST['page'])) : 1;
4766 $per_page = isset($_POST['per_page']) ? min(100, max(1, intval($_POST['per_page']))) : 50;
4767 $offset = ($page - 1) * $per_page;
4768 $search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
4769 $callback_filter = isset($_POST['callback_filter']) ? sanitize_text_field($_POST['callback_filter']) : '';
4770 $sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] === 'asc' ? 'ASC' : 'DESC';
4771
4772 // Build WHERE clause
4773 $where = '1=1';
4774 $params = array();
4775
4776 if ($search) {
4777 $search_like = '%' . $wpdb->esc_like($search) . '%';
4778 $where .= ' AND (intent_label LIKE %s OR phrases LIKE %s)';
4779 $params[] = $search_like;
4780 $params[] = $search_like;
4781 }
4782
4783 if ($callback_filter) {
4784 $where .= ' AND callback_function = %s';
4785 $params[] = $callback_filter;
4786 }
4787
4788 // Get total count
4789 $count_query = "SELECT COUNT(*) FROM $table_name WHERE $where";
4790 if (!empty($params)) {
4791 $count_query = $wpdb->prepare($count_query, $params);
4792 }
4793 $total_actions = $wpdb->get_var($count_query);
4794
4795 // Get actions
4796 $query = "SELECT * FROM $table_name WHERE $where ORDER BY id $sort_order LIMIT %d OFFSET %d";
4797 $all_params = array_merge($params, array($per_page, $offset));
4798 $actions = $wpdb->get_results($wpdb->prepare($query, $all_params));
4799
4800 // Get available callbacks for labels/icons
4801 $available_callbacks = $this->mxchat_get_available_callbacks();
4802
4803 // Prefetch individual phrase counts for all fetched actions
4804 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
4805 $phrase_counts = array();
4806 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
4807 $action_ids = wp_list_pluck($actions, 'id');
4808 if (!empty($action_ids)) {
4809 $id_placeholders = implode(',', array_fill(0, count($action_ids), '%d'));
4810 $count_results = $wpdb->get_results($wpdb->prepare(
4811 "SELECT intent_id, COUNT(*) as cnt FROM $phrases_table WHERE intent_id IN ($id_placeholders) GROUP BY intent_id",
4812 $action_ids
4813 ));
4814 foreach ($count_results as $row) {
4815 $phrase_counts[$row->intent_id] = intval($row->cnt);
4816 }
4817 }
4818 }
4819
4820 // Format actions for response
4821 $formatted_actions = array();
4822 foreach ($actions as $action) {
4823 $callback_data = isset($available_callbacks[$action->callback_function])
4824 ? $available_callbacks[$action->callback_function]
4825 : array('label' => $action->callback_function, 'icon' => 'admin-generic');
4826
4827 $enabled_bots = json_decode($action->enabled_bots, true);
4828 if (!is_array($enabled_bots)) {
4829 $enabled_bots = array('default');
4830 }
4831
4832 $formatted_actions[] = array(
4833 'id' => intval($action->id),
4834 'label' => $action->intent_label,
4835 'phrases' => $action->phrases,
4836 'callback_function' => $action->callback_function,
4837 'callback_label' => $callback_data['label'],
4838 'icon' => isset($callback_data['icon']) ? $callback_data['icon'] : 'admin-generic',
4839 'threshold' => round($action->similarity_threshold * 100),
4840 'enabled' => (bool) $action->enabled,
4841 'enabled_bots' => $enabled_bots,
4842 'has_legacy_vector' => !empty($action->embedding_vector),
4843 'individual_phrase_count' => isset($phrase_counts[$action->id]) ? $phrase_counts[$action->id] : 0,
4844 );
4845 }
4846
4847 $total_pages = ceil($total_actions / $per_page);
4848 $showing_start = $total_actions > 0 ? $offset + 1 : 0;
4849 $showing_end = min($offset + $per_page, $total_actions);
4850
4851 wp_send_json_success(array(
4852 'actions' => $formatted_actions,
4853 'page' => $page,
4854 'per_page' => $per_page,
4855 'total_actions' => intval($total_actions),
4856 'total_pages' => $total_pages,
4857 'showing_start' => $showing_start,
4858 'showing_end' => $showing_end,
4859 ));
4860 }
4861
4862 /**
4863 * AJAX handler to toggle action enabled status
4864 */
4865 public function mxchat_toggle_action_status() {
4866 check_ajax_referer('mxchat_actions_nonce', 'security');
4867
4868 if (!current_user_can('manage_options')) {
4869 wp_send_json_error(__('Unauthorized', 'mxchat'));
4870 }
4871
4872 $action_id = isset($_POST['action_id']) ? intval($_POST['action_id']) : 0;
4873 $enabled = isset($_POST['enabled']) ? intval($_POST['enabled']) : 0;
4874
4875 if (!$action_id) {
4876 wp_send_json_error(__('Invalid action ID', 'mxchat'));
4877 }
4878
4879 global $wpdb;
4880 $table_name = $wpdb->prefix . 'mxchat_intents';
4881
4882 $result = $wpdb->update(
4883 $table_name,
4884 array('enabled' => $enabled ? 1 : 0),
4885 array('id' => $action_id),
4886 array('%d'),
4887 array('%d')
4888 );
4889
4890 if ($result === false) {
4891 wp_send_json_error(__('Failed to update action status', 'mxchat'));
4892 }
4893
4894 wp_send_json_success(array('enabled' => (bool) $enabled));
4895 }
4896
4897 /**
4898 * AJAX handler to bulk delete actions
4899 */
4900 public function mxchat_bulk_delete_actions() {
4901 check_ajax_referer('mxchat_delete_intent_nonce', 'security');
4902
4903 if (!current_user_can('manage_options')) {
4904 wp_send_json_error(__('Unauthorized', 'mxchat'));
4905 }
4906
4907 $action_ids = isset($_POST['action_ids']) ? array_map('intval', (array) $_POST['action_ids']) : array();
4908
4909 if (empty($action_ids)) {
4910 wp_send_json_error(__('No actions selected', 'mxchat'));
4911 }
4912
4913 global $wpdb;
4914 $table_name = $wpdb->prefix . 'mxchat_intents';
4915
4916 $placeholders = implode(',', array_fill(0, count($action_ids), '%d'));
4917 $query = $wpdb->prepare("DELETE FROM $table_name WHERE id IN ($placeholders)", $action_ids);
4918 $result = $wpdb->query($query);
4919
4920 if ($result === false) {
4921 wp_send_json_error(__('Failed to delete actions', 'mxchat'));
4922 }
4923
4924 // Also delete individual phrases for these intents
4925 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
4926 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
4927 $wpdb->query($wpdb->prepare("DELETE FROM $phrases_table WHERE intent_id IN ($placeholders)", $action_ids));
4928 }
4929
4930 wp_send_json_success(array('deleted' => $result));
4931 }
4932
4933 /**
4934 * AJAX handler to add a new intent/action
4935 */
4936 public function mxchat_add_intent_ajax() {
4937 check_ajax_referer('mxchat_add_intent_nonce', 'security');
4938
4939 if (!current_user_can('manage_options')) {
4940 wp_send_json_error(__('Unauthorized', 'mxchat'));
4941 }
4942
4943 global $wpdb;
4944 $table_name = $wpdb->prefix . 'mxchat_intents';
4945
4946 // Sanitize input
4947 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
4948 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
4949 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
4950 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
4951 $enabled_bots = isset($_POST['enabled_bots']) ? array_map('sanitize_text_field', (array) $_POST['enabled_bots']) : array('default');
4952
4953 // Validate
4954 // Check if using new individual phrases mode or legacy mode
4955 $individual_phrases = isset($_POST['individual_phrases']) ? array_filter(array_map('sanitize_text_field', (array) $_POST['individual_phrases'])) : array();
4956 $use_individual = !empty($individual_phrases);
4957
4958 // Validate required fields (phrases not required when using individual mode)
4959 if (empty($intent_label) || empty($callback_function)) {
4960 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
4961 }
4962 if (!$use_individual && empty($phrases_input)) {
4963 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
4964 }
4965
4966 // Ensure default bot is included
4967 if (!in_array('default', $enabled_bots)) {
4968 $enabled_bots[] = 'default';
4969 }
4970 $enabled_bots_json = json_encode($enabled_bots);
4971
4972 if ($use_individual) {
4973 // New mode: individual phrases each get their own vector
4974 // Insert the intent row with empty legacy fields
4975 $result = $wpdb->insert(
4976 $table_name,
4977 array(
4978 'intent_label' => $intent_label,
4979 'phrases' => '',
4980 'embedding_vector' => '',
4981 'similarity_threshold' => $similarity_threshold,
4982 'callback_function' => $callback_function,
4983 'enabled' => 1,
4984 'enabled_bots' => $enabled_bots_json,
4985 )
4986 );
4987
4988 if ($result === false) {
4989 wp_send_json_error(__('Failed to add action to database.', 'mxchat'));
4990 }
4991
4992 $intent_id = $wpdb->insert_id;
4993
4994 // Insert each phrase individually with its own embedding
4995 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
4996 $failed_phrases = array();
4997 foreach ($individual_phrases as $phrase) {
4998 $phrase = trim($phrase);
4999 if (empty($phrase)) continue;
5000
5001 $embedding_vector = $this->mxchat_generate_embedding($phrase);
5002 if (is_wp_error($embedding_vector)) {
5003 $failed_phrases[] = $phrase;
5004 continue;
5005 }
5006
5007 $wpdb->insert(
5008 $phrases_table,
5009 array(
5010 'intent_id' => $intent_id,
5011 'phrase' => $phrase,
5012 'embedding_vector' => maybe_serialize($embedding_vector),
5013 'embedding_model' => MxChat_Utils::get_active_embedding_model(),
5014 )
5015 );
5016 }
5017
5018 $response = array('id' => $intent_id);
5019 if (!empty($failed_phrases)) {
5020 $response['failed_phrases'] = $failed_phrases;
5021 }
5022 wp_send_json_success($response);
5023
5024 } else {
5025 // Legacy mode: combine all phrases into one embedding (backwards compatible)
5026 $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
5027 if (empty($phrases_array)) {
5028 wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
5029 }
5030
5031 // Generate embedding (combine phrases into single string for embedding)
5032 $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
5033 if (is_wp_error($embedding_vector)) {
5034 // Fallback: store without embedding
5035 $serialized_vector = null;
5036 } else {
5037 $serialized_vector = maybe_serialize($embedding_vector);
5038 }
5039
5040 // Insert
5041 $result = $wpdb->insert(
5042 $table_name,
5043 array(
5044 'intent_label' => $intent_label,
5045 'phrases' => implode(', ', $phrases_array),
5046 'embedding_vector' => $serialized_vector,
5047 'similarity_threshold' => $similarity_threshold,
5048 'callback_function' => $callback_function,
5049 'enabled' => 1,
5050 'enabled_bots' => $enabled_bots_json,
5051 'embedding_model' => MxChat_Utils::get_active_embedding_model(),
5052 )
5053 );
5054
5055 if ($result === false) {
5056 wp_send_json_error(__('Failed to add action to database.', 'mxchat'));
5057 }
5058
5059 wp_send_json_success(array('id' => $wpdb->insert_id));
5060 }
5061 }
5062
5063 /**
5064 * AJAX handler to edit an existing intent/action
5065 */
5066 public function mxchat_edit_intent_ajax() {
5067 check_ajax_referer('mxchat_edit_intent', 'security');
5068
5069 if (!current_user_can('manage_options')) {
5070 wp_send_json_error(__('Unauthorized', 'mxchat'));
5071 }
5072
5073 global $wpdb;
5074 $table_name = $wpdb->prefix . 'mxchat_intents';
5075
5076 // Sanitize input
5077 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5078 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
5079 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
5080 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
5081 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
5082 $enabled_bots = isset($_POST['enabled_bots']) ? array_map('sanitize_text_field', (array) $_POST['enabled_bots']) : array('default');
5083
5084 // Validate
5085 if (!$intent_id || empty($intent_label)) {
5086 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
5087 }
5088
5089 // Check if phrases are managed individually (empty phrases_input means individual mode)
5090 $uses_individual_phrases = empty($phrases_input);
5091
5092 if ($uses_individual_phrases) {
5093 // Individual phrase mode: only update non-phrase fields, skip embedding regeneration
5094 $update_data = array(
5095 'intent_label' => $intent_label,
5096 'similarity_threshold' => $similarity_threshold,
5097 'callback_function' => $callback_function,
5098 'enabled_bots' => json_encode($enabled_bots),
5099 );
5100 } else {
5101 // Legacy mode: process phrases and regenerate embedding (backwards compatible for add-ons)
5102 $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
5103 if (empty($phrases_array)) {
5104 wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
5105 }
5106
5107 // Generate new embedding (combine phrases into single string for embedding)
5108 $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
5109 if (is_wp_error($embedding_vector)) {
5110 // Keep existing embedding
5111 $update_data = array(
5112 'intent_label' => $intent_label,
5113 'phrases' => implode(', ', $phrases_array),
5114 'similarity_threshold' => $similarity_threshold,
5115 'callback_function' => $callback_function,
5116 'enabled_bots' => json_encode($enabled_bots),
5117 );
5118 } else {
5119 $serialized_vector = maybe_serialize($embedding_vector);
5120 $update_data = array(
5121 'intent_label' => $intent_label,
5122 'phrases' => implode(', ', $phrases_array),
5123 'embedding_vector' => $serialized_vector,
5124 'similarity_threshold' => $similarity_threshold,
5125 'callback_function' => $callback_function,
5126 'enabled_bots' => json_encode($enabled_bots),
5127 'embedding_model' => MxChat_Utils::get_active_embedding_model(),
5128 );
5129 }
5130 }
5131
5132 // Ensure default bot is included
5133 if (!in_array('default', $enabled_bots)) {
5134 $enabled_bots[] = 'default';
5135 }
5136 $update_data['enabled_bots'] = json_encode($enabled_bots);
5137
5138 $result = $wpdb->update(
5139 $table_name,
5140 $update_data,
5141 array('id' => $intent_id),
5142 null,
5143 array('%d')
5144 );
5145
5146 if ($result === false) {
5147 wp_send_json_error(__('Failed to update action.', 'mxchat'));
5148 }
5149
5150 wp_send_json_success(array('updated' => true));
5151 }
5152
5153 /**
5154 * AJAX handler to delete a single intent/action
5155 */
5156 public function mxchat_delete_intent_ajax() {
5157 check_ajax_referer('mxchat_delete_intent_nonce', 'security');
5158
5159 if (!current_user_can('manage_options')) {
5160 wp_send_json_error(__('Unauthorized', 'mxchat'));
5161 }
5162
5163 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5164
5165 if (!$intent_id) {
5166 wp_send_json_error(__('Invalid action ID', 'mxchat'));
5167 }
5168
5169 global $wpdb;
5170 $table_name = $wpdb->prefix . 'mxchat_intents';
5171
5172 $result = $wpdb->delete($table_name, array('id' => $intent_id), array('%d'));
5173
5174 if ($result === false) {
5175 wp_send_json_error(__('Failed to delete action', 'mxchat'));
5176 }
5177
5178 // Also delete individual phrases for this intent
5179 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5180 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5181 $wpdb->delete($phrases_table, array('intent_id' => $intent_id), array('%d'));
5182 }
5183
5184 wp_send_json_success(array('deleted' => true));
5185 }
5186
5187 /**
5188 * AJAX handler to add a single phrase with its own embedding to an intent
5189 */
5190 public function mxchat_add_phrase_ajax() {
5191 check_ajax_referer('mxchat_add_phrase_nonce', 'security');
5192
5193 if (!current_user_can('manage_options')) {
5194 wp_send_json_error(__('Unauthorized', 'mxchat'));
5195 }
5196
5197 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5198 $phrase = isset($_POST['phrase']) ? sanitize_text_field($_POST['phrase']) : '';
5199
5200 if (!$intent_id || empty($phrase)) {
5201 wp_send_json_error(__('Please provide an intent ID and phrase.', 'mxchat'));
5202 }
5203
5204 // Verify the intent exists
5205 global $wpdb;
5206 $intents_table = $wpdb->prefix . 'mxchat_intents';
5207 $intent = $wpdb->get_row($wpdb->prepare("SELECT id FROM $intents_table WHERE id = %d", $intent_id));
5208 if (!$intent) {
5209 wp_send_json_error(__('Action not found.', 'mxchat'));
5210 }
5211
5212 // Generate embedding for this single phrase
5213 $embedding_vector = $this->mxchat_generate_embedding($phrase);
5214 if (is_wp_error($embedding_vector)) {
5215 wp_send_json_error(__('Failed to generate embedding: ', 'mxchat') . $embedding_vector->get_error_message());
5216 }
5217
5218 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5219 $result = $wpdb->insert(
5220 $phrases_table,
5221 array(
5222 'intent_id' => $intent_id,
5223 'phrase' => $phrase,
5224 'embedding_vector' => maybe_serialize($embedding_vector),
5225 'embedding_model' => MxChat_Utils::get_active_embedding_model(),
5226 )
5227 );
5228
5229 if ($result === false) {
5230 wp_send_json_error(__('Failed to add phrase.', 'mxchat'));
5231 }
5232
5233 wp_send_json_success(array('id' => $wpdb->insert_id, 'phrase' => $phrase));
5234 }
5235
5236 /**
5237 * AJAX handler to delete a single phrase from wp_mxchat_intent_phrases
5238 */
5239 public function mxchat_delete_phrase_ajax() {
5240 check_ajax_referer('mxchat_delete_phrase_nonce', 'security');
5241
5242 if (!current_user_can('manage_options')) {
5243 wp_send_json_error(__('Unauthorized', 'mxchat'));
5244 }
5245
5246 $phrase_id = isset($_POST['phrase_id']) ? intval($_POST['phrase_id']) : 0;
5247 if (!$phrase_id) {
5248 wp_send_json_error(__('Invalid phrase ID.', 'mxchat'));
5249 }
5250
5251 global $wpdb;
5252 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5253 $result = $wpdb->delete($phrases_table, array('id' => $phrase_id), array('%d'));
5254
5255 if ($result === false) {
5256 wp_send_json_error(__('Failed to delete phrase.', 'mxchat'));
5257 }
5258
5259 wp_send_json_success(array('deleted' => true));
5260 }
5261
5262 /**
5263 * AJAX handler to fetch individual phrases for an intent
5264 */
5265 public function mxchat_get_phrases_ajax() {
5266 check_ajax_referer('mxchat_get_phrases_nonce', 'security');
5267
5268 if (!current_user_can('manage_options')) {
5269 wp_send_json_error(__('Unauthorized', 'mxchat'));
5270 }
5271
5272 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5273 if (!$intent_id) {
5274 wp_send_json_error(__('Invalid intent ID.', 'mxchat'));
5275 }
5276
5277 global $wpdb;
5278 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5279
5280 $phrases = array();
5281 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5282 $phrases = $wpdb->get_results($wpdb->prepare(
5283 "SELECT id, phrase, created_at FROM $phrases_table WHERE intent_id = %d ORDER BY created_at ASC",
5284 $intent_id
5285 ));
5286 }
5287
5288 wp_send_json_success(array('phrases' => $phrases));
5289 }
5290
5291 /**
5292 * AJAX handler to clear legacy phrases and embedding from the main intents table
5293 */
5294 public function mxchat_delete_legacy_phrases_ajax() {
5295 check_ajax_referer('mxchat_delete_legacy_nonce', 'security');
5296
5297 if (!current_user_can('manage_options')) {
5298 wp_send_json_error(__('Unauthorized', 'mxchat'));
5299 }
5300
5301 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5302 if (!$intent_id) {
5303 wp_send_json_error(__('Invalid intent ID.', 'mxchat'));
5304 }
5305
5306 global $wpdb;
5307 $table_name = $wpdb->prefix . 'mxchat_intents';
5308
5309 $result = $wpdb->update(
5310 $table_name,
5311 array('phrases' => '', 'embedding_vector' => ''),
5312 array('id' => $intent_id),
5313 array('%s', '%s'),
5314 array('%d')
5315 );
5316
5317 if ($result === false) {
5318 wp_send_json_error(__('Failed to clear legacy phrases.', 'mxchat'));
5319 }
5320
5321 wp_send_json_success(array('cleared' => true));
5322 }
5323
5324 /**
5325 * Enhanced get_available_callbacks function with form action exclusion
5326 *
5327 * @param bool $grouped Whether to return callbacks grouped by category
5328 * @param bool $include_all Whether to include all potential actions (even if add-on not installed)
5329 * @return array Callbacks data with icons, descriptions and availability status
5330 */
5331 private function mxchat_get_available_callbacks($grouped = false, $include_all = true) {
5332 // Load WordPress plugin functions if needed
5333 if (!function_exists('get_plugins')) {
5334 require_once ABSPATH . 'wp-admin/includes/plugin.php';
5335 }
5336
5337 // Get active plugins
5338 $active_plugins = get_option('active_plugins', array());
5339
5340 // Functions to exclude from the action selector only if Pro is activated
5341 // If user doesn't have Pro, show these so they can see what they're missing
5342 $excluded_when_pro_active_functions = array(
5343 'mxchat_handle_form_collection' // Forms add-on action
5344 );
5345
5346 // Always excluded functions (regardless of Pro status)
5347 $always_excluded_functions = array();
5348
5349 // Combine exclusion lists based on Pro activation status
5350 $excluded_functions = $always_excluded_functions;
5351 if ($this->is_activated) {
5352 // Only exclude add-on managed functions if Pro is active
5353 $excluded_functions = array_merge($excluded_functions, $excluded_when_pro_active_functions);
5354 }
5355
5356 // Define add-on plugin files and their corresponding action functions
5357 $addon_plugins = array(
5358 'mxchat-woo/mxchat-woo.php' => array(
5359 'functions' => array(
5360 'mxchat_handle_product_recommendations',
5361 'mxchat_handle_order_history',
5362 'mxchat_show_product_card',
5363 'mxchat_add_to_cart',
5364 'mxchat_checkout_redirect',
5365 'mxchat_handle_featured_products'
5366 ),
5367 'name' => __('WooCommerce Add-on', 'mxchat'),
5368 'pro_required' => true
5369 ),
5370 'mxchat-perplexity/mxchat-perplexity.php' => array(
5371 'functions' => array('mxchat_perplexity_research'),
5372 'name' => __('Perplexity Add-on', 'mxchat'),
5373 'pro_required' => true
5374 ),
5375 'mxchat-forms/mxchat-forms.php' => array(
5376 'functions' => array('mxchat_handle_form_collection'),
5377 'name' => __('Forms Add-on', 'mxchat'),
5378 'pro_required' => true
5379 ),
5380 // Add other add-ons and their functions here
5381 );
5382
5383 // Get the functions that are provided by active add-ons
5384 $addon_provided_functions = array();
5385 $addon_function_mapping = array(); // Maps functions to their add-on info
5386
5387 // Check which add-ons are active
5388 foreach ($addon_plugins as $plugin_file => $addon_info) {
5389 $is_active = in_array($plugin_file, $active_plugins);
5390
5391 // For each function in this addon
5392 foreach ($addon_info['functions'] as $function) {
5393 // Consider a function installed only if:
5394 // 1. The add-on is active AND
5395 // 2. Either it doesn't require Pro OR Pro is activated
5396 $is_installed = $is_active && (!$addon_info['pro_required'] || $this->is_activated);
5397
5398 // If the add-on is installed, mark this function as provided by an add-on
5399 if ($is_installed) {
5400 $addon_provided_functions[] = $function;
5401 }
5402
5403 // Store addon info for this function regardless of installation status
5404 $addon_function_mapping[$function] = array(
5405 'addon' => basename(dirname($plugin_file)),
5406 'addon_name' => $addon_info['name'],
5407 'pro_required' => $addon_info['pro_required'],
5408 'is_active' => $is_active,
5409 'is_installed' => $is_installed
5410 );
5411 }
5412 }
5413
5414 // Core callbacks - always available in the base plugin
5415 $core_callbacks = array(
5416 'mxchat_handle_email_capture' => array(
5417 'label' => __('Loops Email Capture', 'mxchat'),
5418 'pro_only' => false,
5419 'group' => __('Customer Engagement', 'mxchat'),
5420 'icon' => 'email-alt',
5421 'description' => __('Collect visitor emails for your mailing list in Loops', 'mxchat'),
5422 'addon' => false, // Not from an add-on
5423 'installed' => true // Always installed with base plugin
5424 ),
5425 'mxchat_handle_search_request' => array(
5426 'label' => __('Brave Web Search', 'mxchat'),
5427 'pro_only' => false,
5428 'group' => __('Search Features', 'mxchat'),
5429 'icon' => 'search',
5430 'description' => __('Let users search the web directly from the chat', 'mxchat'),
5431 'addon' => false,
5432 'installed' => true
5433 ),
5434 'mxchat_handle_image_search_request' => array(
5435 'label' => __('Brave Image Search', 'mxchat'),
5436 'pro_only' => false,
5437 'group' => __('Search Features', 'mxchat'),
5438 'icon' => 'format-image',
5439 'description' => __('Search and display images in the chat conversation', 'mxchat'),
5440 'addon' => false,
5441 'installed' => true
5442 ),
5443 // Pro core features - check is_activated property
5444 'mxchat_generate_image' => array(
5445 'label' => __('Generate Image (OpenAI)', 'mxchat'),
5446 'pro_only' => false,
5447 'group' => __('Other Features', 'mxchat'),
5448 'icon' => 'art',
5449 'description' => __('Create images with GPT Image from OpenAI (requires OpenAI API key)', 'mxchat'),
5450 'addon' => false,
5451 'installed' => true
5452 ),
5453 'mxchat_generate_gemini_image' => array(
5454 'label' => __('Generate Image (Gemini)', 'mxchat'),
5455 'pro_only' => false,
5456 'group' => __('Other Features', 'mxchat'),
5457 'icon' => 'art',
5458 'description' => __('Create images with Imagen from Google (requires Gemini API key)', 'mxchat'),
5459 'addon' => false,
5460 'installed' => true
5461 ),
5462 'mxchat_handle_pdf_discussion' => array(
5463 'label' => __('Chat with PDF', 'mxchat'),
5464 'pro_only' => false,
5465 'group' => __('Other Features', 'mxchat'),
5466 'icon' => 'media-document',
5467 'description' => __('Answer questions about uploaded PDF documents', 'mxchat'),
5468 'addon' => false,
5469 'installed' => true
5470 ),
5471 'mxchat_live_agent_handover' => array(
5472 'label' => __('Slack Live Agent', 'mxchat'),
5473 'pro_only' => false,
5474 'group' => __('Customer Engagement', 'mxchat'),
5475 'icon' => 'admin-users',
5476 'description' => __('Transfer conversation to a human support agent on Slack', 'mxchat'),
5477 'addon' => false,
5478 'installed' => true
5479 ),
5480 'mxchat_telegram_live_agent_handover' => array(
5481 'label' => __('Telegram Live Agent', 'mxchat'),
5482 'pro_only' => false,
5483 'group' => __('Customer Engagement', 'mxchat'),
5484 'icon' => 'format-chat',
5485 'description' => __('Transfer conversation to a human support agent on Telegram', 'mxchat'),
5486 'addon' => false,
5487 'installed' => true
5488 ),
5489 'mxchat_handle_switch_to_chatbot_intent' => array(
5490 'label' => __('Back to Chatbot', 'mxchat'),
5491 'pro_only' => false,
5492 'group' => __('Customer Engagement', 'mxchat'),
5493 'icon' => 'backup',
5494 'description' => __('Return from live agent mode to AI chatbot', 'mxchat'),
5495 'addon' => false,
5496 'installed' => true
5497 ),
5498 );
5499
5500 // Add-on callbacks with placeholders - only include if the add-on is NOT active
5501 // These are promotional/informational only — not selectable as real actions
5502 $addon_callbacks = array(
5503 // WooCommerce Add-on
5504 'mxchat_handle_product_recommendations' => array(
5505 'label' => __('Product Recommendations', 'mxchat'),
5506 'pro_only' => false,
5507 'addon_promo' => true,
5508 'group' => __('WooCommerce Features', 'mxchat'),
5509 'icon' => 'cart',
5510 'description' => __('Suggest products based on customer preferences', 'mxchat'),
5511 ),
5512 'mxchat_handle_order_history' => array(
5513 'label' => __('Order History', 'mxchat'),
5514 'pro_only' => false,
5515 'addon_promo' => true,
5516 'group' => __('WooCommerce Features', 'mxchat'),
5517 'icon' => 'clipboard',
5518 'description' => __('Allow customers to check their order status', 'mxchat'),
5519 ),
5520 'mxchat_show_product_card' => array(
5521 'label' => __('Show Product Card', 'mxchat'),
5522 'pro_only' => false,
5523 'addon_promo' => true,
5524 'group' => __('WooCommerce Features', 'mxchat'),
5525 'icon' => 'products',
5526 'description' => __('Display product information in the chat', 'mxchat'),
5527 ),
5528 'mxchat_add_to_cart' => array(
5529 'label' => __('Add to Cart', 'mxchat'),
5530 'pro_only' => false,
5531 'addon_promo' => true,
5532 'group' => __('WooCommerce Features', 'mxchat'),
5533 'icon' => 'plus-alt',
5534 'description' => __('Add products to cart directly from chat', 'mxchat'),
5535 ),
5536 'mxchat_checkout_redirect' => array(
5537 'label' => __('Proceed to Checkout', 'mxchat'),
5538 'pro_only' => false,
5539 'addon_promo' => true,
5540 'group' => __('WooCommerce Features', 'mxchat'),
5541 'icon' => 'arrow-right-alt',
5542 'description' => __('Redirect customer to checkout page', 'mxchat'),
5543 ),
5544 'mxchat_handle_featured_products' => array(
5545 'label' => __('Featured Products Showcase', 'mxchat'),
5546 'pro_only' => false,
5547 'addon_promo' => true,
5548 'group' => __('WooCommerce Features', 'mxchat'),
5549 'icon' => 'star-filled',
5550 'description' => __('Display a curated selection of products with an AI-generated message', 'mxchat'),
5551 ),
5552
5553 // Perplexity Add-on
5554 'mxchat_perplexity_research' => array(
5555 'label' => __('Perplexity Research', 'mxchat'),
5556 'pro_only' => false,
5557 'addon_promo' => true,
5558 'group' => __('Search Features', 'mxchat'),
5559 'icon' => 'book-alt',
5560 'description' => __('Allows the chatbot to search the web for accurate, up-to-date answers', 'mxchat'),
5561 ),
5562
5563 // Forms Add-on
5564 'mxchat_handle_form_collection' => array(
5565 'label' => __('Form Collection', 'mxchat'),
5566 'pro_only' => false,
5567 'addon_promo' => true,
5568 'group' => __('Form Features', 'mxchat'),
5569 'icon' => 'feedback',
5570 'description' => __('Collect user information through custom forms in chat', 'mxchat'),
5571 ),
5572 );
5573
5574 // Enhance add-on callbacks with installation status and addon info
5575 foreach ($addon_callbacks as $function => $data) {
5576 if (isset($addon_function_mapping[$function])) {
5577 $addon_info = $addon_function_mapping[$function];
5578
5579 $addon_callbacks[$function]['addon'] = $addon_info['addon'];
5580 $addon_callbacks[$function]['addon_name'] = $addon_info['addon_name'];
5581 $addon_callbacks[$function]['installed'] = $addon_info['is_installed'];
5582
5583 // Set pro_only based on add-on configuration
5584 $addon_callbacks[$function]['pro_only'] = $addon_info['pro_required'];
5585 } else {
5586 $addon_callbacks[$function]['addon'] = 'unknown';
5587 $addon_callbacks[$function]['addon_name'] = __('Unknown Add-on', 'mxchat');
5588 $addon_callbacks[$function]['installed'] = false;
5589 }
5590 }
5591
5592 // Initialize callbacks with core features
5593 $callbacks = $core_callbacks;
5594
5595 // Get callbacks from active add-ons
5596 $active_addon_callbacks = apply_filters('mxchat_available_callbacks', array());
5597
5598 // Add placeholder callbacks only for add-ons that aren't active
5599 if ($include_all) {
5600 foreach ($addon_callbacks as $function => $data) {
5601 // Skip placeholders for functions provided by active add-ons
5602 if (in_array($function, $addon_provided_functions)) {
5603 continue;
5604 }
5605
5606 // Skip excluded functions
5607 if (in_array($function, $excluded_functions)) {
5608 continue;
5609 }
5610
5611 // Add the placeholder
5612 $callbacks[$function] = $data;
5613 }
5614 }
5615
5616 // Add callbacks from active add-ons (will override placeholders)
5617 foreach ($active_addon_callbacks as $function => $data) {
5618 // Skip excluded functions
5619 if (in_array($function, $excluded_functions)) {
5620 continue;
5621 }
5622
5623 // Always include callbacks from add-ons
5624 $callbacks[$function] = $data;
5625
5626 // Ensure they have the proper add-on info
5627 if (isset($addon_function_mapping[$function])) {
5628 $addon_info = $addon_function_mapping[$function];
5629 $callbacks[$function]['addon'] = $addon_info['addon'];
5630 $callbacks[$function]['addon_name'] = $addon_info['addon_name'];
5631 $callbacks[$function]['installed'] = $addon_info['is_installed'];
5632 $callbacks[$function]['pro_only'] = $addon_info['pro_required'];
5633 }
5634 }
5635
5636 // Just before returning callbacks, sort them to prioritize free features
5637 if (!$grouped) {
5638 // Create temporary arrays for sorting
5639 $free_callbacks = array();
5640 $pro_callbacks = array();
5641
5642 // Split callbacks into free and pro
5643 foreach ($callbacks as $key => $data) {
5644 if (isset($data['pro_only']) && $data['pro_only']) {
5645 $pro_callbacks[$key] = $data;
5646 } else {
5647 $free_callbacks[$key] = $data;
5648 }
5649 }
5650
5651 // Merge with free callbacks first
5652 $callbacks = array_merge($free_callbacks, $pro_callbacks);
5653 }
5654
5655 // Return grouped structure if requested
5656 if ($grouped) {
5657 $grouped_callbacks = array();
5658 foreach ($callbacks as $key => $data) {
5659 $group_label = isset($data['group']) ? $data['group'] : __('Other Features', 'mxchat');
5660
5661 // Ensure we carry forward all the new fields in grouped mode
5662 $callback_data = array(
5663 'label' => $data['label'],
5664 'pro_only' => isset($data['pro_only']) ? $data['pro_only'] : false,
5665 'icon' => isset($data['icon']) ? $data['icon'] : 'admin-generic',
5666 'description' => isset($data['description']) ? $data['description'] : __('Custom action for your chatbot', 'mxchat'),
5667 'addon' => isset($data['addon']) ? $data['addon'] : false,
5668 'addon_name' => isset($data['addon_name']) ? $data['addon_name'] : '',
5669 'installed' => isset($data['installed']) ? $data['installed'] : true
5670 );
5671
5672 $grouped_callbacks[$group_label][$key] = $callback_data;
5673 }
5674
5675 // Sort within each group to prioritize free features
5676 foreach ($grouped_callbacks as $group => $items) {
5677 $free_items = array();
5678 $pro_items = array();
5679
5680 foreach ($items as $key => $data) {
5681 if (isset($data['pro_only']) && $data['pro_only']) {
5682 $pro_items[$key] = $data;
5683 } else {
5684 $free_items[$key] = $data;
5685 }
5686 }
5687
5688 $grouped_callbacks[$group] = array_merge($free_items, $pro_items);
5689 }
5690
5691 return $grouped_callbacks;
5692 }
5693
5694 return $callbacks;
5695 }
5696
5697 public function mxchat_page_init() {
5698 register_setting(
5699 'mxchat_option_group',
5700 'mxchat_options',
5701 array($this, 'mxchat_sanitize')
5702 );
5703
5704 register_setting(
5705 'mxchat_option_group',
5706 'mxchat_similarity_threshold',
5707 array(
5708 'type' => 'number',
5709 'sanitize_callback' => function($value) {
5710 $value = absint($value);
5711 return min(max($value, 20), 95);
5712 },
5713 'default' => 80,
5714 )
5715 );
5716
5717 // Chatbot Settings Section
5718 add_settings_section(
5719 'mxchat_chatbot_section',
5720 esc_html__('Chatbot Settings', 'mxchat'),
5721 null,
5722 'mxchat-chatbot'
5723 );
5724
5725 // API Keys Settings Section
5726 add_settings_section(
5727 'mxchat_api_keys_section',
5728 esc_html__('API Keys', 'mxchat'),
5729 array($this, 'mxchat_api_keys_section_callback'),
5730 'mxchat-api-keys'
5731 );
5732
5733 // OpenAI API Key
5734 add_settings_field(
5735 'api_key',
5736 esc_html__('OpenAI API Key', 'mxchat'),
5737 array($this, 'api_key_callback'),
5738 'mxchat-api-keys',
5739 'mxchat_api_keys_section'
5740 );
5741
5742 // X.AI API Key
5743 add_settings_field(
5744 'xai_api_key',
5745 esc_html__('X.AI API Key', 'mxchat'),
5746 array($this, 'xai_api_key_callback'),
5747 'mxchat-api-keys',
5748 'mxchat_api_keys_section'
5749 );
5750
5751 // Claude API Key
5752 add_settings_field(
5753 'claude_api_key',
5754 esc_html__('Claude API Key', 'mxchat'),
5755 array($this, 'claude_api_key_callback'),
5756 'mxchat-api-keys',
5757 'mxchat_api_keys_section'
5758 );
5759
5760 // DeepSeek API Key
5761 add_settings_field(
5762 'deepseek_api_key',
5763 esc_html__('DeepSeek API Key', 'mxchat'),
5764 array($this, 'deepseek_api_key_callback'),
5765 'mxchat-api-keys',
5766 'mxchat_api_keys_section'
5767 );
5768
5769 // Google Gemini API Key
5770 add_settings_field(
5771 'gemini_api_key',
5772 esc_html__('Google Gemini API Key', 'mxchat'),
5773 array($this, 'gemini_api_key_callback'),
5774 'mxchat-api-keys',
5775 'mxchat_api_keys_section'
5776 );
5777
5778 // Voyage AI API Key
5779 add_settings_field(
5780 'voyage_api_key',
5781 esc_html__('Voyage AI API Key', 'mxchat'),
5782 array($this, 'voyage_api_key_callback'),
5783 'mxchat-api-keys',
5784 'mxchat_api_keys_section'
5785 );
5786
5787 // OpenRouter API Key
5788 add_settings_field(
5789 'openrouter_api_key',
5790 esc_html__('OpenRouter API Key', 'mxchat'),
5791 array($this, 'openrouter_api_key_callback'),
5792 'mxchat-api-keys',
5793 'mxchat_api_keys_section'
5794 );
5795
5796 // Loops API Key
5797 add_settings_field(
5798 'loops_api_key',
5799 esc_html__('Loops API Key', 'mxchat'),
5800 array($this, 'mxchat_loops_api_key_callback'),
5801 'mxchat-api-keys',
5802 'mxchat_api_keys_section'
5803 );
5804
5805 // Brave Search API Key
5806 add_settings_field(
5807 'brave_api_key',
5808 __('Brave API Key', 'mxchat'),
5809 array($this, 'mxchat_brave_api_key_callback'),
5810 'mxchat-api-keys',
5811 'mxchat_api_keys_section'
5812 );
5813
5814 // Similarity Threshold Slider
5815 add_settings_field(
5816 'similarity_threshold', // Field ID
5817 esc_html__('Similarity Threshold', 'mxchat'), // Field title
5818 array($this, 'mxchat_similarity_threshold_callback'), // Callback function
5819 'mxchat-chatbot', // Page
5820 'mxchat_chatbot_section' // Section
5821 );
5822
5823 // RAG Sources Limit Slider
5824 add_settings_field(
5825 'rag_sources_limit', // Field ID
5826 esc_html__('RAG Sources Limit', 'mxchat'), // Field title
5827 array($this, 'mxchat_rag_sources_limit_callback'), // Callback function
5828 'mxchat-chatbot', // Page
5829 'mxchat_chatbot_section' // Section
5830 );
5831
5832 // RAG Chunks Limit Slider
5833 add_settings_field(
5834 'rag_chunks_limit', // Field ID
5835 esc_html__('RAG Chunks Limit', 'mxchat'), // Field title
5836 array($this, 'mxchat_rag_chunks_limit_callback'), // Callback function
5837 'mxchat-chatbot', // Page
5838 'mxchat_chatbot_section' // Section
5839 );
5840
5841 add_settings_field(
5842 'append_to_body',
5843 esc_html__('Auto-Display Chatbot', 'mxchat'),
5844 array($this, 'mxchat_append_to_body_callback'),
5845 'mxchat-chatbot',
5846 'mxchat_chatbot_section'
5847 );
5848
5849 add_settings_field(
5850 'contextual_awareness_toggle',
5851 esc_html__('Contextual Awareness', 'mxchat'),
5852 array($this, 'mxchat_contextual_awareness_callback'),
5853 'mxchat-chatbot',
5854 'mxchat_chatbot_section'
5855 );
5856
5857 add_settings_field(
5858 'citation_links_toggle',
5859 esc_html__('Citation Links', 'mxchat'),
5860 array($this, 'mxchat_citation_links_toggle_callback'),
5861 'mxchat-chatbot',
5862 'mxchat_chatbot_section'
5863 );
5864
5865 add_settings_field(
5866 'enable_streaming_toggle',
5867 esc_html__('Enable Streaming', 'mxchat'),
5868 array($this, 'enable_streaming_toggle_callback'),
5869 'mxchat-chatbot',
5870 'mxchat_chatbot_section', // Same section as your working toggle
5871 array(
5872 'class' => 'mxchat-setting-row streaming-setting',
5873 'style' => 'display: none;' // Hidden by default, shown when OpenAI/Claude selected
5874 )
5875 );
5876
5877
5878 add_settings_field(
5879 'model',
5880 esc_html__('Chat Model', 'mxchat'),
5881 array($this, 'mxchat_model_callback'),
5882 'mxchat-chatbot',
5883 'mxchat_chatbot_section'
5884 );
5885
5886 add_settings_field(
5887 'embedding_model',
5888 esc_html__('Embedding Model', 'mxchat'),
5889 array($this, 'embedding_model_callback'),
5890 'mxchat-chatbot',
5891 'mxchat_chatbot_section'
5892 );
5893
5894 add_settings_field(
5895 'system_prompt_instructions',
5896 esc_html__('AI Instructions (Behavior)', 'mxchat'),
5897 array($this, 'system_prompt_instructions_callback'),
5898 'mxchat-chatbot',
5899 'mxchat_chatbot_section'
5900 );
5901
5902
5903 add_settings_field(
5904 'top_bar_title',
5905 esc_html__('Top Bar Title', 'mxchat'),
5906 array($this, 'mxchat_top_bar_title_callback'),
5907 'mxchat-chatbot',
5908 'mxchat_chatbot_section'
5909 );
5910
5911 add_settings_field(
5912 'ai_agent_text',
5913 esc_html__('AI Agent Text', 'mxchat'),
5914 array($this, 'mxchat_ai_agent_text_callback'),
5915 'mxchat-chatbot',
5916 'mxchat_chatbot_section'
5917 );
5918
5919 add_settings_field(
5920 'enable_email_block',
5921 esc_html__('Require Email To Chat', 'mxchat'),
5922 array($this, 'enable_email_block_callback'),
5923 'mxchat-chatbot',
5924 'mxchat_chatbot_section'
5925 );
5926
5927 add_settings_field(
5928 'email_blocker_header_content',
5929 esc_html__('Require Email Chat Content', 'mxchat'),
5930 array($this, 'email_blocker_header_content_callback'),
5931 'mxchat-chatbot',
5932 'mxchat_chatbot_section'
5933 );
5934
5935 add_settings_field(
5936 'email_blocker_button_text',
5937 esc_html__('Require Email Chat Button Text', 'mxchat'),
5938 [$this, 'email_blocker_button_text_callback'],
5939 'mxchat-chatbot',
5940 'mxchat_chatbot_section'
5941 );
5942
5943 add_settings_field(
5944 'enable_name_field',
5945 esc_html__('Require Name Field', 'mxchat'),
5946 array($this, 'enable_name_field_callback'),
5947 'mxchat-chatbot',
5948 'mxchat_chatbot_section'
5949 );
5950
5951 add_settings_field(
5952 'name_field_placeholder',
5953 esc_html__('Name Field Placeholder', 'mxchat'),
5954 array($this, 'name_field_placeholder_callback'),
5955 'mxchat-chatbot',
5956 'mxchat_chatbot_section'
5957 );
5958
5959 add_settings_field(
5960 'intro_message',
5961 esc_html__('Introductory Message', 'mxchat'),
5962 array($this, 'mxchat_intro_message_callback'),
5963 'mxchat-chatbot',
5964 'mxchat_chatbot_section'
5965 );
5966
5967 add_settings_field(
5968 'input_copy',
5969 esc_html__('Input Copy', 'mxchat'),
5970 array($this, 'mxchat_input_copy_callback'),
5971 'mxchat-chatbot',
5972 'mxchat_chatbot_section'
5973 );
5974
5975 add_settings_field(
5976 'pre_chat_message',
5977 esc_html__('Chat Teaser Pop-up', 'mxchat'),
5978 array($this, 'mxchat_pre_chat_message_callback'),
5979 'mxchat-chatbot',
5980 'mxchat_chatbot_section'
5981 );
5982
5983 add_settings_field(
5984 'privacy_toggle',
5985 esc_html__('Toggle Privacy Notice', 'mxchat'),
5986 array($this, 'mxchat_privacy_toggle_callback'),
5987 'mxchat-chatbot',
5988 'mxchat_chatbot_section'
5989 );
5990
5991 add_settings_field(
5992 'complianz_toggle',
5993 esc_html__('Enable Complianz', 'mxchat'),
5994 array($this, 'mxchat_complianz_toggle_callback'),
5995 'mxchat-chatbot',
5996 'mxchat_chatbot_section'
5997 );
5998
5999 add_settings_field(
6000 'link_target_toggle',
6001 esc_html__('Open Links in a New Tab', 'mxchat'),
6002 array($this, 'mxchat_link_target_toggle_callback'),
6003 'mxchat-chatbot',
6004 'mxchat_chatbot_section'
6005 );
6006
6007 add_settings_field(
6008 'chat_persistence_toggle',
6009 esc_html__('Enable Chat Persistence', 'mxchat'),
6010 array($this, 'mxchat_chat_persistence_toggle_callback'),
6011 'mxchat-chatbot',
6012 'mxchat_chatbot_section'
6013 );
6014
6015 add_settings_field(
6016 'popular_question_1',
6017 esc_html__('Quick Question 1', 'mxchat'),
6018 array($this, 'mxchat_popular_question_1_callback'),
6019 'mxchat-chatbot',
6020 'mxchat_chatbot_section'
6021 );
6022
6023 add_settings_field(
6024 'popular_question_2',
6025 esc_html__('Quick Question 2', 'mxchat'),
6026 array($this, 'mxchat_popular_question_2_callback'),
6027 'mxchat-chatbot',
6028 'mxchat_chatbot_section'
6029 );
6030
6031 add_settings_field(
6032 'popular_question_3',
6033 esc_html__('Quick Question 3', 'mxchat'),
6034 array($this, 'mxchat_popular_question_3_callback'),
6035 'mxchat-chatbot',
6036 'mxchat_chatbot_section'
6037 );
6038
6039 add_settings_field(
6040 'additional_popular_questions',
6041 esc_html__('Additional Quick Questions', 'mxchat'),
6042 array($this, 'mxchat_additional_popular_questions_callback'),
6043 'mxchat-chatbot',
6044 'mxchat_chatbot_section'
6045 );
6046
6047
6048 add_settings_field(
6049 'rate_limits',
6050 __('Rate Limits Settings', 'mxchat'),
6051 array($this, 'mxchat_rate_limits_callback'),
6052 'mxchat-chatbot',
6053 'mxchat_chatbot_section'
6054 );
6055
6056 // Loops Settings Section
6057 add_settings_section(
6058 'mxchat_loops_section',
6059 esc_html__('Loops Settings', 'mxchat'),
6060 null,
6061 'mxchat-embed'
6062 );
6063
6064 // Loops Settings Fields (API Key moved to API Keys tab)
6065 add_settings_field(
6066 'loops_mailing_list',
6067 esc_html__('Loops Mailing List', 'mxchat'),
6068 array($this, 'mxchat_loops_mailing_list_callback'),
6069 'mxchat-embed',
6070 'mxchat_loops_section'
6071 );
6072
6073 add_settings_field(
6074 'triggered_phrase_response',
6075 esc_html__('Triggered Phrase Response', 'mxchat'),
6076 array($this, 'mxchat_triggered_phrase_response_callback'),
6077 'mxchat-embed',
6078 'mxchat_loops_section'
6079 );
6080
6081 add_settings_field(
6082 'email_capture_response',
6083 esc_html__('Email Capture Response', 'mxchat'),
6084 array($this, 'mxchat_email_capture_response_callback'),
6085 'mxchat-embed',
6086 'mxchat_loops_section'
6087 );
6088
6089 // Brave Search Settings Fields
6090 add_settings_section(
6091 'mxchat_brave_section',
6092 __('Brave Search Settings', 'mxchat'),
6093 array($this, 'mxchat_brave_section_callback'),
6094 'mxchat-embed'
6095 );
6096
6097 // Brave API Key moved to API Keys tab
6098 add_settings_field(
6099 'brave_image_count',
6100 __('Number of Images to Return', 'mxchat'),
6101 array($this, 'mxchat_brave_image_count_callback'),
6102 'mxchat-embed',
6103 'mxchat_brave_section'
6104 );
6105
6106 add_settings_field(
6107 'brave_safe_search',
6108 __('Safe Search', 'mxchat'),
6109 array($this, 'mxchat_brave_safe_search_callback'),
6110 'mxchat-embed',
6111 'mxchat_brave_section'
6112 );
6113
6114 add_settings_field(
6115 'brave_news_count',
6116 __('Number of News Articles', 'mxchat'),
6117 array($this, 'mxchat_brave_news_count_callback'),
6118 'mxchat-embed',
6119 'mxchat_brave_section'
6120 );
6121
6122 add_settings_field(
6123 'brave_country',
6124 __('Country', 'mxchat'),
6125 array($this, 'mxchat_brave_country_callback'),
6126 'mxchat-embed',
6127 'mxchat_brave_section'
6128 );
6129
6130 add_settings_field(
6131 'brave_language',
6132 __('Language', 'mxchat'),
6133 array($this, 'mxchat_brave_language_callback'),
6134 'mxchat-embed',
6135 'mxchat_brave_section'
6136 );
6137
6138 // Chat with PDF Intent Settings Fields
6139 add_settings_section(
6140 'mxchat_pdf_intent_section',
6141 __('Toolbar Settings & Intents', 'mxchat'),
6142 array($this, 'mxchat_pdf_intent_section_callback'),
6143 'mxchat-embed'
6144 );
6145
6146 add_settings_field(
6147 'chat_toolbar_toggle',
6148 __('Show Chat Toolbar', 'mxchat'),
6149 array($this, 'mxchat_chat_toolbar_toggle_callback'),
6150 'mxchat-embed',
6151 'mxchat_pdf_intent_section'
6152 );
6153
6154 // PDF Upload Button Toggle
6155 add_settings_field(
6156 'show_pdf_upload_button',
6157 __('Show PDF Upload Button', 'mxchat'),
6158 array($this, 'mxchat_show_pdf_upload_button_callback'),
6159 'mxchat-embed',
6160 'mxchat_pdf_intent_section'
6161 );
6162
6163 // Word Upload Button Toggle
6164 add_settings_field(
6165 'show_word_upload_button',
6166 __('Show Word Upload Button', 'mxchat'),
6167 array($this, 'mxchat_show_word_upload_button_callback'),
6168 'mxchat-embed',
6169 'mxchat_pdf_intent_section'
6170 );
6171
6172 add_settings_field(
6173 'pdf_intent_trigger_text',
6174 __('Intent Trigger Text', 'mxchat'),
6175 array($this, 'mxchat_pdf_intent_trigger_text_callback'),
6176 'mxchat-embed',
6177 'mxchat_pdf_intent_section'
6178 );
6179
6180 add_settings_field(
6181 'pdf_intent_success_text',
6182 __('Success Text', 'mxchat'),
6183 array($this, 'mxchat_pdf_intent_success_text_callback'),
6184 'mxchat-embed',
6185 'mxchat_pdf_intent_section'
6186 );
6187
6188 add_settings_field(
6189 'pdf_intent_error_text',
6190 __('Error Text', 'mxchat'),
6191 array($this, 'mxchat_pdf_intent_error_text_callback'),
6192 'mxchat-embed',
6193 'mxchat_pdf_intent_section'
6194 );
6195
6196 // Add PDF Maximum Pages Field
6197 add_settings_field(
6198 'pdf_max_pages',
6199 __('Maximum Document Pages', 'mxchat'),
6200 array($this, 'mxchat_pdf_max_pages_callback'),
6201 'mxchat-embed',
6202 'mxchat_pdf_intent_section'
6203 );
6204
6205 // Live Agent Settings Fields
6206 add_settings_section(
6207 'mxchat_live_agent_section',
6208 __('Live Agent Settings', 'mxchat'),
6209 array($this, 'mxchat_live_agent_section_callback'),
6210 'mxchat-embed'
6211 );
6212
6213 // Live Agent Status Fields (add at top of live agent settings)
6214 add_settings_field(
6215 'live_agent_status',
6216 __('Live Agent Status', 'mxchat'),
6217 array($this, 'mxchat_live_agent_status_callback'),
6218 'mxchat-embed',
6219 'mxchat_live_agent_section'
6220 );
6221
6222 add_settings_field(
6223 'live_agent_notification_message',
6224 __('Notification Message', 'mxchat'),
6225 array($this, 'mxchat_live_agent_notification_message_callback'),
6226 'mxchat-embed',
6227 'mxchat_live_agent_section'
6228 );
6229
6230 add_settings_field(
6231 'live_agent_away_message',
6232 __('Away Message', 'mxchat'),
6233 array($this, 'mxchat_live_agent_away_message_callback'),
6234 'mxchat-embed',
6235 'mxchat_live_agent_section'
6236 );
6237
6238 add_settings_field(
6239 'live_agent_user_ids',
6240 __('Slack Agent User IDs', 'mxchat'),
6241 array($this, 'mxchat_live_agent_user_ids_callback'),
6242 'mxchat-embed',
6243 'mxchat_live_agent_section'
6244 );
6245
6246 add_settings_field(
6247 'live_agent_webhook_url',
6248 __('Slack Webhook URL', 'mxchat'),
6249 array($this, 'mxchat_live_agent_webhook_url_callback'),
6250 'mxchat-embed',
6251 'mxchat_live_agent_section'
6252 );
6253
6254 add_settings_field(
6255 'live_agent_secret_key',
6256 __('Slack Secret Key', 'mxchat'),
6257 array($this, 'mxchat_live_agent_secret_key_callback'),
6258 'mxchat-embed',
6259 'mxchat_live_agent_section'
6260 );
6261
6262 // Live Agent Integration Fields
6263 add_settings_field(
6264 'live_agent_bot_token',
6265 __('Slack Bot OAuth Token', 'mxchat'),
6266 array($this, 'mxchat_live_agent_bot_token_callback'),
6267 'mxchat-embed',
6268 'mxchat_live_agent_section'
6269 );
6270
6271 // Telegram Integration Section
6272 add_settings_section(
6273 'mxchat_telegram_section',
6274 __('Telegram Settings', 'mxchat'),
6275 array($this, 'mxchat_telegram_section_callback'),
6276 'mxchat-embed'
6277 );
6278
6279 add_settings_field(
6280 'telegram_status',
6281 __('Live Agent Status', 'mxchat'),
6282 array($this, 'mxchat_telegram_status_callback'),
6283 'mxchat-embed',
6284 'mxchat_telegram_section'
6285 );
6286
6287 add_settings_field(
6288 'telegram_notification_message',
6289 __('Notification Message', 'mxchat'),
6290 array($this, 'mxchat_telegram_notification_message_callback'),
6291 'mxchat-embed',
6292 'mxchat_telegram_section'
6293 );
6294
6295 add_settings_field(
6296 'telegram_away_message',
6297 __('Away Message', 'mxchat'),
6298 array($this, 'mxchat_telegram_away_message_callback'),
6299 'mxchat-embed',
6300 'mxchat_telegram_section'
6301 );
6302
6303 add_settings_field(
6304 'telegram_bot_token',
6305 __('Telegram Bot Token', 'mxchat'),
6306 array($this, 'mxchat_telegram_bot_token_callback'),
6307 'mxchat-embed',
6308 'mxchat_telegram_section'
6309 );
6310
6311 add_settings_field(
6312 'telegram_group_id',
6313 __('Telegram Group ID', 'mxchat'),
6314 array($this, 'mxchat_telegram_group_id_callback'),
6315 'mxchat-embed',
6316 'mxchat_telegram_section'
6317 );
6318
6319 add_settings_field(
6320 'telegram_webhook_secret',
6321 __('Webhook Secret Token', 'mxchat'),
6322 array($this, 'mxchat_telegram_webhook_secret_callback'),
6323 'mxchat-embed',
6324 'mxchat_telegram_section'
6325 );
6326
6327 // General Settings Section
6328 add_settings_section(
6329 'mxchat_general_section',
6330 esc_html__('YouTube Tutorials', 'mxchat'),
6331 null,
6332 'mxchat-general'
6333 );
6334 }
6335
6336 public function mxchat_prompts_page_init() {
6337 register_setting(
6338 'mxchat_prompts_options',
6339 'mxchat_prompts_options',
6340 array(
6341 'type' => 'array',
6342 'description' => __('MXChat Knowledge Base Settings', 'mxchat'),
6343 'default' => array(
6344 'mxchat_auto_sync_posts' => 0,
6345 'mxchat_auto_sync_pages' => 0,
6346 'mxchat_use_pinecone' => 0,
6347 'mxchat_pinecone_api_key' => '',
6348 'mxchat_pinecone_environment' => '',
6349 'mxchat_pinecone_index' => '',
6350 'mxchat_pinecone_host' => '',
6351 ),
6352 'sanitize_callback' => array($this, 'sanitize_prompts_options'),
6353 )
6354 );
6355
6356 add_action('admin_notices', array($this, 'sync_settings_notice'));
6357 }
6358
6359 public function mxchat_transcripts_page_init() {
6360 register_setting(
6361 'mxchat_transcripts_options',
6362 'mxchat_transcripts_options',
6363 array(
6364 'type' => 'array',
6365 'description' => __('MXChat Transcripts Notification Settings', 'mxchat'),
6366 'default' => array(
6367 'mxchat_enable_notifications' => 0,
6368 'mxchat_notification_email' => get_option('admin_email'),
6369 'mxchat_auto_delete_transcripts' => 'never',
6370 ),
6371 'sanitize_callback' => array($this, 'sanitize_transcripts_options'),
6372 )
6373 );
6374
6375 add_settings_section(
6376 'mxchat_transcripts_notification_section',
6377 esc_html__('Chat Notification Settings', 'mxchat'),
6378 array($this, 'mxchat_transcripts_notification_section_callback'),
6379 'mxchat-transcripts'
6380 );
6381
6382 add_settings_field(
6383 'mxchat_enable_notifications',
6384 esc_html__('Enable Chat Notifications', 'mxchat'),
6385 array($this, 'mxchat_enable_notifications_callback'),
6386 'mxchat-transcripts',
6387 'mxchat_transcripts_notification_section'
6388 );
6389
6390 add_settings_field(
6391 'mxchat_notification_email',
6392 esc_html__('Notification Email Address', 'mxchat'),
6393 array($this, 'mxchat_notification_email_callback'),
6394 'mxchat-transcripts',
6395 'mxchat_transcripts_notification_section'
6396 );
6397
6398 add_settings_field(
6399 'mxchat_auto_delete_transcripts',
6400 esc_html__('Auto-Delete Old Transcripts', 'mxchat'),
6401 array($this, 'mxchat_auto_delete_transcripts_callback'),
6402 'mxchat-transcripts',
6403 'mxchat_transcripts_notification_section'
6404 );
6405
6406 add_settings_field(
6407 'mxchat_auto_email_transcript',
6408 esc_html__('Auto-Email Full Transcript', 'mxchat'),
6409 array($this, 'mxchat_auto_email_transcript_callback'),
6410 'mxchat-transcripts',
6411 'mxchat_transcripts_notification_section'
6412 );
6413 }
6414
6415
6416 /**
6417 * Sanitize all prompts options
6418 *
6419 * @param array $input The unsanitized options array
6420 * @return array The sanitized options array
6421 */
6422 public function sanitize_prompts_options($input) {
6423 // Log the incoming input.
6424 //error_log('Sanitizing inputs: ' . print_r($input, true));
6425
6426 $sanitized = array();
6427
6428 // Boolean options
6429 $sanitized['mxchat_auto_sync_posts'] = isset($input['mxchat_auto_sync_posts']) ? 1 : 0;
6430 $sanitized['mxchat_auto_sync_pages'] = isset($input['mxchat_auto_sync_pages']) ? 1 : 0;
6431 $sanitized['mxchat_use_pinecone'] = !empty($input['mxchat_use_pinecone']) ? 1 : 0;
6432
6433 // API Key: if less than 32 characters, flag as invalid.
6434 $api_key = sanitize_text_field($input['mxchat_pinecone_api_key'] ?? '');
6435 if (!empty($api_key) && strlen($api_key) < 32) {
6436 add_settings_error(
6437 'mxchat_prompts_options',
6438 'invalid_api_key',
6439 __('The Pinecone API key appears to be invalid. Please check your API key.', 'mxchat')
6440 );
6441 $existing_options = get_option('mxchat_prompts_options', array());
6442 $sanitized['mxchat_pinecone_api_key'] = $existing_options['mxchat_pinecone_api_key'] ?? '';
6443 } else {
6444 $sanitized['mxchat_pinecone_api_key'] = $api_key;
6445 }
6446
6447 // Environment and Index Name
6448 $sanitized['mxchat_pinecone_environment'] = sanitize_text_field($input['mxchat_pinecone_environment'] ?? '');
6449 $sanitized['mxchat_pinecone_index'] = sanitize_text_field($input['mxchat_pinecone_index'] ?? '');
6450
6451 // Host: Remove protocol and validate format.
6452 $host = sanitize_text_field($input['mxchat_pinecone_host'] ?? '');
6453 $host = preg_replace('#^https?://#', '', $host);
6454 //error_log('Host after removing protocol: ' . $host);
6455 if (!empty($host)) {
6456 if (!preg_match('/^[\w-]+\.svc\.[\w-]+\.pinecone\.io$/', $host)) {
6457 add_settings_error(
6458 'mxchat_prompts_options',
6459 'invalid_host',
6460 __('The Pinecone host appears to be invalid. It should look like "mxchat-vectors-zrmsquq.svc.aped-4627-b74a.pinecone.io"', 'mxchat')
6461 );
6462 $existing_options = get_option('mxchat_prompts_options', array());
6463 $sanitized['mxchat_pinecone_host'] = $existing_options['mxchat_pinecone_host'] ?? '';
6464 } else {
6465 $sanitized['mxchat_pinecone_host'] = $host;
6466 }
6467 } else {
6468 $sanitized['mxchat_pinecone_host'] = '';
6469 }
6470
6471 //error_log('Final sanitized array: ' . print_r($sanitized, true));
6472
6473 return $sanitized;
6474 }
6475
6476 public function sync_settings_notice() {
6477 // Only show notice on our plugin page
6478 if (!isset($_GET['page']) || $_GET['page'] !== 'mxchat-prompts') {
6479 return;
6480 }
6481
6482 // Check if settings were updated
6483 if (isset($_GET['settings-updated'])) {
6484
6485 ?>
6486 <div class="notice notice-success is-dismissible">
6487 <p><?php esc_html_e('Sync settings updated successfully.', 'mxchat'); ?></p>
6488 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php esc_html_e('Dismiss this notice.', 'mxchat'); ?></span></button>
6489 </div>
6490 <?php
6491
6492 }
6493 }
6494 // Add this sanitization function to your class
6495 public function sanitize_sync_setting($input) {
6496 return (bool)$input ? __('1', 'mxchat') : __('', 'mxchat');
6497 }
6498
6499 public function mxchat_rate_limits_callback() {
6500 $all_options = get_option('mxchat_options', []);
6501
6502 // Define available rate limits
6503 $rate_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited');
6504
6505 // Define available timeframes
6506 $timeframes = array(
6507 'hourly' => __('Per Hour', 'mxchat'),
6508 'daily' => __('Per Day', 'mxchat'),
6509 'weekly' => __('Per Week', 'mxchat'),
6510 'monthly' => __('Per Month', 'mxchat')
6511 );
6512
6513 // Get all roles plus a "logged_out" pseudo-role
6514 $roles = wp_roles()->get_names();
6515 $roles['logged_out'] = __('Logged Out Users', 'mxchat');
6516
6517 // Start the wrapper
6518 echo '<div class="pro-feature-wrapper active">';
6519 echo '<div class="mxchat-rate-limits-container">';
6520
6521 echo '<p class="description" style="margin-bottom: 20px;">' .
6522 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') .
6523 '</p>';
6524
6525 // Add markdown link documentation
6526 echo '<div class="notice notice-info inline" style="margin-bottom: 20px; padding: 10px;">';
6527 echo '<p><strong>' . esc_html__('Markdown Links Supported:', 'mxchat') . '</strong></p>';
6528 echo '<p>' . esc_html__('You can include clickable links in your custom messages using markdown syntax:', 'mxchat') . '</p>';
6529 echo '<ul style="margin-left: 20px;">';
6530 echo '<li><code>[Link text](https://example.com)</code> - Creates a clickable link</li>';
6531 echo '<li><code>[Visit our pricing](https://example.com/pricing)</code> - Link with custom text</li>';
6532 echo '<li><code>Plain URLs like https://example.com will also become clickable</code></li>';
6533 echo '</ul>';
6534 echo '</div>';
6535
6536 // Output the controls for each role
6537 foreach ($roles as $role_id => $role_name) {
6538 // Get saved options or defaults
6539 $default_limit = ($role_id === 'logged_out') ? '10' : '100';
6540 $default_timeframe = 'daily';
6541 $default_message = __('Rate limit exceeded. Please try again later.', 'mxchat');
6542
6543 $selected_limit = isset($all_options['rate_limits'][$role_id]['limit'])
6544 ? $all_options['rate_limits'][$role_id]['limit']
6545 : $default_limit;
6546
6547 $selected_timeframe = isset($all_options['rate_limits'][$role_id]['timeframe'])
6548 ? $all_options['rate_limits'][$role_id]['timeframe']
6549 : $default_timeframe;
6550
6551 $custom_message = isset($all_options['rate_limits'][$role_id]['message'])
6552 ? $all_options['rate_limits'][$role_id]['message']
6553 : $default_message;
6554
6555 // Output the row
6556 echo '<div class="mxchat-rate-limit-row mxchat-autosave-section">';
6557
6558 // Role label
6559 echo '<div class="mxchat-rate-limit-role">' . esc_html($role_name) . '</div>';
6560
6561 // Controls section
6562 echo '<div class="mxchat-rate-limit-controls-wrapper">';
6563
6564 // Rate limit and timeframe controls
6565 echo '<div class="mxchat-rate-limit-controls">';
6566
6567 // Limit dropdown
6568 echo '<div>';
6569 echo '<label for="rate_limits_' . esc_attr($role_id) . '_limit">' . esc_html__('Limit:', 'mxchat') . '</label>';
6570 echo '<select
6571 id="rate_limits_' . esc_attr($role_id) . '_limit"
6572 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][limit]"
6573 class="mxchat-autosave-field">';
6574 foreach ($rate_limits as $limit) {
6575 echo '<option value="' . esc_attr($limit) . '" ' . selected($selected_limit, $limit, false) . '>' . esc_html($limit) . '</option>';
6576 }
6577 echo '</select>';
6578 echo '</div>';
6579
6580 // Timeframe dropdown
6581 echo '<div>';
6582 echo '<label for="rate_limits_' . esc_attr($role_id) . '_timeframe">' . esc_html__('Timeframe:', 'mxchat') . '</label>';
6583 echo '<select
6584 id="rate_limits_' . esc_attr($role_id) . '_timeframe"
6585 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][timeframe]"
6586 class="mxchat-autosave-field">';
6587 foreach ($timeframes as $value => $label) {
6588 echo '<option value="' . esc_attr($value) . '" ' . selected($selected_timeframe, $value, false) . '>' . esc_html($label) . '</option>';
6589 }
6590 echo '</select>';
6591 echo '</div>';
6592
6593 echo '</div>'; // End controls
6594
6595 // Custom message textarea
6596 echo '<div class="mxchat-rate-limit-message">';
6597 echo '<label for="rate_limits_' . esc_attr($role_id) . '_message">' . esc_html__('Custom Message:', 'mxchat') . '</label>';
6598 echo '<textarea
6599 id="rate_limits_' . esc_attr($role_id) . '_message"
6600 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][message]"
6601 class="mxchat-autosave-field"
6602 placeholder="' . esc_attr__('Enter custom message when rate limit is exceeded', 'mxchat') . '">' .
6603 esc_textarea($custom_message) .
6604 '</textarea>';
6605 echo '<p class="description">' .
6606 esc_html__('Example: Rate limit reached! [Visit our pricing page](https://example.com/pricing) to upgrade.', 'mxchat') .
6607 '</p>';
6608 echo '</div>'; // End message
6609
6610 echo '</div>'; // End controls wrapper
6611
6612 echo '</div>'; // End row
6613 }
6614
6615 echo '</div>'; // End container
6616
6617 echo '</div>'; // End pro-feature-wrapper
6618 }
6619
6620 private function mxchat_add_option_field($id, $title, $callback = '') {
6621 add_settings_field(
6622 $id,
6623 __($title, 'mxchat'),
6624 $callback ? array($this, $callback) : array($this, $id . '_callback'),
6625 'mxchat-max',
6626 'mxchat_setting_section_id',
6627 $id === 'model' ? ['label_for' => 'model'] : []
6628 );
6629 }
6630
6631 // API Keys Section Callback
6632 public function mxchat_api_keys_section_callback() {
6633 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>';
6634 }
6635
6636 // OpenAI API Key
6637 public function api_key_callback() {
6638 $apiKey = isset($this->options['api_key']) ? esc_attr($this->options['api_key']) : '';
6639 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6640
6641 echo '<div class="api-key-wrapper">';
6642 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 . '" />';
6643 echo '<button type="button" id="toggleApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6644 echo '<p class="description">' . esc_html__('Required for OpenAI GPT models and OpenAI embeddings. Get your API key from OpenAI Platform.', 'mxchat') . '</p>';
6645 echo '</div>';
6646 }
6647
6648 // X.AI API Key
6649 public function xai_api_key_callback() {
6650 $xaiApiKey = isset($this->options['xai_api_key']) ? esc_attr($this->options['xai_api_key']) : '';
6651 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6652
6653 echo '<div class="api-key-wrapper">';
6654 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 . '" />';
6655 echo '<button type="button" id="toggleXaiApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6656 echo '<p class="description">' . esc_html__('Required for X.AI Grok models. Get your API key from X.AI Console.', 'mxchat') . '</p>';
6657 echo '</div>';
6658 }
6659 // Claude API Key
6660 public function claude_api_key_callback() {
6661 $claudeApiKey = isset($this->options['claude_api_key']) ? esc_attr($this->options['claude_api_key']) : '';
6662 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6663
6664 echo '<div class="api-key-wrapper">';
6665 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 . '" />';
6666 echo '<button type="button" id="toggleClaudeApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6667 echo '<p class="description">' . esc_html__('Required for Anthropic Claude models. Get your API key from Anthropic Console.', 'mxchat') . '</p>';
6668 echo '</div>';
6669 }
6670
6671 // DeepSeek API Key
6672 public function deepseek_api_key_callback() {
6673 $apiKey = isset($this->options['deepseek_api_key']) ? esc_attr($this->options['deepseek_api_key']) : '';
6674 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6675
6676 echo '<div class="api-key-wrapper">';
6677 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 . '" />';
6678 echo '<button type="button" id="toggleDeepSeekApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6679 echo '<p class="description">' . esc_html__('Required for DeepSeek models. Get your API key from DeepSeek Platform.', 'mxchat') . '</p>';
6680 echo '</div>';
6681 }
6682
6683 // Gemini API Key
6684 public function gemini_api_key_callback() {
6685 $geminiApiKey = isset($this->options['gemini_api_key']) ? esc_attr($this->options['gemini_api_key']) : '';
6686 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6687
6688 echo '<div class="api-key-wrapper">';
6689 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 . '" />';
6690 echo '<button type="button" id="toggleGeminiApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6691 echo '<p class="description">' . esc_html__('Required for Google Gemini models and embeddings. Get your API key from Google AI Studio.', 'mxchat') . '</p>';
6692 echo '</div>';
6693 }
6694
6695
6696 // OpenRouter API Key
6697 public function openrouter_api_key_callback() {
6698 $openrouterApiKey = isset($this->options['openrouter_api_key']) ? esc_attr($this->options['openrouter_api_key']) : '';
6699 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6700
6701 echo '<div class="api-key-wrapper">';
6702 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 . '" />';
6703 echo '<button type="button" id="toggleOpenRouterApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6704 echo '<p class="description">' . esc_html__('Required for OpenRouter models. Get your API key from OpenRouter.ai', 'mxchat') . '</p>';
6705 echo '</div>';
6706 }
6707
6708 // Voyage API Key
6709 public function voyage_api_key_callback() {
6710 $apiKey = isset($this->options['voyage_api_key']) ? esc_attr($this->options['voyage_api_key']) : '';
6711 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6712
6713 echo '<div class="api-key-wrapper">';
6714 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 . '" />';
6715 echo '<button type="button" id="toggleVoyageAPIKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6716 echo '<p class="description">' . esc_html__('Required for Voyage AI embedding models. Get your API key from Voyage AI.', 'mxchat') . '</p>';
6717 echo '</div>';
6718 }
6719
6720 public function mxchat_loops_api_key_callback() {
6721 $loops_api_key = isset($this->options['loops_api_key']) ? esc_attr($this->options['loops_api_key']) : '';
6722 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6723
6724 echo '<div class="api-key-wrapper">';
6725 echo sprintf(
6726 '<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" />',
6727 $loops_api_key,
6728 $nonce
6729 );
6730 echo '<button type="button" id="toggleLoopsApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6731 echo '</div>';
6732 echo '<p class="description">' . esc_html__('Required for Loops email integration. Get your API key from Loops.so', 'mxchat') . '</p>';
6733 }
6734 public function mxchat_loops_mailing_list_callback() {
6735 // Add error handling and type checking
6736 $loops_api_key = '';
6737 $selected_list = '';
6738 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6739
6740 // Safely get the API key
6741 if (isset($this->options['loops_api_key']) && is_string($this->options['loops_api_key'])) {
6742 $loops_api_key = $this->options['loops_api_key'];
6743 }
6744
6745 // Safely get the selected list
6746 if (isset($this->options['loops_mailing_list']) && is_string($this->options['loops_mailing_list'])) {
6747 $selected_list = $this->options['loops_mailing_list'];
6748 }
6749
6750 if (!empty($loops_api_key)) {
6751 $lists = $this->mxchat_fetch_loops_mailing_lists($loops_api_key);
6752 if (is_array($lists) && !empty($lists)) {
6753 echo '<div class="mxchat-field-wrapper">';
6754 echo '<select id="loops_mailing_list" name="loops_mailing_list" class="mxchat-autosave-field" data-nonce="' . $nonce . '">';
6755
6756 // Add a default "Select a list" option
6757 echo '<option value="" ' . selected($selected_list, '', false) . '>' . esc_html__('Select a list', 'mxchat') . '</option>';
6758
6759 foreach ($lists as $list) {
6760 if (is_array($list) && isset($list['id']) && isset($list['name'])) {
6761 echo sprintf(
6762 '<option value="%s" %s>%s</option>',
6763 esc_attr($list['id']),
6764 selected($selected_list, $list['id'], false),
6765 esc_html($list['name'])
6766 );
6767 }
6768 }
6769 echo '</select>';
6770 echo '</div>';
6771 echo '<p class="description">' . esc_html__('Please select a mailing list to use with Loops.', 'mxchat') . '</p>';
6772 } else {
6773 echo '<p class="description">' . esc_html__('No lists found. Please verify your API Key.', 'mxchat') . '</p>';
6774 }
6775 } else {
6776 echo '<p class="description">' . esc_html__('Enter a valid Loops API Key to load mailing lists.', 'mxchat') . '</p>';
6777 }
6778 }
6779 public function mxchat_triggered_phrase_response_callback() {
6780 $default_response = __('Would you like to join our mailing list? Please provide your email below.', 'mxchat');
6781 $triggered_response = isset($this->options['triggered_phrase_response'])
6782 ? $this->options['triggered_phrase_response']
6783 : $default_response;
6784 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6785
6786 echo '<div class="mxchat-field-wrapper">';
6787 echo sprintf(
6788 '<textarea id="triggered_phrase_response" name="triggered_phrase_response" rows="3" cols="50" class="mxchat-autosave-field" data-nonce="%s">%s</textarea>',
6789 $nonce,
6790 esc_textarea($triggered_response)
6791 );
6792 echo '</div>';
6793 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>';
6794 }
6795
6796 public function mxchat_email_capture_response_callback() {
6797 $default_response = __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
6798 $email_capture_response = isset($this->options['email_capture_response'])
6799 ? $this->options['email_capture_response']
6800 : $default_response;
6801 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6802
6803 echo '<div class="mxchat-field-wrapper">';
6804 echo sprintf(
6805 '<textarea id="email_capture_response" name="email_capture_response" rows="3" cols="50" class="mxchat-autosave-field" data-nonce="%s">%s</textarea>',
6806 $nonce,
6807 esc_textarea($email_capture_response)
6808 );
6809 echo '</div>';
6810 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>';
6811 }
6812 public function mxchat_pre_chat_message_callback() {
6813 // Load the entire 'mxchat_options' array
6814 $all_options = get_option('mxchat_options', []);
6815
6816 // Retrieve the saved message or use the default value
6817 $default_message = __('Hey there! Ask me anything!', 'mxchat');
6818 $pre_chat_message = isset($all_options['pre_chat_message']) ? $all_options['pre_chat_message'] : $default_message;
6819
6820 // Output the textarea
6821 printf(
6822 '<textarea id="pre_chat_message" name="pre_chat_message" rows="5" cols="50">%s</textarea>',
6823 esc_textarea($pre_chat_message)
6824 );
6825 }
6826
6827 // Callback for AI Instructions textarea
6828 public function system_prompt_instructions_callback() {
6829 // Retrieve the current value of the system prompt instructions
6830 $instructions = isset($this->options['system_prompt_instructions']) ? esc_textarea($this->options['system_prompt_instructions']) : '';
6831 // Render the textarea field
6832 printf(
6833 '<textarea id="system_prompt_instructions" name="system_prompt_instructions" rows="5" cols="50">%s</textarea>',
6834 $instructions
6835 );
6836 // Personalization hint
6837 echo '<p class="description" style="margin-top: 8px;">';
6838 echo esc_html__('Use {visitor_name} to personalize AI responses when lead capture is enabled.', 'mxchat') . '<br>';
6839 echo '<code style="font-size: 12px;">' . esc_html__('Example: The visitor\'s name is {visitor_name}. Address them by name.', 'mxchat') . '</code>';
6840 echo '</p>';
6841 // Sample instructions button
6842 echo '<div class="mxchat-instructions-container">';
6843 echo '<button type="button" class="mxchat-instructions-btn" id="mxchatViewSampleBtn">';
6844 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">';
6845 echo '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>';
6846 echo '<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>';
6847 echo '</svg>';
6848 echo esc_html__('View Sample Instructions', 'mxchat');
6849 echo '</button>';
6850 echo '</div>';
6851
6852 // Add modal to WordPress admin footer instead of inline
6853 add_action('admin_footer', array($this, 'render_sample_instructions_modal'));
6854 }
6855
6856 // New method to render modal in admin footer
6857 public function render_sample_instructions_modal() {
6858 static $modal_rendered = false;
6859 if ($modal_rendered) return; // Prevent duplicate modals
6860 $modal_rendered = true;
6861
6862 echo '<div class="mxchat-instructions-modal-overlay" id="mxchatSampleModal">';
6863 echo '<div class="mxchat-instructions-modal-content">';
6864 echo '<div class="mxchat-instructions-modal-header">';
6865 echo '<h3 class="mxchat-instructions-modal-title">';
6866 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">';
6867 echo '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>';
6868 echo '<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>';
6869 echo '</svg>';
6870 echo esc_html__('Sample AI Instructions', 'mxchat');
6871 echo '</h3>';
6872 echo '<button type="button" class="mxchat-instructions-modal-close" id="mxchatModalClose">';
6873 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">';
6874 echo '<line x1="18" y1="6" x2="6" y2="18"/>';
6875 echo '<line x1="6" y1="6" x2="18" y2="18"/>';
6876 echo '</svg>';
6877 echo '</button>';
6878 echo '</div>';
6879 echo '<div class="mxchat-instructions-modal-body">';
6880 echo '<div class="mxchat-instructions-content">';
6881 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:
6882
6883 # Response Style - CRITICALLY IMPORTANT
6884 - MAXIMUM LENGTH: 1-3 short sentences per response
6885 - Ultra-concise: Get straight to the answer with no filler
6886 - No introductions like "Sure!" or "I\'d be happy to help"
6887 - No phrases like "based on my knowledge" or "according to information"
6888 - No explanatory text before giving the answer
6889 - No summaries or repetition
6890 - Hyperlink all URLs
6891 - Respond in user\'s language
6892 - Minor chit chat or conversation is okay, but try to keep it focused on [insert topic]
6893
6894 # Knowledge Base Requirements - PREVENT HALLUCINATIONS
6895 - ONLY answer using information explicitly provided in OFFICIAL KNOWLEDGE DATABASE CONTENT sections marked with ===== delimiters
6896 - If required information is NOT in the knowledge database: "I don\'t have enough information in my knowledge base to answer that question accurately."
6897 - NEVER invent or hallucinate URLs, links, product specs, procedures, dates, statistics, names, contacts, or company information
6898 - When knowledge base information is unclear or contradictory, acknowledge the limitation rather than guessing
6899 - Better to admit insufficient information than provide inaccurate answers');
6900 echo '</div>';
6901 echo '<button type="button" class="mxchat-instructions-copy-btn" id="mxchatCopyBtn">';
6902 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">';
6903 echo '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>';
6904 echo '<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>';
6905 echo '</svg>';
6906 echo esc_html__('Copy Instructions', 'mxchat');
6907 echo '</button>';
6908 echo '</div>';
6909 echo '<div class="mxchat-instructions-modal-footer">';
6910 echo '<button type="button" class="mxchat-instructions-btn-secondary" id="mxchatCloseBtn">' . esc_html__('Close', 'mxchat') . '</button>';
6911 echo '</div>';
6912 echo '</div>';
6913 echo '</div>';
6914 }
6915
6916
6917 public function mxchat_model_callback() {
6918 // Define available models grouped by provider
6919 $models = array(
6920 esc_html__('OpenRouter (100+ Models)', 'mxchat') => array(
6921 'openrouter' => esc_html__('OpenRouter - Select after entering API key', 'mxchat'),
6922 ),
6923 esc_html__('Google Gemini Models', 'mxchat') => array(
6924 'gemini-3-pro-preview' => esc_html__('Gemini 3 Pro (Most Intelligent, Multimodal)', 'mxchat'),
6925 'gemini-3-flash-preview' => esc_html__('Gemini 3 Flash (Balanced Speed & Scale)', 'mxchat'),
6926 'gemini-2.5-pro' => esc_html__('Gemini 2.5 Pro (Advanced Thinking)', 'mxchat'),
6927 'gemini-2.5-flash' => esc_html__('Gemini 2.5 Flash (Best Price-Performance)', 'mxchat'),
6928 'gemini-2.5-flash-lite' => esc_html__('Gemini 2.5 Flash-Lite (Ultra Fast)', 'mxchat'),
6929 'gemini-2.0-flash' => esc_html__('Gemini 2.0 Flash (Deprecated Mar 2026)', 'mxchat'),
6930 'gemini-2.0-flash-lite' => esc_html__('Gemini 2.0 Flash-Lite (Deprecated Mar 2026)', 'mxchat'),
6931 'gemini-1.5-pro' => esc_html__('Gemini 1.5 Pro (Deprecated Sep 2025)', 'mxchat'),
6932 'gemini-1.5-flash' => esc_html__('Gemini 1.5 Flash (Deprecated Sep 2025)', 'mxchat'),
6933 ),
6934 esc_html__('X.AI Models', 'mxchat') => array(
6935 'grok-3-beta' => esc_html__('Grok-3 (Powerful)', 'mxchat'),
6936 'grok-3-fast-beta' => esc_html__('Grok-3 Fast (High Performance)', 'mxchat'),
6937 'grok-3-mini-beta' => esc_html__('Grok-3 Mini (Affordable)', 'mxchat'),
6938 'grok-3-mini-fast-beta' => esc_html__('Grok-3 Mini Fast (Quick Response)', 'mxchat'),
6939 'grok-2' => esc_html__('Grok 2', 'mxchat'),
6940 'grok-4-0709' => esc_html__('Grok 4 (Latest Flagship)', 'mxchat'),
6941 'grok-4-1-fast-reasoning' => esc_html__('Grok 4.1 Fast (Reasoning)', 'mxchat'),
6942 'grok-4-1-fast-non-reasoning' => esc_html__('Grok 4.1 Fast (Non-Reasoning)', 'mxchat'),
6943 ),
6944 esc_html__('DeepSeek Models', 'mxchat') => array(
6945 'deepseek-chat' => esc_html__('DeepSeek-V3', 'mxchat'),
6946 ),
6947 esc_html__('Claude Models', 'mxchat') => array(
6948 'claude-opus-4-6' => esc_html__('Claude Opus 4.6 (Most Capable - Recommended)', 'mxchat'),
6949 'claude-sonnet-4-6' => esc_html__('Claude Sonnet 4.6 (Latest Sonnet - Fast & Capable)', 'mxchat'),
6950 'claude-opus-4-5' => esc_html__('Claude Opus 4.5 (Highly Capable)', 'mxchat'),
6951 'claude-sonnet-4-5-20250929' => esc_html__('Claude Sonnet 4.5 (Best for Agents & Coding)', 'mxchat'),
6952 'claude-opus-4-1-20250805' => esc_html__('Claude Opus 4.1 (Exceptional for Complex Tasks)', 'mxchat'),
6953 'claude-haiku-4-5-20251001' => esc_html__('Claude Haiku 4.5 (Fastest & Most Intelligent)', 'mxchat'),
6954 'claude-opus-4-20250514' => esc_html__('Claude 4 Opus (Complex Tasks)', 'mxchat'),
6955 'claude-sonnet-4-20250514' => esc_html__('Claude 4 Sonnet (High Performance)', 'mxchat'),
6956 ),
6957 esc_html__('OpenAI Models', 'mxchat') => array(
6958 'gpt-5.4' => esc_html__('GPT-5.4 (Flagship Reasoning & Coding)', 'mxchat'),
6959 'gpt-5.4-mini' => esc_html__('GPT-5.4 Mini (Fast, 400K Context)', 'mxchat'),
6960 'gpt-5.4-nano' => esc_html__('GPT-5.4 Nano (Fastest & Cheapest)', 'mxchat'),
6961 'gpt-5.3-chat-latest' => esc_html__('GPT-5.3 Chat (Conversational)', 'mxchat'),
6962 'gpt-5.2' => esc_html__('GPT-5.2 (Best General-Purpose & Agentic Model)', 'mxchat'),
6963 'gpt-5.1-chat-latest' => esc_html__('GPT-5.1 Chat Latest (Recommended)', 'mxchat'),
6964 'gpt-5.1-2025-11-13' => esc_html__('GPT-5.1 (Flagship for Coding & Agentic Tasks)', 'mxchat'),
6965 'gpt-5' => esc_html__('GPT-5 (Flagship for Coding, Reasoning & Agents)', 'mxchat'),
6966 'gpt-5-mini' => esc_html__('GPT-5 Mini (Fast and Lightweight)', 'mxchat'),
6967 'gpt-5-nano' => esc_html__('GPT-5 Nano (Fastest & Cheapest for Summarization/Classification)', 'mxchat'),
6968 ),
6969 );
6970
6971 // Retrieve the currently selected model from saved options
6972 $selected_model = isset($this->options['model']) ? esc_attr($this->options['model']) : 'gpt-5.1-chat-latest';
6973
6974 // Begin the select dropdown
6975 echo '<select id="model" name="model">';
6976
6977 // Iterate over groups of models
6978 foreach ($models as $group_label => $group_models) {
6979 echo '<optgroup label="' . esc_attr($group_label) . '">';
6980
6981 foreach ($group_models as $model_value => $model_label) {
6982 echo '<option value="' . esc_attr($model_value) . '" ' . selected($selected_model, $model_value, false) . '>' . esc_html($model_label) . '</option>';
6983 }
6984
6985 echo '</optgroup>';
6986 }
6987
6988 echo '</select>';
6989
6990 // Add a note for OpenRouter
6991 echo '<p class="description" id="openrouter-model-note" style="display:none; color: #d63638; font-weight: 500;">';
6992 echo '<span class="dashicons dashicons-info" style="font-size: 16px; vertical-align: middle;"></span> ';
6993 echo esc_html__('After entering your OpenRouter API key above, click the button below to load available models.', 'mxchat');
6994 echo '</p>';
6995
6996 // API Key Status Messages (hidden by default, shown by JS based on selected model)
6997 $has_openai_key = !empty($this->options['api_key']);
6998 $has_claude_key = !empty($this->options['claude_api_key']);
6999 $has_xai_key = !empty($this->options['xai_api_key']);
7000 $has_deepseek_key = !empty($this->options['deepseek_api_key']);
7001 $has_gemini_key = !empty($this->options['gemini_api_key']);
7002 $has_openrouter_key = !empty($this->options['openrouter_api_key']);
7003
7004 // OpenAI/GPT models
7005 echo '<p class="mxchat-api-status" data-provider="openai" style="display:none;">';
7006 if ($has_openai_key) {
7007 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenAI detected', 'mxchat') . '</span>';
7008 } else {
7009 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenAI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7010 }
7011 echo '</p>';
7012
7013 // Claude models
7014 echo '<p class="mxchat-api-status" data-provider="claude" style="display:none;">';
7015 if ($has_claude_key) {
7016 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Anthropic (Claude) detected', 'mxchat') . '</span>';
7017 } else {
7018 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Anthropic (Claude) detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7019 }
7020 echo '</p>';
7021
7022 // X.AI models
7023 echo '<p class="mxchat-api-status" data-provider="xai" style="display:none;">';
7024 if ($has_xai_key) {
7025 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for X.AI (Grok) detected', 'mxchat') . '</span>';
7026 } else {
7027 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>';
7028 }
7029 echo '</p>';
7030
7031 // DeepSeek models
7032 echo '<p class="mxchat-api-status" data-provider="deepseek" style="display:none;">';
7033 if ($has_deepseek_key) {
7034 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for DeepSeek detected', 'mxchat') . '</span>';
7035 } else {
7036 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for DeepSeek detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7037 }
7038 echo '</p>';
7039
7040 // Gemini models
7041 echo '<p class="mxchat-api-status" data-provider="gemini" style="display:none;">';
7042 if ($has_gemini_key) {
7043 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Google Gemini detected', 'mxchat') . '</span>';
7044 } else {
7045 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Google Gemini detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7046 }
7047 echo '</p>';
7048
7049 // OpenRouter models
7050 echo '<p class="mxchat-api-status" data-provider="openrouter" style="display:none;">';
7051 if ($has_openrouter_key) {
7052 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenRouter detected', 'mxchat') . '</span>';
7053 } else {
7054 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenRouter detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7055 }
7056 echo '</p>';
7057
7058 // ADD THESE HIDDEN FIELDS RIGHT HERE:
7059 $openrouter_model = isset($this->options['openrouter_selected_model']) ? esc_attr($this->options['openrouter_selected_model']) : '';
7060 $openrouter_model_name = isset($this->options['openrouter_selected_model_name']) ? esc_attr($this->options['openrouter_selected_model_name']) : '';
7061
7062 echo '<input type="hidden" id="openrouter_selected_model" name="openrouter_selected_model" value="' . $openrouter_model . '" />';
7063 echo '<input type="hidden" id="openrouter_selected_model_name" name="openrouter_selected_model_name" value="' . $openrouter_model_name . '" />';
7064 }
7065
7066 // Update your existing callback method
7067 public function enable_streaming_toggle_callback() {
7068 // Get value from options array, default to 'on'
7069 $enabled = isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on';
7070 $checked = ($enabled === 'on') ? 'checked' : '';
7071
7072 echo '<label class="toggle-switch">';
7073 echo sprintf(
7074 '<input type="checkbox" id="enable_streaming_toggle" name="enable_streaming_toggle" value="on" %s />',
7075 esc_attr($checked)
7076 );
7077 echo '<span class="slider"></span>';
7078 echo '</label>';
7079
7080 // Test button with better styling
7081 echo '<div style="margin-top: 15px;">';
7082 echo '<button type="button" id="mxchat-test-streaming-btn" class="button button-secondary">';
7083 echo '<span class="dashicons dashicons-admin-tools" style="vertical-align: middle; margin-right: 5px;"></span>';
7084 echo esc_html__('Test Streaming Compatibility', 'mxchat');
7085 echo '</button>';
7086 echo '<p id="mxchat-test-streaming-result" style="margin-top:10px; font-weight: bold;"></p>';
7087 echo '</div>';
7088 }
7089
7090 // Web Search toggle callback
7091 public function enable_web_search_toggle_callback() {
7092 // Get value from options array, default to 'off'
7093 $enabled = isset($this->options['enable_web_search']) ? $this->options['enable_web_search'] : 'off';
7094 $checked = ($enabled === 'on') ? 'checked' : '';
7095
7096 // Get current model to determine if we should show/enable the toggle
7097 $current_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest';
7098
7099 // Models that DON'T support web search (per OpenAI docs)
7100 // gpt-5 with minimal reasoning is handled at API level, gpt-4.1-nano doesn't support it
7101 $unsupported_models = array('gpt-4.1-nano');
7102
7103 // Check if current model is an OpenAI model that supports web search
7104 $openai_models = array(
7105 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.4-nano', 'gpt-5.3-chat-latest',
7106 'gpt-5.2', 'gpt-5.1-chat-latest', 'gpt-5.1-2025-11-13', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano'
7107 );
7108
7109 $is_openai = in_array($current_model, $openai_models);
7110 $is_supported = $is_openai && !in_array($current_model, $unsupported_models);
7111
7112 // Wrapper div with data attributes for JS to show/hide
7113 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;"' : '') . '>';
7114
7115 echo '<label class="toggle-switch">';
7116 echo sprintf(
7117 '<input type="checkbox" id="enable_web_search" name="enable_web_search" value="on" %s />',
7118 esc_attr($checked)
7119 );
7120 echo '<span class="slider"></span>';
7121 echo '</label>';
7122
7123 echo '</div>';
7124
7125 // Message shown when non-OpenAI model is selected
7126 echo '<p id="web-search-unavailable-message" class="description" style="color: #666;' . ($is_supported ? ' display:none;' : '') . '">';
7127 echo '<span class="dashicons dashicons-info" style="font-size: 16px; vertical-align: middle; margin-right: 4px;"></span>';
7128 echo esc_html__('Web search is only available for OpenAI models.', 'mxchat');
7129 echo '</p>';
7130 }
7131
7132 // AJAX handler to fetch OpenRouter models
7133 public function fetch_openrouter_models() {
7134 check_ajax_referer('mxchat_fetch_openrouter_models', 'nonce');
7135
7136 $api_key = isset($_POST['api_key']) ? sanitize_text_field($_POST['api_key']) : '';
7137
7138 if (empty($api_key)) {
7139 wp_send_json_error(array('message' => 'API key is required'));
7140 }
7141
7142 $response = wp_remote_get('https://openrouter.ai/api/v1/models', array(
7143 'headers' => array(
7144 'Authorization' => 'Bearer ' . $api_key,
7145 'Content-Type' => 'application/json',
7146 ),
7147 'timeout' => 15,
7148 ));
7149
7150 if (is_wp_error($response)) {
7151 wp_send_json_error(array('message' => $response->get_error_message()));
7152 }
7153
7154 $body = wp_remote_retrieve_body($response);
7155 $data = json_decode($body, true);
7156
7157 if (isset($data['data']) && is_array($data['data'])) {
7158 // Format the models for the frontend
7159 $models = array_map(function($model) {
7160 return array(
7161 'id' => $model['id'],
7162 'name' => $model['name'] ?? $model['id'],
7163 'description' => $model['description'] ?? '',
7164 'context_length' => $model['context_length'] ?? 0,
7165 'pricing' => array(
7166 'prompt' => $model['pricing']['prompt'] ?? 0,
7167 'completion' => $model['pricing']['completion'] ?? 0,
7168 ),
7169 );
7170 }, $data['data']);
7171
7172 wp_send_json_success(array('models' => $models));
7173 } else {
7174 wp_send_json_error(array('message' => 'Invalid response from OpenRouter'));
7175 }
7176 }
7177
7178 // Callback function for embedding model selection
7179 public function embedding_model_callback() {
7180 $models = array(
7181 esc_html__('OpenAI Embeddings', 'mxchat') => array(
7182 'text-embedding-3-small' => esc_html__('TE3 Small (1536, Efficient)', 'mxchat'),
7183 'text-embedding-ada-002' => esc_html__('Ada 2 (1536, Recommended)', 'mxchat'),
7184 'text-embedding-3-large' => esc_html__('TE3 Large (3072, Powerful)', 'mxchat'),
7185 ),
7186 esc_html__('Voyage AI Embeddings', 'mxchat') => array(
7187 'voyage-3-large' => esc_html__('Voyage-3 Large (2048, Most Capable)', 'mxchat'),
7188 ),
7189 esc_html__('Google Gemini Embeddings', 'mxchat') => array(
7190 'gemini-embedding-001' => esc_html__('Gemini Embedding (1536, Stable)', 'mxchat'),
7191 )
7192 );
7193 $selected_model = isset($this->options['embedding_model']) ? esc_attr($this->options['embedding_model']) : 'text-embedding-ada-002';
7194 echo '<select id="embedding_model" name="embedding_model">';
7195 foreach ($models as $group_label => $group_models) {
7196 echo '<optgroup label="' . esc_attr($group_label) . '">';
7197 foreach ($group_models as $model_value => $model_label) {
7198 echo '<option value="' . esc_attr($model_value) . '" ' . selected($selected_model, $model_value, false) . '>' . esc_html($model_label) . '</option>';
7199 }
7200 echo '</optgroup>';
7201 }
7202 echo '</select>';
7203
7204 // API Key Status Messages for Embedding Models
7205 $has_openai_key = !empty($this->options['api_key']);
7206 $has_voyage_key = !empty($this->options['voyage_api_key']);
7207 $has_gemini_key = !empty($this->options['gemini_api_key']);
7208
7209 // OpenAI Embeddings
7210 echo '<p class="mxchat-embedding-api-status" data-provider="openai" style="display:none;">';
7211 if ($has_openai_key) {
7212 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenAI detected', 'mxchat') . '</span>';
7213 } else {
7214 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenAI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7215 }
7216 echo '</p>';
7217
7218 // Voyage AI Embeddings
7219 echo '<p class="mxchat-embedding-api-status" data-provider="voyage" style="display:none;">';
7220 if ($has_voyage_key) {
7221 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Voyage AI detected', 'mxchat') . '</span>';
7222 } else {
7223 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Voyage AI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7224 }
7225 echo '</p>';
7226
7227 // Gemini Embeddings
7228 echo '<p class="mxchat-embedding-api-status" data-provider="gemini" style="display:none;">';
7229 if ($has_gemini_key) {
7230 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Google Gemini detected', 'mxchat') . '</span>';
7231 } else {
7232 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Google Gemini detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7233 }
7234 echo '</p>';
7235
7236 }
7237
7238
7239 public function mxchat_top_bar_title_callback() {
7240 // Retrieve the current value of the top bar title from saved options
7241 $top_bar_title = isset($this->options['top_bar_title']) ? esc_attr($this->options['top_bar_title']) : '';
7242
7243 // Render the input field
7244 echo '<input type="text" id="top_bar_title" name="top_bar_title" value="' . $top_bar_title . '" />';
7245 }
7246 public function mxchat_ai_agent_text_callback() {
7247 // Retrieve the current value of the AI agent text from saved options
7248 $ai_agent_text = isset($this->options['ai_agent_text']) ? esc_attr($this->options['ai_agent_text']) : '';
7249 // Render the input field
7250 echo '<input type="text" id="ai_agent_text" name="ai_agent_text" value="' . $ai_agent_text . '" />';
7251 }
7252
7253
7254 public function enable_email_block_callback() {
7255 // Load full plugin options array
7256 $all_options = get_option('mxchat_options', []);
7257
7258 // Get the value, default to 'off'
7259 $enable_email_block = isset($all_options['enable_email_block']) ? $all_options['enable_email_block'] : 'off';
7260
7261 // Check if it's 'on'
7262 $checked = ($enable_email_block === 'on') ? 'checked' : '';
7263
7264 echo '<label class="toggle-switch">';
7265 echo sprintf(
7266 '<input type="checkbox" id="enable_email_block" name="enable_email_block" value="on" %s />',
7267 esc_attr($checked)
7268 );
7269 echo '<span class="slider"></span>';
7270 echo '</label>';
7271 }
7272
7273 public function email_blocker_header_content_callback() {
7274 // Load the entire 'mxchat_options' array
7275 $all_options = get_option('mxchat_options', []);
7276
7277 // Retrieve the saved content or default to empty
7278 $content = isset($all_options['email_blocker_header_content'])
7279 ? $all_options['email_blocker_header_content']
7280 : '';
7281
7282 // Render the textarea - IMPORTANT: name should be just "email_blocker_header_content"
7283 echo '<textarea
7284 id="email_blocker_header_content"
7285 name="email_blocker_header_content"
7286 rows="5"
7287 cols="70"
7288 data-setting="email_blocker_header_content"
7289 >' . esc_textarea($content) . '</textarea>';
7290 }
7291
7292 public function email_blocker_button_text_callback() {
7293 // Load the entire 'mxchat_options' array
7294 $all_options = get_option('mxchat_options', []);
7295
7296 // Retrieve the saved button text or default to empty
7297 $button_text = isset($all_options['email_blocker_button_text'])
7298 ? $all_options['email_blocker_button_text']
7299 : '';
7300
7301 // Use esc_attr to safely render the existing text
7302 echo '<input type="text" id="email_blocker_button_text" name="email_blocker_button_text" value="' . esc_attr($button_text) . '" style="width: 300px;" />';
7303 }
7304
7305 //Enable name field callback
7306 public function enable_name_field_callback() {
7307 // Load full plugin options array
7308 $all_options = get_option('mxchat_options', []);
7309 // Get the value, default to 'off'
7310 $enable_name_field = isset($all_options['enable_name_field']) ? $all_options['enable_name_field'] : 'off';
7311 // Check if it's 'on'
7312 $checked = ($enable_name_field === 'on') ? 'checked' : '';
7313 echo '<label class="toggle-switch">';
7314 echo sprintf(
7315 '<input type="checkbox" id="enable_name_field" name="enable_name_field" value="on" %s />',
7316 esc_attr($checked)
7317 );
7318 echo '<span class="slider"></span>';
7319 echo '</label>';
7320 }
7321 //Name field placeholder callback
7322 public function name_field_placeholder_callback() {
7323 $all_options = get_option('mxchat_options', []);
7324 $placeholder = isset($all_options['name_field_placeholder'])
7325 ? $all_options['name_field_placeholder']
7326 : esc_html__('Enter your name', 'mxchat');
7327
7328 echo '<input type="text" id="name_field_placeholder" name="name_field_placeholder" value="' . esc_attr($placeholder) . '" style="width: 300px;" />';
7329 }
7330
7331
7332
7333 public function mxchat_intro_message_callback() {
7334 // Load the entire 'mxchat_options' array
7335 $all_options = get_option('mxchat_options', []);
7336 // Retrieve the saved intro message or use the default
7337 $default_message = __('Hello! How can I assist you today?', 'mxchat');
7338 $saved_message = isset($all_options['intro_message']) ? $all_options['intro_message'] : $default_message;
7339 // Output the textarea with the saved value without escaping HTML
7340 ?>
7341 <textarea id="intro_message" name="intro_message" rows="5" cols="50"><?php echo $saved_message; ?></textarea>
7342 <p class="description" style="margin-top: 8px;">
7343 <?php esc_html_e('Use {visitor_name} to personalize greetings when lead capture is enabled.', 'mxchat'); ?><br>
7344 <code style="font-size: 12px;"><?php esc_html_e('Example: Hello {visitor_name}! How can I help you today?', 'mxchat'); ?></code>
7345 </p>
7346 <?php
7347 }
7348
7349 public function mxchat_input_copy_callback() {
7350 // Load the entire 'mxchat_options' array
7351 $all_options = get_option('mxchat_options', []);
7352
7353 // Retrieve the saved input copy or use the default value
7354 $default_copy = __('How can I assist?', 'mxchat');
7355 $input_copy = isset($all_options['input_copy']) ? $all_options['input_copy'] : $default_copy;
7356
7357 // Output the input field with the saved value
7358 printf(
7359 '<input type="text" id="input_copy" name="input_copy" value="%s" placeholder="%s" />',
7360 esc_attr($input_copy),
7361 esc_attr__('How can I assist?', 'mxchat')
7362 );
7363 }
7364
7365
7366 public function mxchat_append_to_body_callback() {
7367 // Fetch fresh options to ensure we have the latest saved values
7368 $options = get_option('mxchat_options', array());
7369
7370 // Get value from options array, default to 'off'
7371 $append_to_body = isset($options['append_to_body']) ? $options['append_to_body'] : 'off';
7372 $checked = ($append_to_body === 'on') ? 'checked' : '';
7373
7374 // Get post type visibility settings
7375 $visibility_mode = isset($options['post_type_visibility_mode']) ? $options['post_type_visibility_mode'] : 'all';
7376 $visibility_list = isset($options['post_type_visibility_list']) ? $options['post_type_visibility_list'] : array();
7377 if (!is_array($visibility_list)) {
7378 $visibility_list = array();
7379 }
7380
7381 echo '<div class="mxchat-autosave-section">';
7382
7383 // Main toggle
7384 echo '<label class="toggle-switch">';
7385 echo sprintf(
7386 '<input type="checkbox" id="append_to_body" name="append_to_body" value="on" %s />',
7387 esc_attr($checked)
7388 );
7389 echo '<span class="slider"></span>';
7390 echo '</label>';
7391
7392 // Post Type Visibility Options (only visible when auto-display is ON)
7393 $display_style = ($append_to_body === 'on') ? '' : 'display: none;';
7394 echo '<div id="post-type-visibility-options" class="mxchat-sub-options" style="' . esc_attr($display_style) . '">';
7395
7396 echo '<div class="mxchat-post-type-visibility-header">';
7397 echo '<h4>' . esc_html__('Post Type Visibility', 'mxchat') . '</h4>';
7398 echo '</div>';
7399
7400 // Mode selector (radio buttons)
7401 echo '<div class="mxchat-visibility-mode">';
7402
7403 echo '<label class="mxchat-radio-label">';
7404 echo '<input type="radio" name="post_type_visibility_mode" value="all" ' . checked($visibility_mode, 'all', false) . ' />';
7405 echo '<span>' . esc_html__('Show on all post types', 'mxchat') . '</span>';
7406 echo '</label>';
7407
7408 echo '<label class="mxchat-radio-label">';
7409 echo '<input type="radio" name="post_type_visibility_mode" value="include" ' . checked($visibility_mode, 'include', false) . ' />';
7410 echo '<span>' . esc_html__('Only show on selected post types', 'mxchat') . '</span>';
7411 echo '</label>';
7412
7413 echo '<label class="mxchat-radio-label">';
7414 echo '<input type="radio" name="post_type_visibility_mode" value="exclude" ' . checked($visibility_mode, 'exclude', false) . ' />';
7415 echo '<span>' . esc_html__('Hide on selected post types', 'mxchat') . '</span>';
7416 echo '</label>';
7417
7418 echo '</div>';
7419
7420 // Post type checkboxes (only visible when mode is include or exclude)
7421 $list_display = ($visibility_mode !== 'all') ? '' : 'display: none;';
7422 echo '<div id="post-type-list" class="mxchat-post-type-list" style="' . esc_attr($list_display) . '">';
7423
7424 // Get all public post types
7425 $post_types = get_post_types(array('public' => true), 'objects');
7426
7427 foreach ($post_types as $post_type) {
7428 // Skip attachments
7429 if ($post_type->name === 'attachment') {
7430 continue;
7431 }
7432
7433 $is_checked = in_array($post_type->name, $visibility_list) ? 'checked' : '';
7434
7435 echo '<label class="mxchat-checkbox-label">';
7436 echo '<input type="checkbox" name="post_type_visibility_list[]" value="' . esc_attr($post_type->name) . '" ' . $is_checked . ' />';
7437 echo '<span>' . esc_html($post_type->label) . '</span>';
7438 echo '</label>';
7439 }
7440
7441 echo '</div>'; // End post-type-list
7442 echo '</div>'; // End post-type-visibility-options
7443 echo '</div>'; // End mxchat-autosave-section
7444 }
7445
7446 public function mxchat_contextual_awareness_callback() {
7447 // Get value from options array, default to 'off'
7448 $contextual_awareness = isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off';
7449 $checked = ($contextual_awareness === 'on') ? 'checked' : '';
7450 echo '<label class="toggle-switch">';
7451 echo sprintf(
7452 '<input type="checkbox" id="contextual_awareness_toggle" name="contextual_awareness_toggle" value="on" %s />',
7453 esc_attr($checked)
7454 );
7455 echo '<span class="slider"></span>';
7456 echo '</label>';
7457 }
7458
7459 public function mxchat_citation_links_toggle_callback() {
7460 // Get value from options array, default to 'on' (enabled by default)
7461 $citation_links = isset($this->options['citation_links_toggle']) ? $this->options['citation_links_toggle'] : 'on';
7462 $checked = ($citation_links === 'on') ? 'checked' : '';
7463 echo '<label class="toggle-switch">';
7464 echo sprintf(
7465 '<input type="checkbox" id="citation_links_toggle" name="citation_links_toggle" value="on" %s />',
7466 esc_attr($checked)
7467 );
7468 echo '<span class="slider"></span>';
7469 echo '</label>';
7470 }
7471
7472 public function mxchat_privacy_toggle_callback() {
7473 // Load from mxchat_options array
7474 $options = get_option('mxchat_options', []);
7475
7476 // Get privacy toggle value with fallback
7477 $privacy_toggle = isset($options['privacy_toggle']) ? $options['privacy_toggle'] : 'off';
7478 $checked = ($privacy_toggle === 'on') ? 'checked' : '';
7479
7480 // Get privacy text with fallback
7481 $privacy_text = isset($options['privacy_text'])
7482 ? $options['privacy_text']
7483 : __('By chatting, you agree to our <a href="https://example.com/privacy-policy" target="_blank">privacy policy</a>.', 'mxchat');
7484
7485 // Output the toggle switch
7486 echo '<label class="toggle-switch">';
7487 echo sprintf(
7488 '<input type="checkbox" id="privacy_toggle" name="privacy_toggle" value="on" %s />',
7489 esc_attr($checked)
7490 );
7491 echo '<span class="slider"></span>';
7492 echo '</label>';
7493
7494 // Output the custom text input field
7495 echo sprintf(
7496 '<textarea id="privacy_text" name="privacy_text" rows="5" cols="50" class="regular-text">%s</textarea>',
7497 esc_textarea($privacy_text)
7498 );
7499 }
7500
7501
7502 public function mxchat_complianz_toggle_callback() {
7503 // Load from mxchat_options array
7504 $options = get_option('mxchat_options', []);
7505
7506 // Get complianz toggle value with fallback
7507 $complianz_toggle = isset($options['complianz_toggle']) ? $options['complianz_toggle'] : 'off';
7508 $checked = ($complianz_toggle === 'on') ? 'checked' : '';
7509
7510 // Output the toggle switch
7511 echo '<label class="toggle-switch">';
7512 echo sprintf(
7513 '<input type="checkbox" id="complianz_toggle" name="complianz_toggle" value="on" %s />',
7514 esc_attr($checked)
7515 );
7516 echo '<span class="slider"></span>';
7517 echo '</label>';
7518 }
7519
7520 public function mxchat_link_target_toggle_callback() {
7521 // Load from mxchat_options array
7522 $options = get_option('mxchat_options', []);
7523
7524 // Get link target toggle value with fallback
7525 $link_target_toggle = isset($options['link_target_toggle']) ? $options['link_target_toggle'] : 'off';
7526 $checked = ($link_target_toggle === 'on') ? 'checked' : '';
7527
7528 // Output the toggle switch
7529 echo '<label class="toggle-switch">';
7530 echo sprintf(
7531 '<input type="checkbox" id="link_target_toggle" name="link_target_toggle" value="on" %s />',
7532 esc_attr($checked)
7533 );
7534 echo '<span class="slider"></span>';
7535 echo '</label>';
7536 }
7537
7538 public function mxchat_chat_persistence_toggle_callback() {
7539 // Load from mxchat_options array
7540 $options = get_option('mxchat_options', []);
7541
7542 // Get chat persistence toggle value with fallback
7543 $chat_persistence_toggle = isset($options['chat_persistence_toggle']) ? $options['chat_persistence_toggle'] : 'off';
7544 $checked = ($chat_persistence_toggle === 'on') ? 'checked' : '';
7545
7546 // Output the toggle switch
7547 echo '<label class="toggle-switch">';
7548 echo sprintf(
7549 '<input type="checkbox" id="chat_persistence_toggle" name="chat_persistence_toggle" value="on" %s />',
7550 esc_attr($checked)
7551 );
7552 echo '<span class="slider"></span>';
7553 echo '</label>';
7554 }
7555
7556 public function mxchat_popular_question_1_callback() {
7557 // Load the full plugin options array
7558 $all_options = get_option('mxchat_options', []);
7559
7560 // Retrieve the specific option for popular_question_1
7561 $popular_question_1 = isset($all_options['popular_question_1']) ? $all_options['popular_question_1'] : '';
7562
7563 // Render the input field
7564 printf(
7565 '<input type="text" id="popular_question_1" name="popular_question_1" value="%s" placeholder="%s" class="regular-text" />',
7566 esc_attr($popular_question_1),
7567 esc_attr__('Enter Quick Question 1', 'mxchat')
7568 );
7569 }
7570
7571
7572 public function mxchat_popular_question_2_callback() {
7573 // Load the full plugin options array
7574 $all_options = get_option('mxchat_options', []);
7575
7576 // Retrieve the specific option for popular_question_2
7577 $popular_question_2 = isset($all_options['popular_question_2']) ? $all_options['popular_question_2'] : '';
7578
7579 // Render the input field
7580 printf(
7581 '<input type="text" id="popular_question_2" name="popular_question_2" value="%s" placeholder="%s" class="regular-text" />',
7582 esc_attr($popular_question_2),
7583 esc_attr__('Enter Quick Question 2', 'mxchat')
7584 );
7585 }
7586
7587
7588 public function mxchat_popular_question_3_callback() {
7589 // Load the full plugin options array
7590 $all_options = get_option('mxchat_options', []);
7591
7592 // Retrieve the specific option for popular_question_3
7593 $popular_question_3 = isset($all_options['popular_question_3']) ? $all_options['popular_question_3'] : '';
7594
7595 // Render the input field
7596 printf(
7597 '<input type="text" id="popular_question_3" name="popular_question_3" value="%s" placeholder="%s" class="regular-text" />',
7598 esc_attr($popular_question_3),
7599 esc_attr(__('Enter Quick Question 3', 'mxchat'))
7600 );
7601 }
7602
7603 public function mxchat_additional_popular_questions_callback() {
7604 $options = get_option('mxchat_options', []);
7605 $additional_questions = isset($options['additional_popular_questions'])
7606 ? $options['additional_popular_questions']
7607 : get_option('additional_popular_questions', array());
7608
7609 echo '<div id="mxchat-additional-questions-container">';
7610 if (!empty($additional_questions)) {
7611 foreach ($additional_questions as $index => $question) {
7612 printf(
7613 '<div class="mxchat-question-row">
7614 <input type="text" name="additional_popular_questions[]"
7615 value="%s"
7616 placeholder="%s"
7617 class="regular-text mxchat-question-input"
7618 data-question-index="%d" />
7619 <button type="button" class="button mxchat-remove-question"
7620 aria-label="%s">%s</button>
7621 </div>',
7622 esc_attr($question),
7623 esc_attr(sprintf(__('Enter Additional Quick Question %d', 'mxchat'), $index + 4)),
7624 $index,
7625 esc_attr(__('Remove question', 'mxchat')),
7626 esc_html__('Remove', 'mxchat')
7627 );
7628 }
7629 } else {
7630 printf(
7631 '<div class="mxchat-question-row">
7632 <input type="text" name="additional_popular_questions[]"
7633 value=""
7634 placeholder="%s"
7635 class="regular-text mxchat-question-input"
7636 data-question-index="0" />
7637 <button type="button" class="button mxchat-remove-question"
7638 aria-label="%s">%s</button>
7639 </div>',
7640 esc_attr(__('Enter Additional Quick Question 4', 'mxchat')),
7641 esc_attr(__('Remove question', 'mxchat')),
7642 esc_html__('Remove', 'mxchat')
7643 );
7644 }
7645 echo '</div>';
7646 printf(
7647 '<button type="button" class="button mxchat-add-question" aria-label="%s">%s</button>',
7648 esc_attr(__('Add question', 'mxchat')),
7649 esc_html__('Add Question', 'mxchat')
7650 );
7651 }
7652
7653 public function mxchat_brave_api_key_callback() {
7654 $brave_api_key = isset($this->options['brave_api_key']) ? esc_attr($this->options['brave_api_key']) : '';
7655 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7656
7657 echo '<div class="api-key-wrapper">';
7658 echo sprintf(
7659 '<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" />',
7660 $brave_api_key,
7661 $nonce
7662 );
7663 echo '<button type="button" id="toggleBraveApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7664 echo '</div>';
7665 echo '<p class="description">' . __('Required for Brave Search integration. Get your API key from Brave Search API.', 'mxchat') . '</p>';
7666 }
7667
7668 public function mxchat_brave_image_count_callback() {
7669 $brave_image_count = isset($this->options['brave_image_count'])
7670 ? intval($this->options['brave_image_count'])
7671 : 4;
7672 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7673
7674 echo '<div class="mxchat-field-wrapper">';
7675 echo sprintf(
7676 '<input type="number" id="brave_image_count" name="brave_image_count"
7677 value="%d" min="1" max="6" class="small-text mxchat-autosave-field" data-nonce="%s" />',
7678 $brave_image_count,
7679 $nonce
7680 );
7681 echo '</div>';
7682 echo '<p class="description">' . __('Select the number of images to return (1-6).', 'mxchat') . '</p>';
7683 }
7684
7685 public function mxchat_brave_safe_search_callback() {
7686 $brave_safe_search = isset($this->options['brave_safe_search'])
7687 ? esc_attr($this->options['brave_safe_search'])
7688 : 'strict';
7689 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7690
7691 echo '<div class="mxchat-field-wrapper">';
7692 echo '<select id="brave_safe_search" name="brave_safe_search" class="mxchat-autosave-field" data-nonce="' . $nonce . '">';
7693 echo sprintf(
7694 '<option value="strict" %s>%s</option>',
7695 selected($brave_safe_search, 'strict', false),
7696 __('Strict', 'mxchat')
7697 );
7698 echo sprintf(
7699 '<option value="off" %s>%s</option>',
7700 selected($brave_safe_search, 'off', false),
7701 __('Off', 'mxchat')
7702 );
7703 echo '</select>';
7704 echo '</div>';
7705 echo '<p class="description">' .
7706 esc_html__('Set the Safe Search level for image searches. Brave Search only supports "Strict" and "Off" options.', 'mxchat') .
7707 '</p>';
7708 }
7709
7710 public function mxchat_brave_news_count_callback() {
7711 $brave_news_count = isset($this->options['brave_news_count'])
7712 ? intval($this->options['brave_news_count'])
7713 : 3;
7714 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7715
7716 echo '<div class="mxchat-field-wrapper">';
7717 echo sprintf(
7718 '<input type="number" id="brave_news_count" name="brave_news_count"
7719 value="%d" min="1" max="10" class="small-text mxchat-autosave-field" data-nonce="%s" />',
7720 $brave_news_count,
7721 $nonce
7722 );
7723 echo '</div>';
7724 echo '<p class="description">' . esc_html__('Select the number of news articles to retrieve (1-10).', 'mxchat') . '</p>';
7725 }
7726
7727 public function mxchat_brave_country_callback() {
7728 $brave_country = isset($this->options['brave_country'])
7729 ? esc_attr($this->options['brave_country'])
7730 : 'us';
7731 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7732
7733 echo '<div class="mxchat-field-wrapper">';
7734 echo sprintf(
7735 '<input type="text" id="brave_country" name="brave_country"
7736 value="%s" maxlength="2" class="small-text mxchat-autosave-field" data-nonce="%s" />',
7737 $brave_country,
7738 $nonce
7739 );
7740 echo '</div>';
7741 echo '<p class="description">' . esc_html__('Enter the country code (e.g., "us" for United States).', 'mxchat') . '</p>';
7742 }
7743
7744 public function mxchat_brave_language_callback() {
7745 $brave_language = isset($this->options['brave_language'])
7746 ? esc_attr($this->options['brave_language'])
7747 : 'en';
7748 $nonce = wp_create_nonce('mxchat_autosave_nonce');
7749
7750 echo '<div class="mxchat-field-wrapper">';
7751 echo sprintf(
7752 '<input type="text" id="brave_language" name="brave_language"
7753 value="%s" maxlength="2" class="small-text mxchat-autosave-field" data-nonce="%s" />',
7754 $brave_language,
7755 $nonce
7756 );
7757 echo '</div>';
7758 echo '<p class="description">' . esc_html__('Enter the language code (e.g., "en" for English).', 'mxchat') . '</p>';
7759 }
7760
7761
7762
7763
7764
7765 // Section Callback
7766 public function mxchat_pdf_intent_section_callback() {
7767 echo '<p>' . esc_html__('Configure the intent settings for the Chat with PDF feature.', 'mxchat') . '</p>';
7768 }
7769
7770 public function mxchat_chat_toolbar_toggle_callback() {
7771 // Get chat toolbar toggle value with fallback
7772 $chat_toolbar_toggle = isset($this->options['chat_toolbar_toggle']) ? $this->options['chat_toolbar_toggle'] : 'off';
7773 $checked = ($chat_toolbar_toggle === 'on') ? 'checked' : '';
7774
7775 // Output the toggle switch
7776 echo '<label class="toggle-switch">';
7777 echo sprintf(
7778 '<input type="checkbox" id="chat_toolbar_toggle" name="chat_toolbar_toggle" value="on" %s />',
7779 esc_attr($checked)
7780 );
7781 echo '<span class="slider"></span>';
7782 echo '</label>';
7783
7784 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>';
7785 }
7786
7787 /**
7788 * Callback for PDF upload button toggle setting
7789 */
7790 public function mxchat_show_pdf_upload_button_callback() {
7791 // Get toggle value with fallback
7792 $show_pdf_button = isset($this->options['show_pdf_upload_button']) ? $this->options['show_pdf_upload_button'] : 'on';
7793 $checked = ($show_pdf_button === 'on') ? 'checked' : '';
7794
7795 // Output the toggle switch
7796 echo '<label class="toggle-switch">';
7797 echo sprintf(
7798 '<input type="checkbox" id="show_pdf_upload_button" name="show_pdf_upload_button" value="on" %s />',
7799 esc_attr($checked)
7800 );
7801 echo '<span class="slider"></span>';
7802 echo '</label>';
7803
7804 echo '<p class="description">' . esc_html__('Enable to show the PDF upload button in the chatbot toolbar. Disable to hide it.', 'mxchat') . '</p>';
7805 }
7806
7807 /**
7808 * Callback for Word upload button toggle setting
7809 */
7810 public function mxchat_show_word_upload_button_callback() {
7811 // Get toggle value with fallback
7812 $show_word_button = isset($this->options['show_word_upload_button']) ? $this->options['show_word_upload_button'] : 'on';
7813 $checked = ($show_word_button === 'on') ? 'checked' : '';
7814
7815 // Output the toggle switch
7816 echo '<label class="toggle-switch">';
7817 echo sprintf(
7818 '<input type="checkbox" id="show_word_upload_button" name="show_word_upload_button" value="on" %s />',
7819 esc_attr($checked)
7820 );
7821 echo '<span class="slider"></span>';
7822 echo '</label>';
7823
7824 echo '<p class="description">' . esc_html__('Enable to show the Word document upload button in the chatbot toolbar. Disable to hide it.', 'mxchat') . '</p>';
7825 }
7826
7827 public function mxchat_pdf_intent_trigger_text_callback() {
7828 $default_text = __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat');
7829
7830 echo sprintf(
7831 '<textarea id="pdf_intent_trigger_text"
7832 name="pdf_intent_trigger_text"
7833 rows="3"
7834 cols="50"
7835 placeholder="%s">%s</textarea>',
7836 esc_attr__('Enter trigger text', 'mxchat'),
7837 isset($this->options['pdf_intent_trigger_text'])
7838 ? esc_textarea($this->options['pdf_intent_trigger_text'])
7839 : esc_textarea($default_text)
7840 );
7841 echo '<p class="description">' . esc_html__('Text displayed when the intent is triggered.', 'mxchat') . '</p>';
7842 }
7843
7844 public function mxchat_pdf_intent_success_text_callback() {
7845 $default_text = __("I've processed the PDF. What questions do you have about it?", 'mxchat');
7846
7847 echo sprintf(
7848 '<textarea id="pdf_intent_success_text"
7849 name="pdf_intent_success_text"
7850 rows="3"
7851 cols="50"
7852 placeholder="%s">%s</textarea>',
7853 esc_attr__('Enter success text', 'mxchat'),
7854 isset($this->options['pdf_intent_success_text'])
7855 ? esc_textarea($this->options['pdf_intent_success_text'])
7856 : esc_textarea($default_text)
7857 );
7858 echo '<p class="description">' . esc_html__('Text displayed when the intent is successful.', 'mxchat') . '</p>';
7859 }
7860
7861 public function mxchat_pdf_intent_error_text_callback() {
7862 $default_text = __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
7863
7864 echo sprintf(
7865 '<textarea id="pdf_intent_error_text"
7866 name="pdf_intent_error_text"
7867 rows="3"
7868 cols="50"
7869 placeholder="%s">%s</textarea>',
7870 esc_attr__('Enter error text', 'mxchat'),
7871 isset($this->options['pdf_intent_error_text'])
7872 ? esc_textarea($this->options['pdf_intent_error_text'])
7873 : esc_textarea($default_text)
7874 );
7875 echo '<p class="description">' . esc_html__('Text displayed when an error occurs during the intent.', 'mxchat') . '</p>';
7876 }
7877
7878 public function mxchat_pdf_max_pages_callback() {
7879 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
7880
7881 echo sprintf(
7882 '<input type="range"
7883 id="pdf_max_pages"
7884 name="pdf_max_pages"
7885 min="1"
7886 max="69"
7887 value="%d"
7888 class="range-slider" />',
7889 esc_attr($max_pages)
7890 );
7891 echo '<span id="pdf_max_pages_output">' . esc_html($max_pages) . '</span>';
7892 echo '<p class="description">' . esc_html__('Set the maximum number of document pages users can upload for processing. (1-69 pages)', 'mxchat') . '</p>';
7893 }
7894
7895 public function mxchat_live_agent_status_callback() {
7896 // Always get fresh options instead of using cached $this->options
7897 $fresh_options = get_option('mxchat_options');
7898 $status = isset($fresh_options['live_agent_status']) ? $fresh_options['live_agent_status'] : 'off';
7899
7900 echo '<label class="toggle-switch">';
7901 echo sprintf(
7902 '<input type="checkbox" id="live_agent_status" name="live_agent_status" value="on" %s />',
7903 checked($status, 'on', false)
7904 );
7905 echo '<span class="slider"></span>';
7906 echo '</label>';
7907 echo '<label for="live_agent_status" class="mxchat-status-label">';
7908 echo '<span class="status-text">' . ($status === 'on' ? esc_html__('Online', 'mxchat') : esc_html__('Offline', 'mxchat')) . '</span>';
7909 echo '</label>';
7910 }
7911
7912 public function mxchat_live_agent_away_message_callback() {
7913 $message = isset($this->options['live_agent_away_message'])
7914 ? $this->options['live_agent_away_message']
7915 : __('Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.', 'mxchat');
7916
7917 printf(
7918 '<textarea id="live_agent_away_message" name="live_agent_away_message" rows="3" cols="50">%s</textarea>',
7919 esc_textarea($message)
7920 );
7921 echo '<p class="description">' . esc_html__('Message shown when live agents are offline.', 'mxchat') . '</p>';
7922 }
7923
7924 public function mxchat_live_agent_notification_message_callback() {
7925 $message = isset($this->options['live_agent_notification_message'])
7926 ? $this->options['live_agent_notification_message']
7927 : __('Live agent has been notified.', 'mxchat');
7928
7929 printf(
7930 '<textarea id="live_agent_notification_message" name="live_agent_notification_message" rows="3" cols="50">%s</textarea>',
7931 esc_textarea($message)
7932 );
7933 echo '<p class="description">' . esc_html__('Message shown when live transfer activated.', 'mxchat') . '</p>';
7934 }
7935
7936 public function mxchat_live_agent_webhook_url_callback() {
7937 $webhook_url = isset($this->options['live_agent_webhook_url'])
7938 ? esc_url($this->options['live_agent_webhook_url'])
7939 : esc_url(get_option('live_agent_webhook_url', ''));
7940
7941 printf(
7942 '<input type="password" id="live_agent_webhook_url" name="live_agent_webhook_url" value="%s" class="regular-text" />',
7943 $webhook_url
7944 );
7945 echo '<button type="button" id="toggleWebhookUrlVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7946 echo '<p class="description">' . esc_html__('Enter your Slack webhook URL for live agent notifications.', 'mxchat') . '</p>';
7947 }
7948
7949 public function mxchat_live_agent_secret_key_callback() {
7950 printf(
7951 '<input type="password" id="live_agent_secret_key" name="live_agent_secret_key" value="%s" class="regular-text" />',
7952 isset($this->options['live_agent_secret_key']) ? esc_attr($this->options['live_agent_secret_key']) : ''
7953 );
7954 echo '<button type="button" id="toggleSecretKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7955 echo '<p class="description">' . esc_html__('Secret key for validating Slack requests. Keep this secure.', 'mxchat') . '</p>';
7956 }
7957
7958 public function mxchat_live_agent_bot_token_callback() {
7959 printf(
7960 '<input type="password" id="live_agent_bot_token" name="live_agent_bot_token" value="%s" class="regular-text" />',
7961 isset($this->options['live_agent_bot_token']) ? esc_attr($this->options['live_agent_bot_token']) : ''
7962 );
7963 echo '<button type="button" id="toggleBotTokenVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7964 echo '<p class="description">' . esc_html__('Your Slack Bot OAuth Token (starts with xoxb-). Keep this secure.', 'mxchat') . '</p>';
7965 }
7966
7967 public function mxchat_live_agent_user_ids_callback() {
7968 $user_ids = isset($this->options['live_agent_user_ids'])
7969 ? esc_textarea($this->options['live_agent_user_ids'])
7970 : '';
7971
7972 printf(
7973 '<textarea id="live_agent_user_ids" name="live_agent_user_ids" rows="4" class="large-text">%s</textarea>',
7974 $user_ids
7975 );
7976 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>';
7977 }
7978
7979 /**
7980 * Telegram Integration Callbacks
7981 */
7982 public function mxchat_telegram_section_callback() {
7983 echo '<p>' . esc_html__('Configure Telegram integration for live agent support.', 'mxchat') . '</p>';
7984 }
7985
7986 public function mxchat_telegram_status_callback() {
7987 $fresh_options = get_option('mxchat_options');
7988 $status = isset($fresh_options['telegram_status']) ? $fresh_options['telegram_status'] : 'off';
7989
7990 echo '<label class="toggle-switch">';
7991 echo sprintf(
7992 '<input type="checkbox" id="telegram_status" name="telegram_status" value="on" %s />',
7993 checked($status, 'on', false)
7994 );
7995 echo '<span class="slider"></span>';
7996 echo '</label>';
7997 echo '<label for="telegram_status" class="mxchat-status-label">';
7998 echo '<span class="status-text">' . ($status === 'on' ? esc_html__('Online', 'mxchat') : esc_html__('Offline', 'mxchat')) . '</span>';
7999 echo '</label>';
8000 }
8001
8002 public function mxchat_telegram_notification_message_callback() {
8003 $message = isset($this->options['telegram_notification_message'])
8004 ? $this->options['telegram_notification_message']
8005 : __("I've notified a support agent. Please allow a moment for them to respond.", 'mxchat');
8006
8007 printf(
8008 '<textarea id="telegram_notification_message" name="telegram_notification_message" rows="3" cols="50">%s</textarea>',
8009 esc_textarea($message)
8010 );
8011 echo '<p class="description">' . esc_html__('Message shown when live transfer activated.', 'mxchat') . '</p>';
8012 }
8013
8014 public function mxchat_telegram_away_message_callback() {
8015 $message = isset($this->options['telegram_away_message'])
8016 ? $this->options['telegram_away_message']
8017 : __('Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.', 'mxchat');
8018
8019 printf(
8020 '<textarea id="telegram_away_message" name="telegram_away_message" rows="3" cols="50">%s</textarea>',
8021 esc_textarea($message)
8022 );
8023 echo '<p class="description">' . esc_html__('Message shown when live agents are offline.', 'mxchat') . '</p>';
8024 }
8025
8026 public function mxchat_telegram_bot_token_callback() {
8027 printf(
8028 '<input type="password" id="telegram_bot_token" name="telegram_bot_token" value="%s" class="regular-text" />',
8029 isset($this->options['telegram_bot_token']) ? esc_attr($this->options['telegram_bot_token']) : ''
8030 );
8031 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>';
8032 echo '<p class="description">' . esc_html__('Your Telegram Bot Token from @BotFather (e.g., 123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ).', 'mxchat') . '</p>';
8033 }
8034
8035 public function mxchat_telegram_group_id_callback() {
8036 printf(
8037 '<input type="text" id="telegram_group_id" name="telegram_group_id" value="%s" class="regular-text" />',
8038 isset($this->options['telegram_group_id']) ? esc_attr($this->options['telegram_group_id']) : ''
8039 );
8040 echo '<p class="description">' . esc_html__('Your Telegram supergroup ID (starts with -100). The group must have forum topics enabled.', 'mxchat') . '</p>';
8041 }
8042
8043 public function mxchat_telegram_webhook_secret_callback() {
8044 $secret = isset($this->options['telegram_webhook_secret']) ? $this->options['telegram_webhook_secret'] : '';
8045
8046 // Auto-generate secret if empty
8047 if (empty($secret)) {
8048 $secret = wp_generate_password(64, false, false);
8049 $options = get_option('mxchat_options', []);
8050 $options['telegram_webhook_secret'] = $secret;
8051 update_option('mxchat_options', $options);
8052 $this->options['telegram_webhook_secret'] = $secret;
8053 }
8054
8055 printf(
8056 '<input type="password" id="telegram_webhook_secret" name="telegram_webhook_secret" value="%s" class="regular-text" />',
8057 esc_attr($secret)
8058 );
8059 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>';
8060 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>';
8061 }
8062
8063 public function mxchat_similarity_threshold_callback() {
8064 // Load from mxchat_options array
8065 $options = get_option('mxchat_options', []);
8066
8067 // Get value from options array with default of 35
8068 $threshold = isset($options['similarity_threshold']) ? $options['similarity_threshold'] : 35;
8069
8070 echo '<div class="slider-container">';
8071 echo sprintf(
8072 '<input type="range"
8073 id="similarity_threshold"
8074 name="similarity_threshold"
8075 min="20"
8076 max="85"
8077 step="1"
8078 value="%s"
8079 class="range-slider" />',
8080 esc_attr($threshold)
8081 );
8082 echo sprintf(
8083 '<span id="threshold_value" class="range-value">%s</span>',
8084 esc_html($threshold)
8085 );
8086 echo '</div>';
8087 }
8088
8089 public function mxchat_rag_sources_limit_callback() {
8090 // Load from mxchat_options array
8091 $options = get_option('mxchat_options', []);
8092
8093 // Get value from options array with default of 3
8094 $rag_sources_limit = isset($options['rag_sources_limit']) ? intval($options['rag_sources_limit']) : 3;
8095
8096 echo '<div class="slider-container">';
8097 echo sprintf(
8098 '<input type="range"
8099 id="rag_sources_limit"
8100 name="rag_sources_limit"
8101 min="3"
8102 max="10"
8103 step="1"
8104 value="%s"
8105 class="range-slider" />',
8106 esc_attr($rag_sources_limit)
8107 );
8108 echo sprintf(
8109 '<span id="rag_sources_limit_value" class="range-value">%s</span>',
8110 esc_html($rag_sources_limit)
8111 );
8112 echo '</div>';
8113 }
8114
8115 public function mxchat_rag_chunks_limit_callback() {
8116 // Load from mxchat_options array
8117 $options = get_option('mxchat_options', []);
8118
8119 // Get value from options array with default of 15
8120 $rag_chunks_limit = isset($options['rag_chunks_limit']) ? intval($options['rag_chunks_limit']) : 15;
8121
8122 echo '<div class="slider-container">';
8123 echo sprintf(
8124 '<input type="range"
8125 id="rag_chunks_limit"
8126 name="rag_chunks_limit"
8127 min="8"
8128 max="20"
8129 step="1"
8130 value="%s"
8131 class="range-slider" />',
8132 esc_attr($rag_chunks_limit)
8133 );
8134 echo sprintf(
8135 '<span id="rag_chunks_limit_value" class="range-value">%s</span>',
8136 esc_html($rag_chunks_limit)
8137 );
8138 echo '</div>';
8139 }
8140
8141 public function mxchat_enqueue_admin_assets() {
8142 // Get plugin version
8143 $version = MXCHAT_VERSION;
8144
8145 // Use file modification time for development (remove in production)
8146 if (defined('WP_DEBUG') && WP_DEBUG) {
8147 $version = filemtime(plugin_dir_path(__FILE__) . '../mxchat-basic.php');
8148 }
8149
8150 $current_page = isset($_GET['page']) ? $_GET['page'] : '';
8151 $plugin_url = plugin_dir_url(__FILE__) . '../';
8152
8153 // Only load on MxChat pages
8154 if (strpos($current_page, 'mxchat') === false) {
8155 return;
8156 }
8157
8158 // Always load these on all MxChat pages
8159 $this->enqueue_core_admin_assets($plugin_url, $version);
8160 $this->enqueue_page_specific_assets($current_page, $plugin_url, $version);
8161 $this->localize_admin_scripts($current_page);
8162 }
8163 private function enqueue_core_admin_assets($plugin_url, $version) {
8164 // Core admin styles
8165 wp_enqueue_style('mxchat-admin-css', $plugin_url . 'css/admin-style.css', array(), $version);
8166 wp_enqueue_style('mxchat-knowledge-css', $plugin_url . 'css/knowledge-style.css', array(), $version);
8167
8168 // New sidebar navigation styles (for main settings page)
8169 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8170
8171 // Core admin scripts
8172 wp_enqueue_script('mxchat-admin-js', $plugin_url . 'js/mxchat-admin.js', array('jquery'), $version, true);
8173 }
8174 private function enqueue_page_specific_assets($current_page, $plugin_url, $version) {
8175 switch ($current_page) {
8176 case 'mxchat-prompts':
8177 // Knowledge processing page assets
8178 wp_enqueue_style('mxchat-content-selector-css', $plugin_url . 'css/content-selector.css', array(), $version);
8179 wp_enqueue_script('mxchat-content-selector-js', $plugin_url . 'js/content-selector.js', array('jquery'), $version, true);
8180 // Add the knowledge processing script (common script needed for WordPress dismiss functionality)
8181 wp_enqueue_script('mxchat-knowledge-processing', $plugin_url . 'js/knowledge-processing.js', array('jquery', 'common'), $version, true);
8182 break;
8183
8184 case 'mxchat-transcripts':
8185 // Load admin sidebar CSS first (shared styles for sidebar navigation)
8186 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8187 // Load transcripts-specific styles
8188 wp_enqueue_style('mxchat-chat-transcripts-css', $plugin_url . 'css/chat-transcripts.css', array('mxchat-admin-sidebar-css'), $version);
8189 wp_enqueue_script('mxchat-transcripts-js', $plugin_url . 'js/mxchat_transcripts.js', array('jquery'), $version, true);
8190 break;
8191
8192 case 'mxchat-actions':
8193 // Load admin sidebar CSS first (shared styles for sidebar navigation)
8194 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8195 // Load actions-specific styles
8196 wp_enqueue_style('mxchat-actions-css', $plugin_url . 'css/actions.css', array('mxchat-admin-sidebar-css'), $version);
8197 wp_enqueue_script('mxchat-actions-js', $plugin_url . 'js/mxchat_actions.js', array('jquery'), $version, true);
8198
8199 // Localize script data for actions page
8200 $is_activated = get_option('mxchat_pro_license_status') === 'active';
8201 wp_localize_script('mxchat-actions-js', 'mxchActionsData', array(
8202 'ajaxUrl' => admin_url('admin-ajax.php'),
8203 'nonce' => wp_create_nonce('mxchat_actions_nonce'),
8204 'addNonce' => wp_create_nonce('mxchat_add_intent_nonce'),
8205 'editNonce' => wp_create_nonce('mxchat_edit_intent'),
8206 'deleteNonce' => wp_create_nonce('mxchat_delete_intent_nonce'),
8207 'toggleNonce' => wp_create_nonce('mxchat_actions_nonce'),
8208 'addPhraseNonce' => wp_create_nonce('mxchat_add_phrase_nonce'),
8209 'deletePhraseNonce' => wp_create_nonce('mxchat_delete_phrase_nonce'),
8210 'getPhrasesNonce' => wp_create_nonce('mxchat_get_phrases_nonce'),
8211 'deleteLegacyNonce' => wp_create_nonce('mxchat_delete_legacy_nonce'),
8212 'isActivated' => $is_activated,
8213 'i18n' => array(
8214 'confirmDelete' => __('Are you sure you want to delete this action?', 'mxchat'),
8215 'confirmBulkDelete' => __('Are you sure you want to delete the selected actions?', 'mxchat'),
8216 'saving' => __('Saving...', 'mxchat'),
8217 'saved' => __('Saved successfully!', 'mxchat'),
8218 'error' => __('An error occurred. Please try again.', 'mxchat'),
8219 'proRequired' => __('This feature requires MxChat Pro.', 'mxchat'),
8220 'addonRequired' => __('This feature requires an add-on.', 'mxchat')
8221 )
8222 ));
8223 break;
8224
8225 case 'mxchat-activation':
8226 // Load admin sidebar CSS first (shared styles for sidebar navigation)
8227 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8228 // Load pro page-specific styles
8229 wp_enqueue_style('mxchat-pro-css', $plugin_url . 'css/admin-pro.css', array('mxchat-admin-sidebar-css'), $version);
8230 // Load pro page JavaScript
8231 wp_enqueue_script('mxchat-pro-js', $plugin_url . 'js/mxchat_pro.js', array('jquery'), $version, true);
8232 // Load activation script for license activation/deactivation
8233 wp_enqueue_script('mxchat-activation-js', $plugin_url . 'js/activation-script.js', array('jquery'), $version, true);
8234 break;
8235
8236 case 'mxchat-content':
8237 // Load admin sidebar CSS (shared styles for sidebar navigation)
8238 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8239 // Load content page-specific styles
8240 wp_enqueue_style('mxchat-content-css', $plugin_url . 'css/admin-content.css', array('mxchat-admin-sidebar-css'), $version);
8241 // Load content page JavaScript
8242 wp_enqueue_script('mxchat-content-js', $plugin_url . 'js/mxchat-content.js', array('jquery'), $version, true);
8243 break;
8244 default:
8245 wp_enqueue_script(
8246 'mxchat-test-streaming-js',
8247 $plugin_url . 'js/mxchat-test-streaming.js',
8248 ['jquery'],
8249 $version,
8250 true
8251 );
8252
8253 wp_localize_script('mxchat-test-streaming-js', 'mxchatTestStreamingAjax', [
8254 'ajax_url' => admin_url('admin-ajax.php'),
8255 'nonce' => wp_create_nonce('mxchat_test_streaming_nonce'),
8256 'settings_nonce' => wp_create_nonce('mxchat_save_setting_nonce')
8257 ]);
8258
8259 // Testing Tab: Load chatbot assets for the embedded testing chatbot
8260 wp_enqueue_style('mxchat-chat-css', $plugin_url . 'css/chat-style.css', array(), $version);
8261 wp_enqueue_style('mxchat-admin-testing-css', $plugin_url . 'css/admin-testing-tab.css', array('mxchat-admin-sidebar-css', 'mxchat-chat-css'), $version);
8262
8263 wp_enqueue_script('mxchat-chat-js', $plugin_url . 'js/chat-script.js', array('jquery'), $version, true);
8264 wp_enqueue_script('mxchat-admin-testing-js', $plugin_url . 'js/admin-testing-tab.js', array('jquery', 'mxchat-chat-js'), $version, true);
8265
8266 // Allow add-ons to enqueue their public CSS/JS for the testing chatbot
8267 do_action('mxchat_enqueue_testing_tab_assets');
8268
8269 // Localize mxchatChat for the chatbot JS (same settings as frontend)
8270 $options = get_option('mxchat_options', array());
8271 $prompts_options = get_option('mxchat_prompts_options', array());
8272 $theme_options = get_option('mxchat_theme_options', array());
8273 $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
8274 $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
8275 $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
8276
8277 wp_localize_script('mxchat-chat-js', 'mxchatChat', array(
8278 'ajax_url' => admin_url('admin-ajax.php'),
8279 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
8280 'model' => isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest',
8281 'enable_streaming_toggle' => isset($options['enable_streaming_toggle']) ? $options['enable_streaming_toggle'] : 'on',
8282 'contextual_awareness_toggle' => isset($options['contextual_awareness_toggle']) ? $options['contextual_awareness_toggle'] : 'off',
8283 'link_target_toggle' => $options['link_target_toggle'] ?? 'off',
8284 'rate_limit_message' => $options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
8285 'complianz_toggle' => isset($options['complianz_toggle']) && $options['complianz_toggle'] === 'on',
8286 'user_message_bg_color' => $options['user_message_bg_color'] ?? '#fff',
8287 'user_message_font_color' => $options['user_message_font_color'] ?? '#212121',
8288 'bot_message_bg_color' => $options['bot_message_bg_color'] ?? '#212121',
8289 'bot_message_font_color' => $options['bot_message_font_color'] ?? '#fff',
8290 'top_bar_bg_color' => $options['top_bar_bg_color'] ?? '#212121',
8291 'send_button_font_color' => $options['send_button_font_color'] ?? '#212121',
8292 'close_button_color' => $options['close_button_color'] ?? '#fff',
8293 'chatbot_background_color' => $options['chatbot_background_color'] ?? '#212121',
8294 'chatbot_bg_color' => $options['chatbot_bg_color'] ?? '#fff',
8295 'icon_color' => $options['icon_color'] ?? '#fff',
8296 'chat_input_font_color' => $options['chat_input_font_color'] ?? '#212121',
8297 'chat_persistence_toggle' => 'off', // Always off for testing chatbot
8298 'appendWidgetToBody' => 'off',
8299 'live_agent_message_bg_color' => $options['live_agent_message_bg_color'] ?? '#ffffff',
8300 'live_agent_message_font_color' => $options['live_agent_message_font_color'] ?? '#333333',
8301 'chat_toolbar_toggle' => $options['chat_toolbar_toggle'] ?? 'off',
8302 'mode_indicator_bg_color' => $options['mode_indicator_bg_color'] ?? '#767676',
8303 'mode_indicator_font_color' => $options['mode_indicator_font_color'] ?? '#ffffff',
8304 'toolbar_icon_color' => $options['toolbar_icon_color'] ?? '#212121',
8305 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
8306 'email_collection_enabled' => 'off', // No email collection in testing
8307 'initial_email_state' => null,
8308 'skip_email_check' => true,
8309 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
8310 'skip_inline_colors' => $skip_inline_colors,
8311 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array()
8312 ));
8313
8314 // Localize testing tab script data
8315 wp_localize_script('mxchat-admin-testing-js', 'mxchatAdminTestData', array(
8316 'ajaxUrl' => admin_url('admin-ajax.php'),
8317 'nonce' => wp_create_nonce('mxchat_test_nonce'),
8318 'isAdmin' => true,
8319 'testingEnabled' => true
8320 ));
8321
8322 // Add testing enabled flag for the chatbot to return debug data
8323 add_action('admin_footer', function() {
8324 echo '<script>window.mxchatTestingEnabled = true;</script>';
8325 });
8326
8327 break;
8328 }
8329 }
8330 private function localize_admin_scripts($current_page) {
8331 // Base localization data for main admin script
8332 $base_data = array(
8333 'ajax_url' => admin_url('admin-ajax.php'),
8334 'nonce' => wp_create_nonce('mxchat_admin_nonce'),
8335 'admin_url' => admin_url(),
8336 'license_nonce' => wp_create_nonce('mxchat_activate_license_nonce'),
8337 'inline_edit_nonce' => wp_create_nonce('mxchat_save_inline_nonce'),
8338 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
8339 'export_nonce' => wp_create_nonce('mxchat_export_transcripts'),
8340 'actions_nonce' => wp_create_nonce('mxchat_actions_nonce'),
8341 'add_intent_nonce' => wp_create_nonce('mxchat_add_intent_nonce'),
8342 'edit_intent_nonce' => wp_create_nonce('mxchat_edit_intent'),
8343 'add_phrase_nonce' => wp_create_nonce('mxchat_add_phrase_nonce'),
8344 'delete_phrase_nonce' => wp_create_nonce('mxchat_delete_phrase_nonce'),
8345 'get_phrases_nonce' => wp_create_nonce('mxchat_get_phrases_nonce'),
8346 'delete_legacy_nonce' => wp_create_nonce('mxchat_delete_legacy_nonce'),
8347 'toggle_action_nonce' => wp_create_nonce('mxchat_actions_nonce'),
8348 'fetch_openrouter_models_nonce' => wp_create_nonce('mxchat_fetch_openrouter_models'),
8349 'is_activated' => $this->is_activated ? '1' : '0',
8350 'status_refresh_interval' => 5000,
8351 'prompts_setting_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
8352 'ajaxurl' => admin_url('admin-ajax.php')
8353 );
8354
8355 // Localize main admin script with base data
8356 wp_localize_script('mxchat-admin-js', 'mxchatAdmin', $base_data);
8357
8358 // Page-specific localizations
8359 $this->localize_page_specific_scripts($current_page);
8360 }
8361 private function localize_page_specific_scripts($current_page) {
8362 switch ($current_page) {
8363 case 'mxchat-prompts':
8364 // Status updater localization
8365 wp_localize_script('mxchat-status-updater', 'mxchat_status_data', array(
8366 'ajax_url' => admin_url('admin-ajax.php'),
8367 'nonce' => wp_create_nonce('mxchat_status_nonce')
8368 ));
8369
8370 // Content selector localization
8371 wp_localize_script('mxchat-content-selector-js', 'mxchatSelector', array(
8372 'ajaxurl' => admin_url('admin-ajax.php'),
8373 'nonce' => wp_create_nonce('mxchat_content_selector_nonce'),
8374 'i18n' => array(
8375 'searchPlaceholder' => __('Search posts and pages...', 'mxchat'),
8376 'selectAll' => __('Select All', 'mxchat'),
8377 'process' => __('Process Selected', 'mxchat'),
8378 'cancel' => __('Cancel', 'mxchat'),
8379 'noResults' => __('No content found.', 'mxchat')
8380 )
8381 ));
8382
8383 wp_localize_script('mxchat-knowledge-processing', 'mxchatAdmin', array(
8384 'ajax_url' => admin_url('admin-ajax.php'),
8385 'status_nonce' => wp_create_nonce('mxchat_status_nonce'),
8386 'queue_nonce' => wp_create_nonce('mxchat_queue_nonce'),
8387 'stop_nonce' => wp_create_nonce('mxchat_stop_processing_action'),
8388 'settings_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
8389 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
8390 'entries_nonce' => wp_create_nonce('mxchat_entries_nonce'),
8391 'admin_url' => admin_url(),
8392 'ajaxurl' => admin_url('admin-ajax.php'),
8393 'status_refresh_interval' => 2000,
8394 'bot_id' => isset($_GET['bot_id']) ? sanitize_text_field($_GET['bot_id']) : 'default'
8395 ));
8396 break;
8397
8398 case 'mxchat-activation':
8399 wp_localize_script('mxchat-activation-js', 'mxchatAdmin', array(
8400 'ajax_url' => admin_url('admin-ajax.php'),
8401 'license_nonce' => wp_create_nonce('mxchat_activate_license_nonce')
8402 ));
8403 break;
8404
8405 case 'mxchat-content':
8406 wp_localize_script('mxchat-content-js', 'mxchatContent', array(
8407 'ajaxUrl' => admin_url('admin-ajax.php'),
8408 'nonce' => wp_create_nonce('mxchat_content_nonce'),
8409 'settingNonce' => wp_create_nonce('mxchat_save_setting_nonce'),
8410 'previewUrl' => home_url('/?p='),
8411 'isActivated' => $this->is_activated(),
8412 'hasAdvancedContent' => apply_filters('mxchat_content_pro_feature', false, 'seo_readability'),
8413 'hasGSC' => apply_filters('mxchat_content_pro_feature', false, 'gsc_integration'),
8414 'seoOptimize' => array(
8415 'meta_description' => ($options['seo_optimize_meta_desc'] ?? 'on') === 'on',
8416 'seo_title' => ($options['seo_optimize_seo_title'] ?? 'on') === 'on',
8417 'slug' => ($options['seo_optimize_slug'] ?? 'on') === 'on',
8418 'readability' => ($options['seo_optimize_readability'] ?? 'on') === 'on',
8419 'internal_links' => ($options['seo_optimize_internal_links'] ?? 'on') === 'on',
8420 'img_alt' => ($options['seo_optimize_img_alt'] ?? 'on') === 'on',
8421 'featured_img' => ($options['seo_optimize_featured_img'] ?? 'on') === 'on',
8422 ),
8423 'i18n' => array(
8424 'generating' => __('Generating...', 'mxchat'),
8425 'planning' => __('Planning content structure...', 'mxchat'),
8426 'images' => __('Generating images...', 'mxchat'),
8427 'writing' => __('Writing full content...', 'mxchat'),
8428 'creating' => __('Creating WordPress post...', 'mxchat'),
8429 'done' => __('Content generated successfully!', 'mxchat'),
8430 'error' => __('An error occurred. Please try again.', 'mxchat'),
8431 'editSuccess' => __('Content updated successfully.', 'mxchat'),
8432 'promptEmpty' => __('Please enter a prompt.', 'mxchat'),
8433 )
8434 ));
8435 break;
8436
8437 case 'mxchat-transcripts':
8438 // Get chart data for localization
8439 $chart_data = $this->get_transcripts_chart_data();
8440
8441 wp_localize_script('mxchat-transcripts-js', 'mxchatAdmin', array(
8442 'ajax_url' => admin_url('admin-ajax.php'),
8443 'export_nonce' => wp_create_nonce('mxchat_export_transcripts'),
8444 'delete_nonce' => wp_create_nonce('mxchat_delete_chat_history'),
8445 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
8446 'translate_nonce' => wp_create_nonce('mxchat_translate_messages')
8447 ));
8448
8449 // Localize chart data separately - use array_values to ensure proper JSON array encoding
8450 wp_localize_script('mxchat-transcripts-js', 'mxchatChartData', array(
8451 'labels' => array_values($chart_data['labels']),
8452 'chats' => array_values($chart_data['chats']),
8453 'messages' => array_values($chart_data['messages'])
8454 ));
8455 break;
8456
8457 case 'mxchat-settings':
8458 default:
8459 // Color picker and settings localization
8460 wp_localize_script('mxchat-color-picker', 'mxchatStyleSettings', array(
8461 'ajax_url' => admin_url('admin-ajax.php'),
8462 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
8463 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
8464 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
8465 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
8466 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
8467 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
8468 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
8469 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
8470 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
8471 'icon_color' => $this->options['icon_color'] ?? '#fff',
8472 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
8473 'pre_chat_message' => $this->options['pre_chat_message'] ?? esc_html__('Hey there! Ask me anything!', 'mxchat'),
8474 'rate_limit_message' => $this->options['rate_limit_message'] ?? esc_html__('Rate limit exceeded. Please try again later.', 'mxchat'),
8475 'loops_api_key' => $this->options['loops_api_key'] ?? '',
8476 'loops_mailing_list' => $this->options['loops_mailing_list'] ?? '',
8477 'triggered_phrase_response' => $this->options['triggered_phrase_response'] ?? esc_html__('Would you like to join our mailing list? Please provide your email below.', 'mxchat'),
8478 'email_capture_response' => $this->options['email_capture_response'] ?? esc_html__('Thank you for providing your email! You\'ve been added to our list.', 'mxchat'),
8479 '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'),
8480 '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'),
8481 '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'),
8482 'pdf_max_pages' => $this->options['pdf_max_pages'] ?? 69,
8483 'live_agent_webhook_url' => $this->options['live_agent_webhook_url'] ?? '',
8484 'live_agent_secret_key' => $this->options['live_agent_secret_key'] ?? '',
8485 'live_agent_bot_token' => $this->options['live_agent_bot_token'] ?? '',
8486 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
8487 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
8488 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
8489 'show_pdf_upload_button' => $this->options['show_pdf_upload_button'] ?? 'on',
8490 'show_word_upload_button' => $this->options['show_word_upload_button'] ?? 'on',
8491 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
8492 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
8493 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
8494 ));
8495 break;
8496 }
8497
8498 // Additional localization that was in the original code
8499 wp_localize_script('mxchat-admin-js', 'mxchatPromptsAdmin', array(
8500 'ajax_url' => admin_url('admin-ajax.php'),
8501 'prompts_setting_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
8502 ));
8503 }
8504
8505 public function mxchat_sanitize($input) {
8506 $new_input = array();
8507
8508 if (isset($input['api_key'])) {
8509 $new_input['api_key'] = sanitize_text_field($input['api_key']);
8510 }
8511
8512 if (isset($input['similarity_threshold'])) {
8513 $new_input['similarity_threshold'] = absint($input['similarity_threshold']); // Ensure it's an integer
8514 $new_input['similarity_threshold'] = min(max($new_input['similarity_threshold'], 20), 85); // Enforce range
8515 }
8516
8517 if (isset($input['rag_sources_limit'])) {
8518 $new_input['rag_sources_limit'] = absint($input['rag_sources_limit']); // Ensure it's an integer
8519 $new_input['rag_sources_limit'] = min(max($new_input['rag_sources_limit'], 3), 10); // Enforce range 3-10
8520 }
8521
8522 if (isset($input['rag_chunks_limit'])) {
8523 $new_input['rag_chunks_limit'] = absint($input['rag_chunks_limit']); // Ensure it's an integer
8524 $new_input['rag_chunks_limit'] = min(max($new_input['rag_chunks_limit'], 8), 20); // Enforce range 8-20
8525 }
8526
8527 if (isset($input['xai_api_key'])) {
8528 $new_input['xai_api_key'] = sanitize_text_field($input['xai_api_key']);
8529 }
8530
8531 if (isset($input['claude_api_key'])) {
8532 $new_input['claude_api_key'] = sanitize_text_field($input['claude_api_key']);
8533 }
8534
8535 if (isset($input['enable_streaming_toggle'])) {
8536 $new_input['enable_streaming_toggle'] = ($input['enable_streaming_toggle'] === 'on') ? 'on' : 'off';
8537 } else {
8538 // If checkbox not checked, it won't be in $input, so set to 'off'
8539 $new_input['enable_streaming_toggle'] = 'off';
8540 }
8541
8542 if (isset($input['enable_web_search'])) {
8543 $new_input['enable_web_search'] = ($input['enable_web_search'] === 'on') ? 'on' : 'off';
8544 } else {
8545 // If checkbox not checked, it won't be in $input, so set to 'off'
8546 $new_input['enable_web_search'] = 'off';
8547 }
8548
8549 if (isset($input['deepseek_api_key'])) {
8550 $new_input['deepseek_api_key'] = sanitize_text_field($input['deepseek_api_key']);
8551 }
8552
8553 if (isset($input['gemini_api_key'])) {
8554 $new_input['gemini_api_key'] = sanitize_text_field($input['gemini_api_key']);
8555 }
8556
8557 if (isset($input['enable_woocommerce_integration'])) {
8558 $new_input['enable_woocommerce_integration'] = $input['enable_woocommerce_integration'] === 'on' ? 'on' : 'off';
8559 }
8560
8561 if (isset($input['privacy_toggle'])) {
8562 $new_input['privacy_toggle'] = $input['privacy_toggle'];
8563 }
8564
8565 if (isset($input['complianz_toggle'])) {
8566 $new_input['complianz_toggle'] = $input['complianz_toggle'];
8567 }
8568
8569 // Handle custom privacy text input
8570 if (isset($input['privacy_text'])) {
8571 // Allow basic HTML for links
8572 $new_input['privacy_text'] = wp_kses_post($input['privacy_text']);
8573 }
8574
8575 if (isset($input['system_prompt_instructions'])) {
8576 $new_input['system_prompt_instructions'] = sanitize_textarea_field($input['system_prompt_instructions']);
8577 }
8578
8579 if (isset($input['mxchat_pro_email'])) {
8580 $new_input['mxchat_pro_email'] = sanitize_email($input['mxchat_pro_email']);
8581 }
8582
8583 if (isset($input['mxchat_activation_key'])) {
8584 $new_input['mxchat_activation_key'] = sanitize_text_field($input['mxchat_activation_key']);
8585 }
8586
8587 if (isset($input['append_to_body'])) {
8588 $new_input['append_to_body'] = $input['append_to_body'] === 'on' ? 'on' : 'off';
8589 }
8590
8591 // Post type visibility settings
8592 if (isset($input['post_type_visibility_mode'])) {
8593 $allowed_modes = array('all', 'include', 'exclude');
8594 $new_input['post_type_visibility_mode'] = in_array($input['post_type_visibility_mode'], $allowed_modes)
8595 ? $input['post_type_visibility_mode']
8596 : 'all';
8597 }
8598
8599 if (isset($input['post_type_visibility_list'])) {
8600 if (is_array($input['post_type_visibility_list'])) {
8601 $new_input['post_type_visibility_list'] = array_map('sanitize_key', $input['post_type_visibility_list']);
8602 } else {
8603 $new_input['post_type_visibility_list'] = array();
8604 }
8605 }
8606
8607 if (isset($input['contextual_awareness_toggle'])) {
8608 $new_input['contextual_awareness_toggle'] = $input['contextual_awareness_toggle'] === 'on' ? 'on' : 'off';
8609 }
8610
8611 if (isset($input['citation_links_toggle'])) {
8612 $new_input['citation_links_toggle'] = $input['citation_links_toggle'] === 'on' ? 'on' : 'off';
8613 }
8614
8615 if (isset($input['top_bar_title'])) {
8616 $new_input['top_bar_title'] = sanitize_text_field($input['top_bar_title']);
8617 }
8618
8619 if (isset($input['ai_agent_text'])) {
8620 $new_input['ai_agent_text'] = sanitize_text_field($input['ai_agent_text']);
8621 }
8622
8623 if (isset($input['enable_email_block'])) {
8624 $new_input['enable_email_block'] = sanitize_text_field($input['enable_email_block']);
8625 }
8626
8627 if (isset($input['email_blocker_header_content'])) {
8628 // wp_kses_post() allows standard HTML tags permitted by WordPress
8629 $new_input['email_blocker_header_content'] = wp_kses_post($input['email_blocker_header_content']);
8630 }
8631 if (isset($input['email_blocker_button_text'])) {
8632 $new_input['email_blocker_button_text'] = sanitize_text_field($input['email_blocker_button_text']);
8633 }
8634 // Sanitize name field toggle
8635 if (isset($input['enable_name_field'])) {
8636 $new_input['enable_name_field'] = ($input['enable_name_field'] === 'on') ? 'on' : 'off';
8637 } else {
8638 $new_input['enable_name_field'] = 'off';
8639 }
8640 // Sanitize name field placeholder
8641 if (isset($input['name_field_placeholder'])) {
8642 $new_input['name_field_placeholder'] = sanitize_text_field($input['name_field_placeholder']);
8643 }
8644 if (isset($input['intro_message'])) {
8645 $new_input['intro_message'] = wp_kses_post($input['intro_message']); // Use wp_kses_post instead
8646 }
8647
8648 if (isset($input['input_copy'])) {
8649 $new_input['input_copy'] = sanitize_text_field($input['input_copy']);
8650 }
8651
8652 if (isset($input['rate_limit_message'])) {
8653 $new_input['rate_limit_message'] = sanitize_text_field($input['rate_limit_message']);
8654 }
8655
8656 // Handle the new rate limits format
8657 if (isset($input['rate_limits']) && is_array($input['rate_limits'])) {
8658 $new_input['rate_limits'] = array();
8659 $allowed_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited');
8660 $allowed_timeframes = array('hourly', 'daily', 'weekly', 'monthly');
8661
8662 foreach ($input['rate_limits'] as $role_id => $settings) {
8663 $new_input['rate_limits'][$role_id] = array();
8664
8665 // Sanitize limit
8666 if (isset($settings['limit'])) {
8667 $limit = sanitize_text_field($settings['limit']);
8668 if (in_array($limit, $allowed_limits, true)) {
8669 $new_input['rate_limits'][$role_id]['limit'] = $limit;
8670 } else {
8671 $new_input['rate_limits'][$role_id]['limit'] = ($role_id === 'logged_out') ? '10' : '100'; // Default
8672 }
8673 }
8674
8675 // Sanitize timeframe
8676 if (isset($settings['timeframe'])) {
8677 $timeframe = sanitize_text_field($settings['timeframe']);
8678 if (in_array($timeframe, $allowed_timeframes, true)) {
8679 $new_input['rate_limits'][$role_id]['timeframe'] = $timeframe;
8680 } else {
8681 $new_input['rate_limits'][$role_id]['timeframe'] = 'daily'; // Default
8682 }
8683 }
8684
8685 // Sanitize message
8686 if (isset($settings['message'])) {
8687 $new_input['rate_limits'][$role_id]['message'] = sanitize_textarea_field($settings['message']);
8688 }
8689 }
8690 }
8691
8692 if (isset($input['pre_chat_message'])) {
8693 $new_input['pre_chat_message'] = sanitize_textarea_field($input['pre_chat_message']);
8694 }
8695
8696 if (isset($input['voyage_api_key'])) {
8697 $new_input['voyage_api_key'] = sanitize_text_field($input['voyage_api_key']);
8698 }
8699
8700 // Add to your sanitize function
8701 if (isset($input['embedding_model'])) {
8702 $allowed_models = array(
8703 'text-embedding-ada-002',
8704 'text-embedding-3-small',
8705 'text-embedding-3-large',
8706 'voyage-3-large',
8707 'gemini-embedding-001'
8708 );
8709 if (in_array($input['embedding_model'], $allowed_models)) {
8710 $new_input['embedding_model'] = sanitize_text_field($input['embedding_model']);
8711 }
8712 }
8713
8714 if (isset($input['model'])) {
8715 if ($input['model'] === 'openrouter') {
8716 $new_input['model'] = 'openrouter';
8717 } else {
8718 $allowed_models = array(
8719 'gemini-3-pro-preview',
8720 'gemini-3-flash-preview',
8721 'gemini-2.5-pro',
8722 'gemini-2.5-flash',
8723 'gemini-2.5-flash-lite',
8724 'gemini-2.0-flash',
8725 'gemini-2.0-flash-lite',
8726 'gemini-1.5-pro',
8727 'gemini-1.5-flash',
8728 'grok-4-0709',
8729 'grok-4-1-fast-reasoning',
8730 'grok-4-1-fast-non-reasoning',
8731 'grok-3-beta',
8732 'grok-3-fast-beta',
8733 'grok-3-mini-beta',
8734 'grok-3-mini-fast-beta',
8735 'grok-2',
8736 'deepseek-chat',
8737 'claude-opus-4-6',
8738 'claude-sonnet-4-6',
8739 'claude-opus-4-5',
8740 'claude-sonnet-4-5-20250929',
8741 'claude-opus-4-1-20250805',
8742 'claude-haiku-4-5-20251001',
8743 'claude-opus-4-20250514',
8744 'claude-sonnet-4-20250514',
8745 'gpt-5.2',
8746 'gpt-5.1-chat-latest',
8747 'gpt-5.1-2025-11-13',
8748 'gpt-5',
8749 'gpt-5-mini',
8750 'gpt-5-nano',
8751 'gpt-5.4',
8752 'gpt-5.4-mini',
8753 'gpt-5.4-nano',
8754 'gpt-5.3-chat-latest',
8755 );
8756
8757 if (in_array($input['model'], $allowed_models)) {
8758 $new_input['model'] = sanitize_text_field($input['model']);
8759 } else {
8760 // Fallback for any deprecated model
8761 $new_input['model'] = 'gpt-5.1-chat-latest';
8762 }
8763 }
8764 }
8765
8766 if (isset($input['openrouter_selected_model'])) {
8767 // Just sanitize it, don't validate against a whitelist
8768 $new_input['openrouter_selected_model'] = sanitize_text_field($input['openrouter_selected_model']);
8769 }
8770
8771 // ADD THIS:
8772 if (isset($input['openrouter_selected_model_name'])) {
8773 $new_input['openrouter_selected_model_name'] = sanitize_text_field($input['openrouter_selected_model_name']);
8774 }
8775
8776 if (isset($input['openrouter_api_key'])) {
8777 $new_input['openrouter_api_key'] = sanitize_text_field($input['openrouter_api_key']);
8778 }
8779
8780
8781
8782 if (isset($input['woocommerce_consumer_key'])) {
8783 $new_input['woocommerce_consumer_key'] = sanitize_text_field($input['woocommerce_consumer_key']);
8784 }
8785
8786 if (isset($input['woocommerce_consumer_secret'])) {
8787 $new_input['woocommerce_consumer_secret'] = sanitize_text_field($input['woocommerce_consumer_secret']);
8788 }
8789
8790
8791 // Sanitize link_target_toggle
8792 if (isset($input['link_target_toggle'])) {
8793 $new_input['link_target_toggle'] = $input['link_target_toggle'] === 'on' ? 'on' : 'off';
8794 }
8795
8796 // Sanitize Loops API Key
8797 if (isset($input['loops_api_key'])) {
8798 $new_input['loops_api_key'] = sanitize_text_field($input['loops_api_key']);
8799 }
8800
8801 if (isset($input['chat_persistence_toggle'])) {
8802 $new_input['chat_persistence_toggle'] = $input['chat_persistence_toggle'] === 'on' ? 'on' : 'off';
8803 }
8804
8805 if (isset($input['popular_question_1'])) {
8806 $new_input['popular_question_1'] = sanitize_text_field($input['popular_question_1']);
8807 }
8808
8809 if (isset($input['popular_question_2'])) {
8810 $new_input['popular_question_2'] = sanitize_text_field($input['popular_question_2']);
8811 }
8812
8813 if (isset($input['popular_question_3'])) {
8814 $new_input['popular_question_3'] = sanitize_text_field($input['popular_question_3']);
8815 }
8816
8817 if (isset($input['additional_popular_questions']) && is_array($input['additional_popular_questions'])) {
8818 $new_input['additional_popular_questions'] = array_map('sanitize_text_field', $input['additional_popular_questions']);
8819 }
8820
8821 // Sanitize Loops Mailing List
8822 if (isset($input['loops_mailing_list'])) {
8823 $new_input['loops_mailing_list'] = sanitize_text_field($input['loops_mailing_list']);
8824 }
8825
8826 // Sanitize Triggered Phrase Response
8827 if (isset($input['triggered_phrase_response'])) {
8828 $new_input['triggered_phrase_response'] = wp_kses_post($input['triggered_phrase_response']);
8829 }
8830 if (isset($input['email_capture_response'])) {
8831 $new_input['email_capture_response'] = wp_kses_post($input['email_capture_response']);
8832 }
8833
8834 // Sanitize Brave Search Settings
8835 if (isset($input['brave_api_key'])) {
8836 $new_input['brave_api_key'] = sanitize_text_field($input['brave_api_key']);
8837 }
8838
8839 if (isset($input['brave_image_count'])) {
8840 $image_count = intval($input['brave_image_count']);
8841 $new_input['brave_image_count'] = ($image_count >=1 && $image_count <=6) ? $image_count : 4;
8842 }
8843
8844 if (isset($input['brave_safe_search'])) {
8845 $allowed = array('strict', 'off');
8846 $new_input['brave_safe_search'] = in_array($input['brave_safe_search'], $allowed, true) ? $input['brave_safe_search'] : 'strict';
8847 }
8848
8849 if (isset($input['brave_news_count'])) {
8850 $news_count = intval($input['brave_news_count']);
8851 $new_input['brave_news_count'] = ($news_count >=1 && $news_count <=10) ? $news_count : 3;
8852 }
8853
8854 if (isset($input['brave_country'])) {
8855 $new_input['brave_country'] = sanitize_text_field($input['brave_country']);
8856 }
8857
8858 if (isset($input['brave_language'])) {
8859 $new_input['brave_language'] = sanitize_text_field($input['brave_language']);
8860 }
8861
8862 if (isset($input['chat_toolbar_toggle'])) {
8863 $new_input['chat_toolbar_toggle'] = $input['chat_toolbar_toggle'] === 'on' ? 'on' : 'off';
8864 }
8865
8866 // Sanitize PDF upload button toggle
8867 if (isset($input['show_pdf_upload_button'])) {
8868 $new_input['show_pdf_upload_button'] = $input['show_pdf_upload_button'] === 'on' ? 'on' : 'off';
8869 } else {
8870 $new_input['show_pdf_upload_button'] = 'off'; // If checkbox is unchecked
8871 }
8872
8873 // Sanitize Word upload button toggle
8874 if (isset($input['show_word_upload_button'])) {
8875 $new_input['show_word_upload_button'] = $input['show_word_upload_button'] === 'on' ? 'on' : 'off';
8876 } else {
8877 $new_input['show_word_upload_button'] = 'off'; // If checkbox is unchecked
8878 }
8879
8880 if (isset($input['pdf_intent_trigger_text'])) {
8881 $new_input['pdf_intent_trigger_text'] = sanitize_text_field($input['pdf_intent_trigger_text']);
8882 }
8883
8884 if (isset($input['pdf_intent_success_text'])) {
8885 $new_input['pdf_intent_success_text'] = sanitize_text_field($input['pdf_intent_success_text']);
8886 }
8887
8888 if (isset($input['pdf_intent_error_text'])) {
8889 $new_input['pdf_intent_error_text'] = sanitize_text_field($input['pdf_intent_error_text']);
8890 }
8891
8892 if (isset($input['pdf_max_pages'])) {
8893 $new_input['pdf_max_pages'] = intval($input['pdf_max_pages']);
8894 if ($new_input['pdf_max_pages'] < 1 || $new_input['pdf_max_pages'] > 69) {
8895 $new_input['pdf_max_pages'] = 69; // Default to 69 if out of range
8896 }
8897 }
8898
8899 if (isset($input['live_agent_webhook_url'])) {
8900 $new_input['live_agent_webhook_url'] = esc_url_raw($input['live_agent_webhook_url']);
8901 }
8902 if (isset($input['live_agent_secret_key'])) {
8903 $new_input['live_agent_secret_key'] = sanitize_text_field($input['live_agent_secret_key']);
8904 }
8905
8906 // Live Agent Integration
8907 if (isset($input['live_agent_bot_token'])) {
8908 $new_input['live_agent_bot_token'] = sanitize_text_field($input['live_agent_bot_token']);
8909 }
8910
8911 if (isset($input['live_agent_user_ids'])) {
8912 $new_input['live_agent_user_ids'] = sanitize_textarea_field($input['live_agent_user_ids']);
8913 }
8914
8915 if (isset($input['live_agent_status'])) {
8916 $new_input['live_agent_status'] = ($input['live_agent_status'] === 'on') ? 'on' : 'off';
8917 }
8918 if (isset($input['live_agent_away_message'])) {
8919 $new_input['live_agent_away_message'] = sanitize_textarea_field($input['live_agent_away_message']);
8920 }
8921 if (isset($input['live_agent_notification_message'])) {
8922 $new_input['live_agent_notification_message'] = sanitize_textarea_field($input['live_agent_notification_message']);
8923 }
8924
8925 // Telegram Integration
8926 if (isset($input['telegram_status'])) {
8927 $new_input['telegram_status'] = ($input['telegram_status'] === 'on') ? 'on' : 'off';
8928 }
8929 if (isset($input['telegram_bot_token'])) {
8930 $new_input['telegram_bot_token'] = sanitize_text_field($input['telegram_bot_token']);
8931 }
8932 if (isset($input['telegram_group_id'])) {
8933 $new_input['telegram_group_id'] = sanitize_text_field($input['telegram_group_id']);
8934 }
8935 if (isset($input['telegram_webhook_secret'])) {
8936 $new_input['telegram_webhook_secret'] = sanitize_text_field($input['telegram_webhook_secret']);
8937 }
8938 if (isset($input['telegram_notification_message'])) {
8939 $new_input['telegram_notification_message'] = sanitize_textarea_field($input['telegram_notification_message']);
8940 }
8941 if (isset($input['telegram_away_message'])) {
8942 $new_input['telegram_away_message'] = sanitize_textarea_field($input['telegram_away_message']);
8943 }
8944
8945 // Sanitize script loading strategy
8946 if (isset($input['script_loading_strategy'])) {
8947 $allowed_strategies = array('default', 'defer', 'delay_1s', 'delay_3s', 'delay_5s', 'on_interaction');
8948 $new_input['script_loading_strategy'] = in_array($input['script_loading_strategy'], $allowed_strategies)
8949 ? $input['script_loading_strategy']
8950 : 'default';
8951 }
8952
8953 // Sanitize debug mode
8954 if (isset($input['debug_mode'])) {
8955 $new_input['debug_mode'] = ($input['debug_mode'] === 'on') ? 'on' : 'off';
8956 }
8957
8958 // Content Generator Settings
8959 if (isset($input['content_model'])) {
8960 $new_input['content_model'] = sanitize_text_field($input['content_model']);
8961 }
8962 if (isset($input['content_image_model'])) {
8963 $new_input['content_image_model'] = sanitize_text_field($input['content_image_model']);
8964 }
8965 if (isset($input['content_enable_images'])) {
8966 $new_input['content_enable_images'] = ($input['content_enable_images'] === 'on') ? 'on' : 'off';
8967 }
8968 if (isset($input['content_use_placeholders'])) {
8969 $new_input['content_use_placeholders'] = ($input['content_use_placeholders'] === 'on') ? 'on' : 'off';
8970 }
8971 if (isset($input['content_internal_linking'])) {
8972 $new_input['content_internal_linking'] = ($input['content_internal_linking'] === 'on') ? 'on' : 'off';
8973 }
8974 if (isset($input['content_tool_use'])) {
8975 $new_input['content_tool_use'] = ($input['content_tool_use'] === 'on') ? 'on' : 'off';
8976 }
8977
8978 // SEO Optimize toggle fields
8979 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) {
8980 if (isset($input[$seo_key])) {
8981 $new_input[$seo_key] = ($input[$seo_key] === 'on') ? 'on' : 'off';
8982 }
8983 }
8984
8985 // Preserve content generator settings when saving from main settings page
8986 // (where content fields are not in the form submission)
8987 $existing = get_option('mxchat_options', array());
8988 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) {
8989 if (!isset($new_input[$key]) && isset($existing[$key])) {
8990 $new_input[$key] = $existing[$key];
8991 }
8992 }
8993
8994 return $new_input;
8995 }
8996
8997 /**
8998 * Log a debug message if debug mode is enabled
8999 *
9000 * @param string $type The type of log entry (settings_save, api_error, activation, etc.)
9001 * @param string $message The log message
9002 * @param array $data Optional additional data to log
9003 * @return bool Whether the message was logged
9004 */
9005 public static function mxchat_log_debug( $type, $message, $data = array() ) {
9006 $options = get_option( 'mxchat_options', array() );
9007
9008 // Check if debug mode is enabled
9009 if ( ! isset( $options['debug_mode'] ) || $options['debug_mode'] !== 'on' ) {
9010 return false;
9011 }
9012
9013 // Get current log
9014 $log = get_option( 'mxchat_debug_log', array() );
9015 if ( ! is_array( $log ) ) {
9016 $log = array();
9017 }
9018
9019 // Add new entry
9020 $entry = array(
9021 'time' => current_time( 'Y-m-d H:i:s' ),
9022 'type' => sanitize_key( $type ),
9023 'message' => sanitize_text_field( $message ),
9024 );
9025
9026 if ( ! empty( $data ) ) {
9027 $entry['data'] = $data;
9028 }
9029
9030 // Add to beginning of array (newest first)
9031 array_unshift( $log, $entry );
9032
9033 // Keep only last 100 entries
9034 if ( count( $log ) > 100 ) {
9035 $log = array_slice( $log, 0, 100 );
9036 }
9037
9038 // Save log
9039 update_option( 'mxchat_debug_log', $log, false );
9040
9041 return true;
9042 }
9043
9044 /**
9045 * Get the debug log entries
9046 *
9047 * @return array The debug log entries
9048 */
9049 public static function mxchat_get_debug_log() {
9050 $log = get_option( 'mxchat_debug_log', array() );
9051 return is_array( $log ) ? $log : array();
9052 }
9053
9054 /**
9055 * Clear the debug log
9056 *
9057 * @return bool Whether the log was cleared
9058 */
9059 public static function mxchat_clear_debug_log() {
9060 return delete_option( 'mxchat_debug_log' );
9061 }
9062
9063 /**
9064 * Export settings as JSON with masked API keys
9065 *
9066 * @return array The sanitized settings array
9067 */
9068 public static function mxchat_export_settings() {
9069 $options = get_option( 'mxchat_options', array() );
9070
9071 if ( ! is_array( $options ) ) {
9072 return array();
9073 }
9074
9075 // List of API key fields to mask
9076 $api_key_fields = array(
9077 'api_key',
9078 'xai_api_key',
9079 'claude_api_key',
9080 'deepseek_api_key',
9081 'voyage_api_key',
9082 'gemini_api_key',
9083 'openrouter_api_key',
9084 'loops_api_key',
9085 'brave_api_key',
9086 'live_agent_secret_key',
9087 'live_agent_bot_token',
9088 'telegram_bot_token',
9089 'telegram_webhook_secret',
9090 'woocommerce_consumer_key',
9091 'woocommerce_consumer_secret',
9092 );
9093
9094 // Mask API keys (show only last 4 characters)
9095 foreach ( $api_key_fields as $field ) {
9096 if ( isset( $options[ $field ] ) && ! empty( $options[ $field ] ) ) {
9097 $value = $options[ $field ];
9098 if ( strlen( $value ) > 4 ) {
9099 $options[ $field ] = str_repeat( '*', strlen( $value ) - 4 ) . substr( $value, -4 );
9100 } else {
9101 $options[ $field ] = '****';
9102 }
9103 }
9104 }
9105
9106 // Add metadata
9107 $export = array(
9108 'plugin_version' => defined( 'MXCHAT_VERSION' ) ? MXCHAT_VERSION : 'unknown',
9109 'export_date' => current_time( 'Y-m-d H:i:s' ),
9110 'wordpress_version' => get_bloginfo( 'version' ),
9111 'php_version' => phpversion(),
9112 'settings' => $options,
9113 );
9114
9115 return $export;
9116 }
9117
9118 /**
9119 * Reset all settings to defaults
9120 *
9121 * @return bool Whether the reset was successful
9122 */
9123 public static function mxchat_reset_all_settings() {
9124 // Delete main options
9125 $deleted = delete_option( 'mxchat_options' );
9126
9127 // Also clear the debug log
9128 delete_option( 'mxchat_debug_log' );
9129
9130 // Log the reset (will create new log since we just cleared it)
9131 // We need to temporarily enable debug mode to log this
9132 $temp_options = array( 'debug_mode' => 'on' );
9133 update_option( 'mxchat_options', $temp_options );
9134
9135 self::mxchat_log_debug( 'reset', 'All settings have been reset to defaults' );
9136
9137 // Now delete again to trigger re-initialization
9138 delete_option( 'mxchat_options' );
9139
9140 return $deleted;
9141 }
9142
9143 // Method to append the chatbot to the body
9144 public function mxchat_append_chatbot_to_body() {
9145 $options = get_option('mxchat_options');
9146 if (isset($options['append_to_body']) && $options['append_to_body'] === 'on') {
9147 echo do_shortcode('[mxchat_chatbot floating="yes"]');
9148 }
9149 }
9150
9151
9152
9153
9154
9155 private function mxchat_fetch_loops_mailing_lists($api_key) {
9156 $url = 'https://app.loops.so/api/v1/lists';
9157 $response = wp_remote_get($url, array(
9158 'headers' => array(
9159 'Authorization' => 'Bearer ' . $api_key,
9160 'Content-Type' => 'application/json'
9161 )
9162 ));
9163
9164 if (is_wp_error($response)) {
9165 return array();
9166 }
9167
9168 $body = wp_remote_retrieve_body($response);
9169 $lists = json_decode($body, true);
9170
9171 return isset($lists) && is_array($lists) ? $lists : array();
9172 }
9173
9174 function mxchat_calculate_cosine_similarity($vec1, $vec2) {
9175 if (empty($vec1) || empty($vec2)) {
9176 return 0.0;
9177 }
9178
9179 $dot_product = 0.0;
9180 $norm_a = 0.0;
9181 $norm_b = 0.0;
9182
9183 for ($i = 0; $i < count($vec1); $i++) {
9184 $dot_product += $vec1[$i] * $vec2[$i];
9185 $norm_a += pow($vec1[$i], 2);
9186 $norm_b += pow($vec2[$i], 2);
9187 }
9188
9189 if ($norm_a == 0.0 || $norm_b == 0.0) {
9190 return 0.0;
9191 } else {
9192 return $dot_product / (sqrt($norm_a) * sqrt($norm_b));
9193 }
9194 }
9195
9196 /**
9197 * Validates nonce and deletes all chat prompts
9198 */
9199 public function mxchat_handle_delete_all_prompts() {
9200 //error_log('=== DELETE ALL DEBUG START ===');
9201
9202 // Verify nonce
9203 if (!isset($_POST['mxchat_delete_all_prompts_nonce']) || !wp_verify_nonce($_POST['mxchat_delete_all_prompts_nonce'], 'mxchat_delete_all_prompts_action')) {
9204 //error_log('DEBUG: Nonce verification failed');
9205 wp_die(__('Nonce verification failed.', 'mxchat'));
9206 }
9207
9208 // Check permissions
9209 if (!current_user_can('manage_options')) {
9210 //error_log('DEBUG: Permission check failed');
9211 wp_die(__('You do not have sufficient permissions to delete all prompts.', 'mxchat'));
9212 }
9213
9214 // Get bot_id and content type filter from POST data
9215 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
9216 $content_type_filter = isset($_POST['content_type_filter']) ? sanitize_text_field($_POST['content_type_filter']) : '';
9217
9218 $success = true;
9219 $error_messages = array();
9220
9221 // Get bot-specific Pinecone configuration
9222 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
9223 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
9224
9225 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
9226
9227 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
9228 // Delete from Pinecone (with optional content type filter)
9229 $result = $pinecone_manager->mxchat_delete_all_from_pinecone($pinecone_options, $content_type_filter);
9230
9231 if (!$result['success']) {
9232 $success = false;
9233 $error_messages[] = $result['message'];
9234 }
9235
9236 } else {
9237 // Delete from WordPress database
9238 global $wpdb;
9239 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
9240
9241 // Build WHERE conditions
9242 $where_clauses = array();
9243 $where_values = array();
9244
9245 // Bot filter
9246 if ($bot_id !== 'default' && class_exists('MxChat_Multi_Bot_Manager')) {
9247 $where_clauses[] = 'bot_id = %s';
9248 $where_values[] = $bot_id;
9249 }
9250
9251 // Content type filter
9252 if (!empty($content_type_filter)) {
9253 $where_clauses[] = 'content_type = %s';
9254 $where_values[] = $content_type_filter;
9255 }
9256
9257 if (!empty($where_clauses)) {
9258 $where_sql = implode(' AND ', $where_clauses);
9259 $result = $wpdb->query($wpdb->prepare("DELETE FROM {$table_name} WHERE {$where_sql}", $where_values));
9260 } else {
9261 // No filters — delete all
9262 $result = $wpdb->query("DELETE FROM {$table_name}");
9263 }
9264
9265 if ($result === false) {
9266 $success = false;
9267 $error_messages[] = 'Failed to delete from WordPress database';
9268 }
9269 }
9270
9271 // Redirect back with a success message and bot_id
9272 $redirect_url = add_query_arg(array(
9273 'page' => 'mxchat-prompts',
9274 'bot_id' => $bot_id,
9275 'all_deleted' => $success ? 'true' : 'false'
9276 ), admin_url('admin.php'));
9277
9278 wp_safe_redirect($redirect_url);
9279 exit;
9280 }
9281
9282
9283
9284 /**
9285 * Handles deletion prompt with nonce validation
9286 */
9287 public function mxchat_handle_delete_prompt() {
9288 // Sanitize and validate nonce
9289 $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : '';
9290 if (empty($nonce) || !wp_verify_nonce($nonce, 'mxchat_delete_prompt_nonce')) {
9291 wp_die(esc_html__('Nonce verification failed.', 'mxchat'));
9292 }
9293
9294 // Check permissions
9295 if (!current_user_can('manage_options')) {
9296 wp_die(esc_html__('You do not have sufficient permissions to delete prompts.', 'mxchat'));
9297 }
9298
9299 // Get ID and source parameters
9300 $id = isset($_GET['id']) ? sanitize_text_field($_GET['id']) : '';
9301 $source = isset($_GET['source']) ? sanitize_text_field($_GET['source']) : '';
9302
9303 if (empty($id)) {
9304 wp_die(esc_html__('Invalid prompt ID.', 'mxchat'));
9305 }
9306
9307 $success = false;
9308 $error_message = '';
9309
9310 // Check if Pinecone is enabled and determine source automatically if not specified
9311 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
9312 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
9313
9314 // If source is not specified, determine based on Pinecone configuration
9315 if (empty($source)) {
9316 $source = $use_pinecone ? 'pinecone' : 'wordpress';
9317 }
9318
9319 if ($source === 'pinecone' || $use_pinecone) {
9320 //error_log('[MXCHAT-DELETE] Deleting from Pinecone, ID: ' . $id);
9321
9322 // Handle Pinecone deletion
9323 if (empty($pinecone_options['mxchat_pinecone_host']) ||
9324 empty($pinecone_options['mxchat_pinecone_api_key'])) {
9325 wp_die(esc_html__('Pinecone configuration is missing.', 'mxchat'));
9326 }
9327
9328 // Delete from Pinecone using the vector ID directly
9329 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
9330 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
9331 $id,
9332 $pinecone_options['mxchat_pinecone_api_key'],
9333 $pinecone_options['mxchat_pinecone_host']
9334 );
9335 if ($result['success']) {
9336 $success = true;
9337
9338 // Remove from vector cache
9339 $pinecone_manager->mxchat_remove_from_pinecone_vector_cache($id);
9340 $pinecone_manager->mxchat_remove_from_processed_content_caches($id);
9341
9342 set_transient('mxchat_admin_notice_success',
9343 esc_html__('Vector deleted successfully from Pinecone.', 'mxchat'), 30);
9344 } else {
9345 $error_message = $result['message'];
9346 set_transient('mxchat_admin_notice_error',
9347 esc_html__('Failed to delete from Pinecone: ', 'mxchat') . esc_html($error_message), 30);
9348 }
9349 } else {
9350 //error_log('[MXCHAT-DELETE] Deleting from WordPress database, ID: ' . $id);
9351
9352 // Handle WordPress database deletion
9353 global $wpdb;
9354 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
9355
9356 // Clear cache and delete prompt
9357 wp_cache_delete('prompt_' . $id, 'mxchat_prompts');
9358
9359 $result = $wpdb->delete(
9360 $table_name,
9361 array('id' => intval($id)),
9362 array('%d')
9363 );
9364
9365 if ($result !== false) {
9366 $success = true;
9367 set_transient('mxchat_admin_notice_success',
9368 esc_html__('Entry deleted successfully.', 'mxchat'), 30);
9369 } else {
9370 set_transient('mxchat_admin_notice_error',
9371 esc_html__('Failed to delete entry from database.', 'mxchat'), 30);
9372 }
9373 }
9374
9375 // Redirect back to the prompts page
9376 wp_safe_redirect(add_query_arg(
9377 array(
9378 'page' => 'mxchat-prompts',
9379 'deleted' => $success ? 'true' : 'false'
9380 ),
9381 admin_url('admin.php')
9382 ));
9383 exit;
9384 }
9385
9386
9387
9388
9389 /**
9390 * Averages multiple vectors into a single vector
9391 */
9392 private function mxchat_average_vectors($vectors) {
9393 $vector_length = count($vectors[0]);
9394 $sum_vector = array_fill(0, $vector_length, 0);
9395
9396 foreach ($vectors as $vector) {
9397 for ($i = 0; $i < $vector_length; $i++) {
9398 $sum_vector[$i] += $vector[$i];
9399 }
9400 }
9401
9402 // Divide each component by the number of vectors to get the average
9403 $num_vectors = count($vectors);
9404 for ($i = 0; $i < $vector_length; $i++) {
9405 $sum_vector[$i] /= $num_vectors;
9406 }
9407
9408 return $sum_vector;
9409 }
9410
9411
9412
9413
9414 /**
9415 * Generates embeddings from input text for MXChat
9416 */
9417 public function mxchat_generate_embedding($text) {
9418 // Enable detailed logging for debugging
9419 //error_log('[MXCHAT-EMBED] Starting embedding generation. Text length: ' . strlen($text) . ' bytes');
9420 //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
9421
9422 $options = get_option('mxchat_options');
9423 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
9424 //error_log('[MXCHAT-EMBED] Selected embedding model: ' . $selected_model);
9425
9426 // Determine provider and endpoint
9427 if (strpos($selected_model, 'voyage') === 0) {
9428 $api_key = $options['voyage_api_key'] ?? '';
9429 $endpoint = 'https://api.voyageai.com/v1/embeddings';
9430 $provider_name = 'Voyage AI';
9431 //error_log('[MXCHAT-EMBED] Using Voyage AI API');
9432 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
9433 $api_key = $options['gemini_api_key'] ?? '';
9434 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
9435 $provider_name = 'Google Gemini';
9436 //error_log('[MXCHAT-EMBED] Using Google Gemini API');
9437 } else {
9438 $api_key = $options['api_key'] ?? '';
9439 $endpoint = 'https://api.openai.com/v1/embeddings';
9440 $provider_name = 'OpenAI';
9441 //error_log('[MXCHAT-EMBED] Using OpenAI API');
9442 }
9443
9444 //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
9445
9446 if (empty($api_key)) {
9447 $error_message = sprintf('Missing %s API key. Please configure your API key in the MxChat settings.', $provider_name);
9448 //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
9449 return $error_message;
9450 }
9451
9452 // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
9453 $estimated_tokens = ceil(str_word_count($text) / 0.75);
9454 //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
9455
9456 if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
9457 //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
9458 // Consider truncating text here
9459 }
9460
9461 // Prepare request body based on provider
9462 if (strpos($selected_model, 'gemini-embedding') === 0) {
9463 // Gemini API format
9464 $request_body = array(
9465 'model' => 'models/' . $selected_model,
9466 'content' => array(
9467 'parts' => array(
9468 array('text' => $text)
9469 )
9470 )
9471 );
9472
9473 // Set output dimensionality to 1536 for consistency with other models
9474 $request_body['outputDimensionality'] = 1536;
9475 } else {
9476 // OpenAI/Voyage API format
9477 $request_body = array(
9478 'model' => $selected_model,
9479 'input' => $text
9480 );
9481
9482 // Add output_dimension for voyage-3-large model
9483 if ($selected_model === 'voyage-3-large') {
9484 $request_body['output_dimension'] = 2048;
9485 }
9486 }
9487
9488 //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
9489
9490 // Prepare headers based on provider
9491 if (strpos($selected_model, 'gemini-embedding') === 0) {
9492 // Gemini uses API key as query parameter
9493 $endpoint .= '?key=' . $api_key;
9494 $headers = array(
9495 'Content-Type' => 'application/json'
9496 );
9497 } else {
9498 // OpenAI/Voyage use Bearer token
9499 $headers = array(
9500 'Authorization' => 'Bearer ' . $api_key,
9501 'Content-Type' => 'application/json'
9502 );
9503 }
9504
9505 // Make API request
9506 //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
9507 $response = wp_remote_post($endpoint, array(
9508 'body' => wp_json_encode($request_body),
9509 'headers' => $headers,
9510 'timeout' => 60 // Increased timeout for large inputs
9511 ));
9512
9513 // Handle wp_remote_post errors
9514 if (is_wp_error($response)) {
9515 $error_message = $response->get_error_message();
9516 //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
9517 return 'Connection error: ' . $error_message;
9518 }
9519
9520 // Get and check HTTP response code
9521 $http_code = wp_remote_retrieve_response_code($response);
9522 //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
9523
9524 if ($http_code !== 200) {
9525 $error_body = wp_remote_retrieve_body($response);
9526 //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
9527
9528 // Try to parse error for more details
9529 $error_json = json_decode($error_body, true);
9530 if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
9531 $error_type = $error_json['error']['type'] ?? 'unknown';
9532 $error_message = $error_json['error']['message'] ?? 'No message';
9533 //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
9534 //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
9535
9536 // Customize error message for common API errors
9537 if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
9538 $error_message = sprintf('Invalid %s API key. Please check your API key in the MxChat settings.', $provider_name);
9539 } elseif ($error_type === 'authentication_error') {
9540 $error_message = sprintf('%s authentication failed. Please verify your API key in the MxChat settings.', $provider_name);
9541 }
9542
9543 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
9544 return $error_message;
9545 }
9546
9547 $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding", $http_code);
9548 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
9549 return $error_message;
9550 }
9551
9552 // Parse response body
9553 $response_body = wp_remote_retrieve_body($response);
9554 //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
9555
9556 $response_data = json_decode($response_body, true);
9557
9558 if (json_last_error() !== JSON_ERROR_NONE) {
9559 $error = json_last_error_msg();
9560 //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
9561 //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
9562 return "Failed to parse API response: $error";
9563 }
9564
9565 // Handle different response formats based on provider
9566 if (strpos($selected_model, 'gemini-embedding') === 0) {
9567 // Gemini API response format
9568 if (isset($response_data['embedding']['values'])) {
9569 $embedding_dimensions = count($response_data['embedding']['values']);
9570 //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
9571
9572 // Check if embedding dimensions are as expected (should be 1536)
9573 if ($embedding_dimensions !== 1536) {
9574 //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
9575 }
9576
9577 MxChat_Utils::stamp_active_embedding_model($selected_model);
9578 return $response_data['embedding']['values'];
9579 } else {
9580 //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
9581 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
9582
9583 if (isset($response_data['error'])) {
9584 $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
9585 //error_log('[MXCHAT-EMBED] ' . $error_message);
9586 return $error_message;
9587 }
9588
9589 $error_message = "Invalid Gemini API response format: No embedding found";
9590 //error_log('[MXCHAT-EMBED] ' . $error_message);
9591 return $error_message;
9592 }
9593 } else {
9594 // OpenAI/Voyage API response format
9595 if (isset($response_data['data'][0]['embedding'])) {
9596 $embedding_dimensions = count($response_data['data'][0]['embedding']);
9597 //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
9598
9599 // Check if embedding dimensions are as expected
9600 if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
9601 ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
9602 //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
9603 }
9604
9605 MxChat_Utils::stamp_active_embedding_model($selected_model);
9606 return $response_data['data'][0]['embedding'];
9607 } else {
9608 //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
9609 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
9610
9611 if (isset($response_data['error'])) {
9612 $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
9613 //error_log('[MXCHAT-EMBED] ' . $error_message);
9614 return $error_message;
9615 }
9616
9617 $error_message = "Invalid API response format: No embedding found";
9618 //error_log('[MXCHAT-EMBED] ' . $error_message);
9619 return $error_message;
9620 }
9621 }
9622 }
9623
9624
9625
9626 }
9627 ?>
9628