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

8,673 lines 354.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 add_action('admin_init', array($this, 'mxchat_transcripts_page_init'));
59 add_action('wp_ajax_dismiss_live_agent_notice', array($this, 'dismiss_live_agent_notice'));
60 add_action('wp_ajax_dismiss_theme_migration_notice', array($this, 'dismiss_theme_migration_notice'));
61 add_action('mxchat_cleanup_old_transcripts', array($this, 'cleanup_old_transcripts'));
62
63 add_action('admin_init', array($this, 'register_pinecone_settings'));
64 add_action('admin_init', array($this, 'register_openai_vectorstore_settings'));
65
66 add_action('admin_notices', array($this, 'display_admin_notices'));
67
68 add_action('wp_ajax_mxchat_test_streaming_actual', [$this, 'mxchat_handle_test_streaming_actual']);
69 add_action('wp_ajax_mxchat_test_streaming', [$this, 'mxchat_handle_test_streaming']); // Keep existing as fallback
70 add_action('wp_ajax_mxchat_test_vectorstore_connection', array($this, 'mxchat_test_vectorstore_connection'));
71
72 add_action('wp_ajax_mxchat_save_selected_bot', array($this, 'mxchat_save_selected_bot'));
73 add_action('wp_ajax_mxchat_fetch_openrouter_models', array($this, 'fetch_openrouter_models'));
74 add_action('wp_ajax_mxchat_get_rag_context', array($this, 'mxchat_get_rag_context'));
75
76 // Actions page AJAX handlers
77 add_action('wp_ajax_mxchat_fetch_actions_list', array($this, 'mxchat_fetch_actions_list'));
78 add_action('wp_ajax_mxchat_toggle_action_status', array($this, 'mxchat_toggle_action_status'));
79 add_action('wp_ajax_mxchat_bulk_delete_actions', array($this, 'mxchat_bulk_delete_actions'));
80 add_action('wp_ajax_mxchat_add_intent_ajax', array($this, 'mxchat_add_intent_ajax'));
81 add_action('wp_ajax_mxchat_edit_intent_ajax', array($this, 'mxchat_edit_intent_ajax'));
82 add_action('wp_ajax_mxchat_delete_intent_ajax', array($this, 'mxchat_delete_intent_ajax'));
83 add_action('wp_ajax_mxchat_add_phrase', array($this, 'mxchat_add_phrase_ajax'));
84 add_action('wp_ajax_mxchat_delete_phrase', array($this, 'mxchat_delete_phrase_ajax'));
85 add_action('wp_ajax_mxchat_get_phrases', array($this, 'mxchat_get_phrases_ajax'));
86 add_action('wp_ajax_mxchat_delete_legacy_phrases', array($this, 'mxchat_delete_legacy_phrases_ajax'));
87
88 // Slack test connection
89 add_action('wp_ajax_mxchat_test_slack_connection', array($this, 'mxchat_test_slack_connection'));
90
91 // Translation handlers
92 add_action('wp_ajax_mxchat_translate_messages', array($this, 'mxchat_translate_messages'));
93 add_action('wp_ajax_mxchat_get_transcript_translation', array($this, 'mxchat_get_transcript_translation'));
94 }
95
96 private function is_license_active() {
97 $license_status = get_option('mxchat_license_status', 'inactive');
98 return ($license_status === 'active');
99 }
100
101 private function initialize_default_options() {
102 $default_options = array(
103 'api_key' => '',
104 'xai_api_key' => '',
105 'claude_api_key' => '',
106 'deepseek_api_key' => '',
107 'voyage_api_key' => '',
108 'gemini_api_key' => '',
109 'enable_streaming_toggle' => 'off',
110 'enable_web_search' => 'off',
111 'embedding_model' => 'text-embedding-ada-002',
112 '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:
113
114 # Response Style - CRITICALLY IMPORTANT
115 - MAXIMUM LENGTH: 1-3 short sentences per response
116 - Ultra-concise: Get straight to the answer with no filler
117 - No introductions like "Sure!" or "I\'d be happy to help"
118 - No phrases like "based on my knowledge" or "according to information"
119 - No explanatory text before giving the answer
120 - No summaries or repetition
121 - Hyperlink all URLs
122 - Respond in user\'s language
123 - Minor chit chat or conversation is okay, but try to keep it focused on [insert topic]
124
125 # Knowledge Base Requirements - PREVENT HALLUCINATIONS
126 - ONLY answer questions using information explicitly provided in OFFICIAL KNOWLEDGE DATABASE CONTENT sections marked with ===== delimiters
127 - If required information is NOT in the knowledge database: "I don\'t have enough information in my knowledge base to answer that question accurately."
128 - NEVER invent or hallucinate URLs, links, product specs, procedures, dates, statistics, names, contacts, or company information
129 - When knowledge base information is unclear or contradictory, acknowledge the limitation rather than guessing
130 - Better to admit insufficient information than provide inaccurate answers',
131 'model' => esc_html__('gpt-5.1-chat-latest', 'mxchat'),
132 'rate_limit_logged_out' => esc_html__('100', 'mxchat'),
133 'role_rate_limits' => array(),
134 'rate_limit_message' => esc_html__('Rate limit exceeded. Please try again later.', 'mxchat'),
135 'enable_email_block' => '',
136 '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'),
137 'email_blocker_button_text' => esc_html__('Start Chat', 'mxchat'),
138 'enable_name_field' => 'off', // NEW
139 'name_field_placeholder' => esc_html__('Enter your name', 'mxchat'), // NEW
140 'top_bar_title' => esc_html__('MxChat', 'mxchat'),
141 'intro_message' => __('Hello! How can I assist you today?', 'mxchat'),
142 'ai_agent_text' => esc_html__('AI Agent', 'mxchat'),
143 'input_copy' => esc_html__('How can I assist?', 'mxchat'),
144 'append_to_body' => esc_html__('off', 'mxchat'),
145 'post_type_visibility_mode' => 'all', // 'all', 'include', 'exclude'
146 'post_type_visibility_list' => array(), // Array of post type slugs
147 'contextual_awareness_toggle' => 'off',
148 'citation_links_toggle' => 'on',
149 'close_button_color' => esc_html__('#fff', 'mxchat'),
150 'chatbot_bg_color' => esc_html__('#fff', 'mxchat'),
151 'user_message_bg_color' => esc_html__('#fff', 'mxchat'),
152 'user_message_font_color' => esc_html__('#212121', 'mxchat'),
153 'bot_message_bg_color' => esc_html__('#212121', 'mxchat'),
154 'bot_message_font_color' => esc_html__('#fff', 'mxchat'),
155 'top_bar_bg_color' => esc_html__('#212121', 'mxchat'),
156 'send_button_font_color' => esc_html__('#212121', 'mxchat'),
157 'chat_input_font_color' => esc_html__('#212121', 'mxchat'),
158 'chatbot_background_color' => esc_html__('#212121', 'mxchat'),
159 'icon_color' => esc_html__('#fff', 'mxchat'),
160 'enable_woocommerce_integration' => esc_html__('0', 'mxchat'),
161 'link_target_toggle' => esc_html__('off', 'mxchat'),
162 'pre_chat_message' => esc_html__('Hey there! Ask me anything!', 'mxchat'),
163
164 // New fields for Loops Integration
165 'loops_api_key' => '',
166 'loops_mailing_list' => '',
167 'triggered_phrase_response' => __('Would you like to join our mailing list? Please provide your email below.', 'mxchat'),
168 'email_capture_response' => __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat'),
169 'popular_question_1' => '',
170 'popular_question_2' => '',
171 'popular_question_3' => '',
172 'pdf_intent_trigger_text' => __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat'),
173 'pdf_intent_success_text' => __("I've processed the PDF. What questions do you have about it?", 'mxchat'),
174 'pdf_intent_error_text' => __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat'),
175 'pdf_max_pages' => 69,
176 'show_pdf_upload_button' => 'on',
177 'show_word_upload_button' => 'on',
178
179 // Live Agent Integration (Slack)
180 'live_agent_webhook_url' => '',
181 'live_agent_secret_key' => '',
182 'live_agent_bot_token' => '',
183 'live_agent_message_bg_color' => esc_html__('#ffffff', 'mxchat'),
184 'live_agent_message_font_color' => esc_html__('#333333', 'mxchat'),
185
186 // Telegram Integration
187 'telegram_status' => 'off',
188 'telegram_bot_token' => '',
189 'telegram_group_id' => '',
190 'telegram_webhook_secret' => '',
191 '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'),
192 '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'),
193
194 'chat_toolbar_toggle' => esc_html__('off', 'mxchat'),
195 'mode_indicator_bg_color' => esc_html__('#767676', 'mxchat'),
196 'mode_indicator_font_color' => esc_html__('#ffffff', 'mxchat'),
197 'toolbar_icon_color' => esc_html__('#212121', 'mxchat'),
198
199 // Optimization settings
200 'script_loading_strategy' => 'default',
201
202 // Debug settings
203 'debug_mode' => 'off',
204 );
205
206
207 // Merge existing options with defaults
208 $existing_options = get_option('mxchat_options', array());
209 $merged_options = wp_parse_args($existing_options, $default_options);
210
211 // Update the options if they have changed
212 if ($existing_options !== $merged_options) {
213 update_option('mxchat_options', $merged_options);
214 }
215
216 // Add default limits for each role
217 $roles = wp_roles()->get_names();
218 foreach ($roles as $role_id => $role_name) {
219 $default_options['role_rate_limits'][$role_id] = esc_html__('100', 'mxchat');
220 }
221
222 return $default_options;
223
224 // Update the $this->options property
225 $this->options = $merged_options;
226 }
227
228 public function mxchat_add_plugin_page() {
229 // Main menu page
230 add_menu_page(
231 esc_html__('MxChat Settings', 'mxchat'),
232 esc_html__('MxChat', 'mxchat'),
233 'manage_options',
234 'mxchat-max',
235 array($this, 'mxchat_create_admin_page'),
236 'dashicons-testimonial',
237 6
238 );
239
240 // Rename the first submenu item from "MxChat" to "Settings"
241 add_submenu_page(
242 'mxchat-max',
243 esc_html__('MxChat Settings', 'mxchat'),
244 esc_html__('Settings', 'mxchat'),
245 'manage_options',
246 'mxchat-max',
247 array($this, 'mxchat_create_admin_page')
248 );
249
250 // Submenu page for Knowledge
251 add_submenu_page(
252 'mxchat-max',
253 esc_html__('Prompts', 'mxchat'),
254 esc_html__('Knowledge', 'mxchat'),
255 'manage_options',
256 'mxchat-prompts',
257 array($this, 'mxchat_create_prompts_page')
258 );
259
260 add_submenu_page(
261 'mxchat-max',
262 esc_html__('Chat Transcripts', 'mxchat'),
263 esc_html__('Transcripts', 'mxchat'),
264 'manage_options',
265 'mxchat-transcripts',
266 array($this, 'mxchat_create_transcripts_page')
267 );
268
269 add_submenu_page(
270 'mxchat-max',
271 esc_html__('MxChat Actions', 'mxchat'),
272 esc_html__('Actions', 'mxchat'),
273 'manage_options',
274 'mxchat-actions',
275 array($this, 'mxchat_actions_page_html')
276 );
277
278 // Content Generator page
279 add_submenu_page(
280 'mxchat-max',
281 esc_html__('Content', 'mxchat'),
282 esc_html__('Content', 'mxchat'),
283 'manage_options',
284 'mxchat-content',
285 array($this, 'mxchat_create_content_page')
286 );
287
288 // Consolidated Pro & Extensions page (replaces separate Add Ons and Pro Upgrade pages)
289 add_submenu_page(
290 'mxchat-max',
291 esc_html__('Pro & Extensions', 'mxchat'),
292 esc_html__('Pro & Extensions', 'mxchat'),
293 'manage_options',
294 'mxchat-activation',
295 array($this, 'mxchat_create_activation_page')
296 );
297 }
298
299 public function mxchat_create_addons_page() {
300 require_once plugin_dir_path(__FILE__) . 'class-mxchat-addons.php';
301 $addons_page = new MxChat_Addons();
302 $addons_page->render_page();
303 }
304
305 /**
306 * Render the Content Generator admin page
307 */
308 public function mxchat_create_content_page() {
309 require_once plugin_dir_path(__FILE__) . 'admin-content-page.php';
310 mxchat_render_content_page($this);
311 }
312
313 /**
314 * Test actual streaming functionality in the WordPress environment
315 */
316 public function mxchat_handle_test_streaming_actual() {
317 check_ajax_referer('mxchat_test_streaming_nonce', 'nonce');
318
319 // Check if headers have already been sent
320 if (headers_sent()) {
321 wp_send_json_error(['message' => 'Headers already sent - streaming not possible']);
322 return;
323 }
324
325 // Check for required functions
326 if (!function_exists('curl_init')) {
327 wp_send_json_error(['message' => 'cURL not available - streaming requires cURL']);
328 return;
329 }
330
331 // Get user's selected model and API key
332 $options = get_option('mxchat_options', []);
333 $selected_model = $options['model'] ?? 'gpt-5.1-chat-latest';
334
335 // Get the provider from the model
336 $model_parts = explode('-', $selected_model);
337 $provider = strtolower($model_parts[0]);
338
339 // Get the appropriate API key
340 $api_key = '';
341 switch ($provider) {
342 case 'gpt':
343 case 'o1':
344 $api_key = $options['api_key'] ?? '';
345 break;
346 case 'claude':
347 $api_key = $options['claude_api_key'] ?? '';
348 break;
349 case 'grok':
350 $api_key = $options['xai_api_key'] ?? '';
351 break;
352 case 'deepseek':
353 $api_key = $options['deepseek_api_key'] ?? '';
354 break;
355 case 'gemini':
356 $api_key = $options['gemini_api_key'] ?? '';
357 break;
358 default:
359 // Default to OpenAI for unknown models
360 $api_key = $options['api_key'] ?? '';
361 $provider = 'gpt';
362 break;
363 }
364
365 if (empty($api_key)) {
366 wp_send_json_error(['message' => "API key not configured for {$provider} provider"]);
367 return;
368 }
369
370 // Test streaming with the selected model and provider
371 try {
372 $this->perform_streaming_test($provider, $selected_model, $api_key);
373 } catch (Exception $e) {
374 wp_send_json_error(['message' => 'Streaming test exception: ' . $e->getMessage()]);
375 }
376 }
377
378 /**
379 * Perform the actual streaming test
380 */
381 private function perform_streaming_test($provider, $model, $api_key) {
382 // Set streaming headers
383 header('Content-Type: text/event-stream');
384 header('Cache-Control: no-cache');
385 header('X-Accel-Buffering: no'); // Disable nginx buffering
386
387 // Prepare test message
388 $test_message = "Please respond with exactly: 'Streaming test successful!' - send this as a short response for testing.";
389
390 // Configure API request based on provider
391 $url = '';
392 $headers = [];
393 $body = [];
394
395 switch ($provider) {
396 case 'gpt':
397 case 'o1':
398 $url = 'https://api.openai.com/v1/chat/completions';
399 $headers = [
400 'Content-Type: application/json',
401 'Authorization: Bearer ' . $api_key
402 ];
403 $body = [
404 'model' => $model,
405 'messages' => [['role' => 'user', 'content' => $test_message]],
406 'max_tokens' => 50,
407 'temperature' => 0.3,
408 'stream' => true
409 ];
410 break;
411
412 case 'claude':
413 $url = 'https://api.anthropic.com/v1/messages';
414 $headers = [
415 'Content-Type: application/json',
416 'x-api-key: ' . $api_key,
417 'anthropic-version: 2023-06-01'
418 ];
419 $body = [
420 'model' => $model,
421 'messages' => [['role' => 'user', 'content' => $test_message]],
422 'max_tokens' => 50,
423 'temperature' => 0.3,
424 'stream' => true
425 ];
426 break;
427
428 case 'grok':
429 $url = 'https://api.x.ai/v1/chat/completions';
430 $headers = [
431 'Content-Type: application/json',
432 'Authorization: Bearer ' . $api_key
433 ];
434 $body = [
435 'model' => $model,
436 'messages' => [['role' => 'user', 'content' => $test_message]],
437 'max_tokens' => 50,
438 'temperature' => 0.3,
439 'stream' => true
440 ];
441 break;
442
443 case 'deepseek':
444 $url = 'https://api.deepseek.com/v1/chat/completions';
445 $headers = [
446 'Content-Type: application/json',
447 'Authorization: Bearer ' . $api_key
448 ];
449 $body = [
450 'model' => $model,
451 'messages' => [['role' => 'user', 'content' => $test_message]],
452 'max_tokens' => 50,
453 'temperature' => 0.3,
454 'stream' => true
455 ];
456 break;
457
458 default:
459 echo "data: " . json_encode(['error' => 'Unsupported provider for streaming test: ' . $provider]) . "\n\n";
460 flush();
461 return;
462 }
463
464 // Initialize cURL
465 $ch = curl_init();
466 curl_setopt($ch, CURLOPT_URL, $url);
467 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
468 curl_setopt($ch, CURLOPT_POST, true);
469 curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
470 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
471 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
472 curl_setopt($ch, CURLOPT_TIMEOUT, 30);
473 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use ($provider) {
474 return $this->process_streaming_test_data($data, $provider);
475 });
476
477 // Send initial test message
478 echo "data: " . json_encode(['content' => '[Starting streaming test...]']) . "\n\n";
479 flush();
480
481 $result = curl_exec($ch);
482 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
483 $curl_error = curl_error($ch);
484 curl_close($ch);
485
486 if ($curl_error) {
487 echo "data: " . json_encode(['error' => 'cURL Error: ' . $curl_error]) . "\n\n";
488 flush();
489 return;
490 }
491
492 if ($http_code !== 200) {
493 echo "data: " . json_encode(['error' => 'API returned HTTP ' . $http_code]) . "\n\n";
494 flush();
495 return;
496 }
497
498 // Send completion signal
499 echo "data: [DONE]\n\n";
500 flush();
501 }
502
503 /**
504 * Process streaming data for the test
505 */
506 private function process_streaming_test_data($data, $provider) {
507 static $chunk_count = 0;
508
509 $lines = explode("\n", $data);
510
511 foreach ($lines as $line) {
512 if (trim($line) === '') {
513 continue;
514 }
515
516 // Handle different provider formats
517 if ($provider === 'claude') {
518 // Claude uses event: and data: format
519 if (strpos($line, 'data: ') === 0) {
520 $json_str = substr($line, 6);
521 $json = json_decode($json_str, true);
522
523 if (isset($json['type']) && $json['type'] === 'content_block_delta') {
524 if (isset($json['delta']['text'])) {
525 $chunk_count++;
526 echo "data: " . json_encode([
527 'content' => $json['delta']['text'],
528 'test_chunk' => $chunk_count
529 ]) . "\n\n";
530 flush();
531 }
532 }
533 }
534 } else {
535 // OpenAI, X.AI, DeepSeek format
536 if (strpos($line, 'data: ') === 0) {
537 $json_str = substr($line, 6);
538
539 if ($json_str === '[DONE]') {
540 // Don't echo [DONE] here, let the main function handle it
541 continue;
542 }
543
544 $json = json_decode($json_str, true);
545 if (isset($json['choices'][0]['delta']['content'])) {
546 $chunk_count++;
547 echo "data: " . json_encode([
548 'content' => $json['choices'][0]['delta']['content'],
549 'test_chunk' => $chunk_count
550 ]) . "\n\n";
551 flush();
552 }
553 }
554 }
555 }
556
557 return strlen($data);
558 }
559
560 /**
561 * Updated version of your existing test method (keep this as a fallback)
562 */
563 public function mxchat_handle_test_streaming() {
564 check_ajax_referer('mxchat_test_streaming_nonce', 'nonce');
565
566 // Use the actual streaming test instead
567 $this->mxchat_handle_test_streaming_actual();
568 }
569
570
571 public function register_pinecone_settings() {
572 register_setting(
573 'mxchat_pinecone_addon_options',
574 'mxchat_pinecone_addon_options',
575 array(
576 'type' => 'array',
577 'sanitize_callback' => array($this, 'sanitize_pinecone_settings'),
578 'default' => array(
579 'mxchat_use_pinecone' => '0',
580 'mxchat_pinecone_api_key' => '',
581 'mxchat_pinecone_host' => '',
582 'mxchat_pinecone_index' => '',
583 'mxchat_pinecone_environment' => ''
584 )
585 )
586 );
587 }
588
589 public function sanitize_pinecone_settings($input) {
590 $sanitized = array();
591
592 $sanitized['mxchat_use_pinecone'] = isset($input['mxchat_use_pinecone']) ? '1' : '0';
593 $sanitized['mxchat_pinecone_api_key'] = sanitize_text_field($input['mxchat_pinecone_api_key'] ?? '');
594 $sanitized['mxchat_pinecone_host'] = sanitize_text_field($input['mxchat_pinecone_host'] ?? '');
595 $sanitized['mxchat_pinecone_index'] = sanitize_text_field($input['mxchat_pinecone_index'] ?? '');
596 $sanitized['mxchat_pinecone_environment'] = sanitize_text_field($input['mxchat_pinecone_environment'] ?? '');
597
598 // Remove https:// from host if present
599 $sanitized['mxchat_pinecone_host'] = str_replace(['https://', 'http://'], '', $sanitized['mxchat_pinecone_host']);
600
601 return $sanitized;
602 }
603
604 public function register_openai_vectorstore_settings() {
605 register_setting(
606 'mxchat_openai_vectorstore_options',
607 'mxchat_openai_vectorstore_options',
608 array(
609 'type' => 'array',
610 'sanitize_callback' => array($this, 'sanitize_openai_vectorstore_settings'),
611 'default' => array(
612 'mxchat_use_openai_vectorstore' => '0',
613 'mxchat_vectorstore_ids' => '',
614 'mxchat_vectorstore_max_results' => 5
615 )
616 )
617 );
618 }
619
620 public function sanitize_openai_vectorstore_settings($input) {
621 $sanitized = array();
622
623 $sanitized['mxchat_use_openai_vectorstore'] = isset($input['mxchat_use_openai_vectorstore']) ? '1' : '0';
624 $sanitized['mxchat_vectorstore_ids'] = sanitize_text_field($input['mxchat_vectorstore_ids'] ?? '');
625 $sanitized['mxchat_vectorstore_max_results'] = absint($input['mxchat_vectorstore_max_results'] ?? 5);
626
627 // Ensure max results is within reasonable range
628 if ($sanitized['mxchat_vectorstore_max_results'] < 1) {
629 $sanitized['mxchat_vectorstore_max_results'] = 1;
630 }
631 if ($sanitized['mxchat_vectorstore_max_results'] > 20) {
632 $sanitized['mxchat_vectorstore_max_results'] = 20;
633 }
634
635 return $sanitized;
636 }
637
638 /**
639 * AJAX handler to test OpenAI Vector Store connection
640 */
641 public function mxchat_test_vectorstore_connection() {
642 check_ajax_referer('mxchat_admin_nonce', 'nonce');
643
644 if (!current_user_can('manage_options')) {
645 wp_send_json_error(array('message' => __('Permission denied.', 'mxchat')));
646 return;
647 }
648
649 $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
650 $vectorstore_ids = $vectorstore_options['mxchat_vectorstore_ids'] ?? '';
651 $mxchat_options = get_option('mxchat_options', array());
652 $api_key = $mxchat_options['api_key'] ?? '';
653
654 if (empty($api_key)) {
655 wp_send_json_error(array('message' => __('OpenAI API key is not configured.', 'mxchat')));
656 return;
657 }
658
659 if (empty($vectorstore_ids)) {
660 wp_send_json_error(array('message' => __('No Vector Store ID configured.', 'mxchat')));
661 return;
662 }
663
664 // Get the first Vector Store ID for testing
665 $ids_array = array_map('trim', explode(',', $vectorstore_ids));
666 $test_id = $ids_array[0];
667
668 // Test by retrieving the Vector Store info
669 $response = wp_remote_get(
670 'https://api.openai.com/v1/vector_stores/' . $test_id,
671 array(
672 'headers' => array(
673 'Authorization' => 'Bearer ' . $api_key,
674 'Content-Type' => 'application/json',
675 'OpenAI-Beta' => 'assistants=v2'
676 ),
677 'timeout' => 30
678 )
679 );
680
681 if (is_wp_error($response)) {
682 wp_send_json_error(array('message' => __('Connection failed: ', 'mxchat') . $response->get_error_message()));
683 return;
684 }
685
686 $status_code = wp_remote_retrieve_response_code($response);
687 $body = json_decode(wp_remote_retrieve_body($response), true);
688
689 if ($status_code === 200 && isset($body['id'])) {
690 $file_count = $body['file_counts']['completed'] ?? 0;
691 $name = $body['name'] ?? $test_id;
692 wp_send_json_success(array(
693 'message' => sprintf(
694 __('Connected successfully! Vector Store: %s (%d files)', 'mxchat'),
695 esc_html($name),
696 $file_count
697 )
698 ));
699 } elseif ($status_code === 404) {
700 wp_send_json_error(array('message' => __('Vector Store not found. Please check the ID.', 'mxchat')));
701 } elseif ($status_code === 401) {
702 wp_send_json_error(array('message' => __('Invalid API key.', 'mxchat')));
703 } else {
704 $error_message = $body['error']['message'] ?? __('Unknown error occurred.', 'mxchat');
705 wp_send_json_error(array('message' => $error_message));
706 }
707 }
708
709 /**
710 * AJAX handler to test Slack connection and validate scopes
711 */
712 public function mxchat_test_slack_connection() {
713 check_ajax_referer('mxchat_admin_nonce', 'nonce');
714
715 if (!current_user_can('manage_options')) {
716 wp_send_json_error(array('message' => __('Permission denied.', 'mxchat')));
717 return;
718 }
719
720 $bot_token = $this->options['live_agent_bot_token'] ?? '';
721
722 if (empty($bot_token)) {
723 wp_send_json_error(array('message' => __('Slack Bot Token is not configured. Please enter your token and save settings first.', 'mxchat')));
724 return;
725 }
726
727 // Test 1: Validate bot token with auth.test
728 $auth_response = wp_remote_post('https://slack.com/api/auth.test', array(
729 'headers' => array(
730 'Authorization' => 'Bearer ' . $bot_token,
731 'Content-Type' => 'application/json'
732 ),
733 'timeout' => 15
734 ));
735
736 if (is_wp_error($auth_response)) {
737 wp_send_json_error(array('message' => __('Connection failed: ', 'mxchat') . $auth_response->get_error_message()));
738 return;
739 }
740
741 $auth_body = json_decode(wp_remote_retrieve_body($auth_response), true);
742
743 if (!isset($auth_body['ok']) || !$auth_body['ok']) {
744 $error = $auth_body['error'] ?? 'unknown_error';
745 $error_messages = array(
746 'invalid_auth' => __('Invalid bot token. Please check your token starts with xoxb-', 'mxchat'),
747 'not_authed' => __('No authentication token provided.', 'mxchat'),
748 'account_inactive' => __('The Slack workspace has been deactivated.', 'mxchat'),
749 'token_revoked' => __('The bot token has been revoked. Please generate a new one.', 'mxchat'),
750 );
751 $message = $error_messages[$error] ?? sprintf(__('Authentication failed: %s', 'mxchat'), $error);
752 wp_send_json_error(array('message' => $message));
753 return;
754 }
755
756 $team_name = $auth_body['team'] ?? 'Unknown Workspace';
757 $bot_name = $auth_body['user'] ?? 'Unknown Bot';
758
759 // Test 2: Check if we can list channels (tests channels:read scope)
760 $channels_response = wp_remote_post('https://slack.com/api/conversations.list', array(
761 'headers' => array(
762 'Authorization' => 'Bearer ' . $bot_token,
763 'Content-Type' => 'application/json'
764 ),
765 'body' => json_encode(array('limit' => 1)),
766 'timeout' => 15
767 ));
768
769 $channels_body = json_decode(wp_remote_retrieve_body($channels_response), true);
770 $can_read_channels = isset($channels_body['ok']) && $channels_body['ok'];
771
772 // Test 3: Check if we can create channels (tests channels:manage scope)
773 // We'll just check the error message without actually creating
774 $create_response = wp_remote_post('https://slack.com/api/conversations.create', array(
775 'headers' => array(
776 'Authorization' => 'Bearer ' . $bot_token,
777 'Content-Type' => 'application/json'
778 ),
779 'body' => json_encode(array('name' => 'mxchat-test-' . time(), 'is_private' => false)),
780 'timeout' => 15
781 ));
782
783 $create_body = json_decode(wp_remote_retrieve_body($create_response), true);
784
785 // If channel was created, delete it immediately
786 if (isset($create_body['ok']) && $create_body['ok'] && isset($create_body['channel']['id'])) {
787 wp_remote_post('https://slack.com/api/conversations.archive', array(
788 'headers' => array(
789 'Authorization' => 'Bearer ' . $bot_token,
790 'Content-Type' => 'application/json'
791 ),
792 'body' => json_encode(array('channel' => $create_body['channel']['id'])),
793 'timeout' => 15
794 ));
795 $can_create_channels = true;
796 } else {
797 $create_error = $create_body['error'] ?? '';
798 // name_taken means we have permission but channel exists
799 $can_create_channels = ($create_error === 'name_taken' || (isset($create_body['ok']) && $create_body['ok']));
800
801 // Check for missing scope errors
802 if ($create_error === 'missing_scope') {
803 $can_create_channels = false;
804 }
805 }
806
807 // Build result message
808 $results = array();
809 $results[] = sprintf(__('Workspace: %s', 'mxchat'), esc_html($team_name));
810 $results[] = sprintf(__('Bot: %s', 'mxchat'), esc_html($bot_name));
811 $results[] = '';
812 $results[] = ($can_read_channels ? '' : '') . ' ' . __('channels:read - List channels', 'mxchat');
813 $results[] = ($can_create_channels ? '' : '') . ' ' . __('channels:manage - Create channels', 'mxchat');
814
815 $missing_scopes = array();
816 if (!$can_read_channels) $missing_scopes[] = 'channels:read';
817 if (!$can_create_channels) $missing_scopes[] = 'channels:manage';
818
819 if (!empty($missing_scopes)) {
820 wp_send_json_error(array(
821 'message' => implode("\n", $results),
822 'missing_scopes' => $missing_scopes,
823 'partial' => true
824 ));
825 } else {
826 wp_send_json_success(array(
827 'message' => implode("\n", $results)
828 ));
829 }
830 }
831
832 public function mxchat_display_admin_notice() {
833 // Success notice
834 if ($message = get_transient('mxchat_admin_notice_success')) {
835 ?>
836 <div class="notice notice-success is-dismissible">
837 <p><?php echo esc_html($message); ?></p>
838 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php echo esc_html__('Dismiss this notice.', 'mxchat'); ?></span></button>
839 </div>
840 <?php
841 delete_transient('mxchat_admin_notice_success'); // Clear the transient after displaying
842 }
843
844 // Error notice
845 if ($message = get_transient('mxchat_admin_notice_error')) {
846 ?>
847 <div class="notice notice-error is-dismissible">
848 <p><?php echo esc_html($message); ?></p>
849 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php echo esc_html__('Dismiss this notice.', 'mxchat'); ?></span></button>
850 </div>
851 <?php
852 delete_transient('mxchat_admin_notice_error'); // Clear the transient after displaying
853 }
854 }
855
856 public function show_live_agent_disabled_banner() {
857 $show_disabled_notice = get_option('mxchat_show_live_agent_disabled_notice', false);
858
859 if ($show_disabled_notice) {
860 ?>
861 <div class="mxchat-live-agent-disabled-notice" id="mxchat-disabled-notice">
862 <div class="mxchat-pro-notification">
863 <button type="button" class="mxchat-dismiss-btn" onclick="dismissLiveAgentNotice()" aria-label="Dismiss notification">
864 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
865 <line x1="18" y1="6" x2="6" y2="18"></line>
866 <line x1="6" y1="6" x2="18" y2="18"></line>
867 </svg>
868 </button>
869 <div class="mxchat-live-agent-content">
870 <h3>🔧 Live Agent Integration Updated!</h3>
871 <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>
872 </div>
873 </div>
874 </div>
875 <?php
876 }
877 }
878 public function mxchat_create_admin_page() {
879 $this->add_live_agent_nonce();
880 $this->add_theme_migration_nonce();
881
882 // Include and render the new sidebar-based settings page
883 require_once plugin_dir_path(__FILE__) . 'admin-settings-page.php';
884 mxchat_render_settings_page($this);
885 }
886
887 public function dismiss_live_agent_notice() {
888 // Add debugging
889 //error_log('dismiss_live_agent_notice called');
890 //error_log('POST data: ' . print_r($_POST, true));
891
892 // Verify nonce
893 if (!wp_verify_nonce($_POST['nonce'], 'dismiss_live_agent_notice')) {
894 //error_log('Nonce verification failed');
895 wp_die('Security check failed');
896 }
897
898 // Remove the notice flag
899 $deleted = delete_option('mxchat_show_live_agent_disabled_notice');
900 //error_log('Option deleted: ' . ($deleted ? 'yes' : 'no'));
901
902 wp_send_json_success();
903 }
904 public function add_live_agent_nonce() {
905 if (get_option('mxchat_show_live_agent_disabled_notice', false)) {
906 // Make sure your admin script is enqueued and localize the data
907 wp_localize_script('mxchat-admin-js', 'mxchatLiveAgent', array(
908 'nonce' => wp_create_nonce('dismiss_live_agent_notice'),
909 'ajaxurl' => admin_url('admin-ajax.php')
910 ));
911 }
912 }
913
914 /**
915 * Show theme migration notice for Pro users with AI-generated themes
916 * Only shown once - dismissible and stored in options
917 */
918 public function show_theme_migration_banner() {
919 // Only show if Pro is activated
920 if (!$this->is_activated) {
921 return;
922 }
923
924 // Check if notice should be shown
925 $show_notice = get_option('mxchat_show_theme_migration_notice', false);
926
927 if ($show_notice) {
928 ?>
929 <div class="mxchat-theme-migration-notice" id="mxchat-theme-migration-notice">
930 <div class="mxchat-pro-notification">
931 <button type="button" class="mxchat-dismiss-btn" onclick="dismissThemeMigrationNotice()" aria-label="Dismiss notification">
932 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
933 <line x1="18" y1="6" x2="6" y2="18"></line>
934 <line x1="6" y1="6" x2="18" y2="18"></line>
935 </svg>
936 </button>
937 <div class="mxchat-theme-migration-content">
938 <h3>🎨 AI Theme Migration Required</h3>
939 <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>
940 </div>
941 </div>
942 </div>
943 <?php
944 }
945 }
946
947 /**
948 * Dismiss theme migration notice via AJAX
949 */
950 public function dismiss_theme_migration_notice() {
951 // Verify nonce
952 if (!wp_verify_nonce($_POST['nonce'], 'dismiss_theme_migration_notice')) {
953 wp_die('Security check failed');
954 }
955
956 // Remove the notice flag
957 delete_option('mxchat_show_theme_migration_notice');
958
959 wp_send_json_success();
960 }
961
962 /**
963 * Add nonce for theme migration notice dismiss
964 */
965 public function add_theme_migration_nonce() {
966 if (get_option('mxchat_show_theme_migration_notice', false) && $this->is_activated) {
967 wp_localize_script('mxchat-admin-js', 'mxchatThemeMigration', array(
968 'nonce' => wp_create_nonce('dismiss_theme_migration_notice'),
969 'ajaxurl' => admin_url('admin-ajax.php')
970 ));
971 }
972 }
973
974 public function mxchat_create_transcripts_page() {
975 global $wpdb;
976 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
977
978 // Get basic stats
979 $total_chats = $wpdb->get_var("SELECT COUNT(DISTINCT session_id) FROM $table_name") ?: 0;
980 $total_messages = $wpdb->get_var("SELECT COUNT(*) FROM $table_name") ?: 0;
981
982 // Count unique users with detailed breakdown
983 $total_users = $wpdb->get_var("
984 SELECT COUNT(DISTINCT
985 CASE
986 WHEN user_email != '' AND user_email IS NOT NULL THEN user_email
987 WHEN user_id != 0 THEN CONCAT('user_', user_id)
988 WHEN user_identifier NOT LIKE 'Tech-Savvy User'
989 AND user_identifier NOT LIKE 'Detail-Oriented User'
990 AND user_identifier NOT LIKE 'Language Learner'
991 AND user_identifier NOT LIKE 'Casual Browser'
992 AND user_identifier NOT LIKE 'Policy Enforcer'
993 AND user_identifier NOT LIKE 'Researcher'
994 AND user_identifier NOT LIKE 'Loyalty Member'
995 AND user_identifier NOT LIKE 'Gift Buyer'
996 AND user_identifier NOT LIKE 'Parent or Caregiver'
997 THEN user_identifier
998 ELSE session_id
999 END
1000 )
1001 FROM $table_name
1002 WHERE role != 'assistant'
1003 ");
1004
1005 // Get user type breakdown
1006 $registered_users = $wpdb->get_var("
1007 SELECT COUNT(DISTINCT user_email)
1008 FROM $table_name
1009 WHERE user_email != '' AND user_email IS NOT NULL
1010 ");
1011
1012 $guest_users = $wpdb->get_var("
1013 SELECT COUNT(DISTINCT user_identifier)
1014 FROM $table_name
1015 WHERE (user_email = '' OR user_email IS NULL)
1016 AND role != 'assistant'
1017 AND user_identifier NOT LIKE 'Tech-Savvy User'
1018 AND user_identifier NOT LIKE 'Detail-Oriented User'
1019 AND user_identifier NOT LIKE 'Language Learner'
1020 AND user_identifier NOT LIKE 'Casual Browser'
1021 AND user_identifier NOT LIKE 'Policy Enforcer'
1022 AND user_identifier NOT LIKE 'Researcher'
1023 AND user_identifier NOT LIKE 'Loyalty Member'
1024 AND user_identifier NOT LIKE 'Gift Buyer'
1025 AND user_identifier NOT LIKE 'Parent or Caregiver'
1026 ");
1027
1028 // Get agent test messages count
1029 $agent_tests = $wpdb->get_var("
1030 SELECT COUNT(DISTINCT session_id)
1031 FROM $table_name
1032 WHERE user_identifier IN (
1033 'Tech-Savvy User',
1034 'Detail-Oriented User',
1035 'Language Learner',
1036 'Casual Browser',
1037 'Policy Enforcer',
1038 'Researcher',
1039 'Loyalty Member',
1040 'Gift Buyer',
1041 'Parent or Caregiver'
1042 )
1043 ");
1044 // Get activity metrics
1045 $today_chats = $wpdb->get_var("
1046 SELECT COUNT(DISTINCT session_id)
1047 FROM $table_name
1048 WHERE DATE(timestamp) = CURDATE()
1049 ");
1050
1051 $week_chats = $wpdb->get_var("
1052 SELECT COUNT(DISTINCT session_id)
1053 FROM $table_name
1054 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)
1055 ");
1056
1057 $month_chats = $wpdb->get_var("
1058 SELECT COUNT(DISTINCT session_id)
1059 FROM $table_name
1060 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 30 DAY)
1061 ");
1062
1063 // Get daily chat data for last 7 days
1064 $daily_stats = $wpdb->get_results("
1065 SELECT
1066 DATE(timestamp) as date,
1067 COUNT(DISTINCT session_id) as chats,
1068 COUNT(*) as messages
1069 FROM $table_name
1070 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)
1071 GROUP BY DATE(timestamp)
1072 ORDER BY date ASC
1073 ");
1074
1075 // Get average messages per chat
1076 $avg_messages = $wpdb->get_var("
1077 SELECT AVG(message_count)
1078 FROM (
1079 SELECT session_id, COUNT(*) as message_count
1080 FROM $table_name
1081 GROUP BY session_id
1082 ) as chat_counts
1083 ");
1084 $avg_messages = $avg_messages ? round($avg_messages, 1) : 0;
1085
1086 // Get busiest hour
1087 $busiest_hour = $wpdb->get_row("
1088 SELECT HOUR(timestamp) as hour, COUNT(DISTINCT session_id) as chat_count
1089 FROM $table_name
1090 GROUP BY HOUR(timestamp)
1091 ORDER BY chat_count DESC
1092 LIMIT 1
1093 ");
1094
1095 // Prepare chart data
1096 $chart_labels = array();
1097 $chart_chats = array();
1098 $chart_messages = array();
1099
1100 // Fill last 7 days with data
1101 for ($i = 6; $i >= 0; $i--) {
1102 $date = date('Y-m-d', strtotime("-$i days"));
1103 $day_name = date('D', strtotime("-$i days"));
1104 $chart_labels[] = $day_name;
1105
1106 $found = false;
1107 foreach ($daily_stats as $stat) {
1108 if ($stat->date === $date) {
1109 $chart_chats[] = (int)$stat->chats;
1110 $chart_messages[] = (int)$stat->messages;
1111 $found = true;
1112 break;
1113 }
1114 }
1115 if (!$found) {
1116 $chart_chats[] = 0;
1117 $chart_messages[] = 0;
1118 }
1119 }
1120
1121 // Prepare page data for the template
1122 $page_data = array(
1123 'total_chats' => $total_chats,
1124 'total_messages' => $total_messages,
1125 'total_users' => $total_users,
1126 'registered_users' => $registered_users,
1127 'guest_users' => $guest_users,
1128 'agent_tests' => $agent_tests,
1129 'today_chats' => $today_chats,
1130 'week_chats' => $week_chats,
1131 'month_chats' => $month_chats,
1132 'avg_messages' => $avg_messages,
1133 'busiest_hour' => $busiest_hour,
1134 'chart_labels' => $chart_labels,
1135 'chart_chats' => $chart_chats,
1136 'chart_messages' => $chart_messages,
1137 );
1138
1139 // Include and render the new template
1140 require_once plugin_dir_path(__FILE__) . 'admin-transcripts-page.php';
1141 mxchat_render_transcripts_page($this, $page_data);
1142 }
1143
1144 /**
1145 * Get chart data for transcripts page
1146 * Used by both page render and script localization
1147 *
1148 * @return array Chart data with labels, chats, and messages arrays
1149 */
1150 private function get_transcripts_chart_data() {
1151 global $wpdb;
1152 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1153
1154 // Get daily chat data for last 7 days - same query as mxchat_transcripts_page()
1155 $daily_stats = $wpdb->get_results("
1156 SELECT
1157 DATE(timestamp) as date,
1158 COUNT(DISTINCT session_id) as chats,
1159 COUNT(*) as messages
1160 FROM $table_name
1161 WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)
1162 GROUP BY DATE(timestamp)
1163 ORDER BY date ASC
1164 ");
1165
1166 // Prepare chart data
1167 $chart_labels = array();
1168 $chart_chats = array();
1169 $chart_messages = array();
1170
1171 // Fill last 7 days with data - same logic as mxchat_transcripts_page()
1172 for ($i = 6; $i >= 0; $i--) {
1173 $date = date('Y-m-d', strtotime("-$i days"));
1174 $day_name = date('D', strtotime("-$i days"));
1175 $chart_labels[] = $day_name;
1176
1177 $found = false;
1178 if ($daily_stats) {
1179 foreach ($daily_stats as $stat) {
1180 if ($stat->date === $date) {
1181 $chart_chats[] = (int)$stat->chats;
1182 $chart_messages[] = (int)$stat->messages;
1183 $found = true;
1184 break;
1185 }
1186 }
1187 }
1188 if (!$found) {
1189 $chart_chats[] = 0;
1190 $chart_messages[] = 0;
1191 }
1192 }
1193
1194 return array(
1195 'labels' => $chart_labels,
1196 'chats' => $chart_chats,
1197 'messages' => $chart_messages
1198 );
1199 }
1200
1201 public function mxchat_transcripts_notification_section_callback() {
1202 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>';
1203 }
1204 public function mxchat_enable_notifications_callback() {
1205 $options = get_option('mxchat_transcripts_options', array());
1206 $enabled = isset($options['mxchat_enable_notifications']) ? $options['mxchat_enable_notifications'] : 0;
1207 ?>
1208 <label for="mxchat_enable_notifications">
1209 <input type="checkbox" id="mxchat_enable_notifications"
1210 name="mxchat_transcripts_options[mxchat_enable_notifications]"
1211 value="1" <?php checked(1, $enabled); ?>>
1212 <?php esc_html_e('Send email notification when a new chat session starts', 'mxchat'); ?>
1213 </label>
1214 <p class="description">
1215 <?php esc_html_e('Enable this option to receive email notifications for new chat sessions.', 'mxchat'); ?>
1216 </p>
1217 <?php
1218 }
1219 public function mxchat_notification_email_callback() {
1220 $options = get_option('mxchat_transcripts_options', array());
1221 $email = isset($options['mxchat_notification_email']) ? $options['mxchat_notification_email'] : get_option('admin_email');
1222 ?>
1223 <input type="email" id="mxchat_notification_email"
1224 name="mxchat_transcripts_options[mxchat_notification_email]"
1225 value="<?php echo esc_attr($email); ?>"
1226 class="regular-text">
1227 <p class="description">
1228 <?php esc_html_e('Enter the email address where notifications should be sent. Defaults to the admin email address.', 'mxchat'); ?>
1229 </p>
1230 <?php
1231 }
1232
1233 public function mxchat_auto_delete_transcripts_callback() {
1234 $options = get_option('mxchat_transcripts_options', array());
1235 $interval = isset($options['mxchat_auto_delete_transcripts']) ? $options['mxchat_auto_delete_transcripts'] : 'never';
1236 ?>
1237 <select id="mxchat_auto_delete_transcripts"
1238 name="mxchat_transcripts_options[mxchat_auto_delete_transcripts]">
1239 <option value="never" <?php selected($interval, 'never'); ?>>
1240 <?php esc_html_e('Never (Keep All Transcripts)', 'mxchat'); ?>
1241 </option>
1242 <option value="1week" <?php selected($interval, '1week'); ?>>
1243 <?php esc_html_e('After 1 Week', 'mxchat'); ?>
1244 </option>
1245 <option value="2weeks" <?php selected($interval, '2weeks'); ?>>
1246 <?php esc_html_e('After 2 Weeks', 'mxchat'); ?>
1247 </option>
1248 <option value="1month" <?php selected($interval, '1month'); ?>>
1249 <?php esc_html_e('After 1 Month', 'mxchat'); ?>
1250 </option>
1251 </select>
1252 <p class="description">
1253 <?php esc_html_e('Automatically delete old chat transcripts after the selected time period. This helps manage database size and privacy.', 'mxchat'); ?>
1254 </p>
1255 <?php
1256 }
1257
1258 public function mxchat_auto_email_transcript_callback() {
1259 $options = get_option('mxchat_transcripts_options', array());
1260 $enabled = isset($options['mxchat_auto_email_transcript_enabled']) ? $options['mxchat_auto_email_transcript_enabled'] : 0;
1261 $delay = isset($options['mxchat_auto_email_transcript_delay']) ? $options['mxchat_auto_email_transcript_delay'] : '30';
1262 $require_contact = isset($options['mxchat_auto_email_transcript_require_contact']) ? $options['mxchat_auto_email_transcript_require_contact'] : 0;
1263 ?>
1264 <label>
1265 <input type="checkbox"
1266 id="mxchat_auto_email_transcript_enabled"
1267 name="mxchat_transcripts_options[mxchat_auto_email_transcript_enabled]"
1268 value="1"
1269 <?php checked($enabled, 1); ?>>
1270 <?php esc_html_e('Enable Auto-Email of Full Transcript', 'mxchat'); ?>
1271 </label>
1272 <br><br>
1273 <label for="mxchat_auto_email_transcript_delay">
1274 <?php esc_html_e('Send transcript after:', 'mxchat'); ?>
1275 </label>
1276 <select id="mxchat_auto_email_transcript_delay"
1277 name="mxchat_transcripts_options[mxchat_auto_email_transcript_delay]">
1278 <option value="15" <?php selected($delay, '15'); ?>>
1279 <?php esc_html_e('15 minutes', 'mxchat'); ?>
1280 </option>
1281 <option value="30" <?php selected($delay, '30'); ?>>
1282 <?php esc_html_e('30 minutes', 'mxchat'); ?>
1283 </option>
1284 <option value="60" <?php selected($delay, '60'); ?>>
1285 <?php esc_html_e('1 hour', 'mxchat'); ?>
1286 </option>
1287 </select>
1288 <br><br>
1289 <label>
1290 <input type="checkbox"
1291 id="mxchat_auto_email_transcript_require_contact"
1292 name="mxchat_transcripts_options[mxchat_auto_email_transcript_require_contact]"
1293 value="1"
1294 <?php checked($require_contact, 1); ?>>
1295 <?php esc_html_e('Only send if visitor provided contact info', 'mxchat'); ?>
1296 </label>
1297 <p class="description">
1298 <?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'); ?>
1299 <br>
1300 <?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'); ?>
1301 </p>
1302 <?php
1303 }
1304
1305
1306
1307 public function sanitize_transcripts_options($input) {
1308 $sanitized = array();
1309
1310 $sanitized['mxchat_enable_notifications'] = isset($input['mxchat_enable_notifications']) ? 1 : 0;
1311
1312 if (isset($input['mxchat_notification_email'])) {
1313 $sanitized['mxchat_notification_email'] = sanitize_email($input['mxchat_notification_email']);
1314 if (!is_email($sanitized['mxchat_notification_email'])) {
1315 add_settings_error(
1316 'mxchat_transcripts_options',
1317 'invalid_email',
1318 __('Please enter a valid email address for notifications.', 'mxchat'),
1319 'error'
1320 );
1321 $sanitized['mxchat_notification_email'] = get_option('admin_email');
1322 }
1323 }
1324
1325 // Sanitize auto-delete setting
1326 $valid_intervals = array('never', '1week', '2weeks', '1month');
1327 if (isset($input['mxchat_auto_delete_transcripts'])) {
1328 $sanitized['mxchat_auto_delete_transcripts'] = in_array($input['mxchat_auto_delete_transcripts'], $valid_intervals)
1329 ? $input['mxchat_auto_delete_transcripts']
1330 : 'never';
1331 } else {
1332 $sanitized['mxchat_auto_delete_transcripts'] = 'never';
1333 }
1334
1335 // Get old value to check if auto-delete setting changed
1336 $old_options = get_option('mxchat_transcripts_options');
1337 $old_interval = isset($old_options['mxchat_auto_delete_transcripts']) ? $old_options['mxchat_auto_delete_transcripts'] : 'never';
1338
1339 // If the auto-delete setting changed, reschedule the cron job
1340 if ($old_interval !== $sanitized['mxchat_auto_delete_transcripts']) {
1341 $this->schedule_transcript_cleanup($sanitized['mxchat_auto_delete_transcripts']);
1342 }
1343
1344 // Sanitize auto-email transcript settings
1345 $sanitized['mxchat_auto_email_transcript_enabled'] = isset($input['mxchat_auto_email_transcript_enabled']) ? 1 : 0;
1346
1347 $valid_delays = array('15', '30', '60');
1348 if (isset($input['mxchat_auto_email_transcript_delay'])) {
1349 $sanitized['mxchat_auto_email_transcript_delay'] = in_array($input['mxchat_auto_email_transcript_delay'], $valid_delays)
1350 ? $input['mxchat_auto_email_transcript_delay']
1351 : '30';
1352 } else {
1353 $sanitized['mxchat_auto_email_transcript_delay'] = '30';
1354 }
1355
1356 // Sanitize require contact info setting
1357 $sanitized['mxchat_auto_email_transcript_require_contact'] = isset($input['mxchat_auto_email_transcript_require_contact']) ? 1 : 0;
1358
1359 return $sanitized;
1360 }
1361
1362 /**
1363 * Schedule or unschedule the transcript cleanup cron job
1364 */
1365 public function schedule_transcript_cleanup($interval) {
1366 // Clear any existing scheduled event
1367 $timestamp = wp_next_scheduled('mxchat_cleanup_old_transcripts');
1368 if ($timestamp) {
1369 wp_unschedule_event($timestamp, 'mxchat_cleanup_old_transcripts');
1370 }
1371
1372 // Schedule new event if not set to "never"
1373 if ($interval !== 'never') {
1374 // Schedule to run daily at 3 AM
1375 $next_run = strtotime('tomorrow 3:00 AM');
1376 wp_schedule_event($next_run, 'daily', 'mxchat_cleanup_old_transcripts');
1377 }
1378 }
1379
1380 /**
1381 * Delete old transcripts based on the configured interval
1382 */
1383 public function cleanup_old_transcripts() {
1384 $options = get_option('mxchat_transcripts_options', array());
1385 $interval = isset($options['mxchat_auto_delete_transcripts']) ? $options['mxchat_auto_delete_transcripts'] : 'never';
1386
1387 // If set to never, don't delete anything
1388 if ($interval === 'never') {
1389 return;
1390 }
1391
1392 // Calculate the cutoff date
1393 $days = 0;
1394 switch ($interval) {
1395 case '1week':
1396 $days = 7;
1397 break;
1398 case '2weeks':
1399 $days = 14;
1400 break;
1401 case '1month':
1402 $days = 30;
1403 break;
1404 default:
1405 return; // Invalid interval, don't delete anything
1406 }
1407
1408 global $wpdb;
1409 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1410 $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
1411
1412 // Calculate the cutoff timestamp
1413 $cutoff_date = date('Y-m-d H:i:s', strtotime("-{$days} days"));
1414
1415 // First, get the session IDs that will be deleted (to clean up translations too)
1416 $sessions_to_delete = $wpdb->get_col(
1417 $wpdb->prepare(
1418 "SELECT DISTINCT session_id FROM {$table_name} WHERE timestamp < %s",
1419 $cutoff_date
1420 )
1421 );
1422
1423 // Delete transcripts older than the cutoff date
1424 $deleted = $wpdb->query(
1425 $wpdb->prepare(
1426 "DELETE FROM {$table_name} WHERE timestamp < %s",
1427 $cutoff_date
1428 )
1429 );
1430
1431 // Also delete any translations for the deleted sessions
1432 if (!empty($sessions_to_delete) && $wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table) {
1433 $placeholders = implode(',', array_fill(0, count($sessions_to_delete), '%s'));
1434 $wpdb->query(
1435 $wpdb->prepare(
1436 "DELETE FROM {$translations_table} WHERE session_id IN ($placeholders)",
1437 $sessions_to_delete
1438 )
1439 );
1440 }
1441
1442 // Log the cleanup action
1443 if ($deleted !== false && $deleted > 0) {
1444 //error_log(sprintf('MXChat: Auto-deleted %d transcripts older than %d days', $deleted, $days));
1445 }
1446 }
1447
1448 public function export_chat_transcripts() {
1449 if (!current_user_can('manage_options')) {
1450 wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'mxchat'));
1451 }
1452
1453 check_ajax_referer('mxchat_export_transcripts', 'security');
1454
1455 global $wpdb;
1456 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1457
1458 // Get all transcripts ordered by session and timestamp
1459 $results = $wpdb->get_results(
1460 "SELECT session_id, user_email, user_identifier, role, message, timestamp
1461 FROM {$table_name}
1462 ORDER BY session_id, timestamp ASC"
1463 );
1464
1465 if (empty($results)) {
1466 wp_send_json_error(array('message' => 'No transcripts found.'));
1467 wp_die();
1468 }
1469
1470 // Set headers for CSV download
1471 header('Content-Type: text/csv');
1472 header('Content-Disposition: attachment; filename="chat-transcripts-' . date('Y-m-d') . '.csv"');
1473 header('Pragma: no-cache');
1474 header('Expires: 0');
1475
1476 // Create output stream
1477 $output = fopen('php://output', 'w');
1478
1479 // Add UTF-8 BOM for proper Excel encoding
1480 fputs($output, "\xEF\xBB\xBF");
1481
1482 // Add CSV headers
1483 fputcsv($output, array(
1484 'Session ID',
1485 'Email',
1486 'User Identifier',
1487 'Role',
1488 'Message',
1489 'Timestamp'
1490 ));
1491
1492 // Add data rows
1493 foreach ($results as $row) {
1494 fputcsv($output, array(
1495 $row->session_id,
1496 $row->user_email,
1497 $row->user_identifier,
1498 $row->role,
1499 $row->message,
1500 $row->timestamp
1501 ));
1502 }
1503
1504 fclose($output);
1505 wp_die();
1506 }
1507
1508 /**
1509 * Handle translation of chat messages via AJAX
1510 */
1511 public function mxchat_translate_messages() {
1512 if (!current_user_can('manage_options')) {
1513 wp_send_json_error(['error' => 'Insufficient permissions']);
1514 wp_die();
1515 }
1516
1517 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
1518 $target_lang = isset($_POST['target_lang']) ? sanitize_text_field($_POST['target_lang']) : 'en';
1519 $messages_json = isset($_POST['messages']) ? wp_unslash($_POST['messages']) : '[]';
1520 $messages = json_decode($messages_json, true);
1521
1522 if (empty($session_id)) {
1523 wp_send_json_error(['error' => 'No session ID provided']);
1524 wp_die();
1525 }
1526
1527 if (empty($messages) || !is_array($messages)) {
1528 wp_send_json_error(['error' => 'No messages to translate']);
1529 wp_die();
1530 }
1531
1532 // Language names for prompting
1533 $languages = [
1534 'en' => 'English',
1535 'es' => 'Spanish',
1536 'fr' => 'French',
1537 'de' => 'German',
1538 'it' => 'Italian',
1539 'pt' => 'Portuguese',
1540 'nl' => 'Dutch',
1541 'ru' => 'Russian',
1542 'zh' => 'Chinese',
1543 'ja' => 'Japanese',
1544 'ko' => 'Korean',
1545 'ar' => 'Arabic',
1546 'hi' => 'Hindi',
1547 'tr' => 'Turkish',
1548 'pl' => 'Polish',
1549 'vi' => 'Vietnamese',
1550 'th' => 'Thai',
1551 'id' => 'Indonesian',
1552 'sv' => 'Swedish',
1553 'da' => 'Danish'
1554 ];
1555
1556 $target_lang_name = isset($languages[$target_lang]) ? $languages[$target_lang] : 'English';
1557
1558 // Build combined text for translation (numbered for parsing)
1559 $numbered_messages = [];
1560 foreach ($messages as $i => $msg) {
1561 $content = isset($msg['content']) ? trim($msg['content']) : '';
1562 if (!empty($content)) {
1563 $numbered_messages[] = "[MSG" . $i . "]" . $content . "[/MSG" . $i . "]";
1564 }
1565 }
1566
1567 if (empty($numbered_messages)) {
1568 wp_send_json_error(['error' => 'No valid messages to translate']);
1569 wp_die();
1570 }
1571
1572 $combined_text = implode("\n\n", $numbered_messages);
1573
1574 // Prepare the translation prompt
1575 $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.";
1576
1577 // Get user's selected model and determine provider
1578 $options = get_option('mxchat_options', []);
1579 $selected_model = $options['model'] ?? 'gpt-5.1-chat-latest';
1580
1581 // Check if using OpenRouter
1582 if ($selected_model === 'openrouter') {
1583 $provider = 'openrouter';
1584 $selected_model = $options['openrouter_selected_model'] ?? '';
1585 $api_key = $options['openrouter_api_key'] ?? '';
1586
1587 if (empty($selected_model)) {
1588 wp_send_json_error(['error' => 'No OpenRouter model selected']);
1589 wp_die();
1590 }
1591 } else {
1592 // Determine provider from model name
1593 $model_parts = explode('-', $selected_model);
1594 $provider = strtolower($model_parts[0]);
1595
1596 // Get the appropriate API key based on provider
1597 $api_key = '';
1598 switch ($provider) {
1599 case 'gpt':
1600 case 'o1':
1601 $api_key = $options['api_key'] ?? '';
1602 break;
1603 case 'claude':
1604 $api_key = $options['claude_api_key'] ?? '';
1605 break;
1606 case 'grok':
1607 $api_key = $options['xai_api_key'] ?? '';
1608 break;
1609 case 'deepseek':
1610 $api_key = $options['deepseek_api_key'] ?? '';
1611 break;
1612 case 'gemini':
1613 $api_key = $options['gemini_api_key'] ?? '';
1614 break;
1615 default:
1616 // Default to OpenAI
1617 $api_key = $options['api_key'] ?? '';
1618 $provider = 'gpt';
1619 break;
1620 }
1621 }
1622
1623 if (empty($api_key)) {
1624 wp_send_json_error(['error' => 'No API key configured for ' . $provider]);
1625 wp_die();
1626 }
1627
1628 // Make API request based on provider
1629 $response = $this->translate_with_provider($provider, $selected_model, $api_key, $system_prompt, $combined_text);
1630
1631 if (is_wp_error($response)) {
1632 wp_send_json_error(['error' => $response->get_error_message()]);
1633 wp_die();
1634 }
1635
1636 // Parse the response to extract translated messages
1637 $translations = [];
1638 foreach ($messages as $msg) {
1639 $index = $msg['index'];
1640 $pattern = '/\[MSG' . $index . '\](.*?)\[\/MSG' . $index . '\]/s';
1641 if (preg_match($pattern, $response, $matches)) {
1642 $translations[] = [
1643 'index' => $index,
1644 'translated' => trim($matches[1])
1645 ];
1646 }
1647 }
1648
1649 // Save translations to database
1650 if (!empty($translations)) {
1651 $this->save_transcript_translation($session_id, $target_lang, $translations);
1652 }
1653
1654 wp_send_json(['success' => true, 'translations' => $translations, 'language' => $target_lang]);
1655 wp_die();
1656 }
1657
1658 /**
1659 * Save transcript translation to database
1660 */
1661 private function save_transcript_translation($session_id, $language_code, $translations) {
1662 global $wpdb;
1663 $table_name = $wpdb->prefix . 'mxchat_transcript_translations';
1664
1665 // Check if table exists, create if not
1666 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") !== $table_name) {
1667 mxchat_create_translations_table();
1668 }
1669
1670 $now = current_time('mysql');
1671 $translations_json = wp_json_encode($translations);
1672
1673 // Use REPLACE to insert or update
1674 $wpdb->query($wpdb->prepare(
1675 "REPLACE INTO $table_name (session_id, language_code, translations, created_at, updated_at)
1676 VALUES (%s, %s, %s, %s, %s)",
1677 $session_id,
1678 $language_code,
1679 $translations_json,
1680 $now,
1681 $now
1682 ));
1683 }
1684
1685 /**
1686 * Get saved translation for a session
1687 */
1688 public function mxchat_get_transcript_translation() {
1689 if (!current_user_can('manage_options')) {
1690 wp_send_json_error(['error' => 'Insufficient permissions']);
1691 wp_die();
1692 }
1693
1694 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
1695
1696 if (empty($session_id)) {
1697 wp_send_json_error(['error' => 'No session ID provided']);
1698 wp_die();
1699 }
1700
1701 global $wpdb;
1702 $table_name = $wpdb->prefix . 'mxchat_transcript_translations';
1703
1704 // Check if table exists
1705 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") !== $table_name) {
1706 wp_send_json(['success' => true, 'has_translation' => false]);
1707 wp_die();
1708 }
1709
1710 // Get the most recent translation for this session
1711 $result = $wpdb->get_row($wpdb->prepare(
1712 "SELECT language_code, translations FROM $table_name WHERE session_id = %s ORDER BY updated_at DESC LIMIT 1",
1713 $session_id
1714 ));
1715
1716 if ($result) {
1717 $translations = json_decode($result->translations, true);
1718 wp_send_json([
1719 'success' => true,
1720 'has_translation' => true,
1721 'language' => $result->language_code,
1722 'translations' => $translations
1723 ]);
1724 } else {
1725 wp_send_json(['success' => true, 'has_translation' => false]);
1726 }
1727 wp_die();
1728 }
1729
1730 /**
1731 * Translate text using the user's selected provider and model
1732 */
1733 private function translate_with_provider($provider, $model, $api_key, $system_prompt, $text) {
1734 switch ($provider) {
1735 case 'claude':
1736 return $this->translate_with_claude($api_key, $model, $system_prompt, $text);
1737 case 'grok':
1738 return $this->translate_with_xai($api_key, $model, $system_prompt, $text);
1739 case 'deepseek':
1740 return $this->translate_with_deepseek($api_key, $model, $system_prompt, $text);
1741 case 'gemini':
1742 return $this->translate_with_gemini($api_key, $model, $system_prompt, $text);
1743 case 'openrouter':
1744 return $this->translate_with_openrouter($api_key, $model, $system_prompt, $text);
1745 case 'gpt':
1746 case 'o1':
1747 default:
1748 return $this->translate_with_openai($api_key, $model, $system_prompt, $text);
1749 }
1750 }
1751
1752 /**
1753 * Translate text using OpenAI API
1754 */
1755 private function translate_with_openai($api_key, $model, $system_prompt, $text) {
1756 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', [
1757 'timeout' => 60,
1758 'headers' => [
1759 'Authorization' => 'Bearer ' . $api_key,
1760 'Content-Type' => 'application/json'
1761 ],
1762 'body' => wp_json_encode([
1763 'model' => $model,
1764 'messages' => [
1765 ['role' => 'system', 'content' => $system_prompt],
1766 ['role' => 'user', 'content' => $text]
1767 ],
1768 'temperature' => 0.3
1769 ])
1770 ]);
1771
1772 if (is_wp_error($response)) {
1773 return $response;
1774 }
1775
1776 $body = json_decode(wp_remote_retrieve_body($response), true);
1777
1778 if (isset($body['error'])) {
1779 return new WP_Error('api_error', $body['error']['message']);
1780 }
1781
1782 if (isset($body['choices'][0]['message']['content'])) {
1783 return $body['choices'][0]['message']['content'];
1784 }
1785
1786 return new WP_Error('api_error', 'Invalid API response');
1787 }
1788
1789 /**
1790 * Translate text using Claude API
1791 */
1792 private function translate_with_claude($api_key, $model, $system_prompt, $text) {
1793 $response = wp_remote_post('https://api.anthropic.com/v1/messages', [
1794 'timeout' => 60,
1795 'headers' => [
1796 'x-api-key' => $api_key,
1797 'anthropic-version' => '2023-06-01',
1798 'Content-Type' => 'application/json'
1799 ],
1800 'body' => wp_json_encode([
1801 'model' => $model,
1802 'max_tokens' => 4096,
1803 'system' => $system_prompt,
1804 'messages' => [
1805 ['role' => 'user', 'content' => $text]
1806 ]
1807 ])
1808 ]);
1809
1810 if (is_wp_error($response)) {
1811 return $response;
1812 }
1813
1814 $body = json_decode(wp_remote_retrieve_body($response), true);
1815
1816 if (isset($body['error'])) {
1817 return new WP_Error('api_error', $body['error']['message']);
1818 }
1819
1820 if (isset($body['content'][0]['text'])) {
1821 return $body['content'][0]['text'];
1822 }
1823
1824 return new WP_Error('api_error', 'Invalid API response');
1825 }
1826
1827 /**
1828 * Translate text using xAI (Grok) API
1829 */
1830 private function translate_with_xai($api_key, $model, $system_prompt, $text) {
1831 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', [
1832 'timeout' => 60,
1833 'headers' => [
1834 'Authorization' => 'Bearer ' . $api_key,
1835 'Content-Type' => 'application/json'
1836 ],
1837 'body' => wp_json_encode([
1838 'model' => $model,
1839 'messages' => [
1840 ['role' => 'system', 'content' => $system_prompt],
1841 ['role' => 'user', 'content' => $text]
1842 ],
1843 'temperature' => 0.3
1844 ])
1845 ]);
1846
1847 if (is_wp_error($response)) {
1848 return $response;
1849 }
1850
1851 $body = json_decode(wp_remote_retrieve_body($response), true);
1852
1853 if (isset($body['error'])) {
1854 return new WP_Error('api_error', $body['error']['message']);
1855 }
1856
1857 if (isset($body['choices'][0]['message']['content'])) {
1858 return $body['choices'][0]['message']['content'];
1859 }
1860
1861 return new WP_Error('api_error', 'Invalid API response');
1862 }
1863
1864 /**
1865 * Translate text using DeepSeek API
1866 */
1867 private function translate_with_deepseek($api_key, $model, $system_prompt, $text) {
1868 $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', [
1869 'timeout' => 60,
1870 'headers' => [
1871 'Authorization' => 'Bearer ' . $api_key,
1872 'Content-Type' => 'application/json'
1873 ],
1874 'body' => wp_json_encode([
1875 'model' => $model,
1876 'messages' => [
1877 ['role' => 'system', 'content' => $system_prompt],
1878 ['role' => 'user', 'content' => $text]
1879 ],
1880 'temperature' => 0.3
1881 ])
1882 ]);
1883
1884 if (is_wp_error($response)) {
1885 return $response;
1886 }
1887
1888 $body = json_decode(wp_remote_retrieve_body($response), true);
1889
1890 if (isset($body['error'])) {
1891 return new WP_Error('api_error', $body['error']['message']);
1892 }
1893
1894 if (isset($body['choices'][0]['message']['content'])) {
1895 return $body['choices'][0]['message']['content'];
1896 }
1897
1898 return new WP_Error('api_error', 'Invalid API response');
1899 }
1900
1901 /**
1902 * Translate text using Google Gemini API
1903 */
1904 private function translate_with_gemini($api_key, $model, $system_prompt, $text) {
1905 $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':generateContent?key=' . $api_key;
1906
1907 $response = wp_remote_post($url, [
1908 'timeout' => 60,
1909 'headers' => [
1910 'Content-Type' => 'application/json'
1911 ],
1912 'body' => wp_json_encode([
1913 'contents' => [
1914 [
1915 'parts' => [
1916 ['text' => $system_prompt . "\n\n" . $text]
1917 ]
1918 ]
1919 ],
1920 'generationConfig' => [
1921 'temperature' => 0.3
1922 ]
1923 ])
1924 ]);
1925
1926 if (is_wp_error($response)) {
1927 return $response;
1928 }
1929
1930 $body = json_decode(wp_remote_retrieve_body($response), true);
1931
1932 if (isset($body['error'])) {
1933 return new WP_Error('api_error', $body['error']['message']);
1934 }
1935
1936 if (isset($body['candidates'][0]['content']['parts'][0]['text'])) {
1937 return $body['candidates'][0]['content']['parts'][0]['text'];
1938 }
1939
1940 return new WP_Error('api_error', 'Invalid API response');
1941 }
1942
1943 /**
1944 * Translate text using OpenRouter API
1945 */
1946 private function translate_with_openrouter($api_key, $model, $system_prompt, $text) {
1947 $response = wp_remote_post('https://openrouter.ai/api/v1/chat/completions', [
1948 'timeout' => 60,
1949 'headers' => [
1950 'Authorization' => 'Bearer ' . $api_key,
1951 'Content-Type' => 'application/json',
1952 'HTTP-Referer' => home_url(),
1953 'X-Title' => 'MxChat Translation'
1954 ],
1955 'body' => wp_json_encode([
1956 'model' => $model,
1957 'messages' => [
1958 ['role' => 'system', 'content' => $system_prompt],
1959 ['role' => 'user', 'content' => $text]
1960 ],
1961 'temperature' => 0.3
1962 ])
1963 ]);
1964
1965 if (is_wp_error($response)) {
1966 return $response;
1967 }
1968
1969 $body = json_decode(wp_remote_retrieve_body($response), true);
1970
1971 if (isset($body['error'])) {
1972 return new WP_Error('api_error', $body['error']['message']);
1973 }
1974
1975 if (isset($body['choices'][0]['message']['content'])) {
1976 return $body['choices'][0]['message']['content'];
1977 }
1978
1979 return new WP_Error('api_error', 'Invalid API response');
1980 }
1981
1982 public function mxchat_fetch_chat_history() {
1983 global $wpdb;
1984 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1985 $url_clicks_table = $wpdb->prefix . 'mxchat_url_clicks';
1986
1987 if (!current_user_can('manage_options')) {
1988 wp_send_json_error(['message' => 'Insufficient permissions']);
1989 wp_die();
1990 }
1991
1992 $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
1993 $per_page = isset($_POST['per_page']) ? absint($_POST['per_page']) : 50;
1994 $offset = ($page - 1) * $per_page;
1995 $search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
1996 $sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] === 'asc' ? 'ASC' : 'DESC';
1997
1998 // Build search condition
1999 $search_condition = '';
2000 $search_params = [];
2001 if (!empty($search)) {
2002 $search_condition = "WHERE (
2003 session_id LIKE %s
2004 OR user_email LIKE %s
2005 OR user_name LIKE %s
2006 OR user_identifier LIKE %s
2007 OR message LIKE %s
2008 )";
2009 $search_params = array_fill(0, 5, '%' . $wpdb->esc_like($search) . '%');
2010 }
2011
2012 // Get total count
2013 $count_query = !empty($search)
2014 ? $wpdb->prepare("SELECT COUNT(DISTINCT session_id) FROM {$table_name} {$search_condition}", $search_params)
2015 : "SELECT COUNT(DISTINCT session_id) FROM {$table_name}";
2016 $total_sessions = (int) $wpdb->get_var($count_query);
2017
2018 // Get session IDs for current page (sorted by newest or oldest)
2019 $session_query = !empty($search)
2020 ? $wpdb->prepare(
2021 "SELECT DISTINCT t.session_id FROM {$table_name} t {$search_condition}
2022 GROUP BY t.session_id ORDER BY MAX(t.timestamp) {$sort_order} LIMIT %d OFFSET %d",
2023 array_merge($search_params, [$per_page, $offset])
2024 )
2025 : $wpdb->prepare(
2026 "SELECT DISTINCT session_id FROM {$table_name}
2027 GROUP BY session_id ORDER BY MAX(timestamp) {$sort_order} LIMIT %d OFFSET %d",
2028 $per_page, $offset
2029 );
2030 $session_ids = $wpdb->get_col($session_query);
2031
2032 $total_pages = ceil($total_sessions / $per_page);
2033
2034 if (empty($session_ids)) {
2035 wp_send_json([
2036 'success' => true,
2037 'sessions' => [],
2038 'page' => $page,
2039 'total_pages' => 0,
2040 'total_sessions' => 0,
2041 'showing_start' => 0,
2042 'showing_end' => 0
2043 ]);
2044 wp_die();
2045 }
2046
2047 // Check for optional columns/tables
2048 $url_table_exists = $wpdb->get_var("SHOW TABLES LIKE '$url_clicks_table'") === $url_clicks_table;
2049 $originating_columns_exist = !empty($wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'"));
2050
2051 // Build session list data
2052 $sessions = [];
2053 foreach ($session_ids as $session_id) {
2054 // Get session metadata
2055 $session_data = $originating_columns_exist
2056 ? $wpdb->get_row($wpdb->prepare(
2057 "SELECT user_email, user_name, user_identifier, originating_page_url, originating_page_title, timestamp
2058 FROM {$table_name} WHERE session_id = %s ORDER BY timestamp ASC LIMIT 1",
2059 $session_id
2060 ))
2061 : $wpdb->get_row($wpdb->prepare(
2062 "SELECT user_email, user_name, user_identifier, timestamp FROM {$table_name}
2063 WHERE session_id = %s LIMIT 1",
2064 $session_id
2065 ));
2066
2067 // Get message count and latest timestamp
2068 $message_stats = $wpdb->get_row($wpdb->prepare(
2069 "SELECT COUNT(*) as count, MAX(timestamp) as latest FROM {$table_name} WHERE session_id = %s",
2070 $session_id
2071 ));
2072
2073 // Get first user message as preview
2074 $first_user_msg = $wpdb->get_var($wpdb->prepare(
2075 "SELECT message FROM {$table_name} WHERE session_id = %s AND role = 'user' ORDER BY timestamp ASC LIMIT 1",
2076 $session_id
2077 ));
2078 $preview = $first_user_msg ? wp_trim_words(wp_strip_all_tags(stripslashes($first_user_msg)), 12, '...') : 'No messages';
2079
2080 // Build display name
2081 $user_email = !empty($session_data->user_email) ? $session_data->user_email : '';
2082 $user_name = !empty($session_data->user_name) ? $session_data->user_name : '';
2083 $user_identifier = !empty($session_data->user_identifier) ? $session_data->user_identifier : 'Guest';
2084
2085 $display_name = $user_name ?: ($user_email ? explode('@', $user_email)[0] : $user_identifier);
2086 $display_sub = $user_email ?: ('ID: ' . $user_identifier);
2087
2088 // Format time - show relative for recent, date for older
2089 $timestamp = strtotime($message_stats->latest . ' UTC');
2090 $now = time();
2091 $diff = $now - $timestamp;
2092 if ($diff < 3600) {
2093 $time_display = floor($diff / 60) . 'm ago';
2094 } elseif ($diff < 86400) {
2095 $time_display = floor($diff / 3600) . 'h ago';
2096 } elseif ($diff < 604800) {
2097 $time_display = floor($diff / 86400) . 'd ago';
2098 } else {
2099 $time_display = wp_date('M j', $timestamp);
2100 }
2101
2102 // Get initials for avatar
2103 $initials = strtoupper(substr($display_name, 0, 2));
2104 if (strlen($display_name) > 2 && strpos($display_name, ' ') !== false) {
2105 $parts = explode(' ', $display_name);
2106 $initials = strtoupper(substr($parts[0], 0, 1) . substr(end($parts), 0, 1));
2107 }
2108
2109 $sessions[] = [
2110 'session_id' => $session_id,
2111 'display_name' => $display_name,
2112 'display_sub' => $display_sub,
2113 'initials' => $initials,
2114 'preview' => $preview,
2115 'message_count' => (int) $message_stats->count,
2116 'time_display' => $time_display,
2117 'timestamp' => $message_stats->latest
2118 ];
2119 }
2120
2121 wp_send_json([
2122 'success' => true,
2123 'sessions' => $sessions,
2124 'page' => $page,
2125 'total_pages' => $total_pages,
2126 'total_sessions' => $total_sessions,
2127 'showing_start' => $offset + 1,
2128 'showing_end' => min($offset + $per_page, $total_sessions)
2129 ]);
2130 wp_die();
2131 }
2132
2133 /**
2134 * Fetch single conversation details for split-panel view
2135 */
2136 public function mxchat_fetch_conversation() {
2137 global $wpdb;
2138 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
2139 $url_clicks_table = $wpdb->prefix . 'mxchat_url_clicks';
2140
2141 if (!current_user_can('manage_options')) {
2142 wp_send_json_error(['message' => 'Insufficient permissions']);
2143 wp_die();
2144 }
2145
2146 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
2147 if (empty($session_id)) {
2148 wp_send_json_error(['message' => 'No session ID provided']);
2149 wp_die();
2150 }
2151
2152 // Check for optional columns/tables
2153 $url_table_exists = $wpdb->get_var("SHOW TABLES LIKE '$url_clicks_table'") === $url_clicks_table;
2154 $originating_columns_exist = !empty($wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'"));
2155
2156 // Get session metadata
2157 $session_data = $originating_columns_exist
2158 ? $wpdb->get_row($wpdb->prepare(
2159 "SELECT user_email, user_name, user_identifier, originating_page_url, originating_page_title, timestamp
2160 FROM {$table_name} WHERE session_id = %s ORDER BY timestamp ASC LIMIT 1",
2161 $session_id
2162 ))
2163 : $wpdb->get_row($wpdb->prepare(
2164 "SELECT user_email, user_name, user_identifier, timestamp FROM {$table_name}
2165 WHERE session_id = %s LIMIT 1",
2166 $session_id
2167 ));
2168
2169 if (!$session_data) {
2170 wp_send_json_error(['message' => 'Session not found']);
2171 wp_die();
2172 }
2173
2174 // Get clicked URLs
2175 $clicked_urls = [];
2176 if ($url_table_exists) {
2177 $url_clicks = $wpdb->get_results($wpdb->prepare(
2178 "SELECT DISTINCT clicked_url FROM {$url_clicks_table}
2179 WHERE session_id = %s ORDER BY click_timestamp ASC",
2180 $session_id
2181 ));
2182 foreach ($url_clicks as $click) {
2183 $clicked_urls[] = $click->clicked_url;
2184 }
2185 }
2186
2187 // Get all messages
2188 $messages = $wpdb->get_results($wpdb->prepare(
2189 "SELECT * FROM {$table_name} WHERE session_id = %s ORDER BY timestamp ASC",
2190 $session_id
2191 ));
2192
2193 // Build user info
2194 $user_email = !empty($session_data->user_email) ? $session_data->user_email : '';
2195 $user_name = !empty($session_data->user_name) ? $session_data->user_name : '';
2196 $user_identifier = !empty($session_data->user_identifier) ? $session_data->user_identifier : 'Guest';
2197
2198 $display_name = $user_name ?: ($user_email ? explode('@', $user_email)[0] : $user_identifier);
2199 $display_sub = $user_email ?: ('ID: ' . $user_identifier);
2200
2201 // Get initials
2202 $initials = strtoupper(substr($display_name, 0, 2));
2203 if (strlen($display_name) > 2 && strpos($display_name, ' ') !== false) {
2204 $parts = explode(' ', $display_name);
2205 $initials = strtoupper(substr($parts[0], 0, 1) . substr(end($parts), 0, 1));
2206 }
2207
2208 // Page info
2209 $page_url = $originating_columns_exist && !empty($session_data->originating_page_url) ? $session_data->originating_page_url : '';
2210 $page_title = '';
2211 if ($page_url) {
2212 $page_title = !empty($session_data->originating_page_title) ? $session_data->originating_page_title : parse_url($page_url, PHP_URL_PATH);
2213 }
2214
2215 // Format messages for output
2216 $formatted_messages = [];
2217 foreach ($messages as $msg) {
2218 $is_user = ($msg->role === 'user');
2219 $is_bot = ($msg->role === 'bot' || $msg->role === 'assistant');
2220
2221 $content = wp_kses(
2222 stripslashes($msg->message),
2223 [
2224 'b' => [], 'strong' => [], 'i' => [], 'em' => [], 'u' => [],
2225 'br' => [], 'p' => [], 'ul' => [], 'ol' => [], 'li' => [],
2226 'a' => ['href' => [], 'title' => [], 'target' => [], 'class' => []],
2227 'div' => ['class' => [], 'id' => [], 'data-nonce' => []],
2228 'img' => ['src' => [], 'alt' => [], 'class' => []],
2229 'h3' => ['class' => []],
2230 'h4' => ['class' => []],
2231 'button' => ['type' => [], 'class' => [], 'data-product-id' => [], 'data-nonce' => [], 'data-product-type' => [], 'data-original-text' => [], 'data-mxchat-action' => []],
2232 'select' => ['class' => [], 'data-attribute' => []],
2233 'option' => ['value' => []],
2234 'span' => ['class' => []],
2235 'del' => [], 'ins' => [],
2236 ]
2237 );
2238 $formatted_content = $this->format_transcript_message($content);
2239
2240 $has_rag = $is_bot && !empty($msg->rag_context);
2241
2242 $formatted_messages[] = [
2243 'id' => $msg->id,
2244 'role' => $msg->role,
2245 'is_user' => $is_user,
2246 'is_bot' => $is_bot,
2247 'content' => $formatted_content,
2248 'timestamp' => wp_date('g:i A', strtotime($msg->timestamp . ' UTC')),
2249 'full_timestamp' => wp_date('F j, Y g:i A', strtotime($msg->timestamp . ' UTC')),
2250 'has_rag' => $has_rag
2251 ];
2252 }
2253
2254 // First message timestamp for "started" display
2255 $started = !empty($messages) ? wp_date('M j, Y g:i A', strtotime($messages[0]->timestamp . ' UTC')) : '-';
2256
2257 wp_send_json([
2258 'success' => true,
2259 'session_id' => $session_id,
2260 'user' => [
2261 'name' => $display_name,
2262 'sub' => $display_sub,
2263 'initials' => $initials,
2264 'email' => $user_email,
2265 'identifier' => $user_identifier
2266 ],
2267 'page' => [
2268 'url' => $page_url,
2269 'title' => $page_title
2270 ],
2271 'clicked_urls' => $clicked_urls,
2272 'messages' => $formatted_messages,
2273 'message_count' => count($messages),
2274 'started' => $started
2275 ]);
2276 wp_die();
2277 }
2278
2279
2280 /**
2281 * ALTERNATIVE: Simpler helper method using string replacement
2282 */
2283 private function highlight_clicked_links($message_content, $clicked_urls) {
2284 if (empty($clicked_urls)) {
2285 return wp_kses(
2286 $message_content,
2287 [
2288 'b' => [], 'strong' => [], 'i' => [], 'em' => [], 'u' => [],
2289 'br' => [], 'p' => [], 'ul' => [], 'ol' => [], 'li' => [],
2290 'a' => ['href' => [], 'title' => [], 'target' => [], 'class' => []]
2291 ]
2292 );
2293 }
2294
2295 // First apply standard sanitization
2296 $message_content = wp_kses(
2297 $message_content,
2298 [
2299 'b' => [], 'strong' => [], 'i' => [], 'em' => [], 'u' => [],
2300 'br' => [], 'p' => [], 'ul' => [], 'ol' => [], 'li' => [],
2301 'a' => ['href' => [], 'title' => [], 'target' => [], 'class' => []]
2302 ]
2303 );
2304
2305 // Process each clicked URL
2306 foreach ($clicked_urls as $clicked_url) {
2307 // Try multiple patterns to catch different link formats
2308 $patterns = [
2309 // Standard link format
2310 '/<a([^>]*href=["\']' . preg_quote($clicked_url, '/') . '["\'][^>]*)>/i',
2311 // Link with trailing slash
2312 '/<a([^>]*href=["\']' . preg_quote(rtrim($clicked_url, '/'), '/') . '\/?["\'][^>]*)>/i',
2313 // Encoded entities version
2314 '/<a([^>]*href=["\']' . preg_quote(htmlentities($clicked_url), '/') . '["\'][^>]*)>/i',
2315 ];
2316
2317 foreach ($patterns as $pattern) {
2318 if (preg_match($pattern, $message_content)) {
2319 $message_content = preg_replace_callback(
2320 $pattern,
2321 function($matches) {
2322 $full_match = $matches[0];
2323 $attributes = $matches[1];
2324
2325 // Check if it already has the class
2326 if (strpos($full_match, 'mxchat-clicked-link') !== false) {
2327 return $full_match;
2328 }
2329
2330 // Check if class attribute exists
2331 if (preg_match('/class=["\']([^"\']*)["\']/', $attributes, $class_matches)) {
2332 // Add to existing class
2333 $new_attributes = preg_replace(
2334 '/class=["\']([^"\']*)["\']/',
2335 'class="$1 mxchat-clicked-link"',
2336 $attributes
2337 );
2338 } else {
2339 // Add new class attribute
2340 $new_attributes = $attributes . ' class="mxchat-clicked-link"';
2341 }
2342
2343 // Add title if not present
2344 if (strpos($new_attributes, 'title=') === false) {
2345 $new_attributes .= ' title="User clicked this link"';
2346 }
2347
2348 return '<a' . $new_attributes . '>';
2349 },
2350 $message_content
2351 );
2352
2353 // If we found and replaced, break out of the patterns loop
2354 break;
2355 }
2356 }
2357 }
2358
2359 return $message_content;
2360 }
2361
2362 public function mxchat_create_prompts_page() {
2363 //error_log('=== DEBUG: mxchat_create_prompts_page started ===');
2364
2365 global $wpdb;
2366 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
2367
2368 $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
2369 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
2370
2371 // Display success message if all prompts were deleted
2372 if (isset($_GET['all_deleted']) && $_GET['all_deleted'] === 'true') {
2373 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>';
2374 }
2375
2376 // Set up pagination, search query, and content type filter
2377 $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field($_GET['_wpnonce']) : '';
2378 $search_query = (!empty($nonce) && wp_verify_nonce($nonce, 'mxchat_prompts_search_nonce') && isset($_GET['search'])) ? sanitize_text_field($_GET['search']) : '';
2379 $content_type_filter = isset($_GET['content_type']) ? sanitize_key($_GET['content_type']) : ''; // ADDED 2.5.6
2380 $current_page = isset($_GET['paged']) ? absint($_GET['paged']) : 1;
2381 $per_page = 25;
2382
2383 //error_log('DEBUG: Search query: ' . $search_query);
2384 //error_log('DEBUG: Content type filter: ' . $content_type_filter);
2385 //error_log('DEBUG: Current page: ' . $current_page);
2386 //error_log('DEBUG: Per page: ' . $per_page);
2387
2388 // ================================
2389 // MULTI-BOT CONFIGURATION
2390 // ================================
2391
2392 // Check for multi-bot and set up bot selection
2393 if (class_exists('MxChat_Multi_Bot_Manager')) {
2394 $multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
2395 $available_bots = $multi_bot_manager->get_available_bots();
2396
2397 // Get saved bot selection (user-specific first, then site-wide default)
2398 $user_id = get_current_user_id();
2399 $saved_bot_id = get_user_meta($user_id, 'mxchat_selected_knowledge_bot', true);
2400 if (empty($saved_bot_id)) {
2401 $saved_bot_id = get_option('mxchat_current_knowledge_bot', 'default');
2402 }
2403
2404 // Allow URL override but default to saved selection
2405 $current_bot_id = isset($_GET['bot_id']) ? sanitize_key($_GET['bot_id']) : $saved_bot_id;
2406 $multibot_active = true;
2407 } else {
2408 $current_bot_id = 'default';
2409 $multibot_active = false;
2410 }
2411
2412 // ================================
2413 // DATA SOURCE CONFIGURATION
2414 // ================================
2415
2416 // Get bot-specific Pinecone settings
2417 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($current_bot_id);
2418 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
2419 $pinecone_api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
2420
2421 // Get OpenAI Vector Store settings
2422 $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
2423 $use_vectorstore = ($vectorstore_options['mxchat_use_openai_vectorstore'] ?? '0') === '1';
2424
2425 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Use Pinecone: ' . ($use_pinecone ? 'YES' : 'NO'));
2426 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Has API Key: ' . (!empty($pinecone_api_key) ? 'YES' : 'NO'));
2427 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Host: ' . ($pinecone_options['mxchat_pinecone_host'] ?? 'NOT SET'));
2428 //error_log('DEBUG: Bot ' . $current_bot_id . ' - Namespace: ' . ($pinecone_options['mxchat_pinecone_namespace'] ?? 'NOT SET'));
2429
2430 if ($use_pinecone && !empty($pinecone_api_key)) {
2431 //error_log('DEBUG: Using PINECONE data source with bot-specific config');
2432 // PINECONE DATA SOURCE
2433 $data_source = 'pinecone';
2434
2435 // IMPORTANT: Pass the bot-specific $pinecone_options, not default options!
2436 // UPDATED 2.5.6: Added content_type_filter parameter
2437 $records = $pinecone_manager->mxchat_fetch_pinecone_records($pinecone_options, $search_query, $current_page, $per_page, $current_bot_id, $content_type_filter);
2438
2439 // TEMPORARY DEBUG - Add this right after the fetch call
2440 //error_log('=== DEBUG: Bot switching issue ===');
2441 //error_log('Current bot ID: ' . $current_bot_id);
2442 //error_log('Use Pinecone: ' . ($use_pinecone ? 'YES' : 'NO'));
2443 //error_log('Records returned: ' . count($records['data'] ?? []));
2444 //error_log('Total records: ' . ($records['total'] ?? 0));
2445
2446 // Check first few records to see their bot_id
2447 if (!empty($records['data'])) {
2448 foreach (array_slice($records['data'], 0, 3) as $i => $record) {
2449 $record_bot_id = $record->bot_id ?? 'NOT_SET';
2450 //error_log('Record ' . ($i+1) . ' bot_id: ' . $record_bot_id . ', content preview: ' . substr($record->article_content ?? '', 0, 30) . '...');
2451 }
2452 }
2453 //error_log('=== END DEBUG ===');
2454
2455 $total_records = $records['total'] ?? 0;
2456 $prompts = $records['data'] ?? array();
2457 $total_in_database = $records['total_in_database'] ?? 0;
2458 $showing_recent_only = $records['showing_recent_only'] ?? false;
2459
2460 $total_pages = ceil($total_records / $per_page);
2461
2462 } else {
2463 //error_log('DEBUG: Using WORDPRESS DB data source');
2464 // WORDPRESS DB DATA SOURCE (your existing logic)
2465 $data_source = 'wordpress';
2466
2467 // Initialize these variables for WordPress DB
2468 $total_in_database = 0;
2469 $showing_recent_only = false;
2470
2471 $offset = ($current_page - 1) * $per_page;
2472
2473 // UPDATED 2.5.6: Build WHERE clause for search and content type filtering
2474 $where_clauses = array();
2475 $where_values = array();
2476
2477 if ($search_query) {
2478 $where_clauses[] = "article_content LIKE %s";
2479 $where_values[] = '%' . $wpdb->esc_like($search_query) . '%';
2480 }
2481
2482 if ($content_type_filter) {
2483 $where_clauses[] = "content_type = %s";
2484 $where_values[] = $content_type_filter;
2485 }
2486
2487 $sql_where = "";
2488 if (!empty($where_clauses)) {
2489 $sql_where = "WHERE " . implode(" AND ", $where_clauses);
2490 }
2491
2492 // UPDATED 2.6.3: Count unique entries (by source_url) instead of individual rows
2493 // This ensures pagination shows X entries per page, not X chunks
2494 // Entries with empty source_url are counted individually
2495 if (!empty($where_values)) {
2496 // Count unique source_urls + count of rows with empty source_url
2497 $count_query = $wpdb->prepare(
2498 "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} {$sql_where} AND source_url != '') +
2499 (SELECT COUNT(*) FROM {$table_name} {$sql_where} AND (source_url = '' OR source_url IS NULL))",
2500 array_merge($where_values, $where_values)
2501 );
2502 } else {
2503 $count_query = "SELECT (SELECT COUNT(DISTINCT source_url) FROM {$table_name} WHERE source_url != '') +
2504 (SELECT COUNT(*) FROM {$table_name} WHERE source_url = '' OR source_url IS NULL)";
2505 }
2506 $total_records = $wpdb->get_var($count_query);
2507 $total_pages = ceil($total_records / $per_page);
2508
2509 // UPDATED 2.6.3: Get unique source_urls for pagination, then fetch all their rows
2510 // Step 1: Get the source_urls for this page (distinct URLs ordered by latest timestamp)
2511 if (!empty($where_values)) {
2512 $urls_query = $wpdb->prepare(
2513 "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name} {$sql_where}
2514 GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2515 array_merge($where_values, array($per_page, $offset))
2516 );
2517 } else {
2518 $urls_query = $wpdb->prepare(
2519 "SELECT source_url, MAX(timestamp) as latest_ts FROM {$table_name}
2520 GROUP BY source_url ORDER BY latest_ts DESC LIMIT %d OFFSET %d",
2521 $per_page, $offset
2522 );
2523 }
2524 $page_urls = $wpdb->get_results($urls_query);
2525
2526 // Step 2: Fetch all rows for these source_urls
2527 $prompts = array();
2528 if (!empty($page_urls)) {
2529 // Build URL order map to preserve newest-first ordering from step 1
2530 $url_order_map = array();
2531 $url_list = array();
2532 $has_empty_url = false;
2533 $order_index = 0;
2534 foreach ($page_urls as $url_row) {
2535 if (empty($url_row->source_url)) {
2536 $has_empty_url = true;
2537 $url_order_map['__empty__'] = $order_index++;
2538 } else {
2539 $url_list[] = $url_row->source_url;
2540 $url_order_map[$url_row->source_url] = $order_index++;
2541 }
2542 }
2543
2544 // Build query to fetch all rows for these URLs
2545 $url_conditions = array();
2546 $url_values = array();
2547
2548 if (!empty($url_list)) {
2549 $placeholders = implode(',', array_fill(0, count($url_list), '%s'));
2550 $url_conditions[] = "source_url IN ($placeholders)";
2551 $url_values = array_merge($url_values, $url_list);
2552 }
2553
2554 if ($has_empty_url) {
2555 $url_conditions[] = "(source_url = '' OR source_url IS NULL)";
2556 }
2557
2558 if (!empty($url_conditions)) {
2559 $url_where = "WHERE (" . implode(" OR ", $url_conditions) . ")";
2560
2561 // Add original filters back
2562 if (!empty($where_clauses)) {
2563 $url_where .= " AND " . implode(" AND ", $where_clauses);
2564 $url_values = array_merge($url_values, $where_values);
2565 }
2566
2567 if (!empty($url_values)) {
2568 $prompts_query = $wpdb->prepare(
2569 "SELECT * FROM {$table_name} {$url_where} ORDER BY timestamp DESC",
2570 $url_values
2571 );
2572 } else {
2573 $prompts_query = "SELECT * FROM {$table_name} {$url_where} ORDER BY timestamp DESC";
2574 }
2575
2576 $prompts = $wpdb->get_results($prompts_query);
2577
2578 // Sort prompts by the original URL order (newest first), then by timestamp within each URL group
2579 usort($prompts, function($a, $b) use ($url_order_map) {
2580 $url_a = empty($a->source_url) ? '__empty__' : $a->source_url;
2581 $url_b = empty($b->source_url) ? '__empty__' : $b->source_url;
2582 $order_a = $url_order_map[$url_a] ?? PHP_INT_MAX;
2583 $order_b = $url_order_map[$url_b] ?? PHP_INT_MAX;
2584
2585 // First sort by URL order (newest URLs first)
2586 if ($order_a !== $order_b) {
2587 return $order_a - $order_b;
2588 }
2589
2590 // Within same URL, sort by timestamp DESC (newest chunks first)
2591 return strtotime($b->timestamp) - strtotime($a->timestamp);
2592 });
2593 }
2594 }
2595 }
2596
2597 // ================================
2598 // PAGINATION GENERATION
2599 // ================================
2600
2601 // Generate pagination links
2602 if ($total_pages > 1) {
2603 // Build a clean base URL with only necessary parameters
2604 $pagination_args = array('page' => 'mxchat-prompts');
2605
2606 // Preserve bot_id parameter if multi-bot is active
2607 if ($multibot_active && !empty($current_bot_id) && $current_bot_id !== 'default') {
2608 $pagination_args['bot_id'] = $current_bot_id;
2609 }
2610
2611 // Preserve search query and nonce if present
2612 if ($search_query) {
2613 $pagination_args['search'] = $search_query;
2614 if (!empty($nonce)) {
2615 $pagination_args['_wpnonce'] = $nonce;
2616 }
2617 }
2618
2619 // Preserve content type filter if present
2620 if ($content_type_filter) {
2621 $pagination_args['content_type'] = $content_type_filter;
2622 }
2623
2624 // Build clean base URL
2625 $base_url = add_query_arg($pagination_args, admin_url('admin.php'));
2626
2627 $page_links = paginate_links(array(
2628 'base' => $base_url . '%_%',
2629 'format' => '&paged=%#%',
2630 'prev_text' => __('&laquo; Previous', 'mxchat'),
2631 'next_text' => __('Next &raquo;', 'mxchat'),
2632 'total' => $total_pages,
2633 'current' => $current_page,
2634 'type' => 'plain',
2635 ));
2636 } else {
2637 $page_links = '';
2638 }
2639
2640 // ================================
2641 // PROCESSING STATUS RETRIEVAL
2642 // ================================
2643
2644 // Retrieve processing statuses using queue-based method
2645 $processing_statuses = $knowledge_manager->mxchat_get_processing_statuses();
2646 $pdf_status = $processing_statuses['pdf_status'];
2647 $sitemap_status = $processing_statuses['sitemap_status'];
2648 $is_processing = $processing_statuses['is_processing'];
2649
2650 //error_log('=== DEBUG: mxchat_create_prompts_page data preparation completed ===');
2651
2652 // ================================
2653 // RENDER PAGE WITH NEW SIDEBAR LAYOUT
2654 // ================================
2655
2656 // Include the new knowledge page template
2657 require_once plugin_dir_path(__FILE__) . 'admin-knowledge-page.php';
2658
2659 // Package all page data for the render function
2660 $page_data = array(
2661 'prompts' => $prompts,
2662 'total_records' => $total_records,
2663 'total_pages' => $total_pages,
2664 'current_page' => $current_page,
2665 'per_page' => $per_page,
2666 'page_links' => $page_links,
2667 'search_query' => $search_query,
2668 'content_type_filter' => $content_type_filter,
2669 'data_source' => $data_source,
2670 'use_pinecone' => $use_pinecone,
2671 'use_vectorstore' => $use_vectorstore,
2672 'multibot_active' => $multibot_active,
2673 'current_bot_id' => $current_bot_id,
2674 'pdf_status' => $pdf_status,
2675 'sitemap_status' => $sitemap_status,
2676 'is_processing' => $is_processing,
2677 'total_in_database' => $total_in_database ?? 0,
2678 'showing_recent_only' => $showing_recent_only ?? false,
2679 );
2680
2681 // Render the new sidebar-based page
2682 mxchat_render_knowledge_page($this, $knowledge_manager, $page_data);
2683 }
2684
2685 /**
2686 * Get bot-specific Pinecone configuration
2687 * Used in the knowledge retrieval functions
2688 */
2689 private function get_bot_pinecone_config($bot_id = 'default') {
2690 //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
2691
2692 // If default bot or multi-bot add-on not active, use default Pinecone config
2693 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
2694 //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
2695 $addon_options = get_option('mxchat_pinecone_addon_options', array());
2696 $config = array(
2697 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
2698 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
2699 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
2700 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
2701 );
2702 //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
2703 return $config;
2704 }
2705
2706 //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
2707
2708 // Hook for multi-bot add-on to provide bot-specific Pinecone config
2709 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
2710
2711 if (!empty($bot_pinecone_config)) {
2712 //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
2713 //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
2714 //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
2715 //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
2716 } else {
2717 //error_log("MXCHAT DEBUG: Filter returned empty config!");
2718 }
2719
2720 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
2721 }
2722
2723 public function mxchat_delete_chat_history() {
2724 if (!current_user_can('manage_options')) {
2725 echo wp_json_encode(['error' => esc_html__('You do not have sufficient permissions.', 'mxchat')]);
2726 wp_die();
2727 }
2728 check_ajax_referer('mxchat_delete_chat_history', 'security');
2729 global $wpdb;
2730 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
2731
2732 if (isset($_POST['delete_session_ids']) && is_array($_POST['delete_session_ids'])) {
2733 $deleted_count = 0;
2734 $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
2735
2736 foreach ($_POST['delete_session_ids'] as $session_id) {
2737 $session_id_sanitized = sanitize_text_field($session_id);
2738
2739 // Clear relevant cache before deletion
2740 $cache_key = 'chat_session_' . $session_id_sanitized;
2741 wp_cache_delete($cache_key, 'mxchat_chat_sessions');
2742
2743 // Perform the deletion from the database table
2744 $wpdb->delete($table_name, ['session_id' => $session_id_sanitized]);
2745
2746 // Delete any saved translations for this session
2747 if ($wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table) {
2748 $wpdb->delete($translations_table, ['session_id' => $session_id_sanitized]);
2749 }
2750
2751 // Delete the corresponding option entry from wp_options table
2752 delete_option("mxchat_history_" . $session_id_sanitized);
2753
2754 // Delete any associated metadata options
2755 delete_option("mxchat_email_" . $session_id_sanitized);
2756 delete_option("mxchat_agent_name_" . $session_id_sanitized);
2757
2758 $deleted_count++;
2759 }
2760
2761 // Optionally, clear a general cache if you have one
2762 wp_cache_delete('all_chat_sessions', 'mxchat_chat_sessions');
2763
2764 echo wp_json_encode([
2765 'success' => sprintf(
2766 esc_html__('%d chat session(s) have been deleted from all storage locations.', 'mxchat'),
2767 $deleted_count
2768 )
2769 ]);
2770 } else {
2771 echo wp_json_encode(['error' => esc_html__('No chat sessions selected for deletion.', 'mxchat')]);
2772 }
2773
2774 wp_die();
2775 }
2776
2777 /**
2778 * Format transcript message content with markdown processing
2779 * Converts markdown links, bold, italic, code blocks, and plain URLs to HTML
2780 */
2781 private function format_transcript_message($text) {
2782 if (empty($text)) {
2783 return '';
2784 }
2785
2786 // Normalize line endings and clean up excessive whitespace
2787 $text = str_replace("\r\n", "\n", $text);
2788 $text = str_replace("\r", "\n", $text);
2789
2790 // Clean up existing <br> tags that may have been saved (legacy data)
2791 // Convert <br>, <br/>, <br /> back to newlines for consistent processing
2792 $text = preg_replace('/<br\s*\/?>\s*/i', "\n", $text);
2793
2794 // Collapse 3+ consecutive newlines to just 2 (paragraph break)
2795 $text = preg_replace('/\n{3,}/', "\n\n", $text);
2796
2797 // Process markdown headers (# Header)
2798 $text = preg_replace_callback('/^(#{1,6})\s+(.+)$/m', function($matches) {
2799 $level = strlen($matches[1]);
2800 $content = esc_html(trim($matches[2]));
2801 return "<h{$level}>{$content}</h{$level}>";
2802 }, $text);
2803
2804 // Process code blocks with triple backticks
2805 $text = preg_replace_callback('/```(\w+)?\n?([\s\S]*?)```/', function($matches) {
2806 $language = !empty($matches[1]) ? ' class="language-' . esc_attr($matches[1]) . '"' : '';
2807 $code = esc_html($matches[2]);
2808 return "<pre><code{$language}>{$code}</code></pre>";
2809 }, $text);
2810
2811 // Process inline code with single backticks
2812 $text = preg_replace('/`([^`]+)`/', '<code>$1</code>', $text);
2813
2814 // Process bold text **text** or __text__
2815 $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
2816 $text = preg_replace('/__(.+?)__/', '<strong>$1</strong>', $text);
2817
2818 // Process italic text *text* or _text_ (but not if part of URL)
2819 $text = preg_replace('/(?<![*_\w])\*([^*]+)\*(?![*\w])/', '<em>$1</em>', $text);
2820 $text = preg_replace('/(?<![*_\w])_([^_]+)_(?![*\w])/', '<em>$1</em>', $text);
2821
2822 // Process markdown links [text](url)
2823 $text = preg_replace_callback('/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/', function($matches) {
2824 $link_text = esc_html($matches[1]);
2825 $url = esc_url($matches[2]);
2826 return "<a href=\"{$url}\" target=\"_blank\" rel=\"noopener\">{$link_text}</a>";
2827 }, $text);
2828
2829 // Process citation-style brackets [URL]
2830 $text = preg_replace_callback('/\[(https?:\/\/[^\]]+)\]/', function($matches) {
2831 $url = esc_url($matches[1]);
2832 return "<a href=\"{$url}\" target=\"_blank\" rel=\"noopener\">{$url}</a>";
2833 }, $text);
2834
2835 // Process standalone URLs (not already in links or img src)
2836 $text = preg_replace_callback(
2837 '/(?<!href="|src="|">)(https?:\/\/[^\s<>"]+)(?![^<]*<\/a>)/',
2838 function($matches) {
2839 $url = esc_url($matches[1]);
2840 // Truncate display URL if too long
2841 $display = strlen($matches[1]) > 50 ? substr($matches[1], 0, 47) . '...' : $matches[1];
2842 return "<a href=\"{$url}\" target=\"_blank\" rel=\"noopener\">{$display}</a>";
2843 },
2844 $text
2845 );
2846
2847 // Process mailto links
2848 $text = preg_replace_callback('/\[([^\]]+)\]\((mailto:[^)]+)\)/', function($matches) {
2849 $link_text = esc_html($matches[1]);
2850 $mailto = esc_url($matches[2]);
2851 return "<a href=\"{$mailto}\">{$link_text}</a>";
2852 }, $text);
2853
2854 // Convert paragraphs: split by double newlines, wrap in <p> tags
2855 // This creates proper paragraph structure instead of excessive <br> tags
2856 $paragraphs = preg_split('/\n\n+/', $text);
2857
2858 // Filter out empty paragraphs but preserve content like "0"
2859 $paragraphs = array_values(array_filter(array_map('trim', $paragraphs), function($p) {
2860 return $p !== '';
2861 }));
2862
2863 if (empty($paragraphs)) {
2864 // No content after filtering
2865 return '';
2866 } elseif (count($paragraphs) > 1) {
2867 // Multiple paragraphs - wrap each in <p> tags, convert single newlines to <br>
2868 $formatted_paragraphs = array_map(function($p) {
2869 return nl2br($p);
2870 }, $paragraphs);
2871 $text = '<p>' . implode('</p><p>', $formatted_paragraphs) . '</p>';
2872 } else {
2873 // Single paragraph - just convert newlines to <br>
2874 $text = nl2br($paragraphs[0]);
2875 }
2876
2877 return $text;
2878 }
2879
2880 /**
2881 * AJAX handler to fetch RAG context for a specific message
2882 * Used by the transcript viewer to show retrieved documents
2883 */
2884 public function mxchat_get_rag_context() {
2885 // Check permissions
2886 if (!current_user_can('manage_options')) {
2887 wp_send_json_error(['message' => esc_html__('You do not have sufficient permissions.', 'mxchat')]);
2888 wp_die();
2889 }
2890
2891 // Validate message ID
2892 if (!isset($_POST['message_id']) || empty($_POST['message_id'])) {
2893 wp_send_json_error(['message' => esc_html__('Message ID is required.', 'mxchat')]);
2894 wp_die();
2895 }
2896
2897 $message_id = absint($_POST['message_id']);
2898
2899 global $wpdb;
2900 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
2901
2902 // Fetch the RAG context for this message
2903 $result = $wpdb->get_row($wpdb->prepare(
2904 "SELECT rag_context FROM {$table_name} WHERE id = %d",
2905 $message_id
2906 ));
2907
2908 if (!$result || empty($result->rag_context)) {
2909 wp_send_json_error(['message' => esc_html__('No RAG context found for this message.', 'mxchat')]);
2910 wp_die();
2911 }
2912
2913 // Decode the JSON data
2914 $rag_context = json_decode($result->rag_context, true);
2915
2916 if (json_last_error() !== JSON_ERROR_NONE) {
2917 wp_send_json_error(['message' => esc_html__('Invalid RAG context data.', 'mxchat')]);
2918 wp_die();
2919 }
2920
2921 wp_send_json_success($rag_context);
2922 wp_die();
2923 }
2924
2925 public function display_admin_notices() {
2926 // Check if we're on a MXChat admin page
2927 $screen = get_current_screen();
2928 if (!$screen || strpos($screen->base, 'mxchat') === false) {
2929 return;
2930 }
2931
2932 $dismiss_button = '<button type="button" class="notice-dismiss"><span class="screen-reader-text">' . esc_html__('Dismiss this notice.', 'mxchat') . '</span></button>';
2933
2934 // Check for error notices
2935 $error_notice = get_transient('mxchat_admin_notice_error');
2936 if ($error_notice) {
2937 echo '<div class="notice notice-error is-dismissible"><p>' . wp_kses_post($error_notice) . '</p>' . $dismiss_button . '</div>';
2938 delete_transient('mxchat_admin_notice_error');
2939 }
2940
2941 // Check for success notices
2942 $success_notice = get_transient('mxchat_admin_notice_success');
2943 if ($success_notice) {
2944 echo '<div class="notice notice-success is-dismissible"><p>' . wp_kses_post($success_notice) . '</p>' . $dismiss_button . '</div>';
2945 delete_transient('mxchat_admin_notice_success');
2946 }
2947
2948 // Check for info notices
2949 $info_notice = get_transient('mxchat_admin_notice_info');
2950 if ($info_notice) {
2951 echo '<div class="notice notice-info is-dismissible"><p>' . wp_kses_post($info_notice) . '</p>' . $dismiss_button . '</div>';
2952 delete_transient('mxchat_admin_notice_info');
2953 }
2954 }
2955
2956
2957 public function mxchat_create_activation_page() {
2958 // Include the new Pro & Extensions page template
2959 require_once plugin_dir_path(__FILE__) . 'admin-pro-page.php';
2960
2961 // Get addons configuration from the MxChat_Addons class
2962 require_once plugin_dir_path(__FILE__) . 'class-mxchat-addons.php';
2963 $addons_instance = new MxChat_Addons();
2964 $addons_config = $addons_instance->get_addons_config();
2965
2966 // Render the consolidated Pro & Extensions page
2967 mxchat_render_pro_page($this, $addons_config);
2968 }
2969
2970 /**
2971 * Check if current activation is linked to a domain
2972 * This checks YOUR website's database, not the user's local database
2973 */
2974 public function is_current_activation_linked($domain) {
2975 $license_key = get_option('mxchat_activation_key');
2976 $email = get_option('mxchat_pro_email');
2977
2978 if (empty($license_key) || empty($email)) {
2979 return false;
2980 }
2981
2982 // Check with YOUR website's API
2983 $response = wp_remote_post('https://mxchat.ai/mxchat-api/check-domain', array(
2984 'body' => array(
2985 'license_key' => $license_key,
2986 'email' => $email,
2987 'domain' => $domain
2988 ),
2989 'timeout' => 10,
2990 'sslverify' => false
2991 ));
2992
2993 if (is_wp_error($response)) {
2994 return false;
2995 }
2996
2997 $body = json_decode(wp_remote_retrieve_body($response), true);
2998 return isset($body['success']) && $body['success'] && isset($body['data']['linked']) && $body['data']['linked'];
2999 }
3000
3001
3002 public function mxchat_actions_page_html() {
3003 if (!current_user_can('manage_options')) {
3004 return;
3005 }
3006
3007 global $wpdb;
3008 $table_name = $wpdb->prefix . 'mxchat_intents';
3009
3010 // Get stats for dashboard
3011 $total_actions = $wpdb->get_var("SELECT COUNT(*) FROM $table_name");
3012 $enabled_actions = $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE enabled = 1");
3013 $disabled_actions = $total_actions - $enabled_actions;
3014
3015 // Get unique action types count
3016 $action_types_count = $wpdb->get_var("SELECT COUNT(DISTINCT callback_function) FROM $table_name");
3017
3018 // Get action type distribution
3019 $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");
3020 $available_callbacks = $this->mxchat_get_available_callbacks();
3021 $callback_groups = $this->mxchat_get_available_callbacks(true, true);
3022
3023 $action_type_distribution = array();
3024 foreach ($type_distribution_raw as $row) {
3025 $label = isset($available_callbacks[$row->callback_function]['label'])
3026 ? $available_callbacks[$row->callback_function]['label']
3027 : $row->callback_function;
3028 $action_type_distribution[$label] = $row->count;
3029 }
3030
3031 // Prepare page data
3032 $page_data = array(
3033 'total_actions' => $total_actions,
3034 'enabled_actions' => $enabled_actions,
3035 'disabled_actions' => $disabled_actions,
3036 'action_types_count' => $action_types_count,
3037 'action_type_distribution' => $action_type_distribution,
3038 'available_callbacks' => $available_callbacks,
3039 'callback_groups' => $callback_groups,
3040 );
3041
3042 // Include and render the new template
3043 require_once plugin_dir_path(__FILE__) . 'admin-actions-page.php';
3044 mxchat_render_actions_page($this, $page_data);
3045 }
3046
3047 /**
3048 * LEGACY HTML - Preserved below for reference, to be removed in future update
3049 */
3050 function mxchat_actions_page_legacy_html() {
3051 // This function is deprecated and no longer used
3052 // The new template is in includes/admin-actions-page.php
3053 ?>
3054 <div class="wrap mxchat-wrapper">
3055 <!-- Hero Section -->
3056 <div class="mxchat-hero">
3057 <h1 class="mxchat-main-title">
3058 <span class="mxchat-gradient-text">Actions</span> Manager
3059 </h1>
3060 <p class="mxchat-hero-subtitle">
3061 <?php esc_html_e('Create and manage custom actions to enhance your chatbot\'s capabilities.', 'mxchat'); ?>
3062 </p>
3063 </div>
3064
3065 <!-- Actions Header with Search and Filter -->
3066 <div class="mxchat-actions-header">
3067 <div class="mxchat-actions-filters">
3068 <form method="get" class="mxchat-search-form">
3069 <input type="hidden" name="page" value="mxchat-actions">
3070 <div class="mxchat-search-group">
3071 <span class="dashicons dashicons-search"></span>
3072 <input type="text" name="s" class="mxchat-search-input"
3073 placeholder="<?php esc_attr_e('Search Actions', 'mxchat'); ?>"
3074 value="<?php echo esc_attr($search_term); ?>">
3075 </div>
3076 <select name="callback_filter" class="mxchat-action-filter">
3077 <option value=""><?php esc_html_e('All Action Types', 'mxchat'); ?></option>
3078 <?php foreach ($available_callbacks as $function => $callback_data) :
3079 $label = $callback_data['label']; ?>
3080 <option value="<?php echo esc_attr($function); ?>"
3081 <?php selected($callback_filter, $function); ?>>
3082 <?php echo esc_html($label); ?>
3083 </option>
3084 <?php endforeach; ?>
3085 </select>
3086 <button type="submit" class="mxchat-button-secondary">
3087 <?php esc_html_e('Filter', 'mxchat'); ?>
3088 </button>
3089 </form>
3090 </div>
3091 <div class="mxchat-actions-controls">
3092 <button type="button" id="mxchat-add-action-btn" class="mxchat-button-primary">
3093 <span class="dashicons dashicons-plus-alt"></span>
3094 <?php esc_html_e('Add New Action', 'mxchat'); ?>
3095 </button>
3096 </div>
3097 </div>
3098
3099 <!-- Actions Grid Layout - All actions in a single grid -->
3100 <div class="mxchat-actions-grid">
3101 <div class="mxchat-cards-container">
3102 <?php if (!empty($actions)) : ?>
3103 <?php foreach ($actions as $action) :
3104 $callback_function = $action->callback_function;
3105 $callback_label = isset($available_callbacks[$callback_function]['label'])
3106 ? $available_callbacks[$callback_function]['label']
3107 : $callback_function;
3108 $threshold_value = isset($action->similarity_threshold)
3109 ? round($action->similarity_threshold * 100)
3110 : 85;
3111
3112 // Check if this is a form action
3113 $is_form_action = strpos($action->intent_label, 'Form ') === 0;
3114
3115 // Get action status (enabled/disabled) - default to true if column doesn't exist
3116 $is_enabled = isset($action->enabled) ? (bool)$action->enabled : true;
3117
3118 // Get enabled bots for display
3119 $enabled_bots = [];
3120 if (isset($action->enabled_bots) && !empty($action->enabled_bots)) {
3121 $enabled_bots = json_decode($action->enabled_bots, true);
3122 if (!is_array($enabled_bots)) {
3123 $enabled_bots = ['default'];
3124 }
3125 } else {
3126 $enabled_bots = ['default']; // Backward compatibility
3127 }
3128 ?>
3129 <div class="mxchat-action-card <?php echo $is_form_action ? 'mxchat-form-action' : ''; ?>">
3130 <div class="mxchat-card-header">
3131 <div class="mxchat-card-title"><?php echo esc_html($action->intent_label); ?></div>
3132 <div class="mxchat-card-toggle">
3133 <label class="mxchat-switch">
3134 <input type="checkbox" class="mxchat-action-toggle"
3135 data-action-id="<?php echo esc_attr($action->id); ?>"
3136 <?php checked($is_enabled); ?>>
3137 <span class="mxchat-slider round"></span>
3138 </label>
3139 </div>
3140 </div>
3141
3142 <div class="mxchat-card-body">
3143 <div class="mxchat-card-description">
3144 <strong><?php esc_html_e('Type:', 'mxchat'); ?></strong>
3145 <?php echo esc_html($callback_label); ?>
3146 </div>
3147
3148 <div class="mxchat-card-phrases">
3149 <strong><?php esc_html_e('Trigger phrases:', 'mxchat'); ?></strong>
3150 <div class="mxchat-phrases-preview">
3151 <?php
3152 // Check if the helper function exists, otherwise use a simple substring
3153 if (method_exists($this, 'get_trimmed_phrases')) {
3154 echo esc_html($this->get_trimmed_phrases($action->phrases));
3155 } else {
3156 echo esc_html(strlen($action->phrases) > 100 ?
3157 substr($action->phrases, 0, 97) . '...' :
3158 $action->phrases);
3159 }
3160 ?>
3161 </div>
3162 </div>
3163
3164 <div class="mxchat-threshold-control">
3165 <div class="mxchat-threshold-label">
3166 <?php esc_html_e('Similarity Threshold:', 'mxchat'); ?>
3167 <span class="mxchat-threshold-value"><?php echo esc_html($threshold_value); ?>%</span>
3168 </div>
3169 </div>
3170
3171 <!-- Bot Availability Display (Read-only) -->
3172 <div class="mxchat-card-bots">
3173 <strong><?php esc_html_e('Assigned bots:', 'mxchat'); ?></strong>
3174 <div class="mxchat-bot-badges">
3175 <?php
3176 foreach ($enabled_bots as $bot_id) {
3177 if ($bot_id === 'default') {
3178 echo '<span class="mxchat-bot-badge">' . esc_html__('Default Bot', 'mxchat') . '</span>';
3179 } else {
3180 // Try to get bot name from multi-bot manager
3181 if (class_exists('MxChat_Multi_Bot_Core_Manager')) {
3182 $multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
3183 $available_bots = $multi_bot_manager->get_available_bots();
3184 $bot_name = isset($available_bots[$bot_id]) ? $available_bots[$bot_id] : $bot_id;
3185 echo '<span class="mxchat-bot-badge">' . esc_html($bot_name) . '</span>';
3186 } else {
3187 echo '<span class="mxchat-bot-badge">' . esc_html($bot_id) . '</span>';
3188 }
3189 }
3190 }
3191 ?>
3192 </div>
3193 </div>
3194 </div>
3195
3196 <div class="mxchat-card-footer">
3197 <?php
3198 // Check if it's a form action
3199 $is_form_action = preg_match('/Form (\d+)/', $action->intent_label, $form_matches);
3200
3201 // Check if it's a recommendation flow action
3202 $is_flow_action = preg_match('/Recommendation Flow (\d+)/', $action->intent_label, $flow_matches);
3203
3204 if ($is_form_action) {
3205 $form_id = isset($form_matches[1]) ? $form_matches[1] : '';
3206 ?>
3207 <a href="<?php echo esc_url(admin_url('admin.php?page=mxchat-forms&action=edit&form_id=' . $form_id)); ?>"
3208 class="mxchat-button-primary">
3209 <span class="dashicons dashicons-feedback"></span>
3210 <?php esc_html_e('Edit Form', 'mxchat'); ?>
3211 </a>
3212 <?php } elseif ($is_flow_action) {
3213 ?>
3214 <a href="<?php echo esc_url(admin_url('admin.php?page=mxchat-smart-recommender')); ?>"
3215 class="mxchat-button-primary">
3216 <span class="dashicons dashicons-list-view"></span>
3217 <?php esc_html_e('Manage Flows', 'mxchat'); ?>
3218 </a>
3219 <?php } else { ?>
3220 <button type="button"
3221 class="mxchat-button-secondary mxchat-edit-button"
3222 data-action-id="<?php echo esc_attr($action->id); ?>"
3223 data-phrases="<?php echo esc_attr($action->phrases); ?>"
3224 data-label="<?php echo esc_attr($action->intent_label); ?>"
3225 data-threshold="<?php echo esc_attr(round($action->similarity_threshold * 100)); ?>"
3226 data-callback-function="<?php echo esc_attr($action->callback_function); ?>"
3227 data-enabled-bots="<?php echo esc_attr(json_encode($enabled_bots)); ?>">
3228 <span class="dashicons dashicons-edit"></span>
3229 <?php esc_html_e('Edit', 'mxchat'); ?>
3230 </button>
3231 <form method="post"
3232 action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
3233 class="mxchat-delete-form"
3234 onsubmit="return confirm('<?php esc_attr_e('Are you sure you want to delete this action?', 'mxchat'); ?>');">
3235 <?php wp_nonce_field('mxchat_delete_intent_nonce'); ?>
3236 <input type="hidden" name="action" value="mxchat_delete_intent">
3237 <input type="hidden" name="intent_id" value="<?php echo esc_attr($action->id); ?>">
3238 <button type="submit" class="mxchat-button-text mxchat-delete-button">
3239 <span class="dashicons dashicons-trash"></span>
3240 <?php esc_html_e('Delete', 'mxchat'); ?>
3241 </button>
3242 </form>
3243 <?php } ?>
3244 </div>
3245 </div>
3246 <?php endforeach; ?>
3247 <?php else : ?>
3248 <!-- If no actions found -->
3249 <div class="mxchat-no-actions">
3250 <div class="mxchat-empty-state">
3251 <span class="dashicons dashicons-format-chat"></span>
3252 <h2><?php esc_html_e('No actions found', 'mxchat'); ?></h2>
3253 <p><?php esc_html_e('Get started by creating your first action to enhance your chatbot.', 'mxchat'); ?></p>
3254 <button type="button" id="mxchat-create-first-action" class="mxchat-button-primary">
3255 <?php esc_html_e('Create Your First Action', 'mxchat'); ?>
3256 </button>
3257 </div>
3258 </div>
3259 <?php endif; ?>
3260 </div>
3261 </div>
3262
3263 <?php if ($total_pages > 1) : ?>
3264 <div class="mxchat-pagination">
3265 <?php
3266 echo paginate_links(array(
3267 'base' => add_query_arg('paged', '%#%'),
3268 'format' => '',
3269 'prev_text' => __('&laquo; Previous', 'mxchat'),
3270 'next_text' => __('Next &raquo;', 'mxchat'),
3271 'total' => $total_pages,
3272 'current' => $page
3273 ));
3274 ?>
3275 </div>
3276 <?php endif; ?>
3277
3278 <!-- Add/Edit Action Modal with Step-Based Approach -->
3279 <!-- Complete Modal HTML with Defined Groups Variable -->
3280 <div id="mxchat-action-modal" class="mxchat-modal" style="display: none;">
3281 <div class="mxchat-modal-content">
3282 <span class="mxchat-modal-close">&times;</span>
3283
3284 <form id="mxchat-action-form" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
3285 <!-- Dynamic nonce field -->
3286 <div id="action-nonce-container">
3287 <?php wp_nonce_field('mxchat_add_intent_nonce', 'add_intent_nonce'); ?>
3288 </div>
3289 <input type="hidden" name="action" id="form_action_type" value="mxchat_add_intent">
3290 <input type="hidden" name="intent_id" id="edit_action_id" value="">
3291 <input type="hidden" name="callback_function" id="callback_function" value="">
3292
3293 <!-- Step 1: Action Type Selection -->
3294 <div id="mxchat-action-step-1" class="mxchat-action-step active">
3295 <div class="mxchat-step-indicator">
3296 <div class="mxchat-step-number">1</div>
3297 <div class="mxchat-step-title"><?php esc_html_e('Select Action Type', 'mxchat'); ?></div>
3298 </div>
3299
3300 <div id="mxchat-action-type-selector" class="mxchat-action-type-selector">
3301 <div class="mxchat-action-type-search">
3302 <span class="dashicons dashicons-search"></span>
3303 <input type="text" id="action-type-search" placeholder="<?php esc_attr_e('Search action types...', 'mxchat'); ?>" class="mxchat-action-type-search-input">
3304 </div>
3305
3306 <?php
3307 // Get the callbacks - IMPORTANT: Define the $groups variable here
3308 $groups = $this->mxchat_get_available_callbacks(true, true);
3309 ?>
3310
3311 <div class="mxchat-action-type-categories">
3312 <button type="button" class="mxchat-category-button active" data-category="all"><?php esc_html_e('All', 'mxchat'); ?></button>
3313 <?php
3314 // Get unique categories from the defined groups
3315 foreach ($groups as $group_label => $group_callbacks) :
3316 $category_slug = sanitize_title($group_label);
3317 ?>
3318 <button type="button" class="mxchat-category-button" data-category="<?php echo esc_attr($category_slug); ?>"><?php echo esc_html($group_label); ?></button>
3319 <?php endforeach; ?>
3320 </div>
3321
3322 <div class="mxchat-action-types-grid">
3323 <?php
3324 // Generate action cards from available callbacks
3325 foreach ($groups as $group_label => $group_callbacks) :
3326 $category_slug = sanitize_title($group_label);
3327
3328 foreach ($group_callbacks as $function => $data) :
3329 $label = $data['label'];
3330 $icon = isset($data['icon']) ? $data['icon'] : 'admin-generic';
3331 $description = isset($data['description']) ? $data['description'] : '';
3332 $is_addon = isset($data['addon']) && $data['addon'] !== false;
3333 $addon_name = isset($data['addon_name']) ? $data['addon_name'] : '';
3334 $is_installed = isset($data['installed']) ? $data['installed'] : true;
3335 $is_promo = !empty($data['addon_promo']) && !$is_installed;
3336
3337 // Determine card status and styling
3338 $card_class = 'mxchat-action-type-card';
3339 $icon_class = 'mxchat-action-type-icon';
3340 $status_badge = '';
3341
3342 if ($is_promo) {
3343 // Promotional card — not selectable, just informational
3344 $card_class .= ' not-installed mxchat-promo-card';
3345 $status_badge = '<span class="mxchat-addon-badge">' . esc_html__('Add-on Required', 'mxchat') . '</span>';
3346 } elseif ($is_addon && !$is_installed) {
3347 // Add-on not installed
3348 $card_class .= ' not-installed';
3349 $status_badge .= '<span class="mxchat-addon-badge">' . esc_html__('Add-on Required', 'mxchat') . '</span>';
3350 }
3351
3352 // Default description if none provided
3353 if (empty($description)) {
3354 $description = sprintf(
3355 esc_html__('Use the %s action in your chatbot', 'mxchat'),
3356 $label
3357 );
3358 }
3359 ?>
3360 <div class="<?php echo esc_attr($card_class); ?>"
3361 data-category="<?php echo esc_attr($category_slug); ?>"
3362 <?php if (!$is_promo) : ?>
3363 data-value="<?php echo esc_attr($function); ?>"
3364 data-label="<?php echo esc_attr($label); ?>"
3365 <?php endif; ?>
3366 data-pro="false"
3367 data-addon="<?php echo esc_attr($is_addon ? $data['addon'] : ''); ?>"
3368 data-installed="<?php echo $is_installed ? 'true' : 'false'; ?>"
3369 <?php if ($is_promo) : ?>data-promo="true"<?php endif; ?>>
3370 <div class="<?php echo esc_attr($icon_class); ?>">
3371 <span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span>
3372 </div>
3373 <div class="mxchat-action-type-info">
3374 <h4><?php echo esc_html($label); ?></h4>
3375 <p><?php echo esc_html($description); ?></p>
3376 <?php if (!empty($status_badge)) : ?>
3377 <?php echo $status_badge; ?>
3378 <?php endif; ?>
3379
3380 <?php if ($is_promo || ($is_addon && !$is_installed)) : ?>
3381 <div class="mxchat-addon-info">
3382 <?php echo esc_html(sprintf(
3383 __('Requires %s', 'mxchat'),
3384 $addon_name
3385 )); ?>
3386 <a href="https://mxchat.ai/" target="_blank"><?php esc_html_e('Get Add-on', 'mxchat'); ?></a>
3387 </div>
3388 <?php endif; ?>
3389 </div>
3390 </div>
3391 <?php endforeach;
3392 endforeach; ?>
3393 </div>
3394 </div>
3395
3396 <div class="mxchat-modal-actions">
3397 <button type="button" class="mxchat-button-secondary mxchat-modal-cancel">
3398 <?php esc_html_e('Cancel', 'mxchat'); ?>
3399 </button>
3400 </div>
3401 </div>
3402
3403 <!-- Step 2: Action Configuration -->
3404 <div id="mxchat-action-step-2" class="mxchat-action-step">
3405 <div class="mxchat-step-indicator">
3406 <div class="mxchat-step-number">2</div>
3407 <div class="mxchat-step-title"><?php esc_html_e('Configure Action', 'mxchat'); ?></div>
3408 </div>
3409
3410 <div class="mxchat-selected-action">
3411 <button type="button" class="mxchat-back-button" id="mxchat-back-to-step-1">
3412 <span class="dashicons dashicons-arrow-left-alt"></span>
3413 <?php esc_html_e('Back to Action Types', 'mxchat'); ?>
3414 </button>
3415 <div class="mxchat-selected-action-info">
3416 <div id="selected-action-icon" class="mxchat-action-type-icon">
3417 <span class="dashicons dashicons-admin-generic"></span>
3418 </div>
3419 <div class="mxchat-selected-action-details">
3420 <h3 id="selected-action-title"><?php esc_html_e('Selected Action', 'mxchat'); ?></h3>
3421 <p id="selected-action-description"><?php esc_html_e('Configure this action for your chatbot', 'mxchat'); ?></p>
3422 </div>
3423 </div>
3424 </div>
3425
3426 <div class="mxchat-form-group">
3427 <label for="intent_label">
3428 <?php esc_html_e('Action Label (For your reference only)', 'mxchat'); ?>
3429 </label>
3430 <input name="intent_label" type="text" id="intent_label" required
3431 class="mxchat-intent-input"
3432 placeholder="<?php esc_attr_e('Example: Newsletter Signup', 'mxchat'); ?>">
3433 </div>
3434
3435 <div class="mxchat-form-group">
3436 <label for="phrases">
3437 <?php esc_html_e('Trigger Phrases (comma-separated)', 'mxchat'); ?>
3438 </label>
3439 <textarea name="phrases" id="action_phrases" rows="5" required
3440 class="mxchat-intent-textarea"
3441 placeholder="<?php esc_attr_e('Example: sign me up, subscribe me, I want to join, add me to the newsletter', 'mxchat'); ?>"></textarea>
3442 </div>
3443
3444 <div class="mxchat-form-group">
3445 <label for="similarity_threshold">
3446 <?php esc_html_e('Similarity Threshold', 'mxchat'); ?>
3447 <span class="mxchat-threshold-value-display">85%</span>
3448 </label>
3449 <div class="mxchat-slider-group modal-slider">
3450 <input type="range"
3451 name="similarity_threshold"
3452 id="similarity_threshold"
3453 min="10"
3454 max="95"
3455 value="85"
3456 class="mxchat-intent-slider"
3457 oninput="document.querySelector('.mxchat-threshold-value-display').textContent = this.value + '%'">
3458 </div>
3459 <div class="mxchat-threshold-hint">
3460 <?php esc_html_e('Lower values (10-30) make the action trigger more easily. Higher values (70-95) require more exact matches.', 'mxchat'); ?>
3461 </div>
3462 </div>
3463
3464 <!-- Bot Selection Section -->
3465 <div class="mxchat-form-group">
3466 <label for="enabled_bots">
3467 <?php esc_html_e('Which bots should we enable this action for?', 'mxchat'); ?>
3468 </label>
3469 <div class="mxchat-bot-selector">
3470 <div class="mxchat-bot-option">
3471 <label class="mxchat-checkbox-label">
3472 <input type="checkbox"
3473 name="enabled_bots[]"
3474 value="default"
3475 id="bot_default"
3476 checked="checked">
3477 <span class="mxchat-checkmark"></span>
3478 <?php esc_html_e('Default Bot', 'mxchat'); ?>
3479 </label>
3480 </div>
3481
3482 <?php if (class_exists('MxChat_Multi_Bot_Core_Manager')) :
3483 $multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
3484 $available_bots = $multi_bot_manager->get_available_bots();
3485
3486 foreach ($available_bots as $bot_id => $bot_name) :
3487 if ($bot_id === 'default') continue; // Skip default, already shown above
3488 ?>
3489 <div class="mxchat-bot-option">
3490 <label class="mxchat-checkbox-label">
3491 <input type="checkbox"
3492 name="enabled_bots[]"
3493 value="<?php echo esc_attr($bot_id); ?>"
3494 id="bot_<?php echo esc_attr($bot_id); ?>">
3495 <span class="mxchat-checkmark"></span>
3496 <?php echo esc_html($bot_name); ?>
3497 </label>
3498 </div>
3499 <?php
3500 endforeach;
3501 endif; ?>
3502 </div>
3503 </div>
3504
3505 <div class="mxchat-modal-actions">
3506 <button type="button" class="mxchat-button-secondary mxchat-modal-cancel">
3507 <?php esc_html_e('Cancel', 'mxchat'); ?>
3508 </button>
3509 <button type="submit" class="mxchat-button-primary" id="mxchat-save-action-btn">
3510 <?php esc_html_e('Save Action', 'mxchat'); ?>
3511 </button>
3512 </div>
3513 </div>
3514 </form>
3515 </div>
3516 </div>
3517
3518 <div id="mxchat-action-loading" class="mxchat-action-loading" style="display: none;">
3519 <div class="mxchat-action-loading-spinner"></div>
3520 <div class="mxchat-action-loading-text">
3521 <?php esc_html_e('Saving action, please wait...', 'mxchat'); ?>
3522 </div>
3523 </div>
3524 </div><!-- .mxchat-wrapper -->
3525 <?php
3526 }
3527 private function get_trimmed_phrases($phrases, $max_length = 100) {
3528 if (strlen($phrases) <= $max_length) {
3529 return $phrases;
3530 }
3531
3532 $trimmed = substr($phrases, 0, $max_length);
3533 $last_comma = strrpos($trimmed, ',');
3534
3535 if ($last_comma !== false) {
3536 $trimmed = substr($trimmed, 0, $last_comma);
3537 }
3538
3539 return $trimmed . '...';
3540 }
3541 public function mxchat_add_enabled_column_to_intents() {
3542 global $wpdb;
3543 $table_name = $wpdb->prefix . 'mxchat_intents';
3544
3545 // Check if the column already exists
3546 $columns = $wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'enabled'");
3547
3548 if (empty($columns)) {
3549 // Add the column with default value of 1 (enabled)
3550 $wpdb->query("ALTER TABLE $table_name ADD COLUMN enabled TINYINT(1) NOT NULL DEFAULT 1");
3551 }
3552 }
3553
3554 public function mxchat_handle_delete_intent() {
3555 if ( ! current_user_can( 'manage_options' ) ) {
3556 wp_die( esc_html__('Unauthorized user', 'mxchat') );
3557 }
3558
3559 check_admin_referer('mxchat_delete_intent_nonce');
3560
3561 if (isset($_POST['intent_id'])) {
3562 global $wpdb;
3563 $table_name = $wpdb->prefix . 'mxchat_intents';
3564 $intent_id = intval($_POST['intent_id']);
3565
3566 $wpdb->delete($table_name, ['id' => $intent_id], ['%d']);
3567 }
3568
3569 wp_safe_redirect(admin_url('admin.php?page=mxchat-actions'));
3570 exit;
3571 }
3572 public function mxchat_handle_edit_intent() {
3573 // Security checks (nonce and permissions)
3574 if (!current_user_can('manage_options')) {
3575 wp_die(esc_html__('Unauthorized user', 'mxchat'));
3576 }
3577 check_admin_referer('mxchat_edit_intent');
3578
3579 // Get POST data
3580 $intent_id = isset($_POST['intent_id']) ? absint($_POST['intent_id']) : 0;
3581 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
3582 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
3583 $threshold_percentage = isset($_POST['similarity_threshold']) ? intval($_POST['similarity_threshold']) : 85;
3584 $similarity_threshold = min(95, max(10, $threshold_percentage)) / 100; // Convert to 0.10–0.95
3585
3586 // Handle enabled_bots
3587 $enabled_bots = isset($_POST['enabled_bots']) ? $_POST['enabled_bots'] : array('default');
3588 $enabled_bots = array_map('sanitize_text_field', $enabled_bots);
3589
3590 // Ensure default is always included for backward compatibility
3591 if (!in_array('default', $enabled_bots)) {
3592 $enabled_bots[] = 'default';
3593 }
3594
3595 $enabled_bots_json = json_encode($enabled_bots);
3596
3597 // Validate inputs
3598 if (!$intent_id || empty($intent_label) || empty($phrases_input)) {
3599 $this->handle_embedding_error(__('Invalid input. Please ensure all fields are filled out.', 'mxchat'));
3600 return;
3601 }
3602
3603 $phrases_array = array_map('sanitize_text_field', array_filter(array_map('trim', explode(',', $phrases_input))));
3604 if (empty($phrases_array)) {
3605 $this->handle_embedding_error(__('Please enter at least one valid phrase.', 'mxchat'));
3606 return;
3607 }
3608
3609 // Generate embeddings with improved error handling
3610 $vectors = [];
3611 $failed_phrases = [];
3612
3613 foreach ($phrases_array as $phrase) {
3614 $embedding_vector = $this->mxchat_generate_embedding($phrase, $this->options['api_key']);
3615 if (is_array($embedding_vector)) {
3616 $vectors[] = $embedding_vector;
3617 } else {
3618 $failed_phrases[] = $phrase;
3619 }
3620 }
3621
3622 if (!empty($failed_phrases)) {
3623 $this->handle_embedding_error(
3624 sprintf(
3625 __('Error generating embeddings for phrases: %s. Check your embedding API.', 'mxchat'),
3626 implode(', ', $failed_phrases)
3627 )
3628 );
3629 return;
3630 }
3631
3632 if (empty($vectors)) {
3633 $this->handle_embedding_error(__('No valid embeddings generated. Please check your phrases.', 'mxchat'));
3634 return;
3635 }
3636
3637 $combined_vector = $this->mxchat_average_vectors($vectors);
3638 $serialized_vector = maybe_serialize($combined_vector);
3639
3640 // Update the database
3641 global $wpdb;
3642 $table_name = $wpdb->prefix . 'mxchat_intents';
3643
3644 $result = $wpdb->update(
3645 $table_name,
3646 array(
3647 'intent_label' => $intent_label,
3648 'phrases' => implode(', ', $phrases_array),
3649 'embedding_vector' => $serialized_vector,
3650 'similarity_threshold' => $similarity_threshold,
3651 'enabled_bots' => $enabled_bots_json // Include enabled_bots in update
3652 ),
3653 array('id' => $intent_id),
3654 array('%s', '%s', '%s', '%f', '%s'), // Format: string, string, string, float, string
3655 array('%d') // Where format: integer
3656 );
3657
3658 if (false === $result) {
3659 $this->handle_embedding_error(__('Failed to update action in database.', 'mxchat'));
3660 return;
3661 }
3662
3663 // Set success message and redirect
3664 set_transient('mxchat_admin_notice_success', __('Intent updated successfully!', 'mxchat'), 60);
3665
3666 $redirect_url = add_query_arg(
3667 array(
3668 'page' => 'mxchat-actions'
3669 ),
3670 admin_url('admin.php')
3671 );
3672 wp_safe_redirect($redirect_url);
3673 exit;
3674 }
3675 public function mxchat_handle_add_intent() {
3676 if (!current_user_can('manage_options')) {
3677 wp_die(esc_html__('Unauthorized user', 'mxchat'));
3678 }
3679 check_admin_referer('mxchat_add_intent_nonce');
3680 global $wpdb;
3681 $table_name = $wpdb->prefix . 'mxchat_intents';
3682
3683 // Sanitize and get form data
3684 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
3685 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
3686 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
3687
3688 // Get similarity threshold from form (convert percentage to decimal)
3689 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
3690
3691 // Handle enabled_bots
3692 $enabled_bots = isset($_POST['enabled_bots']) ? $_POST['enabled_bots'] : array('default');
3693 $enabled_bots = array_map('sanitize_text_field', $enabled_bots);
3694
3695 // Ensure default is always included for backward compatibility with existing actions
3696 if (!in_array('default', $enabled_bots)) {
3697 $enabled_bots[] = 'default';
3698 }
3699
3700 $enabled_bots_json = json_encode($enabled_bots);
3701
3702 // Validate required fields
3703 if (empty($intent_label) || empty($callback_function) || empty($phrases_input)) {
3704 $this->handle_embedding_error(__('Invalid input. Please ensure all fields are filled out.', 'mxchat'));
3705 return;
3706 }
3707
3708 // Validate callback function
3709 $available_callbacks = $this->mxchat_get_available_callbacks();
3710 if (!array_key_exists($callback_function, $available_callbacks)) {
3711 $this->handle_embedding_error(__('Invalid callback function selected.', 'mxchat'));
3712 return;
3713 }
3714
3715 // Check if this is an add-on promotional placeholder (not a real action)
3716 if (!empty($available_callbacks[$callback_function]['addon_promo'])) {
3717 $addon_name = isset($available_callbacks[$callback_function]['addon_name']) ? $available_callbacks[$callback_function]['addon_name'] : __('an add-on', 'mxchat');
3718 $this->handle_embedding_error(sprintf(
3719 __('This action requires the %s to be installed and activated.', 'mxchat'),
3720 $addon_name
3721 ));
3722 return;
3723 }
3724
3725 // Process phrases
3726 $phrases_array = array_map('sanitize_text_field', array_filter(array_map('trim', explode(',', $phrases_input))));
3727 if (empty($phrases_array)) {
3728 $this->handle_embedding_error(__('Please enter at least one valid phrase.', 'mxchat'));
3729 return;
3730 }
3731
3732 // Generate embeddings with improved error handling
3733 $vectors = [];
3734 $failed_phrases = [];
3735 foreach ($phrases_array as $phrase) {
3736 $embedding_vector = $this->mxchat_generate_embedding($phrase, $this->options['api_key']);
3737 if (is_array($embedding_vector)) {
3738 $vectors[] = $embedding_vector;
3739 } else {
3740 $failed_phrases[] = $phrase;
3741 }
3742 }
3743
3744 // Check for embedding failures
3745 if (!empty($failed_phrases)) {
3746 $this->handle_embedding_error(
3747 sprintf(
3748 __('Error generating embeddings for phrases: %s. Check your embedding API.', 'mxchat'),
3749 implode(', ', $failed_phrases)
3750 )
3751 );
3752 return;
3753 }
3754
3755 if (empty($vectors)) {
3756 $this->handle_embedding_error(__('No valid embeddings generated. Please check your phrases.', 'mxchat'));
3757 return;
3758 }
3759
3760 // Create combined vector and insert into database
3761 $combined_vector = $this->mxchat_average_vectors($vectors);
3762 $serialized_vector = maybe_serialize($combined_vector);
3763
3764 $result = $wpdb->insert($table_name, [
3765 'intent_label' => $intent_label,
3766 'phrases' => implode(', ', $phrases_array),
3767 'embedding_vector' => $serialized_vector,
3768 'callback_function' => $callback_function,
3769 'similarity_threshold' => $similarity_threshold,
3770 'enabled_bots' => $enabled_bots_json // NEW field
3771 ]);
3772
3773 if ($result === false) {
3774 $this->handle_embedding_error(__('Database error: ', 'mxchat') . $wpdb->last_error);
3775 return;
3776 }
3777
3778 // Set success message and redirect
3779 set_transient('mxchat_admin_notice_success', __('New intent added successfully!', 'mxchat'), 60);
3780 wp_safe_redirect(admin_url('admin.php?page=mxchat-actions'));
3781 exit;
3782 }
3783
3784
3785
3786
3787 private function handle_embedding_error($message, $redirect = true) {
3788 // Store the error message in the existing transient
3789 set_transient('mxchat_admin_notice_error', $message, 60);
3790
3791 if ($redirect) {
3792 // Redirect back to the actions page
3793 $redirect_url = add_query_arg(
3794 array(
3795 'page' => 'mxchat-actions'
3796 ),
3797 admin_url('admin.php')
3798 );
3799 wp_safe_redirect($redirect_url);
3800 exit;
3801 }
3802 }
3803
3804 /**
3805 * AJAX handler to fetch actions list for the new split-panel UI
3806 */
3807 public function mxchat_fetch_actions_list() {
3808 check_ajax_referer('mxchat_actions_nonce', 'security');
3809
3810 if (!current_user_can('manage_options')) {
3811 wp_send_json_error(__('Unauthorized', 'mxchat'));
3812 }
3813
3814 global $wpdb;
3815 $table_name = $wpdb->prefix . 'mxchat_intents';
3816
3817 $page = isset($_POST['page']) ? max(1, intval($_POST['page'])) : 1;
3818 $per_page = isset($_POST['per_page']) ? min(100, max(1, intval($_POST['per_page']))) : 50;
3819 $offset = ($page - 1) * $per_page;
3820 $search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
3821 $callback_filter = isset($_POST['callback_filter']) ? sanitize_text_field($_POST['callback_filter']) : '';
3822 $sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] === 'asc' ? 'ASC' : 'DESC';
3823
3824 // Build WHERE clause
3825 $where = '1=1';
3826 $params = array();
3827
3828 if ($search) {
3829 $search_like = '%' . $wpdb->esc_like($search) . '%';
3830 $where .= ' AND (intent_label LIKE %s OR phrases LIKE %s)';
3831 $params[] = $search_like;
3832 $params[] = $search_like;
3833 }
3834
3835 if ($callback_filter) {
3836 $where .= ' AND callback_function = %s';
3837 $params[] = $callback_filter;
3838 }
3839
3840 // Get total count
3841 $count_query = "SELECT COUNT(*) FROM $table_name WHERE $where";
3842 if (!empty($params)) {
3843 $count_query = $wpdb->prepare($count_query, $params);
3844 }
3845 $total_actions = $wpdb->get_var($count_query);
3846
3847 // Get actions
3848 $query = "SELECT * FROM $table_name WHERE $where ORDER BY id $sort_order LIMIT %d OFFSET %d";
3849 $all_params = array_merge($params, array($per_page, $offset));
3850 $actions = $wpdb->get_results($wpdb->prepare($query, $all_params));
3851
3852 // Get available callbacks for labels/icons
3853 $available_callbacks = $this->mxchat_get_available_callbacks();
3854
3855 // Prefetch individual phrase counts for all fetched actions
3856 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
3857 $phrase_counts = array();
3858 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
3859 $action_ids = wp_list_pluck($actions, 'id');
3860 if (!empty($action_ids)) {
3861 $id_placeholders = implode(',', array_fill(0, count($action_ids), '%d'));
3862 $count_results = $wpdb->get_results($wpdb->prepare(
3863 "SELECT intent_id, COUNT(*) as cnt FROM $phrases_table WHERE intent_id IN ($id_placeholders) GROUP BY intent_id",
3864 $action_ids
3865 ));
3866 foreach ($count_results as $row) {
3867 $phrase_counts[$row->intent_id] = intval($row->cnt);
3868 }
3869 }
3870 }
3871
3872 // Format actions for response
3873 $formatted_actions = array();
3874 foreach ($actions as $action) {
3875 $callback_data = isset($available_callbacks[$action->callback_function])
3876 ? $available_callbacks[$action->callback_function]
3877 : array('label' => $action->callback_function, 'icon' => 'admin-generic');
3878
3879 $enabled_bots = json_decode($action->enabled_bots, true);
3880 if (!is_array($enabled_bots)) {
3881 $enabled_bots = array('default');
3882 }
3883
3884 $formatted_actions[] = array(
3885 'id' => intval($action->id),
3886 'label' => $action->intent_label,
3887 'phrases' => $action->phrases,
3888 'callback_function' => $action->callback_function,
3889 'callback_label' => $callback_data['label'],
3890 'icon' => isset($callback_data['icon']) ? $callback_data['icon'] : 'admin-generic',
3891 'threshold' => round($action->similarity_threshold * 100),
3892 'enabled' => (bool) $action->enabled,
3893 'enabled_bots' => $enabled_bots,
3894 'has_legacy_vector' => !empty($action->embedding_vector),
3895 'individual_phrase_count' => isset($phrase_counts[$action->id]) ? $phrase_counts[$action->id] : 0,
3896 );
3897 }
3898
3899 $total_pages = ceil($total_actions / $per_page);
3900 $showing_start = $total_actions > 0 ? $offset + 1 : 0;
3901 $showing_end = min($offset + $per_page, $total_actions);
3902
3903 wp_send_json_success(array(
3904 'actions' => $formatted_actions,
3905 'page' => $page,
3906 'per_page' => $per_page,
3907 'total_actions' => intval($total_actions),
3908 'total_pages' => $total_pages,
3909 'showing_start' => $showing_start,
3910 'showing_end' => $showing_end,
3911 ));
3912 }
3913
3914 /**
3915 * AJAX handler to toggle action enabled status
3916 */
3917 public function mxchat_toggle_action_status() {
3918 check_ajax_referer('mxchat_actions_nonce', 'security');
3919
3920 if (!current_user_can('manage_options')) {
3921 wp_send_json_error(__('Unauthorized', 'mxchat'));
3922 }
3923
3924 $action_id = isset($_POST['action_id']) ? intval($_POST['action_id']) : 0;
3925 $enabled = isset($_POST['enabled']) ? intval($_POST['enabled']) : 0;
3926
3927 if (!$action_id) {
3928 wp_send_json_error(__('Invalid action ID', 'mxchat'));
3929 }
3930
3931 global $wpdb;
3932 $table_name = $wpdb->prefix . 'mxchat_intents';
3933
3934 $result = $wpdb->update(
3935 $table_name,
3936 array('enabled' => $enabled ? 1 : 0),
3937 array('id' => $action_id),
3938 array('%d'),
3939 array('%d')
3940 );
3941
3942 if ($result === false) {
3943 wp_send_json_error(__('Failed to update action status', 'mxchat'));
3944 }
3945
3946 wp_send_json_success(array('enabled' => (bool) $enabled));
3947 }
3948
3949 /**
3950 * AJAX handler to bulk delete actions
3951 */
3952 public function mxchat_bulk_delete_actions() {
3953 check_ajax_referer('mxchat_delete_intent_nonce', 'security');
3954
3955 if (!current_user_can('manage_options')) {
3956 wp_send_json_error(__('Unauthorized', 'mxchat'));
3957 }
3958
3959 $action_ids = isset($_POST['action_ids']) ? array_map('intval', (array) $_POST['action_ids']) : array();
3960
3961 if (empty($action_ids)) {
3962 wp_send_json_error(__('No actions selected', 'mxchat'));
3963 }
3964
3965 global $wpdb;
3966 $table_name = $wpdb->prefix . 'mxchat_intents';
3967
3968 $placeholders = implode(',', array_fill(0, count($action_ids), '%d'));
3969 $query = $wpdb->prepare("DELETE FROM $table_name WHERE id IN ($placeholders)", $action_ids);
3970 $result = $wpdb->query($query);
3971
3972 if ($result === false) {
3973 wp_send_json_error(__('Failed to delete actions', 'mxchat'));
3974 }
3975
3976 // Also delete individual phrases for these intents
3977 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
3978 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
3979 $wpdb->query($wpdb->prepare("DELETE FROM $phrases_table WHERE intent_id IN ($placeholders)", $action_ids));
3980 }
3981
3982 wp_send_json_success(array('deleted' => $result));
3983 }
3984
3985 /**
3986 * AJAX handler to add a new intent/action
3987 */
3988 public function mxchat_add_intent_ajax() {
3989 check_ajax_referer('mxchat_add_intent_nonce', 'security');
3990
3991 if (!current_user_can('manage_options')) {
3992 wp_send_json_error(__('Unauthorized', 'mxchat'));
3993 }
3994
3995 global $wpdb;
3996 $table_name = $wpdb->prefix . 'mxchat_intents';
3997
3998 // Sanitize input
3999 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
4000 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
4001 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
4002 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
4003 $enabled_bots = isset($_POST['enabled_bots']) ? array_map('sanitize_text_field', (array) $_POST['enabled_bots']) : array('default');
4004
4005 // Validate
4006 // Check if using new individual phrases mode or legacy mode
4007 $individual_phrases = isset($_POST['individual_phrases']) ? array_filter(array_map('sanitize_text_field', (array) $_POST['individual_phrases'])) : array();
4008 $use_individual = !empty($individual_phrases);
4009
4010 // Validate required fields (phrases not required when using individual mode)
4011 if (empty($intent_label) || empty($callback_function)) {
4012 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
4013 }
4014 if (!$use_individual && empty($phrases_input)) {
4015 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
4016 }
4017
4018 // Ensure default bot is included
4019 if (!in_array('default', $enabled_bots)) {
4020 $enabled_bots[] = 'default';
4021 }
4022 $enabled_bots_json = json_encode($enabled_bots);
4023
4024 if ($use_individual) {
4025 // New mode: individual phrases each get their own vector
4026 // Insert the intent row with empty legacy fields
4027 $result = $wpdb->insert(
4028 $table_name,
4029 array(
4030 'intent_label' => $intent_label,
4031 'phrases' => '',
4032 'embedding_vector' => '',
4033 'similarity_threshold' => $similarity_threshold,
4034 'callback_function' => $callback_function,
4035 'enabled' => 1,
4036 'enabled_bots' => $enabled_bots_json,
4037 )
4038 );
4039
4040 if ($result === false) {
4041 wp_send_json_error(__('Failed to add action to database.', 'mxchat'));
4042 }
4043
4044 $intent_id = $wpdb->insert_id;
4045
4046 // Insert each phrase individually with its own embedding
4047 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
4048 $failed_phrases = array();
4049 foreach ($individual_phrases as $phrase) {
4050 $phrase = trim($phrase);
4051 if (empty($phrase)) continue;
4052
4053 $embedding_vector = $this->mxchat_generate_embedding($phrase);
4054 if (is_wp_error($embedding_vector)) {
4055 $failed_phrases[] = $phrase;
4056 continue;
4057 }
4058
4059 $wpdb->insert(
4060 $phrases_table,
4061 array(
4062 'intent_id' => $intent_id,
4063 'phrase' => $phrase,
4064 'embedding_vector' => maybe_serialize($embedding_vector),
4065 )
4066 );
4067 }
4068
4069 $response = array('id' => $intent_id);
4070 if (!empty($failed_phrases)) {
4071 $response['failed_phrases'] = $failed_phrases;
4072 }
4073 wp_send_json_success($response);
4074
4075 } else {
4076 // Legacy mode: combine all phrases into one embedding (backwards compatible)
4077 $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
4078 if (empty($phrases_array)) {
4079 wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
4080 }
4081
4082 // Generate embedding (combine phrases into single string for embedding)
4083 $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
4084 if (is_wp_error($embedding_vector)) {
4085 // Fallback: store without embedding
4086 $serialized_vector = null;
4087 } else {
4088 $serialized_vector = maybe_serialize($embedding_vector);
4089 }
4090
4091 // Insert
4092 $result = $wpdb->insert(
4093 $table_name,
4094 array(
4095 'intent_label' => $intent_label,
4096 'phrases' => implode(', ', $phrases_array),
4097 'embedding_vector' => $serialized_vector,
4098 'similarity_threshold' => $similarity_threshold,
4099 'callback_function' => $callback_function,
4100 'enabled' => 1,
4101 'enabled_bots' => $enabled_bots_json,
4102 )
4103 );
4104
4105 if ($result === false) {
4106 wp_send_json_error(__('Failed to add action to database.', 'mxchat'));
4107 }
4108
4109 wp_send_json_success(array('id' => $wpdb->insert_id));
4110 }
4111 }
4112
4113 /**
4114 * AJAX handler to edit an existing intent/action
4115 */
4116 public function mxchat_edit_intent_ajax() {
4117 check_ajax_referer('mxchat_edit_intent', 'security');
4118
4119 if (!current_user_can('manage_options')) {
4120 wp_send_json_error(__('Unauthorized', 'mxchat'));
4121 }
4122
4123 global $wpdb;
4124 $table_name = $wpdb->prefix . 'mxchat_intents';
4125
4126 // Sanitize input
4127 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
4128 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
4129 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
4130 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
4131 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
4132 $enabled_bots = isset($_POST['enabled_bots']) ? array_map('sanitize_text_field', (array) $_POST['enabled_bots']) : array('default');
4133
4134 // Validate
4135 if (!$intent_id || empty($intent_label)) {
4136 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
4137 }
4138
4139 // Check if phrases are managed individually (empty phrases_input means individual mode)
4140 $uses_individual_phrases = empty($phrases_input);
4141
4142 if ($uses_individual_phrases) {
4143 // Individual phrase mode: only update non-phrase fields, skip embedding regeneration
4144 $update_data = array(
4145 'intent_label' => $intent_label,
4146 'similarity_threshold' => $similarity_threshold,
4147 'callback_function' => $callback_function,
4148 'enabled_bots' => json_encode($enabled_bots),
4149 );
4150 } else {
4151 // Legacy mode: process phrases and regenerate embedding (backwards compatible for add-ons)
4152 $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
4153 if (empty($phrases_array)) {
4154 wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
4155 }
4156
4157 // Generate new embedding (combine phrases into single string for embedding)
4158 $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
4159 if (is_wp_error($embedding_vector)) {
4160 // Keep existing embedding
4161 $update_data = array(
4162 'intent_label' => $intent_label,
4163 'phrases' => implode(', ', $phrases_array),
4164 'similarity_threshold' => $similarity_threshold,
4165 'callback_function' => $callback_function,
4166 'enabled_bots' => json_encode($enabled_bots),
4167 );
4168 } else {
4169 $serialized_vector = maybe_serialize($embedding_vector);
4170 $update_data = array(
4171 'intent_label' => $intent_label,
4172 'phrases' => implode(', ', $phrases_array),
4173 'embedding_vector' => $serialized_vector,
4174 'similarity_threshold' => $similarity_threshold,
4175 'callback_function' => $callback_function,
4176 'enabled_bots' => json_encode($enabled_bots),
4177 );
4178 }
4179 }
4180
4181 // Ensure default bot is included
4182 if (!in_array('default', $enabled_bots)) {
4183 $enabled_bots[] = 'default';
4184 }
4185 $update_data['enabled_bots'] = json_encode($enabled_bots);
4186
4187 $result = $wpdb->update(
4188 $table_name,
4189 $update_data,
4190 array('id' => $intent_id),
4191 null,
4192 array('%d')
4193 );
4194
4195 if ($result === false) {
4196 wp_send_json_error(__('Failed to update action.', 'mxchat'));
4197 }
4198
4199 wp_send_json_success(array('updated' => true));
4200 }
4201
4202 /**
4203 * AJAX handler to delete a single intent/action
4204 */
4205 public function mxchat_delete_intent_ajax() {
4206 check_ajax_referer('mxchat_delete_intent_nonce', 'security');
4207
4208 if (!current_user_can('manage_options')) {
4209 wp_send_json_error(__('Unauthorized', 'mxchat'));
4210 }
4211
4212 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
4213
4214 if (!$intent_id) {
4215 wp_send_json_error(__('Invalid action ID', 'mxchat'));
4216 }
4217
4218 global $wpdb;
4219 $table_name = $wpdb->prefix . 'mxchat_intents';
4220
4221 $result = $wpdb->delete($table_name, array('id' => $intent_id), array('%d'));
4222
4223 if ($result === false) {
4224 wp_send_json_error(__('Failed to delete action', 'mxchat'));
4225 }
4226
4227 // Also delete individual phrases for this intent
4228 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
4229 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
4230 $wpdb->delete($phrases_table, array('intent_id' => $intent_id), array('%d'));
4231 }
4232
4233 wp_send_json_success(array('deleted' => true));
4234 }
4235
4236 /**
4237 * AJAX handler to add a single phrase with its own embedding to an intent
4238 */
4239 public function mxchat_add_phrase_ajax() {
4240 check_ajax_referer('mxchat_add_phrase_nonce', 'security');
4241
4242 if (!current_user_can('manage_options')) {
4243 wp_send_json_error(__('Unauthorized', 'mxchat'));
4244 }
4245
4246 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
4247 $phrase = isset($_POST['phrase']) ? sanitize_text_field($_POST['phrase']) : '';
4248
4249 if (!$intent_id || empty($phrase)) {
4250 wp_send_json_error(__('Please provide an intent ID and phrase.', 'mxchat'));
4251 }
4252
4253 // Verify the intent exists
4254 global $wpdb;
4255 $intents_table = $wpdb->prefix . 'mxchat_intents';
4256 $intent = $wpdb->get_row($wpdb->prepare("SELECT id FROM $intents_table WHERE id = %d", $intent_id));
4257 if (!$intent) {
4258 wp_send_json_error(__('Action not found.', 'mxchat'));
4259 }
4260
4261 // Generate embedding for this single phrase
4262 $embedding_vector = $this->mxchat_generate_embedding($phrase);
4263 if (is_wp_error($embedding_vector)) {
4264 wp_send_json_error(__('Failed to generate embedding: ', 'mxchat') . $embedding_vector->get_error_message());
4265 }
4266
4267 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
4268 $result = $wpdb->insert(
4269 $phrases_table,
4270 array(
4271 'intent_id' => $intent_id,
4272 'phrase' => $phrase,
4273 'embedding_vector' => maybe_serialize($embedding_vector),
4274 )
4275 );
4276
4277 if ($result === false) {
4278 wp_send_json_error(__('Failed to add phrase.', 'mxchat'));
4279 }
4280
4281 wp_send_json_success(array('id' => $wpdb->insert_id, 'phrase' => $phrase));
4282 }
4283
4284 /**
4285 * AJAX handler to delete a single phrase from wp_mxchat_intent_phrases
4286 */
4287 public function mxchat_delete_phrase_ajax() {
4288 check_ajax_referer('mxchat_delete_phrase_nonce', 'security');
4289
4290 if (!current_user_can('manage_options')) {
4291 wp_send_json_error(__('Unauthorized', 'mxchat'));
4292 }
4293
4294 $phrase_id = isset($_POST['phrase_id']) ? intval($_POST['phrase_id']) : 0;
4295 if (!$phrase_id) {
4296 wp_send_json_error(__('Invalid phrase ID.', 'mxchat'));
4297 }
4298
4299 global $wpdb;
4300 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
4301 $result = $wpdb->delete($phrases_table, array('id' => $phrase_id), array('%d'));
4302
4303 if ($result === false) {
4304 wp_send_json_error(__('Failed to delete phrase.', 'mxchat'));
4305 }
4306
4307 wp_send_json_success(array('deleted' => true));
4308 }
4309
4310 /**
4311 * AJAX handler to fetch individual phrases for an intent
4312 */
4313 public function mxchat_get_phrases_ajax() {
4314 check_ajax_referer('mxchat_get_phrases_nonce', 'security');
4315
4316 if (!current_user_can('manage_options')) {
4317 wp_send_json_error(__('Unauthorized', 'mxchat'));
4318 }
4319
4320 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
4321 if (!$intent_id) {
4322 wp_send_json_error(__('Invalid intent ID.', 'mxchat'));
4323 }
4324
4325 global $wpdb;
4326 $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
4327
4328 $phrases = array();
4329 if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
4330 $phrases = $wpdb->get_results($wpdb->prepare(
4331 "SELECT id, phrase, created_at FROM $phrases_table WHERE intent_id = %d ORDER BY created_at ASC",
4332 $intent_id
4333 ));
4334 }
4335
4336 wp_send_json_success(array('phrases' => $phrases));
4337 }
4338
4339 /**
4340 * AJAX handler to clear legacy phrases and embedding from the main intents table
4341 */
4342 public function mxchat_delete_legacy_phrases_ajax() {
4343 check_ajax_referer('mxchat_delete_legacy_nonce', 'security');
4344
4345 if (!current_user_can('manage_options')) {
4346 wp_send_json_error(__('Unauthorized', 'mxchat'));
4347 }
4348
4349 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
4350 if (!$intent_id) {
4351 wp_send_json_error(__('Invalid intent ID.', 'mxchat'));
4352 }
4353
4354 global $wpdb;
4355 $table_name = $wpdb->prefix . 'mxchat_intents';
4356
4357 $result = $wpdb->update(
4358 $table_name,
4359 array('phrases' => '', 'embedding_vector' => ''),
4360 array('id' => $intent_id),
4361 array('%s', '%s'),
4362 array('%d')
4363 );
4364
4365 if ($result === false) {
4366 wp_send_json_error(__('Failed to clear legacy phrases.', 'mxchat'));
4367 }
4368
4369 wp_send_json_success(array('cleared' => true));
4370 }
4371
4372 /**
4373 * Enhanced get_available_callbacks function with form action exclusion
4374 *
4375 * @param bool $grouped Whether to return callbacks grouped by category
4376 * @param bool $include_all Whether to include all potential actions (even if add-on not installed)
4377 * @return array Callbacks data with icons, descriptions and availability status
4378 */
4379 private function mxchat_get_available_callbacks($grouped = false, $include_all = true) {
4380 // Load WordPress plugin functions if needed
4381 if (!function_exists('get_plugins')) {
4382 require_once ABSPATH . 'wp-admin/includes/plugin.php';
4383 }
4384
4385 // Get active plugins
4386 $active_plugins = get_option('active_plugins', array());
4387
4388 // Functions to exclude from the action selector only if Pro is activated
4389 // If user doesn't have Pro, show these so they can see what they're missing
4390 $excluded_when_pro_active_functions = array(
4391 'mxchat_handle_form_collection' // Forms add-on action
4392 );
4393
4394 // Always excluded functions (regardless of Pro status)
4395 $always_excluded_functions = array();
4396
4397 // Combine exclusion lists based on Pro activation status
4398 $excluded_functions = $always_excluded_functions;
4399 if ($this->is_activated) {
4400 // Only exclude add-on managed functions if Pro is active
4401 $excluded_functions = array_merge($excluded_functions, $excluded_when_pro_active_functions);
4402 }
4403
4404 // Define add-on plugin files and their corresponding action functions
4405 $addon_plugins = array(
4406 'mxchat-woo/mxchat-woo.php' => array(
4407 'functions' => array(
4408 'mxchat_handle_product_recommendations',
4409 'mxchat_handle_order_history',
4410 'mxchat_show_product_card',
4411 'mxchat_add_to_cart',
4412 'mxchat_checkout_redirect',
4413 'mxchat_handle_featured_products'
4414 ),
4415 'name' => __('WooCommerce Add-on', 'mxchat'),
4416 'pro_required' => true
4417 ),
4418 'mxchat-perplexity/mxchat-perplexity.php' => array(
4419 'functions' => array('mxchat_perplexity_research'),
4420 'name' => __('Perplexity Add-on', 'mxchat'),
4421 'pro_required' => true
4422 ),
4423 'mxchat-forms/mxchat-forms.php' => array(
4424 'functions' => array('mxchat_handle_form_collection'),
4425 'name' => __('Forms Add-on', 'mxchat'),
4426 'pro_required' => true
4427 ),
4428 // Add other add-ons and their functions here
4429 );
4430
4431 // Get the functions that are provided by active add-ons
4432 $addon_provided_functions = array();
4433 $addon_function_mapping = array(); // Maps functions to their add-on info
4434
4435 // Check which add-ons are active
4436 foreach ($addon_plugins as $plugin_file => $addon_info) {
4437 $is_active = in_array($plugin_file, $active_plugins);
4438
4439 // For each function in this addon
4440 foreach ($addon_info['functions'] as $function) {
4441 // Consider a function installed only if:
4442 // 1. The add-on is active AND
4443 // 2. Either it doesn't require Pro OR Pro is activated
4444 $is_installed = $is_active && (!$addon_info['pro_required'] || $this->is_activated);
4445
4446 // If the add-on is installed, mark this function as provided by an add-on
4447 if ($is_installed) {
4448 $addon_provided_functions[] = $function;
4449 }
4450
4451 // Store addon info for this function regardless of installation status
4452 $addon_function_mapping[$function] = array(
4453 'addon' => basename(dirname($plugin_file)),
4454 'addon_name' => $addon_info['name'],
4455 'pro_required' => $addon_info['pro_required'],
4456 'is_active' => $is_active,
4457 'is_installed' => $is_installed
4458 );
4459 }
4460 }
4461
4462 // Core callbacks - always available in the base plugin
4463 $core_callbacks = array(
4464 'mxchat_handle_email_capture' => array(
4465 'label' => __('Loops Email Capture', 'mxchat'),
4466 'pro_only' => false,
4467 'group' => __('Customer Engagement', 'mxchat'),
4468 'icon' => 'email-alt',
4469 'description' => __('Collect visitor emails for your mailing list in Loops', 'mxchat'),
4470 'addon' => false, // Not from an add-on
4471 'installed' => true // Always installed with base plugin
4472 ),
4473 'mxchat_handle_search_request' => array(
4474 'label' => __('Brave Web Search', 'mxchat'),
4475 'pro_only' => false,
4476 'group' => __('Search Features', 'mxchat'),
4477 'icon' => 'search',
4478 'description' => __('Let users search the web directly from the chat', 'mxchat'),
4479 'addon' => false,
4480 'installed' => true
4481 ),
4482 'mxchat_handle_image_search_request' => array(
4483 'label' => __('Brave Image Search', 'mxchat'),
4484 'pro_only' => false,
4485 'group' => __('Search Features', 'mxchat'),
4486 'icon' => 'format-image',
4487 'description' => __('Search and display images in the chat conversation', 'mxchat'),
4488 'addon' => false,
4489 'installed' => true
4490 ),
4491 // Pro core features - check is_activated property
4492 'mxchat_generate_image' => array(
4493 'label' => __('Generate Image (OpenAI)', 'mxchat'),
4494 'pro_only' => false,
4495 'group' => __('Other Features', 'mxchat'),
4496 'icon' => 'art',
4497 'description' => __('Create images with GPT Image from OpenAI (requires OpenAI API key)', 'mxchat'),
4498 'addon' => false,
4499 'installed' => true
4500 ),
4501 'mxchat_generate_gemini_image' => array(
4502 'label' => __('Generate Image (Gemini)', 'mxchat'),
4503 'pro_only' => false,
4504 'group' => __('Other Features', 'mxchat'),
4505 'icon' => 'art',
4506 'description' => __('Create images with Imagen from Google (requires Gemini API key)', 'mxchat'),
4507 'addon' => false,
4508 'installed' => true
4509 ),
4510 'mxchat_handle_pdf_discussion' => array(
4511 'label' => __('Chat with PDF', 'mxchat'),
4512 'pro_only' => false,
4513 'group' => __('Other Features', 'mxchat'),
4514 'icon' => 'media-document',
4515 'description' => __('Answer questions about uploaded PDF documents', 'mxchat'),
4516 'addon' => false,
4517 'installed' => true
4518 ),
4519 'mxchat_live_agent_handover' => array(
4520 'label' => __('Slack Live Agent', 'mxchat'),
4521 'pro_only' => false,
4522 'group' => __('Customer Engagement', 'mxchat'),
4523 'icon' => 'admin-users',
4524 'description' => __('Transfer conversation to a human support agent on Slack', 'mxchat'),
4525 'addon' => false,
4526 'installed' => true
4527 ),
4528 'mxchat_telegram_live_agent_handover' => array(
4529 'label' => __('Telegram Live Agent', 'mxchat'),
4530 'pro_only' => false,
4531 'group' => __('Customer Engagement', 'mxchat'),
4532 'icon' => 'format-chat',
4533 'description' => __('Transfer conversation to a human support agent on Telegram', 'mxchat'),
4534 'addon' => false,
4535 'installed' => true
4536 ),
4537 'mxchat_handle_switch_to_chatbot_intent' => array(
4538 'label' => __('Back to Chatbot', 'mxchat'),
4539 'pro_only' => false,
4540 'group' => __('Customer Engagement', 'mxchat'),
4541 'icon' => 'backup',
4542 'description' => __('Return from live agent mode to AI chatbot', 'mxchat'),
4543 'addon' => false,
4544 'installed' => true
4545 ),
4546 );
4547
4548 // Add-on callbacks with placeholders - only include if the add-on is NOT active
4549 // These are promotional/informational only — not selectable as real actions
4550 $addon_callbacks = array(
4551 // WooCommerce Add-on
4552 'mxchat_handle_product_recommendations' => array(
4553 'label' => __('Product Recommendations', 'mxchat'),
4554 'pro_only' => false,
4555 'addon_promo' => true,
4556 'group' => __('WooCommerce Features', 'mxchat'),
4557 'icon' => 'cart',
4558 'description' => __('Suggest products based on customer preferences', 'mxchat'),
4559 ),
4560 'mxchat_handle_order_history' => array(
4561 'label' => __('Order History', 'mxchat'),
4562 'pro_only' => false,
4563 'addon_promo' => true,
4564 'group' => __('WooCommerce Features', 'mxchat'),
4565 'icon' => 'clipboard',
4566 'description' => __('Allow customers to check their order status', 'mxchat'),
4567 ),
4568 'mxchat_show_product_card' => array(
4569 'label' => __('Show Product Card', 'mxchat'),
4570 'pro_only' => false,
4571 'addon_promo' => true,
4572 'group' => __('WooCommerce Features', 'mxchat'),
4573 'icon' => 'products',
4574 'description' => __('Display product information in the chat', 'mxchat'),
4575 ),
4576 'mxchat_add_to_cart' => array(
4577 'label' => __('Add to Cart', 'mxchat'),
4578 'pro_only' => false,
4579 'addon_promo' => true,
4580 'group' => __('WooCommerce Features', 'mxchat'),
4581 'icon' => 'plus-alt',
4582 'description' => __('Add products to cart directly from chat', 'mxchat'),
4583 ),
4584 'mxchat_checkout_redirect' => array(
4585 'label' => __('Proceed to Checkout', 'mxchat'),
4586 'pro_only' => false,
4587 'addon_promo' => true,
4588 'group' => __('WooCommerce Features', 'mxchat'),
4589 'icon' => 'arrow-right-alt',
4590 'description' => __('Redirect customer to checkout page', 'mxchat'),
4591 ),
4592 'mxchat_handle_featured_products' => array(
4593 'label' => __('Featured Products Showcase', 'mxchat'),
4594 'pro_only' => false,
4595 'addon_promo' => true,
4596 'group' => __('WooCommerce Features', 'mxchat'),
4597 'icon' => 'star-filled',
4598 'description' => __('Display a curated selection of products with an AI-generated message', 'mxchat'),
4599 ),
4600
4601 // Perplexity Add-on
4602 'mxchat_perplexity_research' => array(
4603 'label' => __('Perplexity Research', 'mxchat'),
4604 'pro_only' => false,
4605 'addon_promo' => true,
4606 'group' => __('Search Features', 'mxchat'),
4607 'icon' => 'book-alt',
4608 'description' => __('Allows the chatbot to search the web for accurate, up-to-date answers', 'mxchat'),
4609 ),
4610
4611 // Forms Add-on
4612 'mxchat_handle_form_collection' => array(
4613 'label' => __('Form Collection', 'mxchat'),
4614 'pro_only' => false,
4615 'addon_promo' => true,
4616 'group' => __('Form Features', 'mxchat'),
4617 'icon' => 'feedback',
4618 'description' => __('Collect user information through custom forms in chat', 'mxchat'),
4619 ),
4620 );
4621
4622 // Enhance add-on callbacks with installation status and addon info
4623 foreach ($addon_callbacks as $function => $data) {
4624 if (isset($addon_function_mapping[$function])) {
4625 $addon_info = $addon_function_mapping[$function];
4626
4627 $addon_callbacks[$function]['addon'] = $addon_info['addon'];
4628 $addon_callbacks[$function]['addon_name'] = $addon_info['addon_name'];
4629 $addon_callbacks[$function]['installed'] = $addon_info['is_installed'];
4630
4631 // Set pro_only based on add-on configuration
4632 $addon_callbacks[$function]['pro_only'] = $addon_info['pro_required'];
4633 } else {
4634 $addon_callbacks[$function]['addon'] = 'unknown';
4635 $addon_callbacks[$function]['addon_name'] = __('Unknown Add-on', 'mxchat');
4636 $addon_callbacks[$function]['installed'] = false;
4637 }
4638 }
4639
4640 // Initialize callbacks with core features
4641 $callbacks = $core_callbacks;
4642
4643 // Get callbacks from active add-ons
4644 $active_addon_callbacks = apply_filters('mxchat_available_callbacks', array());
4645
4646 // Add placeholder callbacks only for add-ons that aren't active
4647 if ($include_all) {
4648 foreach ($addon_callbacks as $function => $data) {
4649 // Skip placeholders for functions provided by active add-ons
4650 if (in_array($function, $addon_provided_functions)) {
4651 continue;
4652 }
4653
4654 // Skip excluded functions
4655 if (in_array($function, $excluded_functions)) {
4656 continue;
4657 }
4658
4659 // Add the placeholder
4660 $callbacks[$function] = $data;
4661 }
4662 }
4663
4664 // Add callbacks from active add-ons (will override placeholders)
4665 foreach ($active_addon_callbacks as $function => $data) {
4666 // Skip excluded functions
4667 if (in_array($function, $excluded_functions)) {
4668 continue;
4669 }
4670
4671 // Always include callbacks from add-ons
4672 $callbacks[$function] = $data;
4673
4674 // Ensure they have the proper add-on info
4675 if (isset($addon_function_mapping[$function])) {
4676 $addon_info = $addon_function_mapping[$function];
4677 $callbacks[$function]['addon'] = $addon_info['addon'];
4678 $callbacks[$function]['addon_name'] = $addon_info['addon_name'];
4679 $callbacks[$function]['installed'] = $addon_info['is_installed'];
4680 $callbacks[$function]['pro_only'] = $addon_info['pro_required'];
4681 }
4682 }
4683
4684 // Just before returning callbacks, sort them to prioritize free features
4685 if (!$grouped) {
4686 // Create temporary arrays for sorting
4687 $free_callbacks = array();
4688 $pro_callbacks = array();
4689
4690 // Split callbacks into free and pro
4691 foreach ($callbacks as $key => $data) {
4692 if (isset($data['pro_only']) && $data['pro_only']) {
4693 $pro_callbacks[$key] = $data;
4694 } else {
4695 $free_callbacks[$key] = $data;
4696 }
4697 }
4698
4699 // Merge with free callbacks first
4700 $callbacks = array_merge($free_callbacks, $pro_callbacks);
4701 }
4702
4703 // Return grouped structure if requested
4704 if ($grouped) {
4705 $grouped_callbacks = array();
4706 foreach ($callbacks as $key => $data) {
4707 $group_label = isset($data['group']) ? $data['group'] : __('Other Features', 'mxchat');
4708
4709 // Ensure we carry forward all the new fields in grouped mode
4710 $callback_data = array(
4711 'label' => $data['label'],
4712 'pro_only' => isset($data['pro_only']) ? $data['pro_only'] : false,
4713 'icon' => isset($data['icon']) ? $data['icon'] : 'admin-generic',
4714 'description' => isset($data['description']) ? $data['description'] : __('Custom action for your chatbot', 'mxchat'),
4715 'addon' => isset($data['addon']) ? $data['addon'] : false,
4716 'addon_name' => isset($data['addon_name']) ? $data['addon_name'] : '',
4717 'installed' => isset($data['installed']) ? $data['installed'] : true
4718 );
4719
4720 $grouped_callbacks[$group_label][$key] = $callback_data;
4721 }
4722
4723 // Sort within each group to prioritize free features
4724 foreach ($grouped_callbacks as $group => $items) {
4725 $free_items = array();
4726 $pro_items = array();
4727
4728 foreach ($items as $key => $data) {
4729 if (isset($data['pro_only']) && $data['pro_only']) {
4730 $pro_items[$key] = $data;
4731 } else {
4732 $free_items[$key] = $data;
4733 }
4734 }
4735
4736 $grouped_callbacks[$group] = array_merge($free_items, $pro_items);
4737 }
4738
4739 return $grouped_callbacks;
4740 }
4741
4742 return $callbacks;
4743 }
4744
4745 public function mxchat_page_init() {
4746 register_setting(
4747 'mxchat_option_group',
4748 'mxchat_options',
4749 array($this, 'mxchat_sanitize')
4750 );
4751
4752 register_setting(
4753 'mxchat_option_group',
4754 'mxchat_similarity_threshold',
4755 array(
4756 'type' => 'number',
4757 'sanitize_callback' => function($value) {
4758 $value = absint($value);
4759 return min(max($value, 20), 95);
4760 },
4761 'default' => 80,
4762 )
4763 );
4764
4765 // Chatbot Settings Section
4766 add_settings_section(
4767 'mxchat_chatbot_section',
4768 esc_html__('Chatbot Settings', 'mxchat'),
4769 null,
4770 'mxchat-chatbot'
4771 );
4772
4773 // API Keys Settings Section
4774 add_settings_section(
4775 'mxchat_api_keys_section',
4776 esc_html__('API Keys', 'mxchat'),
4777 array($this, 'mxchat_api_keys_section_callback'),
4778 'mxchat-api-keys'
4779 );
4780
4781 // OpenAI API Key
4782 add_settings_field(
4783 'api_key',
4784 esc_html__('OpenAI API Key', 'mxchat'),
4785 array($this, 'api_key_callback'),
4786 'mxchat-api-keys',
4787 'mxchat_api_keys_section'
4788 );
4789
4790 // X.AI API Key
4791 add_settings_field(
4792 'xai_api_key',
4793 esc_html__('X.AI API Key', 'mxchat'),
4794 array($this, 'xai_api_key_callback'),
4795 'mxchat-api-keys',
4796 'mxchat_api_keys_section'
4797 );
4798
4799 // Claude API Key
4800 add_settings_field(
4801 'claude_api_key',
4802 esc_html__('Claude API Key', 'mxchat'),
4803 array($this, 'claude_api_key_callback'),
4804 'mxchat-api-keys',
4805 'mxchat_api_keys_section'
4806 );
4807
4808 // DeepSeek API Key
4809 add_settings_field(
4810 'deepseek_api_key',
4811 esc_html__('DeepSeek API Key', 'mxchat'),
4812 array($this, 'deepseek_api_key_callback'),
4813 'mxchat-api-keys',
4814 'mxchat_api_keys_section'
4815 );
4816
4817 // Google Gemini API Key
4818 add_settings_field(
4819 'gemini_api_key',
4820 esc_html__('Google Gemini API Key', 'mxchat'),
4821 array($this, 'gemini_api_key_callback'),
4822 'mxchat-api-keys',
4823 'mxchat_api_keys_section'
4824 );
4825
4826 // Voyage AI API Key
4827 add_settings_field(
4828 'voyage_api_key',
4829 esc_html__('Voyage AI API Key', 'mxchat'),
4830 array($this, 'voyage_api_key_callback'),
4831 'mxchat-api-keys',
4832 'mxchat_api_keys_section'
4833 );
4834
4835 // OpenRouter API Key
4836 add_settings_field(
4837 'openrouter_api_key',
4838 esc_html__('OpenRouter API Key', 'mxchat'),
4839 array($this, 'openrouter_api_key_callback'),
4840 'mxchat-api-keys',
4841 'mxchat_api_keys_section'
4842 );
4843
4844 // Loops API Key
4845 add_settings_field(
4846 'loops_api_key',
4847 esc_html__('Loops API Key', 'mxchat'),
4848 array($this, 'mxchat_loops_api_key_callback'),
4849 'mxchat-api-keys',
4850 'mxchat_api_keys_section'
4851 );
4852
4853 // Brave Search API Key
4854 add_settings_field(
4855 'brave_api_key',
4856 __('Brave API Key', 'mxchat'),
4857 array($this, 'mxchat_brave_api_key_callback'),
4858 'mxchat-api-keys',
4859 'mxchat_api_keys_section'
4860 );
4861
4862 // Similarity Threshold Slider
4863 add_settings_field(
4864 'similarity_threshold', // Field ID
4865 esc_html__('Similarity Threshold', 'mxchat'), // Field title
4866 array($this, 'mxchat_similarity_threshold_callback'), // Callback function
4867 'mxchat-chatbot', // Page
4868 'mxchat_chatbot_section' // Section
4869 );
4870
4871 // RAG Sources Limit Slider
4872 add_settings_field(
4873 'rag_sources_limit', // Field ID
4874 esc_html__('RAG Sources Limit', 'mxchat'), // Field title
4875 array($this, 'mxchat_rag_sources_limit_callback'), // Callback function
4876 'mxchat-chatbot', // Page
4877 'mxchat_chatbot_section' // Section
4878 );
4879
4880 // RAG Chunks Limit Slider
4881 add_settings_field(
4882 'rag_chunks_limit', // Field ID
4883 esc_html__('RAG Chunks Limit', 'mxchat'), // Field title
4884 array($this, 'mxchat_rag_chunks_limit_callback'), // Callback function
4885 'mxchat-chatbot', // Page
4886 'mxchat_chatbot_section' // Section
4887 );
4888
4889 add_settings_field(
4890 'append_to_body',
4891 esc_html__('Auto-Display Chatbot', 'mxchat'),
4892 array($this, 'mxchat_append_to_body_callback'),
4893 'mxchat-chatbot',
4894 'mxchat_chatbot_section'
4895 );
4896
4897 add_settings_field(
4898 'contextual_awareness_toggle',
4899 esc_html__('Contextual Awareness', 'mxchat'),
4900 array($this, 'mxchat_contextual_awareness_callback'),
4901 'mxchat-chatbot',
4902 'mxchat_chatbot_section'
4903 );
4904
4905 add_settings_field(
4906 'citation_links_toggle',
4907 esc_html__('Citation Links', 'mxchat'),
4908 array($this, 'mxchat_citation_links_toggle_callback'),
4909 'mxchat-chatbot',
4910 'mxchat_chatbot_section'
4911 );
4912
4913 add_settings_field(
4914 'enable_streaming_toggle',
4915 esc_html__('Enable Streaming', 'mxchat'),
4916 array($this, 'enable_streaming_toggle_callback'),
4917 'mxchat-chatbot',
4918 'mxchat_chatbot_section', // Same section as your working toggle
4919 array(
4920 'class' => 'mxchat-setting-row streaming-setting',
4921 'style' => 'display: none;' // Hidden by default, shown when OpenAI/Claude selected
4922 )
4923 );
4924
4925
4926 add_settings_field(
4927 'model',
4928 esc_html__('Chat Model', 'mxchat'),
4929 array($this, 'mxchat_model_callback'),
4930 'mxchat-chatbot',
4931 'mxchat_chatbot_section'
4932 );
4933
4934 add_settings_field(
4935 'embedding_model',
4936 esc_html__('Embedding Model', 'mxchat'),
4937 array($this, 'embedding_model_callback'),
4938 'mxchat-chatbot',
4939 'mxchat_chatbot_section'
4940 );
4941
4942 add_settings_field(
4943 'system_prompt_instructions',
4944 esc_html__('AI Instructions (Behavior)', 'mxchat'),
4945 array($this, 'system_prompt_instructions_callback'),
4946 'mxchat-chatbot',
4947 'mxchat_chatbot_section'
4948 );
4949
4950
4951 add_settings_field(
4952 'top_bar_title',
4953 esc_html__('Top Bar Title', 'mxchat'),
4954 array($this, 'mxchat_top_bar_title_callback'),
4955 'mxchat-chatbot',
4956 'mxchat_chatbot_section'
4957 );
4958
4959 add_settings_field(
4960 'ai_agent_text',
4961 esc_html__('AI Agent Text', 'mxchat'),
4962 array($this, 'mxchat_ai_agent_text_callback'),
4963 'mxchat-chatbot',
4964 'mxchat_chatbot_section'
4965 );
4966
4967 add_settings_field(
4968 'enable_email_block',
4969 esc_html__('Require Email To Chat', 'mxchat'),
4970 array($this, 'enable_email_block_callback'),
4971 'mxchat-chatbot',
4972 'mxchat_chatbot_section'
4973 );
4974
4975 add_settings_field(
4976 'email_blocker_header_content',
4977 esc_html__('Require Email Chat Content', 'mxchat'),
4978 array($this, 'email_blocker_header_content_callback'),
4979 'mxchat-chatbot',
4980 'mxchat_chatbot_section'
4981 );
4982
4983 add_settings_field(
4984 'email_blocker_button_text',
4985 esc_html__('Require Email Chat Button Text', 'mxchat'),
4986 [$this, 'email_blocker_button_text_callback'],
4987 'mxchat-chatbot',
4988 'mxchat_chatbot_section'
4989 );
4990
4991 add_settings_field(
4992 'enable_name_field',
4993 esc_html__('Require Name Field', 'mxchat'),
4994 array($this, 'enable_name_field_callback'),
4995 'mxchat-chatbot',
4996 'mxchat_chatbot_section'
4997 );
4998
4999 add_settings_field(
5000 'name_field_placeholder',
5001 esc_html__('Name Field Placeholder', 'mxchat'),
5002 array($this, 'name_field_placeholder_callback'),
5003 'mxchat-chatbot',
5004 'mxchat_chatbot_section'
5005 );
5006
5007 add_settings_field(
5008 'intro_message',
5009 esc_html__('Introductory Message', 'mxchat'),
5010 array($this, 'mxchat_intro_message_callback'),
5011 'mxchat-chatbot',
5012 'mxchat_chatbot_section'
5013 );
5014
5015 add_settings_field(
5016 'input_copy',
5017 esc_html__('Input Copy', 'mxchat'),
5018 array($this, 'mxchat_input_copy_callback'),
5019 'mxchat-chatbot',
5020 'mxchat_chatbot_section'
5021 );
5022
5023 add_settings_field(
5024 'pre_chat_message',
5025 esc_html__('Chat Teaser Pop-up', 'mxchat'),
5026 array($this, 'mxchat_pre_chat_message_callback'),
5027 'mxchat-chatbot',
5028 'mxchat_chatbot_section'
5029 );
5030
5031 add_settings_field(
5032 'privacy_toggle',
5033 esc_html__('Toggle Privacy Notice', 'mxchat'),
5034 array($this, 'mxchat_privacy_toggle_callback'),
5035 'mxchat-chatbot',
5036 'mxchat_chatbot_section'
5037 );
5038
5039 add_settings_field(
5040 'complianz_toggle',
5041 esc_html__('Enable Complianz', 'mxchat'),
5042 array($this, 'mxchat_complianz_toggle_callback'),
5043 'mxchat-chatbot',
5044 'mxchat_chatbot_section'
5045 );
5046
5047 add_settings_field(
5048 'link_target_toggle',
5049 esc_html__('Open Links in a New Tab', 'mxchat'),
5050 array($this, 'mxchat_link_target_toggle_callback'),
5051 'mxchat-chatbot',
5052 'mxchat_chatbot_section'
5053 );
5054
5055 add_settings_field(
5056 'chat_persistence_toggle',
5057 esc_html__('Enable Chat Persistence', 'mxchat'),
5058 array($this, 'mxchat_chat_persistence_toggle_callback'),
5059 'mxchat-chatbot',
5060 'mxchat_chatbot_section'
5061 );
5062
5063 add_settings_field(
5064 'popular_question_1',
5065 esc_html__('Quick Question 1', 'mxchat'),
5066 array($this, 'mxchat_popular_question_1_callback'),
5067 'mxchat-chatbot',
5068 'mxchat_chatbot_section'
5069 );
5070
5071 add_settings_field(
5072 'popular_question_2',
5073 esc_html__('Quick Question 2', 'mxchat'),
5074 array($this, 'mxchat_popular_question_2_callback'),
5075 'mxchat-chatbot',
5076 'mxchat_chatbot_section'
5077 );
5078
5079 add_settings_field(
5080 'popular_question_3',
5081 esc_html__('Quick Question 3', 'mxchat'),
5082 array($this, 'mxchat_popular_question_3_callback'),
5083 'mxchat-chatbot',
5084 'mxchat_chatbot_section'
5085 );
5086
5087 add_settings_field(
5088 'additional_popular_questions',
5089 esc_html__('Additional Quick Questions', 'mxchat'),
5090 array($this, 'mxchat_additional_popular_questions_callback'),
5091 'mxchat-chatbot',
5092 'mxchat_chatbot_section'
5093 );
5094
5095
5096 add_settings_field(
5097 'rate_limits',
5098 __('Rate Limits Settings', 'mxchat'),
5099 array($this, 'mxchat_rate_limits_callback'),
5100 'mxchat-chatbot',
5101 'mxchat_chatbot_section'
5102 );
5103
5104 // Loops Settings Section
5105 add_settings_section(
5106 'mxchat_loops_section',
5107 esc_html__('Loops Settings', 'mxchat'),
5108 null,
5109 'mxchat-embed'
5110 );
5111
5112 // Loops Settings Fields (API Key moved to API Keys tab)
5113 add_settings_field(
5114 'loops_mailing_list',
5115 esc_html__('Loops Mailing List', 'mxchat'),
5116 array($this, 'mxchat_loops_mailing_list_callback'),
5117 'mxchat-embed',
5118 'mxchat_loops_section'
5119 );
5120
5121 add_settings_field(
5122 'triggered_phrase_response',
5123 esc_html__('Triggered Phrase Response', 'mxchat'),
5124 array($this, 'mxchat_triggered_phrase_response_callback'),
5125 'mxchat-embed',
5126 'mxchat_loops_section'
5127 );
5128
5129 add_settings_field(
5130 'email_capture_response',
5131 esc_html__('Email Capture Response', 'mxchat'),
5132 array($this, 'mxchat_email_capture_response_callback'),
5133 'mxchat-embed',
5134 'mxchat_loops_section'
5135 );
5136
5137 // Brave Search Settings Fields
5138 add_settings_section(
5139 'mxchat_brave_section',
5140 __('Brave Search Settings', 'mxchat'),
5141 array($this, 'mxchat_brave_section_callback'),
5142 'mxchat-embed'
5143 );
5144
5145 // Brave API Key moved to API Keys tab
5146 add_settings_field(
5147 'brave_image_count',
5148 __('Number of Images to Return', 'mxchat'),
5149 array($this, 'mxchat_brave_image_count_callback'),
5150 'mxchat-embed',
5151 'mxchat_brave_section'
5152 );
5153
5154 add_settings_field(
5155 'brave_safe_search',
5156 __('Safe Search', 'mxchat'),
5157 array($this, 'mxchat_brave_safe_search_callback'),
5158 'mxchat-embed',
5159 'mxchat_brave_section'
5160 );
5161
5162 add_settings_field(
5163 'brave_news_count',
5164 __('Number of News Articles', 'mxchat'),
5165 array($this, 'mxchat_brave_news_count_callback'),
5166 'mxchat-embed',
5167 'mxchat_brave_section'
5168 );
5169
5170 add_settings_field(
5171 'brave_country',
5172 __('Country', 'mxchat'),
5173 array($this, 'mxchat_brave_country_callback'),
5174 'mxchat-embed',
5175 'mxchat_brave_section'
5176 );
5177
5178 add_settings_field(
5179 'brave_language',
5180 __('Language', 'mxchat'),
5181 array($this, 'mxchat_brave_language_callback'),
5182 'mxchat-embed',
5183 'mxchat_brave_section'
5184 );
5185
5186 // Chat with PDF Intent Settings Fields
5187 add_settings_section(
5188 'mxchat_pdf_intent_section',
5189 __('Toolbar Settings & Intents', 'mxchat'),
5190 array($this, 'mxchat_pdf_intent_section_callback'),
5191 'mxchat-embed'
5192 );
5193
5194 add_settings_field(
5195 'chat_toolbar_toggle',
5196 __('Show Chat Toolbar', 'mxchat'),
5197 array($this, 'mxchat_chat_toolbar_toggle_callback'),
5198 'mxchat-embed',
5199 'mxchat_pdf_intent_section'
5200 );
5201
5202 // PDF Upload Button Toggle
5203 add_settings_field(
5204 'show_pdf_upload_button',
5205 __('Show PDF Upload Button', 'mxchat'),
5206 array($this, 'mxchat_show_pdf_upload_button_callback'),
5207 'mxchat-embed',
5208 'mxchat_pdf_intent_section'
5209 );
5210
5211 // Word Upload Button Toggle
5212 add_settings_field(
5213 'show_word_upload_button',
5214 __('Show Word Upload Button', 'mxchat'),
5215 array($this, 'mxchat_show_word_upload_button_callback'),
5216 'mxchat-embed',
5217 'mxchat_pdf_intent_section'
5218 );
5219
5220 add_settings_field(
5221 'pdf_intent_trigger_text',
5222 __('Intent Trigger Text', 'mxchat'),
5223 array($this, 'mxchat_pdf_intent_trigger_text_callback'),
5224 'mxchat-embed',
5225 'mxchat_pdf_intent_section'
5226 );
5227
5228 add_settings_field(
5229 'pdf_intent_success_text',
5230 __('Success Text', 'mxchat'),
5231 array($this, 'mxchat_pdf_intent_success_text_callback'),
5232 'mxchat-embed',
5233 'mxchat_pdf_intent_section'
5234 );
5235
5236 add_settings_field(
5237 'pdf_intent_error_text',
5238 __('Error Text', 'mxchat'),
5239 array($this, 'mxchat_pdf_intent_error_text_callback'),
5240 'mxchat-embed',
5241 'mxchat_pdf_intent_section'
5242 );
5243
5244 // Add PDF Maximum Pages Field
5245 add_settings_field(
5246 'pdf_max_pages',
5247 __('Maximum Document Pages', 'mxchat'),
5248 array($this, 'mxchat_pdf_max_pages_callback'),
5249 'mxchat-embed',
5250 'mxchat_pdf_intent_section'
5251 );
5252
5253 // Live Agent Settings Fields
5254 add_settings_section(
5255 'mxchat_live_agent_section',
5256 __('Live Agent Settings', 'mxchat'),
5257 array($this, 'mxchat_live_agent_section_callback'),
5258 'mxchat-embed'
5259 );
5260
5261 // Live Agent Status Fields (add at top of live agent settings)
5262 add_settings_field(
5263 'live_agent_status',
5264 __('Live Agent Status', 'mxchat'),
5265 array($this, 'mxchat_live_agent_status_callback'),
5266 'mxchat-embed',
5267 'mxchat_live_agent_section'
5268 );
5269
5270 add_settings_field(
5271 'live_agent_notification_message',
5272 __('Notification Message', 'mxchat'),
5273 array($this, 'mxchat_live_agent_notification_message_callback'),
5274 'mxchat-embed',
5275 'mxchat_live_agent_section'
5276 );
5277
5278 add_settings_field(
5279 'live_agent_away_message',
5280 __('Away Message', 'mxchat'),
5281 array($this, 'mxchat_live_agent_away_message_callback'),
5282 'mxchat-embed',
5283 'mxchat_live_agent_section'
5284 );
5285
5286 add_settings_field(
5287 'live_agent_user_ids',
5288 __('Slack Agent User IDs', 'mxchat'),
5289 array($this, 'mxchat_live_agent_user_ids_callback'),
5290 'mxchat-embed',
5291 'mxchat_live_agent_section'
5292 );
5293
5294 add_settings_field(
5295 'live_agent_webhook_url',
5296 __('Slack Webhook URL', 'mxchat'),
5297 array($this, 'mxchat_live_agent_webhook_url_callback'),
5298 'mxchat-embed',
5299 'mxchat_live_agent_section'
5300 );
5301
5302 add_settings_field(
5303 'live_agent_secret_key',
5304 __('Slack Secret Key', 'mxchat'),
5305 array($this, 'mxchat_live_agent_secret_key_callback'),
5306 'mxchat-embed',
5307 'mxchat_live_agent_section'
5308 );
5309
5310 // Live Agent Integration Fields
5311 add_settings_field(
5312 'live_agent_bot_token',
5313 __('Slack Bot OAuth Token', 'mxchat'),
5314 array($this, 'mxchat_live_agent_bot_token_callback'),
5315 'mxchat-embed',
5316 'mxchat_live_agent_section'
5317 );
5318
5319 // Telegram Integration Section
5320 add_settings_section(
5321 'mxchat_telegram_section',
5322 __('Telegram Settings', 'mxchat'),
5323 array($this, 'mxchat_telegram_section_callback'),
5324 'mxchat-embed'
5325 );
5326
5327 add_settings_field(
5328 'telegram_status',
5329 __('Live Agent Status', 'mxchat'),
5330 array($this, 'mxchat_telegram_status_callback'),
5331 'mxchat-embed',
5332 'mxchat_telegram_section'
5333 );
5334
5335 add_settings_field(
5336 'telegram_notification_message',
5337 __('Notification Message', 'mxchat'),
5338 array($this, 'mxchat_telegram_notification_message_callback'),
5339 'mxchat-embed',
5340 'mxchat_telegram_section'
5341 );
5342
5343 add_settings_field(
5344 'telegram_away_message',
5345 __('Away Message', 'mxchat'),
5346 array($this, 'mxchat_telegram_away_message_callback'),
5347 'mxchat-embed',
5348 'mxchat_telegram_section'
5349 );
5350
5351 add_settings_field(
5352 'telegram_bot_token',
5353 __('Telegram Bot Token', 'mxchat'),
5354 array($this, 'mxchat_telegram_bot_token_callback'),
5355 'mxchat-embed',
5356 'mxchat_telegram_section'
5357 );
5358
5359 add_settings_field(
5360 'telegram_group_id',
5361 __('Telegram Group ID', 'mxchat'),
5362 array($this, 'mxchat_telegram_group_id_callback'),
5363 'mxchat-embed',
5364 'mxchat_telegram_section'
5365 );
5366
5367 add_settings_field(
5368 'telegram_webhook_secret',
5369 __('Webhook Secret Token', 'mxchat'),
5370 array($this, 'mxchat_telegram_webhook_secret_callback'),
5371 'mxchat-embed',
5372 'mxchat_telegram_section'
5373 );
5374
5375 // General Settings Section
5376 add_settings_section(
5377 'mxchat_general_section',
5378 esc_html__('YouTube Tutorials', 'mxchat'),
5379 null,
5380 'mxchat-general'
5381 );
5382 }
5383
5384 public function mxchat_prompts_page_init() {
5385 register_setting(
5386 'mxchat_prompts_options',
5387 'mxchat_prompts_options',
5388 array(
5389 'type' => 'array',
5390 'description' => __('MXChat Knowledge Base Settings', 'mxchat'),
5391 'default' => array(
5392 'mxchat_auto_sync_posts' => 0,
5393 'mxchat_auto_sync_pages' => 0,
5394 'mxchat_use_pinecone' => 0,
5395 'mxchat_pinecone_api_key' => '',
5396 'mxchat_pinecone_environment' => '',
5397 'mxchat_pinecone_index' => '',
5398 'mxchat_pinecone_host' => '',
5399 ),
5400 'sanitize_callback' => array($this, 'sanitize_prompts_options'),
5401 )
5402 );
5403
5404 add_action('admin_notices', array($this, 'sync_settings_notice'));
5405 }
5406
5407 public function mxchat_transcripts_page_init() {
5408 register_setting(
5409 'mxchat_transcripts_options',
5410 'mxchat_transcripts_options',
5411 array(
5412 'type' => 'array',
5413 'description' => __('MXChat Transcripts Notification Settings', 'mxchat'),
5414 'default' => array(
5415 'mxchat_enable_notifications' => 0,
5416 'mxchat_notification_email' => get_option('admin_email'),
5417 'mxchat_auto_delete_transcripts' => 'never',
5418 ),
5419 'sanitize_callback' => array($this, 'sanitize_transcripts_options'),
5420 )
5421 );
5422
5423 add_settings_section(
5424 'mxchat_transcripts_notification_section',
5425 esc_html__('Chat Notification Settings', 'mxchat'),
5426 array($this, 'mxchat_transcripts_notification_section_callback'),
5427 'mxchat-transcripts'
5428 );
5429
5430 add_settings_field(
5431 'mxchat_enable_notifications',
5432 esc_html__('Enable Chat Notifications', 'mxchat'),
5433 array($this, 'mxchat_enable_notifications_callback'),
5434 'mxchat-transcripts',
5435 'mxchat_transcripts_notification_section'
5436 );
5437
5438 add_settings_field(
5439 'mxchat_notification_email',
5440 esc_html__('Notification Email Address', 'mxchat'),
5441 array($this, 'mxchat_notification_email_callback'),
5442 'mxchat-transcripts',
5443 'mxchat_transcripts_notification_section'
5444 );
5445
5446 add_settings_field(
5447 'mxchat_auto_delete_transcripts',
5448 esc_html__('Auto-Delete Old Transcripts', 'mxchat'),
5449 array($this, 'mxchat_auto_delete_transcripts_callback'),
5450 'mxchat-transcripts',
5451 'mxchat_transcripts_notification_section'
5452 );
5453
5454 add_settings_field(
5455 'mxchat_auto_email_transcript',
5456 esc_html__('Auto-Email Full Transcript', 'mxchat'),
5457 array($this, 'mxchat_auto_email_transcript_callback'),
5458 'mxchat-transcripts',
5459 'mxchat_transcripts_notification_section'
5460 );
5461 }
5462
5463
5464 /**
5465 * Sanitize all prompts options
5466 *
5467 * @param array $input The unsanitized options array
5468 * @return array The sanitized options array
5469 */
5470 public function sanitize_prompts_options($input) {
5471 // Log the incoming input.
5472 //error_log('Sanitizing inputs: ' . print_r($input, true));
5473
5474 $sanitized = array();
5475
5476 // Boolean options
5477 $sanitized['mxchat_auto_sync_posts'] = isset($input['mxchat_auto_sync_posts']) ? 1 : 0;
5478 $sanitized['mxchat_auto_sync_pages'] = isset($input['mxchat_auto_sync_pages']) ? 1 : 0;
5479 $sanitized['mxchat_use_pinecone'] = !empty($input['mxchat_use_pinecone']) ? 1 : 0;
5480
5481 // API Key: if less than 32 characters, flag as invalid.
5482 $api_key = sanitize_text_field($input['mxchat_pinecone_api_key'] ?? '');
5483 if (!empty($api_key) && strlen($api_key) < 32) {
5484 add_settings_error(
5485 'mxchat_prompts_options',
5486 'invalid_api_key',
5487 __('The Pinecone API key appears to be invalid. Please check your API key.', 'mxchat')
5488 );
5489 $existing_options = get_option('mxchat_prompts_options', array());
5490 $sanitized['mxchat_pinecone_api_key'] = $existing_options['mxchat_pinecone_api_key'] ?? '';
5491 } else {
5492 $sanitized['mxchat_pinecone_api_key'] = $api_key;
5493 }
5494
5495 // Environment and Index Name
5496 $sanitized['mxchat_pinecone_environment'] = sanitize_text_field($input['mxchat_pinecone_environment'] ?? '');
5497 $sanitized['mxchat_pinecone_index'] = sanitize_text_field($input['mxchat_pinecone_index'] ?? '');
5498
5499 // Host: Remove protocol and validate format.
5500 $host = sanitize_text_field($input['mxchat_pinecone_host'] ?? '');
5501 $host = preg_replace('#^https?://#', '', $host);
5502 //error_log('Host after removing protocol: ' . $host);
5503 if (!empty($host)) {
5504 if (!preg_match('/^[\w-]+\.svc\.[\w-]+\.pinecone\.io$/', $host)) {
5505 add_settings_error(
5506 'mxchat_prompts_options',
5507 'invalid_host',
5508 __('The Pinecone host appears to be invalid. It should look like "mxchat-vectors-zrmsquq.svc.aped-4627-b74a.pinecone.io"', 'mxchat')
5509 );
5510 $existing_options = get_option('mxchat_prompts_options', array());
5511 $sanitized['mxchat_pinecone_host'] = $existing_options['mxchat_pinecone_host'] ?? '';
5512 } else {
5513 $sanitized['mxchat_pinecone_host'] = $host;
5514 }
5515 } else {
5516 $sanitized['mxchat_pinecone_host'] = '';
5517 }
5518
5519 //error_log('Final sanitized array: ' . print_r($sanitized, true));
5520
5521 return $sanitized;
5522 }
5523
5524 public function sync_settings_notice() {
5525 // Only show notice on our plugin page
5526 if (!isset($_GET['page']) || $_GET['page'] !== 'mxchat-prompts') {
5527 return;
5528 }
5529
5530 // Check if settings were updated
5531 if (isset($_GET['settings-updated'])) {
5532
5533 ?>
5534 <div class="notice notice-success is-dismissible">
5535 <p><?php esc_html_e('Sync settings updated successfully.', 'mxchat'); ?></p>
5536 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php esc_html_e('Dismiss this notice.', 'mxchat'); ?></span></button>
5537 </div>
5538 <?php
5539
5540 }
5541 }
5542 // Add this sanitization function to your class
5543 public function sanitize_sync_setting($input) {
5544 return (bool)$input ? __('1', 'mxchat') : __('', 'mxchat');
5545 }
5546
5547 public function mxchat_rate_limits_callback() {
5548 $all_options = get_option('mxchat_options', []);
5549
5550 // Define available rate limits
5551 $rate_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited');
5552
5553 // Define available timeframes
5554 $timeframes = array(
5555 'hourly' => __('Per Hour', 'mxchat'),
5556 'daily' => __('Per Day', 'mxchat'),
5557 'weekly' => __('Per Week', 'mxchat'),
5558 'monthly' => __('Per Month', 'mxchat')
5559 );
5560
5561 // Get all roles plus a "logged_out" pseudo-role
5562 $roles = wp_roles()->get_names();
5563 $roles['logged_out'] = __('Logged Out Users', 'mxchat');
5564
5565 // Start the wrapper
5566 echo '<div class="pro-feature-wrapper active">';
5567 echo '<div class="mxchat-rate-limits-container">';
5568
5569 echo '<p class="description" style="margin-bottom: 20px;">' .
5570 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') .
5571 '</p>';
5572
5573 // Add markdown link documentation
5574 echo '<div class="notice notice-info inline" style="margin-bottom: 20px; padding: 10px;">';
5575 echo '<p><strong>' . esc_html__('Markdown Links Supported:', 'mxchat') . '</strong></p>';
5576 echo '<p>' . esc_html__('You can include clickable links in your custom messages using markdown syntax:', 'mxchat') . '</p>';
5577 echo '<ul style="margin-left: 20px;">';
5578 echo '<li><code>[Link text](https://example.com)</code> - Creates a clickable link</li>';
5579 echo '<li><code>[Visit our pricing](https://example.com/pricing)</code> - Link with custom text</li>';
5580 echo '<li><code>Plain URLs like https://example.com will also become clickable</code></li>';
5581 echo '</ul>';
5582 echo '</div>';
5583
5584 // Output the controls for each role
5585 foreach ($roles as $role_id => $role_name) {
5586 // Get saved options or defaults
5587 $default_limit = ($role_id === 'logged_out') ? '10' : '100';
5588 $default_timeframe = 'daily';
5589 $default_message = __('Rate limit exceeded. Please try again later.', 'mxchat');
5590
5591 $selected_limit = isset($all_options['rate_limits'][$role_id]['limit'])
5592 ? $all_options['rate_limits'][$role_id]['limit']
5593 : $default_limit;
5594
5595 $selected_timeframe = isset($all_options['rate_limits'][$role_id]['timeframe'])
5596 ? $all_options['rate_limits'][$role_id]['timeframe']
5597 : $default_timeframe;
5598
5599 $custom_message = isset($all_options['rate_limits'][$role_id]['message'])
5600 ? $all_options['rate_limits'][$role_id]['message']
5601 : $default_message;
5602
5603 // Output the row
5604 echo '<div class="mxchat-rate-limit-row mxchat-autosave-section">';
5605
5606 // Role label
5607 echo '<div class="mxchat-rate-limit-role">' . esc_html($role_name) . '</div>';
5608
5609 // Controls section
5610 echo '<div class="mxchat-rate-limit-controls-wrapper">';
5611
5612 // Rate limit and timeframe controls
5613 echo '<div class="mxchat-rate-limit-controls">';
5614
5615 // Limit dropdown
5616 echo '<div>';
5617 echo '<label for="rate_limits_' . esc_attr($role_id) . '_limit">' . esc_html__('Limit:', 'mxchat') . '</label>';
5618 echo '<select
5619 id="rate_limits_' . esc_attr($role_id) . '_limit"
5620 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][limit]"
5621 class="mxchat-autosave-field">';
5622 foreach ($rate_limits as $limit) {
5623 echo '<option value="' . esc_attr($limit) . '" ' . selected($selected_limit, $limit, false) . '>' . esc_html($limit) . '</option>';
5624 }
5625 echo '</select>';
5626 echo '</div>';
5627
5628 // Timeframe dropdown
5629 echo '<div>';
5630 echo '<label for="rate_limits_' . esc_attr($role_id) . '_timeframe">' . esc_html__('Timeframe:', 'mxchat') . '</label>';
5631 echo '<select
5632 id="rate_limits_' . esc_attr($role_id) . '_timeframe"
5633 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][timeframe]"
5634 class="mxchat-autosave-field">';
5635 foreach ($timeframes as $value => $label) {
5636 echo '<option value="' . esc_attr($value) . '" ' . selected($selected_timeframe, $value, false) . '>' . esc_html($label) . '</option>';
5637 }
5638 echo '</select>';
5639 echo '</div>';
5640
5641 echo '</div>'; // End controls
5642
5643 // Custom message textarea
5644 echo '<div class="mxchat-rate-limit-message">';
5645 echo '<label for="rate_limits_' . esc_attr($role_id) . '_message">' . esc_html__('Custom Message:', 'mxchat') . '</label>';
5646 echo '<textarea
5647 id="rate_limits_' . esc_attr($role_id) . '_message"
5648 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][message]"
5649 class="mxchat-autosave-field"
5650 placeholder="' . esc_attr__('Enter custom message when rate limit is exceeded', 'mxchat') . '">' .
5651 esc_textarea($custom_message) .
5652 '</textarea>';
5653 echo '<p class="description">' .
5654 esc_html__('Example: Rate limit reached! [Visit our pricing page](https://example.com/pricing) to upgrade.', 'mxchat') .
5655 '</p>';
5656 echo '</div>'; // End message
5657
5658 echo '</div>'; // End controls wrapper
5659
5660 echo '</div>'; // End row
5661 }
5662
5663 echo '</div>'; // End container
5664
5665 echo '</div>'; // End pro-feature-wrapper
5666 }
5667
5668 private function mxchat_add_option_field($id, $title, $callback = '') {
5669 add_settings_field(
5670 $id,
5671 __($title, 'mxchat'),
5672 $callback ? array($this, $callback) : array($this, $id . '_callback'),
5673 'mxchat-max',
5674 'mxchat_setting_section_id',
5675 $id === 'model' ? ['label_for' => 'model'] : []
5676 );
5677 }
5678
5679 // API Keys Section Callback
5680 public function mxchat_api_keys_section_callback() {
5681 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>';
5682 }
5683
5684 // OpenAI API Key
5685 public function api_key_callback() {
5686 $apiKey = isset($this->options['api_key']) ? esc_attr($this->options['api_key']) : '';
5687 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5688
5689 echo '<div class="api-key-wrapper">';
5690 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 . '" />';
5691 echo '<button type="button" id="toggleApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
5692 echo '<p class="description">' . esc_html__('Required for OpenAI GPT models and OpenAI embeddings. Get your API key from OpenAI Platform.', 'mxchat') . '</p>';
5693 echo '</div>';
5694 }
5695
5696 // X.AI API Key
5697 public function xai_api_key_callback() {
5698 $xaiApiKey = isset($this->options['xai_api_key']) ? esc_attr($this->options['xai_api_key']) : '';
5699 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5700
5701 echo '<div class="api-key-wrapper">';
5702 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 . '" />';
5703 echo '<button type="button" id="toggleXaiApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
5704 echo '<p class="description">' . esc_html__('Required for X.AI Grok models. Get your API key from X.AI Console.', 'mxchat') . '</p>';
5705 echo '</div>';
5706 }
5707 // Claude API Key
5708 public function claude_api_key_callback() {
5709 $claudeApiKey = isset($this->options['claude_api_key']) ? esc_attr($this->options['claude_api_key']) : '';
5710 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5711
5712 echo '<div class="api-key-wrapper">';
5713 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 . '" />';
5714 echo '<button type="button" id="toggleClaudeApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
5715 echo '<p class="description">' . esc_html__('Required for Anthropic Claude models. Get your API key from Anthropic Console.', 'mxchat') . '</p>';
5716 echo '</div>';
5717 }
5718
5719 // DeepSeek API Key
5720 public function deepseek_api_key_callback() {
5721 $apiKey = isset($this->options['deepseek_api_key']) ? esc_attr($this->options['deepseek_api_key']) : '';
5722 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5723
5724 echo '<div class="api-key-wrapper">';
5725 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 . '" />';
5726 echo '<button type="button" id="toggleDeepSeekApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
5727 echo '<p class="description">' . esc_html__('Required for DeepSeek models. Get your API key from DeepSeek Platform.', 'mxchat') . '</p>';
5728 echo '</div>';
5729 }
5730
5731 // Gemini API Key
5732 public function gemini_api_key_callback() {
5733 $geminiApiKey = isset($this->options['gemini_api_key']) ? esc_attr($this->options['gemini_api_key']) : '';
5734 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5735
5736 echo '<div class="api-key-wrapper">';
5737 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 . '" />';
5738 echo '<button type="button" id="toggleGeminiApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
5739 echo '<p class="description">' . esc_html__('Required for Google Gemini models and embeddings. Get your API key from Google AI Studio.', 'mxchat') . '</p>';
5740 echo '</div>';
5741 }
5742
5743
5744 // OpenRouter API Key
5745 public function openrouter_api_key_callback() {
5746 $openrouterApiKey = isset($this->options['openrouter_api_key']) ? esc_attr($this->options['openrouter_api_key']) : '';
5747 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5748
5749 echo '<div class="api-key-wrapper">';
5750 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 . '" />';
5751 echo '<button type="button" id="toggleOpenRouterApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
5752 echo '<p class="description">' . esc_html__('Required for OpenRouter models. Get your API key from OpenRouter.ai', 'mxchat') . '</p>';
5753 echo '</div>';
5754 }
5755
5756 // Voyage API Key
5757 public function voyage_api_key_callback() {
5758 $apiKey = isset($this->options['voyage_api_key']) ? esc_attr($this->options['voyage_api_key']) : '';
5759 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5760
5761 echo '<div class="api-key-wrapper">';
5762 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 . '" />';
5763 echo '<button type="button" id="toggleVoyageAPIKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
5764 echo '<p class="description">' . esc_html__('Required for Voyage AI embedding models. Get your API key from Voyage AI.', 'mxchat') . '</p>';
5765 echo '</div>';
5766 }
5767
5768 public function mxchat_loops_api_key_callback() {
5769 $loops_api_key = isset($this->options['loops_api_key']) ? esc_attr($this->options['loops_api_key']) : '';
5770 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5771
5772 echo '<div class="api-key-wrapper">';
5773 echo sprintf(
5774 '<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" />',
5775 $loops_api_key,
5776 $nonce
5777 );
5778 echo '<button type="button" id="toggleLoopsApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
5779 echo '</div>';
5780 echo '<p class="description">' . esc_html__('Required for Loops email integration. Get your API key from Loops.so', 'mxchat') . '</p>';
5781 }
5782 public function mxchat_loops_mailing_list_callback() {
5783 // Add error handling and type checking
5784 $loops_api_key = '';
5785 $selected_list = '';
5786 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5787
5788 // Safely get the API key
5789 if (isset($this->options['loops_api_key']) && is_string($this->options['loops_api_key'])) {
5790 $loops_api_key = $this->options['loops_api_key'];
5791 }
5792
5793 // Safely get the selected list
5794 if (isset($this->options['loops_mailing_list']) && is_string($this->options['loops_mailing_list'])) {
5795 $selected_list = $this->options['loops_mailing_list'];
5796 }
5797
5798 if (!empty($loops_api_key)) {
5799 $lists = $this->mxchat_fetch_loops_mailing_lists($loops_api_key);
5800 if (is_array($lists) && !empty($lists)) {
5801 echo '<div class="mxchat-field-wrapper">';
5802 echo '<select id="loops_mailing_list" name="loops_mailing_list" class="mxchat-autosave-field" data-nonce="' . $nonce . '">';
5803
5804 // Add a default "Select a list" option
5805 echo '<option value="" ' . selected($selected_list, '', false) . '>' . esc_html__('Select a list', 'mxchat') . '</option>';
5806
5807 foreach ($lists as $list) {
5808 if (is_array($list) && isset($list['id']) && isset($list['name'])) {
5809 echo sprintf(
5810 '<option value="%s" %s>%s</option>',
5811 esc_attr($list['id']),
5812 selected($selected_list, $list['id'], false),
5813 esc_html($list['name'])
5814 );
5815 }
5816 }
5817 echo '</select>';
5818 echo '</div>';
5819 echo '<p class="description">' . esc_html__('Please select a mailing list to use with Loops.', 'mxchat') . '</p>';
5820 } else {
5821 echo '<p class="description">' . esc_html__('No lists found. Please verify your API Key.', 'mxchat') . '</p>';
5822 }
5823 } else {
5824 echo '<p class="description">' . esc_html__('Enter a valid Loops API Key to load mailing lists.', 'mxchat') . '</p>';
5825 }
5826 }
5827 public function mxchat_triggered_phrase_response_callback() {
5828 $default_response = __('Would you like to join our mailing list? Please provide your email below.', 'mxchat');
5829 $triggered_response = isset($this->options['triggered_phrase_response'])
5830 ? $this->options['triggered_phrase_response']
5831 : $default_response;
5832 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5833
5834 echo '<div class="mxchat-field-wrapper">';
5835 echo sprintf(
5836 '<textarea id="triggered_phrase_response" name="triggered_phrase_response" rows="3" cols="50" class="mxchat-autosave-field" data-nonce="%s">%s</textarea>',
5837 $nonce,
5838 esc_textarea($triggered_response)
5839 );
5840 echo '</div>';
5841 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>';
5842 }
5843
5844 public function mxchat_email_capture_response_callback() {
5845 $default_response = __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
5846 $email_capture_response = isset($this->options['email_capture_response'])
5847 ? $this->options['email_capture_response']
5848 : $default_response;
5849 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5850
5851 echo '<div class="mxchat-field-wrapper">';
5852 echo sprintf(
5853 '<textarea id="email_capture_response" name="email_capture_response" rows="3" cols="50" class="mxchat-autosave-field" data-nonce="%s">%s</textarea>',
5854 $nonce,
5855 esc_textarea($email_capture_response)
5856 );
5857 echo '</div>';
5858 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>';
5859 }
5860 public function mxchat_pre_chat_message_callback() {
5861 // Load the entire 'mxchat_options' array
5862 $all_options = get_option('mxchat_options', []);
5863
5864 // Retrieve the saved message or use the default value
5865 $default_message = __('Hey there! Ask me anything!', 'mxchat');
5866 $pre_chat_message = isset($all_options['pre_chat_message']) ? $all_options['pre_chat_message'] : $default_message;
5867
5868 // Output the textarea
5869 printf(
5870 '<textarea id="pre_chat_message" name="pre_chat_message" rows="5" cols="50">%s</textarea>',
5871 esc_textarea($pre_chat_message)
5872 );
5873 }
5874
5875 // Callback for AI Instructions textarea
5876 public function system_prompt_instructions_callback() {
5877 // Retrieve the current value of the system prompt instructions
5878 $instructions = isset($this->options['system_prompt_instructions']) ? esc_textarea($this->options['system_prompt_instructions']) : '';
5879 // Render the textarea field
5880 printf(
5881 '<textarea id="system_prompt_instructions" name="system_prompt_instructions" rows="5" cols="50">%s</textarea>',
5882 $instructions
5883 );
5884 // Personalization hint
5885 echo '<p class="description" style="margin-top: 8px;">';
5886 echo esc_html__('Use {visitor_name} to personalize AI responses when lead capture is enabled.', 'mxchat') . '<br>';
5887 echo '<code style="font-size: 12px;">' . esc_html__('Example: The visitor\'s name is {visitor_name}. Address them by name.', 'mxchat') . '</code>';
5888 echo '</p>';
5889 // Sample instructions button
5890 echo '<div class="mxchat-instructions-container">';
5891 echo '<button type="button" class="mxchat-instructions-btn" id="mxchatViewSampleBtn">';
5892 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">';
5893 echo '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>';
5894 echo '<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>';
5895 echo '</svg>';
5896 echo esc_html__('View Sample Instructions', 'mxchat');
5897 echo '</button>';
5898 echo '</div>';
5899
5900 // Add modal to WordPress admin footer instead of inline
5901 add_action('admin_footer', array($this, 'render_sample_instructions_modal'));
5902 }
5903
5904 // New method to render modal in admin footer
5905 public function render_sample_instructions_modal() {
5906 static $modal_rendered = false;
5907 if ($modal_rendered) return; // Prevent duplicate modals
5908 $modal_rendered = true;
5909
5910 echo '<div class="mxchat-instructions-modal-overlay" id="mxchatSampleModal">';
5911 echo '<div class="mxchat-instructions-modal-content">';
5912 echo '<div class="mxchat-instructions-modal-header">';
5913 echo '<h3 class="mxchat-instructions-modal-title">';
5914 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">';
5915 echo '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>';
5916 echo '<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>';
5917 echo '</svg>';
5918 echo esc_html__('Sample AI Instructions', 'mxchat');
5919 echo '</h3>';
5920 echo '<button type="button" class="mxchat-instructions-modal-close" id="mxchatModalClose">';
5921 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">';
5922 echo '<line x1="18" y1="6" x2="6" y2="18"/>';
5923 echo '<line x1="6" y1="6" x2="18" y2="18"/>';
5924 echo '</svg>';
5925 echo '</button>';
5926 echo '</div>';
5927 echo '<div class="mxchat-instructions-modal-body">';
5928 echo '<div class="mxchat-instructions-content">';
5929 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:
5930
5931 # Response Style - CRITICALLY IMPORTANT
5932 - MAXIMUM LENGTH: 1-3 short sentences per response
5933 - Ultra-concise: Get straight to the answer with no filler
5934 - No introductions like "Sure!" or "I\'d be happy to help"
5935 - No phrases like "based on my knowledge" or "according to information"
5936 - No explanatory text before giving the answer
5937 - No summaries or repetition
5938 - Hyperlink all URLs
5939 - Respond in user\'s language
5940 - Minor chit chat or conversation is okay, but try to keep it focused on [insert topic]
5941
5942 # Knowledge Base Requirements - PREVENT HALLUCINATIONS
5943 - ONLY answer using information explicitly provided in OFFICIAL KNOWLEDGE DATABASE CONTENT sections marked with ===== delimiters
5944 - If required information is NOT in the knowledge database: "I don\'t have enough information in my knowledge base to answer that question accurately."
5945 - NEVER invent or hallucinate URLs, links, product specs, procedures, dates, statistics, names, contacts, or company information
5946 - When knowledge base information is unclear or contradictory, acknowledge the limitation rather than guessing
5947 - Better to admit insufficient information than provide inaccurate answers');
5948 echo '</div>';
5949 echo '<button type="button" class="mxchat-instructions-copy-btn" id="mxchatCopyBtn">';
5950 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">';
5951 echo '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>';
5952 echo '<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>';
5953 echo '</svg>';
5954 echo esc_html__('Copy Instructions', 'mxchat');
5955 echo '</button>';
5956 echo '</div>';
5957 echo '<div class="mxchat-instructions-modal-footer">';
5958 echo '<button type="button" class="mxchat-instructions-btn-secondary" id="mxchatCloseBtn">' . esc_html__('Close', 'mxchat') . '</button>';
5959 echo '</div>';
5960 echo '</div>';
5961 echo '</div>';
5962 }
5963
5964
5965 public function mxchat_model_callback() {
5966 // Define available models grouped by provider
5967 $models = array(
5968 esc_html__('OpenRouter (100+ Models)', 'mxchat') => array(
5969 'openrouter' => esc_html__('OpenRouter - Select after entering API key', 'mxchat'),
5970 ),
5971 esc_html__('Google Gemini Models', 'mxchat') => array(
5972 'gemini-3-pro-preview' => esc_html__('Gemini 3 Pro (Most Intelligent, Multimodal)', 'mxchat'),
5973 'gemini-3-flash-preview' => esc_html__('Gemini 3 Flash (Balanced Speed & Scale)', 'mxchat'),
5974 'gemini-2.5-pro' => esc_html__('Gemini 2.5 Pro (Advanced Thinking)', 'mxchat'),
5975 'gemini-2.5-flash' => esc_html__('Gemini 2.5 Flash (Best Price-Performance)', 'mxchat'),
5976 'gemini-2.5-flash-lite' => esc_html__('Gemini 2.5 Flash-Lite (Ultra Fast)', 'mxchat'),
5977 'gemini-2.0-flash' => esc_html__('Gemini 2.0 Flash (Deprecated Mar 2026)', 'mxchat'),
5978 'gemini-2.0-flash-lite' => esc_html__('Gemini 2.0 Flash-Lite (Deprecated Mar 2026)', 'mxchat'),
5979 'gemini-1.5-pro' => esc_html__('Gemini 1.5 Pro (Deprecated Sep 2025)', 'mxchat'),
5980 'gemini-1.5-flash' => esc_html__('Gemini 1.5 Flash (Deprecated Sep 2025)', 'mxchat'),
5981 ),
5982 esc_html__('X.AI Models', 'mxchat') => array(
5983 'grok-3-beta' => esc_html__('Grok-3 (Powerful)', 'mxchat'),
5984 'grok-3-fast-beta' => esc_html__('Grok-3 Fast (High Performance)', 'mxchat'),
5985 'grok-3-mini-beta' => esc_html__('Grok-3 Mini (Affordable)', 'mxchat'),
5986 'grok-3-mini-fast-beta' => esc_html__('Grok-3 Mini Fast (Quick Response)', 'mxchat'),
5987 'grok-2' => esc_html__('Grok 2', 'mxchat'),
5988 'grok-4-0709' => esc_html__('Grok 4 (Latest Flagship)', 'mxchat'),
5989 'grok-4-1-fast-reasoning' => esc_html__('Grok 4.1 Fast (Reasoning)', 'mxchat'),
5990 'grok-4-1-fast-non-reasoning' => esc_html__('Grok 4.1 Fast (Non-Reasoning)', 'mxchat'),
5991 ),
5992 esc_html__('DeepSeek Models', 'mxchat') => array(
5993 'deepseek-chat' => esc_html__('DeepSeek-V3', 'mxchat'),
5994 ),
5995 esc_html__('Claude Models', 'mxchat') => array(
5996 'claude-opus-4-6' => esc_html__('Claude Opus 4.6 (Most Capable - Recommended)', 'mxchat'),
5997 'claude-sonnet-4-6' => esc_html__('Claude Sonnet 4.6 (Latest Sonnet - Fast & Capable)', 'mxchat'),
5998 'claude-opus-4-5' => esc_html__('Claude Opus 4.5 (Highly Capable)', 'mxchat'),
5999 'claude-sonnet-4-5-20250929' => esc_html__('Claude Sonnet 4.5 (Best for Agents & Coding)', 'mxchat'),
6000 'claude-opus-4-1-20250805' => esc_html__('Claude Opus 4.1 (Exceptional for Complex Tasks)', 'mxchat'),
6001 'claude-haiku-4-5-20251001' => esc_html__('Claude Haiku 4.5 (Fastest & Most Intelligent)', 'mxchat'),
6002 'claude-opus-4-20250514' => esc_html__('Claude 4 Opus (Complex Tasks)', 'mxchat'),
6003 'claude-sonnet-4-20250514' => esc_html__('Claude 4 Sonnet (High Performance)', 'mxchat'),
6004 ),
6005 esc_html__('OpenAI Models', 'mxchat') => array(
6006 'gpt-5.4' => esc_html__('GPT-5.4 (Flagship Reasoning & Coding)', 'mxchat'),
6007 'gpt-5.4-mini' => esc_html__('GPT-5.4 Mini (Fast, 400K Context)', 'mxchat'),
6008 'gpt-5.4-nano' => esc_html__('GPT-5.4 Nano (Fastest & Cheapest)', 'mxchat'),
6009 'gpt-5.3-chat-latest' => esc_html__('GPT-5.3 Chat (Conversational)', 'mxchat'),
6010 'gpt-5.2' => esc_html__('GPT-5.2 (Best General-Purpose & Agentic Model)', 'mxchat'),
6011 'gpt-5.1-chat-latest' => esc_html__('GPT-5.1 Chat Latest (Recommended)', 'mxchat'),
6012 'gpt-5.1-2025-11-13' => esc_html__('GPT-5.1 (Flagship for Coding & Agentic Tasks)', 'mxchat'),
6013 'gpt-5' => esc_html__('GPT-5 (Flagship for Coding, Reasoning & Agents)', 'mxchat'),
6014 'gpt-5-mini' => esc_html__('GPT-5 Mini (Fast and Lightweight)', 'mxchat'),
6015 'gpt-5-nano' => esc_html__('GPT-5 Nano (Fastest & Cheapest for Summarization/Classification)', 'mxchat'),
6016 ),
6017 );
6018
6019 // Retrieve the currently selected model from saved options
6020 $selected_model = isset($this->options['model']) ? esc_attr($this->options['model']) : 'gpt-5.1-chat-latest';
6021
6022 // Begin the select dropdown
6023 echo '<select id="model" name="model">';
6024
6025 // Iterate over groups of models
6026 foreach ($models as $group_label => $group_models) {
6027 echo '<optgroup label="' . esc_attr($group_label) . '">';
6028
6029 foreach ($group_models as $model_value => $model_label) {
6030 echo '<option value="' . esc_attr($model_value) . '" ' . selected($selected_model, $model_value, false) . '>' . esc_html($model_label) . '</option>';
6031 }
6032
6033 echo '</optgroup>';
6034 }
6035
6036 echo '</select>';
6037
6038 // Add a note for OpenRouter
6039 echo '<p class="description" id="openrouter-model-note" style="display:none; color: #d63638; font-weight: 500;">';
6040 echo '<span class="dashicons dashicons-info" style="font-size: 16px; vertical-align: middle;"></span> ';
6041 echo esc_html__('After entering your OpenRouter API key above, click the button below to load available models.', 'mxchat');
6042 echo '</p>';
6043
6044 // API Key Status Messages (hidden by default, shown by JS based on selected model)
6045 $has_openai_key = !empty($this->options['api_key']);
6046 $has_claude_key = !empty($this->options['claude_api_key']);
6047 $has_xai_key = !empty($this->options['xai_api_key']);
6048 $has_deepseek_key = !empty($this->options['deepseek_api_key']);
6049 $has_gemini_key = !empty($this->options['gemini_api_key']);
6050 $has_openrouter_key = !empty($this->options['openrouter_api_key']);
6051
6052 // OpenAI/GPT models
6053 echo '<p class="mxchat-api-status" data-provider="openai" style="display:none;">';
6054 if ($has_openai_key) {
6055 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenAI detected', 'mxchat') . '</span>';
6056 } else {
6057 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenAI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
6058 }
6059 echo '</p>';
6060
6061 // Claude models
6062 echo '<p class="mxchat-api-status" data-provider="claude" style="display:none;">';
6063 if ($has_claude_key) {
6064 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Anthropic (Claude) detected', 'mxchat') . '</span>';
6065 } else {
6066 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Anthropic (Claude) detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
6067 }
6068 echo '</p>';
6069
6070 // X.AI models
6071 echo '<p class="mxchat-api-status" data-provider="xai" style="display:none;">';
6072 if ($has_xai_key) {
6073 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for X.AI (Grok) detected', 'mxchat') . '</span>';
6074 } else {
6075 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>';
6076 }
6077 echo '</p>';
6078
6079 // DeepSeek models
6080 echo '<p class="mxchat-api-status" data-provider="deepseek" style="display:none;">';
6081 if ($has_deepseek_key) {
6082 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for DeepSeek detected', 'mxchat') . '</span>';
6083 } else {
6084 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for DeepSeek detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
6085 }
6086 echo '</p>';
6087
6088 // Gemini models
6089 echo '<p class="mxchat-api-status" data-provider="gemini" style="display:none;">';
6090 if ($has_gemini_key) {
6091 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Google Gemini detected', 'mxchat') . '</span>';
6092 } else {
6093 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Google Gemini detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
6094 }
6095 echo '</p>';
6096
6097 // OpenRouter models
6098 echo '<p class="mxchat-api-status" data-provider="openrouter" style="display:none;">';
6099 if ($has_openrouter_key) {
6100 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenRouter detected', 'mxchat') . '</span>';
6101 } else {
6102 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenRouter detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
6103 }
6104 echo '</p>';
6105
6106 // ADD THESE HIDDEN FIELDS RIGHT HERE:
6107 $openrouter_model = isset($this->options['openrouter_selected_model']) ? esc_attr($this->options['openrouter_selected_model']) : '';
6108 $openrouter_model_name = isset($this->options['openrouter_selected_model_name']) ? esc_attr($this->options['openrouter_selected_model_name']) : '';
6109
6110 echo '<input type="hidden" id="openrouter_selected_model" name="openrouter_selected_model" value="' . $openrouter_model . '" />';
6111 echo '<input type="hidden" id="openrouter_selected_model_name" name="openrouter_selected_model_name" value="' . $openrouter_model_name . '" />';
6112 }
6113
6114 // Update your existing callback method
6115 public function enable_streaming_toggle_callback() {
6116 // Get value from options array, default to 'on'
6117 $enabled = isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on';
6118 $checked = ($enabled === 'on') ? 'checked' : '';
6119
6120 echo '<label class="toggle-switch">';
6121 echo sprintf(
6122 '<input type="checkbox" id="enable_streaming_toggle" name="enable_streaming_toggle" value="on" %s />',
6123 esc_attr($checked)
6124 );
6125 echo '<span class="slider"></span>';
6126 echo '</label>';
6127
6128 // Test button with better styling
6129 echo '<div style="margin-top: 15px;">';
6130 echo '<button type="button" id="mxchat-test-streaming-btn" class="button button-secondary">';
6131 echo '<span class="dashicons dashicons-admin-tools" style="vertical-align: middle; margin-right: 5px;"></span>';
6132 echo esc_html__('Test Streaming Compatibility', 'mxchat');
6133 echo '</button>';
6134 echo '<p id="mxchat-test-streaming-result" style="margin-top:10px; font-weight: bold;"></p>';
6135 echo '</div>';
6136 }
6137
6138 // Web Search toggle callback
6139 public function enable_web_search_toggle_callback() {
6140 // Get value from options array, default to 'off'
6141 $enabled = isset($this->options['enable_web_search']) ? $this->options['enable_web_search'] : 'off';
6142 $checked = ($enabled === 'on') ? 'checked' : '';
6143
6144 // Get current model to determine if we should show/enable the toggle
6145 $current_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest';
6146
6147 // Models that DON'T support web search (per OpenAI docs)
6148 // gpt-5 with minimal reasoning is handled at API level, gpt-4.1-nano doesn't support it
6149 $unsupported_models = array('gpt-4.1-nano');
6150
6151 // Check if current model is an OpenAI model that supports web search
6152 $openai_models = array(
6153 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.4-nano', 'gpt-5.3-chat-latest',
6154 'gpt-5.2', 'gpt-5.1-chat-latest', 'gpt-5.1-2025-11-13', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano'
6155 );
6156
6157 $is_openai = in_array($current_model, $openai_models);
6158 $is_supported = $is_openai && !in_array($current_model, $unsupported_models);
6159
6160 // Wrapper div with data attributes for JS to show/hide
6161 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;"' : '') . '>';
6162
6163 echo '<label class="toggle-switch">';
6164 echo sprintf(
6165 '<input type="checkbox" id="enable_web_search" name="enable_web_search" value="on" %s />',
6166 esc_attr($checked)
6167 );
6168 echo '<span class="slider"></span>';
6169 echo '</label>';
6170
6171 echo '</div>';
6172
6173 // Message shown when non-OpenAI model is selected
6174 echo '<p id="web-search-unavailable-message" class="description" style="color: #666;' . ($is_supported ? ' display:none;' : '') . '">';
6175 echo '<span class="dashicons dashicons-info" style="font-size: 16px; vertical-align: middle; margin-right: 4px;"></span>';
6176 echo esc_html__('Web search is only available for OpenAI models.', 'mxchat');
6177 echo '</p>';
6178 }
6179
6180 // AJAX handler to fetch OpenRouter models
6181 public function fetch_openrouter_models() {
6182 check_ajax_referer('mxchat_fetch_openrouter_models', 'nonce');
6183
6184 $api_key = isset($_POST['api_key']) ? sanitize_text_field($_POST['api_key']) : '';
6185
6186 if (empty($api_key)) {
6187 wp_send_json_error(array('message' => 'API key is required'));
6188 }
6189
6190 $response = wp_remote_get('https://openrouter.ai/api/v1/models', array(
6191 'headers' => array(
6192 'Authorization' => 'Bearer ' . $api_key,
6193 'Content-Type' => 'application/json',
6194 ),
6195 'timeout' => 15,
6196 ));
6197
6198 if (is_wp_error($response)) {
6199 wp_send_json_error(array('message' => $response->get_error_message()));
6200 }
6201
6202 $body = wp_remote_retrieve_body($response);
6203 $data = json_decode($body, true);
6204
6205 if (isset($data['data']) && is_array($data['data'])) {
6206 // Format the models for the frontend
6207 $models = array_map(function($model) {
6208 return array(
6209 'id' => $model['id'],
6210 'name' => $model['name'] ?? $model['id'],
6211 'description' => $model['description'] ?? '',
6212 'context_length' => $model['context_length'] ?? 0,
6213 'pricing' => array(
6214 'prompt' => $model['pricing']['prompt'] ?? 0,
6215 'completion' => $model['pricing']['completion'] ?? 0,
6216 ),
6217 );
6218 }, $data['data']);
6219
6220 wp_send_json_success(array('models' => $models));
6221 } else {
6222 wp_send_json_error(array('message' => 'Invalid response from OpenRouter'));
6223 }
6224 }
6225
6226 // Callback function for embedding model selection
6227 public function embedding_model_callback() {
6228 $models = array(
6229 esc_html__('OpenAI Embeddings', 'mxchat') => array(
6230 'text-embedding-3-small' => esc_html__('TE3 Small (1536, Efficient)', 'mxchat'),
6231 'text-embedding-ada-002' => esc_html__('Ada 2 (1536, Recommended)', 'mxchat'),
6232 'text-embedding-3-large' => esc_html__('TE3 Large (3072, Powerful)', 'mxchat'),
6233 ),
6234 esc_html__('Voyage AI Embeddings', 'mxchat') => array(
6235 'voyage-3-large' => esc_html__('Voyage-3 Large (2048, Most Capable)', 'mxchat'),
6236 ),
6237 esc_html__('Google Gemini Embeddings', 'mxchat') => array(
6238 'gemini-embedding-001' => esc_html__('Gemini Embedding (1536, Stable)', 'mxchat'),
6239 )
6240 );
6241 $selected_model = isset($this->options['embedding_model']) ? esc_attr($this->options['embedding_model']) : 'text-embedding-ada-002';
6242 echo '<select id="embedding_model" name="embedding_model">';
6243 foreach ($models as $group_label => $group_models) {
6244 echo '<optgroup label="' . esc_attr($group_label) . '">';
6245 foreach ($group_models as $model_value => $model_label) {
6246 echo '<option value="' . esc_attr($model_value) . '" ' . selected($selected_model, $model_value, false) . '>' . esc_html($model_label) . '</option>';
6247 }
6248 echo '</optgroup>';
6249 }
6250 echo '</select>';
6251
6252 // API Key Status Messages for Embedding Models
6253 $has_openai_key = !empty($this->options['api_key']);
6254 $has_voyage_key = !empty($this->options['voyage_api_key']);
6255 $has_gemini_key = !empty($this->options['gemini_api_key']);
6256
6257 // OpenAI Embeddings
6258 echo '<p class="mxchat-embedding-api-status" data-provider="openai" style="display:none;">';
6259 if ($has_openai_key) {
6260 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenAI detected', 'mxchat') . '</span>';
6261 } else {
6262 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenAI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
6263 }
6264 echo '</p>';
6265
6266 // Voyage AI Embeddings
6267 echo '<p class="mxchat-embedding-api-status" data-provider="voyage" style="display:none;">';
6268 if ($has_voyage_key) {
6269 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Voyage AI detected', 'mxchat') . '</span>';
6270 } else {
6271 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Voyage AI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
6272 }
6273 echo '</p>';
6274
6275 // Gemini Embeddings
6276 echo '<p class="mxchat-embedding-api-status" data-provider="gemini" style="display:none;">';
6277 if ($has_gemini_key) {
6278 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Google Gemini detected', 'mxchat') . '</span>';
6279 } else {
6280 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Google Gemini detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
6281 }
6282 echo '</p>';
6283 }
6284
6285
6286 public function mxchat_top_bar_title_callback() {
6287 // Retrieve the current value of the top bar title from saved options
6288 $top_bar_title = isset($this->options['top_bar_title']) ? esc_attr($this->options['top_bar_title']) : '';
6289
6290 // Render the input field
6291 echo '<input type="text" id="top_bar_title" name="top_bar_title" value="' . $top_bar_title . '" />';
6292 }
6293 public function mxchat_ai_agent_text_callback() {
6294 // Retrieve the current value of the AI agent text from saved options
6295 $ai_agent_text = isset($this->options['ai_agent_text']) ? esc_attr($this->options['ai_agent_text']) : '';
6296 // Render the input field
6297 echo '<input type="text" id="ai_agent_text" name="ai_agent_text" value="' . $ai_agent_text . '" />';
6298 }
6299
6300
6301 public function enable_email_block_callback() {
6302 // Load full plugin options array
6303 $all_options = get_option('mxchat_options', []);
6304
6305 // Get the value, default to 'off'
6306 $enable_email_block = isset($all_options['enable_email_block']) ? $all_options['enable_email_block'] : 'off';
6307
6308 // Check if it's 'on'
6309 $checked = ($enable_email_block === 'on') ? 'checked' : '';
6310
6311 echo '<label class="toggle-switch">';
6312 echo sprintf(
6313 '<input type="checkbox" id="enable_email_block" name="enable_email_block" value="on" %s />',
6314 esc_attr($checked)
6315 );
6316 echo '<span class="slider"></span>';
6317 echo '</label>';
6318 }
6319
6320 public function email_blocker_header_content_callback() {
6321 // Load the entire 'mxchat_options' array
6322 $all_options = get_option('mxchat_options', []);
6323
6324 // Retrieve the saved content or default to empty
6325 $content = isset($all_options['email_blocker_header_content'])
6326 ? $all_options['email_blocker_header_content']
6327 : '';
6328
6329 // Render the textarea - IMPORTANT: name should be just "email_blocker_header_content"
6330 echo '<textarea
6331 id="email_blocker_header_content"
6332 name="email_blocker_header_content"
6333 rows="5"
6334 cols="70"
6335 data-setting="email_blocker_header_content"
6336 >' . esc_textarea($content) . '</textarea>';
6337 }
6338
6339 public function email_blocker_button_text_callback() {
6340 // Load the entire 'mxchat_options' array
6341 $all_options = get_option('mxchat_options', []);
6342
6343 // Retrieve the saved button text or default to empty
6344 $button_text = isset($all_options['email_blocker_button_text'])
6345 ? $all_options['email_blocker_button_text']
6346 : '';
6347
6348 // Use esc_attr to safely render the existing text
6349 echo '<input type="text" id="email_blocker_button_text" name="email_blocker_button_text" value="' . esc_attr($button_text) . '" style="width: 300px;" />';
6350 }
6351
6352 //Enable name field callback
6353 public function enable_name_field_callback() {
6354 // Load full plugin options array
6355 $all_options = get_option('mxchat_options', []);
6356 // Get the value, default to 'off'
6357 $enable_name_field = isset($all_options['enable_name_field']) ? $all_options['enable_name_field'] : 'off';
6358 // Check if it's 'on'
6359 $checked = ($enable_name_field === 'on') ? 'checked' : '';
6360 echo '<label class="toggle-switch">';
6361 echo sprintf(
6362 '<input type="checkbox" id="enable_name_field" name="enable_name_field" value="on" %s />',
6363 esc_attr($checked)
6364 );
6365 echo '<span class="slider"></span>';
6366 echo '</label>';
6367 }
6368 //Name field placeholder callback
6369 public function name_field_placeholder_callback() {
6370 $all_options = get_option('mxchat_options', []);
6371 $placeholder = isset($all_options['name_field_placeholder'])
6372 ? $all_options['name_field_placeholder']
6373 : esc_html__('Enter your name', 'mxchat');
6374
6375 echo '<input type="text" id="name_field_placeholder" name="name_field_placeholder" value="' . esc_attr($placeholder) . '" style="width: 300px;" />';
6376 }
6377
6378
6379
6380 public function mxchat_intro_message_callback() {
6381 // Load the entire 'mxchat_options' array
6382 $all_options = get_option('mxchat_options', []);
6383 // Retrieve the saved intro message or use the default
6384 $default_message = __('Hello! How can I assist you today?', 'mxchat');
6385 $saved_message = isset($all_options['intro_message']) ? $all_options['intro_message'] : $default_message;
6386 // Output the textarea with the saved value without escaping HTML
6387 ?>
6388 <textarea id="intro_message" name="intro_message" rows="5" cols="50"><?php echo $saved_message; ?></textarea>
6389 <p class="description" style="margin-top: 8px;">
6390 <?php esc_html_e('Use {visitor_name} to personalize greetings when lead capture is enabled.', 'mxchat'); ?><br>
6391 <code style="font-size: 12px;"><?php esc_html_e('Example: Hello {visitor_name}! How can I help you today?', 'mxchat'); ?></code>
6392 </p>
6393 <?php
6394 }
6395
6396 public function mxchat_input_copy_callback() {
6397 // Load the entire 'mxchat_options' array
6398 $all_options = get_option('mxchat_options', []);
6399
6400 // Retrieve the saved input copy or use the default value
6401 $default_copy = __('How can I assist?', 'mxchat');
6402 $input_copy = isset($all_options['input_copy']) ? $all_options['input_copy'] : $default_copy;
6403
6404 // Output the input field with the saved value
6405 printf(
6406 '<input type="text" id="input_copy" name="input_copy" value="%s" placeholder="%s" />',
6407 esc_attr($input_copy),
6408 esc_attr__('How can I assist?', 'mxchat')
6409 );
6410 }
6411
6412
6413 public function mxchat_append_to_body_callback() {
6414 // Fetch fresh options to ensure we have the latest saved values
6415 $options = get_option('mxchat_options', array());
6416
6417 // Get value from options array, default to 'off'
6418 $append_to_body = isset($options['append_to_body']) ? $options['append_to_body'] : 'off';
6419 $checked = ($append_to_body === 'on') ? 'checked' : '';
6420
6421 // Get post type visibility settings
6422 $visibility_mode = isset($options['post_type_visibility_mode']) ? $options['post_type_visibility_mode'] : 'all';
6423 $visibility_list = isset($options['post_type_visibility_list']) ? $options['post_type_visibility_list'] : array();
6424 if (!is_array($visibility_list)) {
6425 $visibility_list = array();
6426 }
6427
6428 echo '<div class="mxchat-autosave-section">';
6429
6430 // Main toggle
6431 echo '<label class="toggle-switch">';
6432 echo sprintf(
6433 '<input type="checkbox" id="append_to_body" name="append_to_body" value="on" %s />',
6434 esc_attr($checked)
6435 );
6436 echo '<span class="slider"></span>';
6437 echo '</label>';
6438
6439 // Post Type Visibility Options (only visible when auto-display is ON)
6440 $display_style = ($append_to_body === 'on') ? '' : 'display: none;';
6441 echo '<div id="post-type-visibility-options" class="mxchat-sub-options" style="' . esc_attr($display_style) . '">';
6442
6443 echo '<div class="mxchat-post-type-visibility-header">';
6444 echo '<h4>' . esc_html__('Post Type Visibility', 'mxchat') . '</h4>';
6445 echo '</div>';
6446
6447 // Mode selector (radio buttons)
6448 echo '<div class="mxchat-visibility-mode">';
6449
6450 echo '<label class="mxchat-radio-label">';
6451 echo '<input type="radio" name="post_type_visibility_mode" value="all" ' . checked($visibility_mode, 'all', false) . ' />';
6452 echo '<span>' . esc_html__('Show on all post types', 'mxchat') . '</span>';
6453 echo '</label>';
6454
6455 echo '<label class="mxchat-radio-label">';
6456 echo '<input type="radio" name="post_type_visibility_mode" value="include" ' . checked($visibility_mode, 'include', false) . ' />';
6457 echo '<span>' . esc_html__('Only show on selected post types', 'mxchat') . '</span>';
6458 echo '</label>';
6459
6460 echo '<label class="mxchat-radio-label">';
6461 echo '<input type="radio" name="post_type_visibility_mode" value="exclude" ' . checked($visibility_mode, 'exclude', false) . ' />';
6462 echo '<span>' . esc_html__('Hide on selected post types', 'mxchat') . '</span>';
6463 echo '</label>';
6464
6465 echo '</div>';
6466
6467 // Post type checkboxes (only visible when mode is include or exclude)
6468 $list_display = ($visibility_mode !== 'all') ? '' : 'display: none;';
6469 echo '<div id="post-type-list" class="mxchat-post-type-list" style="' . esc_attr($list_display) . '">';
6470
6471 // Get all public post types
6472 $post_types = get_post_types(array('public' => true), 'objects');
6473
6474 foreach ($post_types as $post_type) {
6475 // Skip attachments
6476 if ($post_type->name === 'attachment') {
6477 continue;
6478 }
6479
6480 $is_checked = in_array($post_type->name, $visibility_list) ? 'checked' : '';
6481
6482 echo '<label class="mxchat-checkbox-label">';
6483 echo '<input type="checkbox" name="post_type_visibility_list[]" value="' . esc_attr($post_type->name) . '" ' . $is_checked . ' />';
6484 echo '<span>' . esc_html($post_type->label) . '</span>';
6485 echo '</label>';
6486 }
6487
6488 echo '</div>'; // End post-type-list
6489 echo '</div>'; // End post-type-visibility-options
6490 echo '</div>'; // End mxchat-autosave-section
6491 }
6492
6493 public function mxchat_contextual_awareness_callback() {
6494 // Get value from options array, default to 'off'
6495 $contextual_awareness = isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off';
6496 $checked = ($contextual_awareness === 'on') ? 'checked' : '';
6497 echo '<label class="toggle-switch">';
6498 echo sprintf(
6499 '<input type="checkbox" id="contextual_awareness_toggle" name="contextual_awareness_toggle" value="on" %s />',
6500 esc_attr($checked)
6501 );
6502 echo '<span class="slider"></span>';
6503 echo '</label>';
6504 }
6505
6506 public function mxchat_citation_links_toggle_callback() {
6507 // Get value from options array, default to 'on' (enabled by default)
6508 $citation_links = isset($this->options['citation_links_toggle']) ? $this->options['citation_links_toggle'] : 'on';
6509 $checked = ($citation_links === 'on') ? 'checked' : '';
6510 echo '<label class="toggle-switch">';
6511 echo sprintf(
6512 '<input type="checkbox" id="citation_links_toggle" name="citation_links_toggle" value="on" %s />',
6513 esc_attr($checked)
6514 );
6515 echo '<span class="slider"></span>';
6516 echo '</label>';
6517 }
6518
6519 public function mxchat_privacy_toggle_callback() {
6520 // Load from mxchat_options array
6521 $options = get_option('mxchat_options', []);
6522
6523 // Get privacy toggle value with fallback
6524 $privacy_toggle = isset($options['privacy_toggle']) ? $options['privacy_toggle'] : 'off';
6525 $checked = ($privacy_toggle === 'on') ? 'checked' : '';
6526
6527 // Get privacy text with fallback
6528 $privacy_text = isset($options['privacy_text'])
6529 ? $options['privacy_text']
6530 : __('By chatting, you agree to our <a href="https://example.com/privacy-policy" target="_blank">privacy policy</a>.', 'mxchat');
6531
6532 // Output the toggle switch
6533 echo '<label class="toggle-switch">';
6534 echo sprintf(
6535 '<input type="checkbox" id="privacy_toggle" name="privacy_toggle" value="on" %s />',
6536 esc_attr($checked)
6537 );
6538 echo '<span class="slider"></span>';
6539 echo '</label>';
6540
6541 // Output the custom text input field
6542 echo sprintf(
6543 '<textarea id="privacy_text" name="privacy_text" rows="5" cols="50" class="regular-text">%s</textarea>',
6544 esc_textarea($privacy_text)
6545 );
6546 }
6547
6548
6549 public function mxchat_complianz_toggle_callback() {
6550 // Load from mxchat_options array
6551 $options = get_option('mxchat_options', []);
6552
6553 // Get complianz toggle value with fallback
6554 $complianz_toggle = isset($options['complianz_toggle']) ? $options['complianz_toggle'] : 'off';
6555 $checked = ($complianz_toggle === 'on') ? 'checked' : '';
6556
6557 // Output the toggle switch
6558 echo '<label class="toggle-switch">';
6559 echo sprintf(
6560 '<input type="checkbox" id="complianz_toggle" name="complianz_toggle" value="on" %s />',
6561 esc_attr($checked)
6562 );
6563 echo '<span class="slider"></span>';
6564 echo '</label>';
6565 }
6566
6567 public function mxchat_link_target_toggle_callback() {
6568 // Load from mxchat_options array
6569 $options = get_option('mxchat_options', []);
6570
6571 // Get link target toggle value with fallback
6572 $link_target_toggle = isset($options['link_target_toggle']) ? $options['link_target_toggle'] : 'off';
6573 $checked = ($link_target_toggle === 'on') ? 'checked' : '';
6574
6575 // Output the toggle switch
6576 echo '<label class="toggle-switch">';
6577 echo sprintf(
6578 '<input type="checkbox" id="link_target_toggle" name="link_target_toggle" value="on" %s />',
6579 esc_attr($checked)
6580 );
6581 echo '<span class="slider"></span>';
6582 echo '</label>';
6583 }
6584
6585 public function mxchat_chat_persistence_toggle_callback() {
6586 // Load from mxchat_options array
6587 $options = get_option('mxchat_options', []);
6588
6589 // Get chat persistence toggle value with fallback
6590 $chat_persistence_toggle = isset($options['chat_persistence_toggle']) ? $options['chat_persistence_toggle'] : 'off';
6591 $checked = ($chat_persistence_toggle === 'on') ? 'checked' : '';
6592
6593 // Output the toggle switch
6594 echo '<label class="toggle-switch">';
6595 echo sprintf(
6596 '<input type="checkbox" id="chat_persistence_toggle" name="chat_persistence_toggle" value="on" %s />',
6597 esc_attr($checked)
6598 );
6599 echo '<span class="slider"></span>';
6600 echo '</label>';
6601 }
6602
6603 public function mxchat_popular_question_1_callback() {
6604 // Load the full plugin options array
6605 $all_options = get_option('mxchat_options', []);
6606
6607 // Retrieve the specific option for popular_question_1
6608 $popular_question_1 = isset($all_options['popular_question_1']) ? $all_options['popular_question_1'] : '';
6609
6610 // Render the input field
6611 printf(
6612 '<input type="text" id="popular_question_1" name="popular_question_1" value="%s" placeholder="%s" class="regular-text" />',
6613 esc_attr($popular_question_1),
6614 esc_attr__('Enter Quick Question 1', 'mxchat')
6615 );
6616 }
6617
6618
6619 public function mxchat_popular_question_2_callback() {
6620 // Load the full plugin options array
6621 $all_options = get_option('mxchat_options', []);
6622
6623 // Retrieve the specific option for popular_question_2
6624 $popular_question_2 = isset($all_options['popular_question_2']) ? $all_options['popular_question_2'] : '';
6625
6626 // Render the input field
6627 printf(
6628 '<input type="text" id="popular_question_2" name="popular_question_2" value="%s" placeholder="%s" class="regular-text" />',
6629 esc_attr($popular_question_2),
6630 esc_attr__('Enter Quick Question 2', 'mxchat')
6631 );
6632 }
6633
6634
6635 public function mxchat_popular_question_3_callback() {
6636 // Load the full plugin options array
6637 $all_options = get_option('mxchat_options', []);
6638
6639 // Retrieve the specific option for popular_question_3
6640 $popular_question_3 = isset($all_options['popular_question_3']) ? $all_options['popular_question_3'] : '';
6641
6642 // Render the input field
6643 printf(
6644 '<input type="text" id="popular_question_3" name="popular_question_3" value="%s" placeholder="%s" class="regular-text" />',
6645 esc_attr($popular_question_3),
6646 esc_attr(__('Enter Quick Question 3', 'mxchat'))
6647 );
6648 }
6649
6650 public function mxchat_additional_popular_questions_callback() {
6651 $options = get_option('mxchat_options', []);
6652 $additional_questions = isset($options['additional_popular_questions'])
6653 ? $options['additional_popular_questions']
6654 : get_option('additional_popular_questions', array());
6655
6656 echo '<div id="mxchat-additional-questions-container">';
6657 if (!empty($additional_questions)) {
6658 foreach ($additional_questions as $index => $question) {
6659 printf(
6660 '<div class="mxchat-question-row">
6661 <input type="text" name="additional_popular_questions[]"
6662 value="%s"
6663 placeholder="%s"
6664 class="regular-text mxchat-question-input"
6665 data-question-index="%d" />
6666 <button type="button" class="button mxchat-remove-question"
6667 aria-label="%s">%s</button>
6668 </div>',
6669 esc_attr($question),
6670 esc_attr(sprintf(__('Enter Additional Quick Question %d', 'mxchat'), $index + 4)),
6671 $index,
6672 esc_attr(__('Remove question', 'mxchat')),
6673 esc_html__('Remove', 'mxchat')
6674 );
6675 }
6676 } else {
6677 printf(
6678 '<div class="mxchat-question-row">
6679 <input type="text" name="additional_popular_questions[]"
6680 value=""
6681 placeholder="%s"
6682 class="regular-text mxchat-question-input"
6683 data-question-index="0" />
6684 <button type="button" class="button mxchat-remove-question"
6685 aria-label="%s">%s</button>
6686 </div>',
6687 esc_attr(__('Enter Additional Quick Question 4', 'mxchat')),
6688 esc_attr(__('Remove question', 'mxchat')),
6689 esc_html__('Remove', 'mxchat')
6690 );
6691 }
6692 echo '</div>';
6693 printf(
6694 '<button type="button" class="button mxchat-add-question" aria-label="%s">%s</button>',
6695 esc_attr(__('Add question', 'mxchat')),
6696 esc_html__('Add Question', 'mxchat')
6697 );
6698 }
6699
6700 public function mxchat_brave_api_key_callback() {
6701 $brave_api_key = isset($this->options['brave_api_key']) ? esc_attr($this->options['brave_api_key']) : '';
6702 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6703
6704 echo '<div class="api-key-wrapper">';
6705 echo sprintf(
6706 '<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" />',
6707 $brave_api_key,
6708 $nonce
6709 );
6710 echo '<button type="button" id="toggleBraveApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6711 echo '</div>';
6712 echo '<p class="description">' . __('Required for Brave Search integration. Get your API key from Brave Search API.', 'mxchat') . '</p>';
6713 }
6714
6715 public function mxchat_brave_image_count_callback() {
6716 $brave_image_count = isset($this->options['brave_image_count'])
6717 ? intval($this->options['brave_image_count'])
6718 : 4;
6719 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6720
6721 echo '<div class="mxchat-field-wrapper">';
6722 echo sprintf(
6723 '<input type="number" id="brave_image_count" name="brave_image_count"
6724 value="%d" min="1" max="6" class="small-text mxchat-autosave-field" data-nonce="%s" />',
6725 $brave_image_count,
6726 $nonce
6727 );
6728 echo '</div>';
6729 echo '<p class="description">' . __('Select the number of images to return (1-6).', 'mxchat') . '</p>';
6730 }
6731
6732 public function mxchat_brave_safe_search_callback() {
6733 $brave_safe_search = isset($this->options['brave_safe_search'])
6734 ? esc_attr($this->options['brave_safe_search'])
6735 : 'strict';
6736 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6737
6738 echo '<div class="mxchat-field-wrapper">';
6739 echo '<select id="brave_safe_search" name="brave_safe_search" class="mxchat-autosave-field" data-nonce="' . $nonce . '">';
6740 echo sprintf(
6741 '<option value="strict" %s>%s</option>',
6742 selected($brave_safe_search, 'strict', false),
6743 __('Strict', 'mxchat')
6744 );
6745 echo sprintf(
6746 '<option value="off" %s>%s</option>',
6747 selected($brave_safe_search, 'off', false),
6748 __('Off', 'mxchat')
6749 );
6750 echo '</select>';
6751 echo '</div>';
6752 echo '<p class="description">' .
6753 esc_html__('Set the Safe Search level for image searches. Brave Search only supports "Strict" and "Off" options.', 'mxchat') .
6754 '</p>';
6755 }
6756
6757 public function mxchat_brave_news_count_callback() {
6758 $brave_news_count = isset($this->options['brave_news_count'])
6759 ? intval($this->options['brave_news_count'])
6760 : 3;
6761 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6762
6763 echo '<div class="mxchat-field-wrapper">';
6764 echo sprintf(
6765 '<input type="number" id="brave_news_count" name="brave_news_count"
6766 value="%d" min="1" max="10" class="small-text mxchat-autosave-field" data-nonce="%s" />',
6767 $brave_news_count,
6768 $nonce
6769 );
6770 echo '</div>';
6771 echo '<p class="description">' . esc_html__('Select the number of news articles to retrieve (1-10).', 'mxchat') . '</p>';
6772 }
6773
6774 public function mxchat_brave_country_callback() {
6775 $brave_country = isset($this->options['brave_country'])
6776 ? esc_attr($this->options['brave_country'])
6777 : 'us';
6778 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6779
6780 echo '<div class="mxchat-field-wrapper">';
6781 echo sprintf(
6782 '<input type="text" id="brave_country" name="brave_country"
6783 value="%s" maxlength="2" class="small-text mxchat-autosave-field" data-nonce="%s" />',
6784 $brave_country,
6785 $nonce
6786 );
6787 echo '</div>';
6788 echo '<p class="description">' . esc_html__('Enter the country code (e.g., "us" for United States).', 'mxchat') . '</p>';
6789 }
6790
6791 public function mxchat_brave_language_callback() {
6792 $brave_language = isset($this->options['brave_language'])
6793 ? esc_attr($this->options['brave_language'])
6794 : 'en';
6795 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6796
6797 echo '<div class="mxchat-field-wrapper">';
6798 echo sprintf(
6799 '<input type="text" id="brave_language" name="brave_language"
6800 value="%s" maxlength="2" class="small-text mxchat-autosave-field" data-nonce="%s" />',
6801 $brave_language,
6802 $nonce
6803 );
6804 echo '</div>';
6805 echo '<p class="description">' . esc_html__('Enter the language code (e.g., "en" for English).', 'mxchat') . '</p>';
6806 }
6807
6808
6809
6810
6811
6812 // Section Callback
6813 public function mxchat_pdf_intent_section_callback() {
6814 echo '<p>' . esc_html__('Configure the intent settings for the Chat with PDF feature.', 'mxchat') . '</p>';
6815 }
6816
6817 public function mxchat_chat_toolbar_toggle_callback() {
6818 // Get chat toolbar toggle value with fallback
6819 $chat_toolbar_toggle = isset($this->options['chat_toolbar_toggle']) ? $this->options['chat_toolbar_toggle'] : 'off';
6820 $checked = ($chat_toolbar_toggle === 'on') ? 'checked' : '';
6821
6822 // Output the toggle switch
6823 echo '<label class="toggle-switch">';
6824 echo sprintf(
6825 '<input type="checkbox" id="chat_toolbar_toggle" name="chat_toolbar_toggle" value="on" %s />',
6826 esc_attr($checked)
6827 );
6828 echo '<span class="slider"></span>';
6829 echo '</label>';
6830
6831 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>';
6832 }
6833
6834 /**
6835 * Callback for PDF upload button toggle setting
6836 */
6837 public function mxchat_show_pdf_upload_button_callback() {
6838 // Get toggle value with fallback
6839 $show_pdf_button = isset($this->options['show_pdf_upload_button']) ? $this->options['show_pdf_upload_button'] : 'on';
6840 $checked = ($show_pdf_button === 'on') ? 'checked' : '';
6841
6842 // Output the toggle switch
6843 echo '<label class="toggle-switch">';
6844 echo sprintf(
6845 '<input type="checkbox" id="show_pdf_upload_button" name="show_pdf_upload_button" value="on" %s />',
6846 esc_attr($checked)
6847 );
6848 echo '<span class="slider"></span>';
6849 echo '</label>';
6850
6851 echo '<p class="description">' . esc_html__('Enable to show the PDF upload button in the chatbot toolbar. Disable to hide it.', 'mxchat') . '</p>';
6852 }
6853
6854 /**
6855 * Callback for Word upload button toggle setting
6856 */
6857 public function mxchat_show_word_upload_button_callback() {
6858 // Get toggle value with fallback
6859 $show_word_button = isset($this->options['show_word_upload_button']) ? $this->options['show_word_upload_button'] : 'on';
6860 $checked = ($show_word_button === 'on') ? 'checked' : '';
6861
6862 // Output the toggle switch
6863 echo '<label class="toggle-switch">';
6864 echo sprintf(
6865 '<input type="checkbox" id="show_word_upload_button" name="show_word_upload_button" value="on" %s />',
6866 esc_attr($checked)
6867 );
6868 echo '<span class="slider"></span>';
6869 echo '</label>';
6870
6871 echo '<p class="description">' . esc_html__('Enable to show the Word document upload button in the chatbot toolbar. Disable to hide it.', 'mxchat') . '</p>';
6872 }
6873
6874 public function mxchat_pdf_intent_trigger_text_callback() {
6875 $default_text = __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat');
6876
6877 echo sprintf(
6878 '<textarea id="pdf_intent_trigger_text"
6879 name="pdf_intent_trigger_text"
6880 rows="3"
6881 cols="50"
6882 placeholder="%s">%s</textarea>',
6883 esc_attr__('Enter trigger text', 'mxchat'),
6884 isset($this->options['pdf_intent_trigger_text'])
6885 ? esc_textarea($this->options['pdf_intent_trigger_text'])
6886 : esc_textarea($default_text)
6887 );
6888 echo '<p class="description">' . esc_html__('Text displayed when the intent is triggered.', 'mxchat') . '</p>';
6889 }
6890
6891 public function mxchat_pdf_intent_success_text_callback() {
6892 $default_text = __("I've processed the PDF. What questions do you have about it?", 'mxchat');
6893
6894 echo sprintf(
6895 '<textarea id="pdf_intent_success_text"
6896 name="pdf_intent_success_text"
6897 rows="3"
6898 cols="50"
6899 placeholder="%s">%s</textarea>',
6900 esc_attr__('Enter success text', 'mxchat'),
6901 isset($this->options['pdf_intent_success_text'])
6902 ? esc_textarea($this->options['pdf_intent_success_text'])
6903 : esc_textarea($default_text)
6904 );
6905 echo '<p class="description">' . esc_html__('Text displayed when the intent is successful.', 'mxchat') . '</p>';
6906 }
6907
6908 public function mxchat_pdf_intent_error_text_callback() {
6909 $default_text = __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
6910
6911 echo sprintf(
6912 '<textarea id="pdf_intent_error_text"
6913 name="pdf_intent_error_text"
6914 rows="3"
6915 cols="50"
6916 placeholder="%s">%s</textarea>',
6917 esc_attr__('Enter error text', 'mxchat'),
6918 isset($this->options['pdf_intent_error_text'])
6919 ? esc_textarea($this->options['pdf_intent_error_text'])
6920 : esc_textarea($default_text)
6921 );
6922 echo '<p class="description">' . esc_html__('Text displayed when an error occurs during the intent.', 'mxchat') . '</p>';
6923 }
6924
6925 public function mxchat_pdf_max_pages_callback() {
6926 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
6927
6928 echo sprintf(
6929 '<input type="range"
6930 id="pdf_max_pages"
6931 name="pdf_max_pages"
6932 min="1"
6933 max="69"
6934 value="%d"
6935 class="range-slider" />',
6936 esc_attr($max_pages)
6937 );
6938 echo '<span id="pdf_max_pages_output">' . esc_html($max_pages) . '</span>';
6939 echo '<p class="description">' . esc_html__('Set the maximum number of document pages users can upload for processing. (1-69 pages)', 'mxchat') . '</p>';
6940 }
6941
6942 public function mxchat_live_agent_status_callback() {
6943 // Always get fresh options instead of using cached $this->options
6944 $fresh_options = get_option('mxchat_options');
6945 $status = isset($fresh_options['live_agent_status']) ? $fresh_options['live_agent_status'] : 'off';
6946
6947 echo '<label class="toggle-switch">';
6948 echo sprintf(
6949 '<input type="checkbox" id="live_agent_status" name="live_agent_status" value="on" %s />',
6950 checked($status, 'on', false)
6951 );
6952 echo '<span class="slider"></span>';
6953 echo '</label>';
6954 echo '<label for="live_agent_status" class="mxchat-status-label">';
6955 echo '<span class="status-text">' . ($status === 'on' ? esc_html__('Online', 'mxchat') : esc_html__('Offline', 'mxchat')) . '</span>';
6956 echo '</label>';
6957 }
6958
6959 public function mxchat_live_agent_away_message_callback() {
6960 $message = isset($this->options['live_agent_away_message'])
6961 ? $this->options['live_agent_away_message']
6962 : __('Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.', 'mxchat');
6963
6964 printf(
6965 '<textarea id="live_agent_away_message" name="live_agent_away_message" rows="3" cols="50">%s</textarea>',
6966 esc_textarea($message)
6967 );
6968 echo '<p class="description">' . esc_html__('Message shown when live agents are offline.', 'mxchat') . '</p>';
6969 }
6970
6971 public function mxchat_live_agent_notification_message_callback() {
6972 $message = isset($this->options['live_agent_notification_message'])
6973 ? $this->options['live_agent_notification_message']
6974 : __('Live agent has been notified.', 'mxchat');
6975
6976 printf(
6977 '<textarea id="live_agent_notification_message" name="live_agent_notification_message" rows="3" cols="50">%s</textarea>',
6978 esc_textarea($message)
6979 );
6980 echo '<p class="description">' . esc_html__('Message shown when live transfer activated.', 'mxchat') . '</p>';
6981 }
6982
6983 public function mxchat_live_agent_webhook_url_callback() {
6984 $webhook_url = isset($this->options['live_agent_webhook_url'])
6985 ? esc_url($this->options['live_agent_webhook_url'])
6986 : esc_url(get_option('live_agent_webhook_url', ''));
6987
6988 printf(
6989 '<input type="password" id="live_agent_webhook_url" name="live_agent_webhook_url" value="%s" class="regular-text" />',
6990 $webhook_url
6991 );
6992 echo '<button type="button" id="toggleWebhookUrlVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6993 echo '<p class="description">' . esc_html__('Enter your Slack webhook URL for live agent notifications.', 'mxchat') . '</p>';
6994 }
6995
6996 public function mxchat_live_agent_secret_key_callback() {
6997 printf(
6998 '<input type="password" id="live_agent_secret_key" name="live_agent_secret_key" value="%s" class="regular-text" />',
6999 isset($this->options['live_agent_secret_key']) ? esc_attr($this->options['live_agent_secret_key']) : ''
7000 );
7001 echo '<button type="button" id="toggleSecretKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7002 echo '<p class="description">' . esc_html__('Secret key for validating Slack requests. Keep this secure.', 'mxchat') . '</p>';
7003 }
7004
7005 public function mxchat_live_agent_bot_token_callback() {
7006 printf(
7007 '<input type="password" id="live_agent_bot_token" name="live_agent_bot_token" value="%s" class="regular-text" />',
7008 isset($this->options['live_agent_bot_token']) ? esc_attr($this->options['live_agent_bot_token']) : ''
7009 );
7010 echo '<button type="button" id="toggleBotTokenVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7011 echo '<p class="description">' . esc_html__('Your Slack Bot OAuth Token (starts with xoxb-). Keep this secure.', 'mxchat') . '</p>';
7012 }
7013
7014 public function mxchat_live_agent_user_ids_callback() {
7015 $user_ids = isset($this->options['live_agent_user_ids'])
7016 ? esc_textarea($this->options['live_agent_user_ids'])
7017 : '';
7018
7019 printf(
7020 '<textarea id="live_agent_user_ids" name="live_agent_user_ids" rows="4" class="large-text">%s</textarea>',
7021 $user_ids
7022 );
7023 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>';
7024 }
7025
7026 /**
7027 * Telegram Integration Callbacks
7028 */
7029 public function mxchat_telegram_section_callback() {
7030 echo '<p>' . esc_html__('Configure Telegram integration for live agent support.', 'mxchat') . '</p>';
7031 }
7032
7033 public function mxchat_telegram_status_callback() {
7034 $fresh_options = get_option('mxchat_options');
7035 $status = isset($fresh_options['telegram_status']) ? $fresh_options['telegram_status'] : 'off';
7036
7037 echo '<label class="toggle-switch">';
7038 echo sprintf(
7039 '<input type="checkbox" id="telegram_status" name="telegram_status" value="on" %s />',
7040 checked($status, 'on', false)
7041 );
7042 echo '<span class="slider"></span>';
7043 echo '</label>';
7044 echo '<label for="telegram_status" class="mxchat-status-label">';
7045 echo '<span class="status-text">' . ($status === 'on' ? esc_html__('Online', 'mxchat') : esc_html__('Offline', 'mxchat')) . '</span>';
7046 echo '</label>';
7047 }
7048
7049 public function mxchat_telegram_notification_message_callback() {
7050 $message = isset($this->options['telegram_notification_message'])
7051 ? $this->options['telegram_notification_message']
7052 : __("I've notified a support agent. Please allow a moment for them to respond.", 'mxchat');
7053
7054 printf(
7055 '<textarea id="telegram_notification_message" name="telegram_notification_message" rows="3" cols="50">%s</textarea>',
7056 esc_textarea($message)
7057 );
7058 echo '<p class="description">' . esc_html__('Message shown when live transfer activated.', 'mxchat') . '</p>';
7059 }
7060
7061 public function mxchat_telegram_away_message_callback() {
7062 $message = isset($this->options['telegram_away_message'])
7063 ? $this->options['telegram_away_message']
7064 : __('Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.', 'mxchat');
7065
7066 printf(
7067 '<textarea id="telegram_away_message" name="telegram_away_message" rows="3" cols="50">%s</textarea>',
7068 esc_textarea($message)
7069 );
7070 echo '<p class="description">' . esc_html__('Message shown when live agents are offline.', 'mxchat') . '</p>';
7071 }
7072
7073 public function mxchat_telegram_bot_token_callback() {
7074 printf(
7075 '<input type="password" id="telegram_bot_token" name="telegram_bot_token" value="%s" class="regular-text" />',
7076 isset($this->options['telegram_bot_token']) ? esc_attr($this->options['telegram_bot_token']) : ''
7077 );
7078 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>';
7079 echo '<p class="description">' . esc_html__('Your Telegram Bot Token from @BotFather (e.g., 123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ).', 'mxchat') . '</p>';
7080 }
7081
7082 public function mxchat_telegram_group_id_callback() {
7083 printf(
7084 '<input type="text" id="telegram_group_id" name="telegram_group_id" value="%s" class="regular-text" />',
7085 isset($this->options['telegram_group_id']) ? esc_attr($this->options['telegram_group_id']) : ''
7086 );
7087 echo '<p class="description">' . esc_html__('Your Telegram supergroup ID (starts with -100). The group must have forum topics enabled.', 'mxchat') . '</p>';
7088 }
7089
7090 public function mxchat_telegram_webhook_secret_callback() {
7091 $secret = isset($this->options['telegram_webhook_secret']) ? $this->options['telegram_webhook_secret'] : '';
7092
7093 // Auto-generate secret if empty
7094 if (empty($secret)) {
7095 $secret = wp_generate_password(64, false, false);
7096 $options = get_option('mxchat_options', []);
7097 $options['telegram_webhook_secret'] = $secret;
7098 update_option('mxchat_options', $options);
7099 $this->options['telegram_webhook_secret'] = $secret;
7100 }
7101
7102 printf(
7103 '<input type="password" id="telegram_webhook_secret" name="telegram_webhook_secret" value="%s" class="regular-text" />',
7104 esc_attr($secret)
7105 );
7106 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>';
7107 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>';
7108 }
7109
7110 public function mxchat_similarity_threshold_callback() {
7111 // Load from mxchat_options array
7112 $options = get_option('mxchat_options', []);
7113
7114 // Get value from options array with default of 35
7115 $threshold = isset($options['similarity_threshold']) ? $options['similarity_threshold'] : 35;
7116
7117 echo '<div class="slider-container">';
7118 echo sprintf(
7119 '<input type="range"
7120 id="similarity_threshold"
7121 name="similarity_threshold"
7122 min="20"
7123 max="85"
7124 step="1"
7125 value="%s"
7126 class="range-slider" />',
7127 esc_attr($threshold)
7128 );
7129 echo sprintf(
7130 '<span id="threshold_value" class="range-value">%s</span>',
7131 esc_html($threshold)
7132 );
7133 echo '</div>';
7134 }
7135
7136 public function mxchat_rag_sources_limit_callback() {
7137 // Load from mxchat_options array
7138 $options = get_option('mxchat_options', []);
7139
7140 // Get value from options array with default of 3
7141 $rag_sources_limit = isset($options['rag_sources_limit']) ? intval($options['rag_sources_limit']) : 3;
7142
7143 echo '<div class="slider-container">';
7144 echo sprintf(
7145 '<input type="range"
7146 id="rag_sources_limit"
7147 name="rag_sources_limit"
7148 min="3"
7149 max="10"
7150 step="1"
7151 value="%s"
7152 class="range-slider" />',
7153 esc_attr($rag_sources_limit)
7154 );
7155 echo sprintf(
7156 '<span id="rag_sources_limit_value" class="range-value">%s</span>',
7157 esc_html($rag_sources_limit)
7158 );
7159 echo '</div>';
7160 }
7161
7162 public function mxchat_rag_chunks_limit_callback() {
7163 // Load from mxchat_options array
7164 $options = get_option('mxchat_options', []);
7165
7166 // Get value from options array with default of 15
7167 $rag_chunks_limit = isset($options['rag_chunks_limit']) ? intval($options['rag_chunks_limit']) : 15;
7168
7169 echo '<div class="slider-container">';
7170 echo sprintf(
7171 '<input type="range"
7172 id="rag_chunks_limit"
7173 name="rag_chunks_limit"
7174 min="8"
7175 max="20"
7176 step="1"
7177 value="%s"
7178 class="range-slider" />',
7179 esc_attr($rag_chunks_limit)
7180 );
7181 echo sprintf(
7182 '<span id="rag_chunks_limit_value" class="range-value">%s</span>',
7183 esc_html($rag_chunks_limit)
7184 );
7185 echo '</div>';
7186 }
7187
7188 public function mxchat_enqueue_admin_assets() {
7189 // Get plugin version
7190 $version = MXCHAT_VERSION;
7191
7192 // Use file modification time for development (remove in production)
7193 if (defined('WP_DEBUG') && WP_DEBUG) {
7194 $version = filemtime(plugin_dir_path(__FILE__) . '../mxchat-basic.php');
7195 }
7196
7197 $current_page = isset($_GET['page']) ? $_GET['page'] : '';
7198 $plugin_url = plugin_dir_url(__FILE__) . '../';
7199
7200 // Only load on MxChat pages
7201 if (strpos($current_page, 'mxchat') === false) {
7202 return;
7203 }
7204
7205 // Always load these on all MxChat pages
7206 $this->enqueue_core_admin_assets($plugin_url, $version);
7207 $this->enqueue_page_specific_assets($current_page, $plugin_url, $version);
7208 $this->localize_admin_scripts($current_page);
7209 }
7210 private function enqueue_core_admin_assets($plugin_url, $version) {
7211 // Core admin styles
7212 wp_enqueue_style('mxchat-admin-css', $plugin_url . 'css/admin-style.css', array(), $version);
7213 wp_enqueue_style('mxchat-knowledge-css', $plugin_url . 'css/knowledge-style.css', array(), $version);
7214
7215 // New sidebar navigation styles (for main settings page)
7216 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
7217
7218 // Core admin scripts
7219 wp_enqueue_script('mxchat-admin-js', $plugin_url . 'js/mxchat-admin.js', array('jquery'), $version, true);
7220 }
7221 private function enqueue_page_specific_assets($current_page, $plugin_url, $version) {
7222 switch ($current_page) {
7223 case 'mxchat-prompts':
7224 // Knowledge processing page assets
7225 wp_enqueue_style('mxchat-content-selector-css', $plugin_url . 'css/content-selector.css', array(), $version);
7226 wp_enqueue_script('mxchat-content-selector-js', $plugin_url . 'js/content-selector.js', array('jquery'), $version, true);
7227 // Add the knowledge processing script (common script needed for WordPress dismiss functionality)
7228 wp_enqueue_script('mxchat-knowledge-processing', $plugin_url . 'js/knowledge-processing.js', array('jquery', 'common'), $version, true);
7229 break;
7230
7231 case 'mxchat-transcripts':
7232 // Load admin sidebar CSS first (shared styles for sidebar navigation)
7233 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
7234 // Load transcripts-specific styles
7235 wp_enqueue_style('mxchat-chat-transcripts-css', $plugin_url . 'css/chat-transcripts.css', array('mxchat-admin-sidebar-css'), $version);
7236 wp_enqueue_script('mxchat-transcripts-js', $plugin_url . 'js/mxchat_transcripts.js', array('jquery'), $version, true);
7237 break;
7238
7239 case 'mxchat-actions':
7240 // Load admin sidebar CSS first (shared styles for sidebar navigation)
7241 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
7242 // Load actions-specific styles
7243 wp_enqueue_style('mxchat-actions-css', $plugin_url . 'css/actions.css', array('mxchat-admin-sidebar-css'), $version);
7244 wp_enqueue_script('mxchat-actions-js', $plugin_url . 'js/mxchat_actions.js', array('jquery'), $version, true);
7245
7246 // Localize script data for actions page
7247 $is_activated = get_option('mxchat_pro_license_status') === 'active';
7248 wp_localize_script('mxchat-actions-js', 'mxchActionsData', array(
7249 'ajaxUrl' => admin_url('admin-ajax.php'),
7250 'nonce' => wp_create_nonce('mxchat_actions_nonce'),
7251 'addNonce' => wp_create_nonce('mxchat_add_intent_nonce'),
7252 'editNonce' => wp_create_nonce('mxchat_edit_intent'),
7253 'deleteNonce' => wp_create_nonce('mxchat_delete_intent_nonce'),
7254 'toggleNonce' => wp_create_nonce('mxchat_actions_nonce'),
7255 'addPhraseNonce' => wp_create_nonce('mxchat_add_phrase_nonce'),
7256 'deletePhraseNonce' => wp_create_nonce('mxchat_delete_phrase_nonce'),
7257 'getPhrasesNonce' => wp_create_nonce('mxchat_get_phrases_nonce'),
7258 'deleteLegacyNonce' => wp_create_nonce('mxchat_delete_legacy_nonce'),
7259 'isActivated' => $is_activated,
7260 'i18n' => array(
7261 'confirmDelete' => __('Are you sure you want to delete this action?', 'mxchat'),
7262 'confirmBulkDelete' => __('Are you sure you want to delete the selected actions?', 'mxchat'),
7263 'saving' => __('Saving...', 'mxchat'),
7264 'saved' => __('Saved successfully!', 'mxchat'),
7265 'error' => __('An error occurred. Please try again.', 'mxchat'),
7266 'proRequired' => __('This feature requires MxChat Pro.', 'mxchat'),
7267 'addonRequired' => __('This feature requires an add-on.', 'mxchat')
7268 )
7269 ));
7270 break;
7271
7272 case 'mxchat-activation':
7273 // Load admin sidebar CSS first (shared styles for sidebar navigation)
7274 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
7275 // Load pro page-specific styles
7276 wp_enqueue_style('mxchat-pro-css', $plugin_url . 'css/admin-pro.css', array('mxchat-admin-sidebar-css'), $version);
7277 // Load pro page JavaScript
7278 wp_enqueue_script('mxchat-pro-js', $plugin_url . 'js/mxchat_pro.js', array('jquery'), $version, true);
7279 // Load activation script for license activation/deactivation
7280 wp_enqueue_script('mxchat-activation-js', $plugin_url . 'js/activation-script.js', array('jquery'), $version, true);
7281 break;
7282
7283 case 'mxchat-content':
7284 // Load admin sidebar CSS (shared styles for sidebar navigation)
7285 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
7286 // Load content page-specific styles
7287 wp_enqueue_style('mxchat-content-css', $plugin_url . 'css/admin-content.css', array('mxchat-admin-sidebar-css'), $version);
7288 // Load content page JavaScript
7289 wp_enqueue_script('mxchat-content-js', $plugin_url . 'js/mxchat-content.js', array('jquery'), $version, true);
7290 break;
7291 default:
7292 wp_enqueue_script(
7293 'mxchat-test-streaming-js',
7294 $plugin_url . 'js/mxchat-test-streaming.js',
7295 ['jquery'],
7296 $version,
7297 true
7298 );
7299
7300 wp_localize_script('mxchat-test-streaming-js', 'mxchatTestStreamingAjax', [
7301 'ajax_url' => admin_url('admin-ajax.php'),
7302 'nonce' => wp_create_nonce('mxchat_test_streaming_nonce'),
7303 'settings_nonce' => wp_create_nonce('mxchat_save_setting_nonce')
7304 ]);
7305
7306 // Testing Tab: Load chatbot assets for the embedded testing chatbot
7307 wp_enqueue_style('mxchat-chat-css', $plugin_url . 'css/chat-style.css', array(), $version);
7308 wp_enqueue_style('mxchat-admin-testing-css', $plugin_url . 'css/admin-testing-tab.css', array('mxchat-admin-sidebar-css', 'mxchat-chat-css'), $version);
7309
7310 wp_enqueue_script('mxchat-chat-js', $plugin_url . 'js/chat-script.js', array('jquery'), $version, true);
7311 wp_enqueue_script('mxchat-admin-testing-js', $plugin_url . 'js/admin-testing-tab.js', array('jquery', 'mxchat-chat-js'), $version, true);
7312
7313 // Allow add-ons to enqueue their public CSS/JS for the testing chatbot
7314 do_action('mxchat_enqueue_testing_tab_assets');
7315
7316 // Localize mxchatChat for the chatbot JS (same settings as frontend)
7317 $options = get_option('mxchat_options', array());
7318 $prompts_options = get_option('mxchat_prompts_options', array());
7319 $theme_options = get_option('mxchat_theme_options', array());
7320 $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
7321 $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
7322 $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
7323
7324 wp_localize_script('mxchat-chat-js', 'mxchatChat', array(
7325 'ajax_url' => admin_url('admin-ajax.php'),
7326 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
7327 'model' => isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest',
7328 'enable_streaming_toggle' => isset($options['enable_streaming_toggle']) ? $options['enable_streaming_toggle'] : 'on',
7329 'contextual_awareness_toggle' => isset($options['contextual_awareness_toggle']) ? $options['contextual_awareness_toggle'] : 'off',
7330 'link_target_toggle' => $options['link_target_toggle'] ?? 'off',
7331 'rate_limit_message' => $options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
7332 'complianz_toggle' => isset($options['complianz_toggle']) && $options['complianz_toggle'] === 'on',
7333 'user_message_bg_color' => $options['user_message_bg_color'] ?? '#fff',
7334 'user_message_font_color' => $options['user_message_font_color'] ?? '#212121',
7335 'bot_message_bg_color' => $options['bot_message_bg_color'] ?? '#212121',
7336 'bot_message_font_color' => $options['bot_message_font_color'] ?? '#fff',
7337 'top_bar_bg_color' => $options['top_bar_bg_color'] ?? '#212121',
7338 'send_button_font_color' => $options['send_button_font_color'] ?? '#212121',
7339 'close_button_color' => $options['close_button_color'] ?? '#fff',
7340 'chatbot_background_color' => $options['chatbot_background_color'] ?? '#212121',
7341 'chatbot_bg_color' => $options['chatbot_bg_color'] ?? '#fff',
7342 'icon_color' => $options['icon_color'] ?? '#fff',
7343 'chat_input_font_color' => $options['chat_input_font_color'] ?? '#212121',
7344 'chat_persistence_toggle' => 'off', // Always off for testing chatbot
7345 'appendWidgetToBody' => 'off',
7346 'live_agent_message_bg_color' => $options['live_agent_message_bg_color'] ?? '#ffffff',
7347 'live_agent_message_font_color' => $options['live_agent_message_font_color'] ?? '#333333',
7348 'chat_toolbar_toggle' => $options['chat_toolbar_toggle'] ?? 'off',
7349 'mode_indicator_bg_color' => $options['mode_indicator_bg_color'] ?? '#767676',
7350 'mode_indicator_font_color' => $options['mode_indicator_font_color'] ?? '#ffffff',
7351 'toolbar_icon_color' => $options['toolbar_icon_color'] ?? '#212121',
7352 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
7353 'email_collection_enabled' => 'off', // No email collection in testing
7354 'initial_email_state' => null,
7355 'skip_email_check' => true,
7356 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
7357 'skip_inline_colors' => $skip_inline_colors,
7358 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array()
7359 ));
7360
7361 // Localize testing tab script data
7362 wp_localize_script('mxchat-admin-testing-js', 'mxchatAdminTestData', array(
7363 'ajaxUrl' => admin_url('admin-ajax.php'),
7364 'nonce' => wp_create_nonce('mxchat_test_nonce'),
7365 'isAdmin' => true,
7366 'testingEnabled' => true
7367 ));
7368
7369 // Add testing enabled flag for the chatbot to return debug data
7370 add_action('admin_footer', function() {
7371 echo '<script>window.mxchatTestingEnabled = true;</script>';
7372 });
7373
7374 break;
7375 }
7376 }
7377 private function localize_admin_scripts($current_page) {
7378 // Base localization data for main admin script
7379 $base_data = array(
7380 'ajax_url' => admin_url('admin-ajax.php'),
7381 'nonce' => wp_create_nonce('mxchat_admin_nonce'),
7382 'admin_url' => admin_url(),
7383 'license_nonce' => wp_create_nonce('mxchat_activate_license_nonce'),
7384 'inline_edit_nonce' => wp_create_nonce('mxchat_save_inline_nonce'),
7385 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
7386 'export_nonce' => wp_create_nonce('mxchat_export_transcripts'),
7387 'actions_nonce' => wp_create_nonce('mxchat_actions_nonce'),
7388 'add_intent_nonce' => wp_create_nonce('mxchat_add_intent_nonce'),
7389 'edit_intent_nonce' => wp_create_nonce('mxchat_edit_intent'),
7390 'add_phrase_nonce' => wp_create_nonce('mxchat_add_phrase_nonce'),
7391 'delete_phrase_nonce' => wp_create_nonce('mxchat_delete_phrase_nonce'),
7392 'get_phrases_nonce' => wp_create_nonce('mxchat_get_phrases_nonce'),
7393 'delete_legacy_nonce' => wp_create_nonce('mxchat_delete_legacy_nonce'),
7394 'toggle_action_nonce' => wp_create_nonce('mxchat_actions_nonce'),
7395 'fetch_openrouter_models_nonce' => wp_create_nonce('mxchat_fetch_openrouter_models'),
7396 'is_activated' => $this->is_activated ? '1' : '0',
7397 'status_refresh_interval' => 5000,
7398 'prompts_setting_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
7399 'ajaxurl' => admin_url('admin-ajax.php')
7400 );
7401
7402 // Localize main admin script with base data
7403 wp_localize_script('mxchat-admin-js', 'mxchatAdmin', $base_data);
7404
7405 // Page-specific localizations
7406 $this->localize_page_specific_scripts($current_page);
7407 }
7408 private function localize_page_specific_scripts($current_page) {
7409 switch ($current_page) {
7410 case 'mxchat-prompts':
7411 // Status updater localization
7412 wp_localize_script('mxchat-status-updater', 'mxchat_status_data', array(
7413 'ajax_url' => admin_url('admin-ajax.php'),
7414 'nonce' => wp_create_nonce('mxchat_status_nonce')
7415 ));
7416
7417 // Content selector localization
7418 wp_localize_script('mxchat-content-selector-js', 'mxchatSelector', array(
7419 'ajaxurl' => admin_url('admin-ajax.php'),
7420 'nonce' => wp_create_nonce('mxchat_content_selector_nonce'),
7421 'i18n' => array(
7422 'searchPlaceholder' => __('Search posts and pages...', 'mxchat'),
7423 'selectAll' => __('Select All', 'mxchat'),
7424 'process' => __('Process Selected', 'mxchat'),
7425 'cancel' => __('Cancel', 'mxchat'),
7426 'noResults' => __('No content found.', 'mxchat')
7427 )
7428 ));
7429
7430 wp_localize_script('mxchat-knowledge-processing', 'mxchatAdmin', array(
7431 'ajax_url' => admin_url('admin-ajax.php'),
7432 'status_nonce' => wp_create_nonce('mxchat_status_nonce'),
7433 'queue_nonce' => wp_create_nonce('mxchat_queue_nonce'),
7434 'stop_nonce' => wp_create_nonce('mxchat_stop_processing_action'),
7435 'settings_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
7436 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
7437 'entries_nonce' => wp_create_nonce('mxchat_entries_nonce'),
7438 'admin_url' => admin_url(),
7439 'ajaxurl' => admin_url('admin-ajax.php'),
7440 'status_refresh_interval' => 2000,
7441 'bot_id' => isset($_GET['bot_id']) ? sanitize_text_field($_GET['bot_id']) : 'default'
7442 ));
7443 break;
7444
7445 case 'mxchat-activation':
7446 wp_localize_script('mxchat-activation-js', 'mxchatAdmin', array(
7447 'ajax_url' => admin_url('admin-ajax.php'),
7448 'license_nonce' => wp_create_nonce('mxchat_activate_license_nonce')
7449 ));
7450 break;
7451
7452 case 'mxchat-content':
7453 wp_localize_script('mxchat-content-js', 'mxchatContent', array(
7454 'ajaxUrl' => admin_url('admin-ajax.php'),
7455 'nonce' => wp_create_nonce('mxchat_content_nonce'),
7456 'settingNonce' => wp_create_nonce('mxchat_save_setting_nonce'),
7457 'previewUrl' => home_url('/?p='),
7458 'isActivated' => $this->is_activated(),
7459 'hasAdvancedContent' => apply_filters('mxchat_content_pro_feature', false, 'seo_readability'),
7460 'hasGSC' => apply_filters('mxchat_content_pro_feature', false, 'gsc_integration'),
7461 'seoOptimize' => array(
7462 'meta_description' => ($options['seo_optimize_meta_desc'] ?? 'on') === 'on',
7463 'seo_title' => ($options['seo_optimize_seo_title'] ?? 'on') === 'on',
7464 'slug' => ($options['seo_optimize_slug'] ?? 'on') === 'on',
7465 'readability' => ($options['seo_optimize_readability'] ?? 'on') === 'on',
7466 'internal_links' => ($options['seo_optimize_internal_links'] ?? 'on') === 'on',
7467 'img_alt' => ($options['seo_optimize_img_alt'] ?? 'on') === 'on',
7468 'featured_img' => ($options['seo_optimize_featured_img'] ?? 'on') === 'on',
7469 ),
7470 'i18n' => array(
7471 'generating' => __('Generating...', 'mxchat'),
7472 'planning' => __('Planning content structure...', 'mxchat'),
7473 'images' => __('Generating images...', 'mxchat'),
7474 'writing' => __('Writing full content...', 'mxchat'),
7475 'creating' => __('Creating WordPress post...', 'mxchat'),
7476 'done' => __('Content generated successfully!', 'mxchat'),
7477 'error' => __('An error occurred. Please try again.', 'mxchat'),
7478 'editSuccess' => __('Content updated successfully.', 'mxchat'),
7479 'promptEmpty' => __('Please enter a prompt.', 'mxchat'),
7480 )
7481 ));
7482 break;
7483
7484 case 'mxchat-transcripts':
7485 // Get chart data for localization
7486 $chart_data = $this->get_transcripts_chart_data();
7487
7488 wp_localize_script('mxchat-transcripts-js', 'mxchatAdmin', array(
7489 'ajax_url' => admin_url('admin-ajax.php'),
7490 'export_nonce' => wp_create_nonce('mxchat_export_transcripts'),
7491 'delete_nonce' => wp_create_nonce('mxchat_delete_chat_history'),
7492 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
7493 'translate_nonce' => wp_create_nonce('mxchat_translate_messages')
7494 ));
7495
7496 // Localize chart data separately - use array_values to ensure proper JSON array encoding
7497 wp_localize_script('mxchat-transcripts-js', 'mxchatChartData', array(
7498 'labels' => array_values($chart_data['labels']),
7499 'chats' => array_values($chart_data['chats']),
7500 'messages' => array_values($chart_data['messages'])
7501 ));
7502 break;
7503
7504 case 'mxchat-settings':
7505 default:
7506 // Color picker and settings localization
7507 wp_localize_script('mxchat-color-picker', 'mxchatStyleSettings', array(
7508 'ajax_url' => admin_url('admin-ajax.php'),
7509 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
7510 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
7511 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
7512 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
7513 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
7514 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
7515 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
7516 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
7517 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
7518 'icon_color' => $this->options['icon_color'] ?? '#fff',
7519 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
7520 'pre_chat_message' => $this->options['pre_chat_message'] ?? esc_html__('Hey there! Ask me anything!', 'mxchat'),
7521 'rate_limit_message' => $this->options['rate_limit_message'] ?? esc_html__('Rate limit exceeded. Please try again later.', 'mxchat'),
7522 'loops_api_key' => $this->options['loops_api_key'] ?? '',
7523 'loops_mailing_list' => $this->options['loops_mailing_list'] ?? '',
7524 'triggered_phrase_response' => $this->options['triggered_phrase_response'] ?? esc_html__('Would you like to join our mailing list? Please provide your email below.', 'mxchat'),
7525 'email_capture_response' => $this->options['email_capture_response'] ?? esc_html__('Thank you for providing your email! You\'ve been added to our list.', 'mxchat'),
7526 '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'),
7527 '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'),
7528 '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'),
7529 'pdf_max_pages' => $this->options['pdf_max_pages'] ?? 69,
7530 'live_agent_webhook_url' => $this->options['live_agent_webhook_url'] ?? '',
7531 'live_agent_secret_key' => $this->options['live_agent_secret_key'] ?? '',
7532 'live_agent_bot_token' => $this->options['live_agent_bot_token'] ?? '',
7533 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
7534 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
7535 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
7536 'show_pdf_upload_button' => $this->options['show_pdf_upload_button'] ?? 'on',
7537 'show_word_upload_button' => $this->options['show_word_upload_button'] ?? 'on',
7538 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
7539 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
7540 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
7541 ));
7542 break;
7543 }
7544
7545 // Additional localization that was in the original code
7546 wp_localize_script('mxchat-admin-js', 'mxchatPromptsAdmin', array(
7547 'ajax_url' => admin_url('admin-ajax.php'),
7548 'prompts_setting_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
7549 ));
7550 }
7551
7552 public function mxchat_sanitize($input) {
7553 $new_input = array();
7554
7555 if (isset($input['api_key'])) {
7556 $new_input['api_key'] = sanitize_text_field($input['api_key']);
7557 }
7558
7559 if (isset($input['similarity_threshold'])) {
7560 $new_input['similarity_threshold'] = absint($input['similarity_threshold']); // Ensure it's an integer
7561 $new_input['similarity_threshold'] = min(max($new_input['similarity_threshold'], 20), 85); // Enforce range
7562 }
7563
7564 if (isset($input['rag_sources_limit'])) {
7565 $new_input['rag_sources_limit'] = absint($input['rag_sources_limit']); // Ensure it's an integer
7566 $new_input['rag_sources_limit'] = min(max($new_input['rag_sources_limit'], 3), 10); // Enforce range 3-10
7567 }
7568
7569 if (isset($input['rag_chunks_limit'])) {
7570 $new_input['rag_chunks_limit'] = absint($input['rag_chunks_limit']); // Ensure it's an integer
7571 $new_input['rag_chunks_limit'] = min(max($new_input['rag_chunks_limit'], 8), 20); // Enforce range 8-20
7572 }
7573
7574 if (isset($input['xai_api_key'])) {
7575 $new_input['xai_api_key'] = sanitize_text_field($input['xai_api_key']);
7576 }
7577
7578 if (isset($input['claude_api_key'])) {
7579 $new_input['claude_api_key'] = sanitize_text_field($input['claude_api_key']);
7580 }
7581
7582 if (isset($input['enable_streaming_toggle'])) {
7583 $new_input['enable_streaming_toggle'] = ($input['enable_streaming_toggle'] === 'on') ? 'on' : 'off';
7584 } else {
7585 // If checkbox not checked, it won't be in $input, so set to 'off'
7586 $new_input['enable_streaming_toggle'] = 'off';
7587 }
7588
7589 if (isset($input['enable_web_search'])) {
7590 $new_input['enable_web_search'] = ($input['enable_web_search'] === 'on') ? 'on' : 'off';
7591 } else {
7592 // If checkbox not checked, it won't be in $input, so set to 'off'
7593 $new_input['enable_web_search'] = 'off';
7594 }
7595
7596 if (isset($input['deepseek_api_key'])) {
7597 $new_input['deepseek_api_key'] = sanitize_text_field($input['deepseek_api_key']);
7598 }
7599
7600 if (isset($input['gemini_api_key'])) {
7601 $new_input['gemini_api_key'] = sanitize_text_field($input['gemini_api_key']);
7602 }
7603
7604 if (isset($input['enable_woocommerce_integration'])) {
7605 $new_input['enable_woocommerce_integration'] = $input['enable_woocommerce_integration'] === 'on' ? 'on' : 'off';
7606 }
7607
7608 if (isset($input['privacy_toggle'])) {
7609 $new_input['privacy_toggle'] = $input['privacy_toggle'];
7610 }
7611
7612 if (isset($input['complianz_toggle'])) {
7613 $new_input['complianz_toggle'] = $input['complianz_toggle'];
7614 }
7615
7616 // Handle custom privacy text input
7617 if (isset($input['privacy_text'])) {
7618 // Allow basic HTML for links
7619 $new_input['privacy_text'] = wp_kses_post($input['privacy_text']);
7620 }
7621
7622 if (isset($input['system_prompt_instructions'])) {
7623 $new_input['system_prompt_instructions'] = sanitize_textarea_field($input['system_prompt_instructions']);
7624 }
7625
7626 if (isset($input['mxchat_pro_email'])) {
7627 $new_input['mxchat_pro_email'] = sanitize_email($input['mxchat_pro_email']);
7628 }
7629
7630 if (isset($input['mxchat_activation_key'])) {
7631 $new_input['mxchat_activation_key'] = sanitize_text_field($input['mxchat_activation_key']);
7632 }
7633
7634 if (isset($input['append_to_body'])) {
7635 $new_input['append_to_body'] = $input['append_to_body'] === 'on' ? 'on' : 'off';
7636 }
7637
7638 // Post type visibility settings
7639 if (isset($input['post_type_visibility_mode'])) {
7640 $allowed_modes = array('all', 'include', 'exclude');
7641 $new_input['post_type_visibility_mode'] = in_array($input['post_type_visibility_mode'], $allowed_modes)
7642 ? $input['post_type_visibility_mode']
7643 : 'all';
7644 }
7645
7646 if (isset($input['post_type_visibility_list'])) {
7647 if (is_array($input['post_type_visibility_list'])) {
7648 $new_input['post_type_visibility_list'] = array_map('sanitize_key', $input['post_type_visibility_list']);
7649 } else {
7650 $new_input['post_type_visibility_list'] = array();
7651 }
7652 }
7653
7654 if (isset($input['contextual_awareness_toggle'])) {
7655 $new_input['contextual_awareness_toggle'] = $input['contextual_awareness_toggle'] === 'on' ? 'on' : 'off';
7656 }
7657
7658 if (isset($input['citation_links_toggle'])) {
7659 $new_input['citation_links_toggle'] = $input['citation_links_toggle'] === 'on' ? 'on' : 'off';
7660 }
7661
7662 if (isset($input['top_bar_title'])) {
7663 $new_input['top_bar_title'] = sanitize_text_field($input['top_bar_title']);
7664 }
7665
7666 if (isset($input['ai_agent_text'])) {
7667 $new_input['ai_agent_text'] = sanitize_text_field($input['ai_agent_text']);
7668 }
7669
7670 if (isset($input['enable_email_block'])) {
7671 $new_input['enable_email_block'] = sanitize_text_field($input['enable_email_block']);
7672 }
7673
7674 if (isset($input['email_blocker_header_content'])) {
7675 // wp_kses_post() allows standard HTML tags permitted by WordPress
7676 $new_input['email_blocker_header_content'] = wp_kses_post($input['email_blocker_header_content']);
7677 }
7678 if (isset($input['email_blocker_button_text'])) {
7679 $new_input['email_blocker_button_text'] = sanitize_text_field($input['email_blocker_button_text']);
7680 }
7681 // Sanitize name field toggle
7682 if (isset($input['enable_name_field'])) {
7683 $new_input['enable_name_field'] = ($input['enable_name_field'] === 'on') ? 'on' : 'off';
7684 } else {
7685 $new_input['enable_name_field'] = 'off';
7686 }
7687 // Sanitize name field placeholder
7688 if (isset($input['name_field_placeholder'])) {
7689 $new_input['name_field_placeholder'] = sanitize_text_field($input['name_field_placeholder']);
7690 }
7691 if (isset($input['intro_message'])) {
7692 $new_input['intro_message'] = wp_kses_post($input['intro_message']); // Use wp_kses_post instead
7693 }
7694
7695 if (isset($input['input_copy'])) {
7696 $new_input['input_copy'] = sanitize_text_field($input['input_copy']);
7697 }
7698
7699 if (isset($input['rate_limit_message'])) {
7700 $new_input['rate_limit_message'] = sanitize_text_field($input['rate_limit_message']);
7701 }
7702
7703 // Handle the new rate limits format
7704 if (isset($input['rate_limits']) && is_array($input['rate_limits'])) {
7705 $new_input['rate_limits'] = array();
7706 $allowed_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited');
7707 $allowed_timeframes = array('hourly', 'daily', 'weekly', 'monthly');
7708
7709 foreach ($input['rate_limits'] as $role_id => $settings) {
7710 $new_input['rate_limits'][$role_id] = array();
7711
7712 // Sanitize limit
7713 if (isset($settings['limit'])) {
7714 $limit = sanitize_text_field($settings['limit']);
7715 if (in_array($limit, $allowed_limits, true)) {
7716 $new_input['rate_limits'][$role_id]['limit'] = $limit;
7717 } else {
7718 $new_input['rate_limits'][$role_id]['limit'] = ($role_id === 'logged_out') ? '10' : '100'; // Default
7719 }
7720 }
7721
7722 // Sanitize timeframe
7723 if (isset($settings['timeframe'])) {
7724 $timeframe = sanitize_text_field($settings['timeframe']);
7725 if (in_array($timeframe, $allowed_timeframes, true)) {
7726 $new_input['rate_limits'][$role_id]['timeframe'] = $timeframe;
7727 } else {
7728 $new_input['rate_limits'][$role_id]['timeframe'] = 'daily'; // Default
7729 }
7730 }
7731
7732 // Sanitize message
7733 if (isset($settings['message'])) {
7734 $new_input['rate_limits'][$role_id]['message'] = sanitize_textarea_field($settings['message']);
7735 }
7736 }
7737 }
7738
7739 if (isset($input['pre_chat_message'])) {
7740 $new_input['pre_chat_message'] = sanitize_textarea_field($input['pre_chat_message']);
7741 }
7742
7743 if (isset($input['voyage_api_key'])) {
7744 $new_input['voyage_api_key'] = sanitize_text_field($input['voyage_api_key']);
7745 }
7746
7747 // Add to your sanitize function
7748 if (isset($input['embedding_model'])) {
7749 $allowed_models = array(
7750 'text-embedding-ada-002',
7751 'text-embedding-3-small',
7752 'text-embedding-3-large',
7753 'voyage-3-large',
7754 'gemini-embedding-001'
7755 );
7756 if (in_array($input['embedding_model'], $allowed_models)) {
7757 $new_input['embedding_model'] = sanitize_text_field($input['embedding_model']);
7758 }
7759 }
7760
7761 if (isset($input['model'])) {
7762 if ($input['model'] === 'openrouter') {
7763 $new_input['model'] = 'openrouter';
7764 } else {
7765 $allowed_models = array(
7766 'gemini-3-pro-preview',
7767 'gemini-3-flash-preview',
7768 'gemini-2.5-pro',
7769 'gemini-2.5-flash',
7770 'gemini-2.5-flash-lite',
7771 'gemini-2.0-flash',
7772 'gemini-2.0-flash-lite',
7773 'gemini-1.5-pro',
7774 'gemini-1.5-flash',
7775 'grok-4-0709',
7776 'grok-4-1-fast-reasoning',
7777 'grok-4-1-fast-non-reasoning',
7778 'grok-3-beta',
7779 'grok-3-fast-beta',
7780 'grok-3-mini-beta',
7781 'grok-3-mini-fast-beta',
7782 'grok-2',
7783 'deepseek-chat',
7784 'claude-opus-4-6',
7785 'claude-sonnet-4-6',
7786 'claude-opus-4-5',
7787 'claude-sonnet-4-5-20250929',
7788 'claude-opus-4-1-20250805',
7789 'claude-haiku-4-5-20251001',
7790 'claude-opus-4-20250514',
7791 'claude-sonnet-4-20250514',
7792 'gpt-5.2',
7793 'gpt-5.1-chat-latest',
7794 'gpt-5.1-2025-11-13',
7795 'gpt-5',
7796 'gpt-5-mini',
7797 'gpt-5-nano',
7798 'gpt-5.4',
7799 'gpt-5.4-mini',
7800 'gpt-5.4-nano',
7801 'gpt-5.3-chat-latest',
7802 );
7803
7804 if (in_array($input['model'], $allowed_models)) {
7805 $new_input['model'] = sanitize_text_field($input['model']);
7806 } else {
7807 // Fallback for any deprecated model
7808 $new_input['model'] = 'gpt-5.1-chat-latest';
7809 }
7810 }
7811 }
7812
7813 if (isset($input['openrouter_selected_model'])) {
7814 // Just sanitize it, don't validate against a whitelist
7815 $new_input['openrouter_selected_model'] = sanitize_text_field($input['openrouter_selected_model']);
7816 }
7817
7818 // ADD THIS:
7819 if (isset($input['openrouter_selected_model_name'])) {
7820 $new_input['openrouter_selected_model_name'] = sanitize_text_field($input['openrouter_selected_model_name']);
7821 }
7822
7823 if (isset($input['openrouter_api_key'])) {
7824 $new_input['openrouter_api_key'] = sanitize_text_field($input['openrouter_api_key']);
7825 }
7826
7827
7828
7829 if (isset($input['woocommerce_consumer_key'])) {
7830 $new_input['woocommerce_consumer_key'] = sanitize_text_field($input['woocommerce_consumer_key']);
7831 }
7832
7833 if (isset($input['woocommerce_consumer_secret'])) {
7834 $new_input['woocommerce_consumer_secret'] = sanitize_text_field($input['woocommerce_consumer_secret']);
7835 }
7836
7837
7838 // Sanitize link_target_toggle
7839 if (isset($input['link_target_toggle'])) {
7840 $new_input['link_target_toggle'] = $input['link_target_toggle'] === 'on' ? 'on' : 'off';
7841 }
7842
7843 // Sanitize Loops API Key
7844 if (isset($input['loops_api_key'])) {
7845 $new_input['loops_api_key'] = sanitize_text_field($input['loops_api_key']);
7846 }
7847
7848 if (isset($input['chat_persistence_toggle'])) {
7849 $new_input['chat_persistence_toggle'] = $input['chat_persistence_toggle'] === 'on' ? 'on' : 'off';
7850 }
7851
7852 if (isset($input['popular_question_1'])) {
7853 $new_input['popular_question_1'] = sanitize_text_field($input['popular_question_1']);
7854 }
7855
7856 if (isset($input['popular_question_2'])) {
7857 $new_input['popular_question_2'] = sanitize_text_field($input['popular_question_2']);
7858 }
7859
7860 if (isset($input['popular_question_3'])) {
7861 $new_input['popular_question_3'] = sanitize_text_field($input['popular_question_3']);
7862 }
7863
7864 if (isset($input['additional_popular_questions']) && is_array($input['additional_popular_questions'])) {
7865 $new_input['additional_popular_questions'] = array_map('sanitize_text_field', $input['additional_popular_questions']);
7866 }
7867
7868 // Sanitize Loops Mailing List
7869 if (isset($input['loops_mailing_list'])) {
7870 $new_input['loops_mailing_list'] = sanitize_text_field($input['loops_mailing_list']);
7871 }
7872
7873 // Sanitize Triggered Phrase Response
7874 if (isset($input['triggered_phrase_response'])) {
7875 $new_input['triggered_phrase_response'] = wp_kses_post($input['triggered_phrase_response']);
7876 }
7877 if (isset($input['email_capture_response'])) {
7878 $new_input['email_capture_response'] = wp_kses_post($input['email_capture_response']);
7879 }
7880
7881 // Sanitize Brave Search Settings
7882 if (isset($input['brave_api_key'])) {
7883 $new_input['brave_api_key'] = sanitize_text_field($input['brave_api_key']);
7884 }
7885
7886 if (isset($input['brave_image_count'])) {
7887 $image_count = intval($input['brave_image_count']);
7888 $new_input['brave_image_count'] = ($image_count >=1 && $image_count <=6) ? $image_count : 4;
7889 }
7890
7891 if (isset($input['brave_safe_search'])) {
7892 $allowed = array('strict', 'off');
7893 $new_input['brave_safe_search'] = in_array($input['brave_safe_search'], $allowed, true) ? $input['brave_safe_search'] : 'strict';
7894 }
7895
7896 if (isset($input['brave_news_count'])) {
7897 $news_count = intval($input['brave_news_count']);
7898 $new_input['brave_news_count'] = ($news_count >=1 && $news_count <=10) ? $news_count : 3;
7899 }
7900
7901 if (isset($input['brave_country'])) {
7902 $new_input['brave_country'] = sanitize_text_field($input['brave_country']);
7903 }
7904
7905 if (isset($input['brave_language'])) {
7906 $new_input['brave_language'] = sanitize_text_field($input['brave_language']);
7907 }
7908
7909 if (isset($input['chat_toolbar_toggle'])) {
7910 $new_input['chat_toolbar_toggle'] = $input['chat_toolbar_toggle'] === 'on' ? 'on' : 'off';
7911 }
7912
7913 // Sanitize PDF upload button toggle
7914 if (isset($input['show_pdf_upload_button'])) {
7915 $new_input['show_pdf_upload_button'] = $input['show_pdf_upload_button'] === 'on' ? 'on' : 'off';
7916 } else {
7917 $new_input['show_pdf_upload_button'] = 'off'; // If checkbox is unchecked
7918 }
7919
7920 // Sanitize Word upload button toggle
7921 if (isset($input['show_word_upload_button'])) {
7922 $new_input['show_word_upload_button'] = $input['show_word_upload_button'] === 'on' ? 'on' : 'off';
7923 } else {
7924 $new_input['show_word_upload_button'] = 'off'; // If checkbox is unchecked
7925 }
7926
7927 if (isset($input['pdf_intent_trigger_text'])) {
7928 $new_input['pdf_intent_trigger_text'] = sanitize_text_field($input['pdf_intent_trigger_text']);
7929 }
7930
7931 if (isset($input['pdf_intent_success_text'])) {
7932 $new_input['pdf_intent_success_text'] = sanitize_text_field($input['pdf_intent_success_text']);
7933 }
7934
7935 if (isset($input['pdf_intent_error_text'])) {
7936 $new_input['pdf_intent_error_text'] = sanitize_text_field($input['pdf_intent_error_text']);
7937 }
7938
7939 if (isset($input['pdf_max_pages'])) {
7940 $new_input['pdf_max_pages'] = intval($input['pdf_max_pages']);
7941 if ($new_input['pdf_max_pages'] < 1 || $new_input['pdf_max_pages'] > 69) {
7942 $new_input['pdf_max_pages'] = 69; // Default to 69 if out of range
7943 }
7944 }
7945
7946 if (isset($input['live_agent_webhook_url'])) {
7947 $new_input['live_agent_webhook_url'] = esc_url_raw($input['live_agent_webhook_url']);
7948 }
7949 if (isset($input['live_agent_secret_key'])) {
7950 $new_input['live_agent_secret_key'] = sanitize_text_field($input['live_agent_secret_key']);
7951 }
7952
7953 // Live Agent Integration
7954 if (isset($input['live_agent_bot_token'])) {
7955 $new_input['live_agent_bot_token'] = sanitize_text_field($input['live_agent_bot_token']);
7956 }
7957
7958 if (isset($input['live_agent_user_ids'])) {
7959 $new_input['live_agent_user_ids'] = sanitize_textarea_field($input['live_agent_user_ids']);
7960 }
7961
7962 if (isset($input['live_agent_status'])) {
7963 $new_input['live_agent_status'] = ($input['live_agent_status'] === 'on') ? 'on' : 'off';
7964 }
7965 if (isset($input['live_agent_away_message'])) {
7966 $new_input['live_agent_away_message'] = sanitize_textarea_field($input['live_agent_away_message']);
7967 }
7968 if (isset($input['live_agent_notification_message'])) {
7969 $new_input['live_agent_notification_message'] = sanitize_textarea_field($input['live_agent_notification_message']);
7970 }
7971
7972 // Telegram Integration
7973 if (isset($input['telegram_status'])) {
7974 $new_input['telegram_status'] = ($input['telegram_status'] === 'on') ? 'on' : 'off';
7975 }
7976 if (isset($input['telegram_bot_token'])) {
7977 $new_input['telegram_bot_token'] = sanitize_text_field($input['telegram_bot_token']);
7978 }
7979 if (isset($input['telegram_group_id'])) {
7980 $new_input['telegram_group_id'] = sanitize_text_field($input['telegram_group_id']);
7981 }
7982 if (isset($input['telegram_webhook_secret'])) {
7983 $new_input['telegram_webhook_secret'] = sanitize_text_field($input['telegram_webhook_secret']);
7984 }
7985 if (isset($input['telegram_notification_message'])) {
7986 $new_input['telegram_notification_message'] = sanitize_textarea_field($input['telegram_notification_message']);
7987 }
7988 if (isset($input['telegram_away_message'])) {
7989 $new_input['telegram_away_message'] = sanitize_textarea_field($input['telegram_away_message']);
7990 }
7991
7992 // Sanitize script loading strategy
7993 if (isset($input['script_loading_strategy'])) {
7994 $allowed_strategies = array('default', 'defer', 'delay_1s', 'delay_3s', 'delay_5s', 'on_interaction');
7995 $new_input['script_loading_strategy'] = in_array($input['script_loading_strategy'], $allowed_strategies)
7996 ? $input['script_loading_strategy']
7997 : 'default';
7998 }
7999
8000 // Sanitize debug mode
8001 if (isset($input['debug_mode'])) {
8002 $new_input['debug_mode'] = ($input['debug_mode'] === 'on') ? 'on' : 'off';
8003 }
8004
8005 // Content Generator Settings
8006 if (isset($input['content_model'])) {
8007 $new_input['content_model'] = sanitize_text_field($input['content_model']);
8008 }
8009 if (isset($input['content_image_model'])) {
8010 $new_input['content_image_model'] = sanitize_text_field($input['content_image_model']);
8011 }
8012 if (isset($input['content_enable_images'])) {
8013 $new_input['content_enable_images'] = ($input['content_enable_images'] === 'on') ? 'on' : 'off';
8014 }
8015 if (isset($input['content_use_placeholders'])) {
8016 $new_input['content_use_placeholders'] = ($input['content_use_placeholders'] === 'on') ? 'on' : 'off';
8017 }
8018 if (isset($input['content_internal_linking'])) {
8019 $new_input['content_internal_linking'] = ($input['content_internal_linking'] === 'on') ? 'on' : 'off';
8020 }
8021 if (isset($input['content_tool_use'])) {
8022 $new_input['content_tool_use'] = ($input['content_tool_use'] === 'on') ? 'on' : 'off';
8023 }
8024
8025 // SEO Optimize toggle fields
8026 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) {
8027 if (isset($input[$seo_key])) {
8028 $new_input[$seo_key] = ($input[$seo_key] === 'on') ? 'on' : 'off';
8029 }
8030 }
8031
8032 // Preserve content generator settings when saving from main settings page
8033 // (where content fields are not in the form submission)
8034 $existing = get_option('mxchat_options', array());
8035 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) {
8036 if (!isset($new_input[$key]) && isset($existing[$key])) {
8037 $new_input[$key] = $existing[$key];
8038 }
8039 }
8040
8041 return $new_input;
8042 }
8043
8044 /**
8045 * Log a debug message if debug mode is enabled
8046 *
8047 * @param string $type The type of log entry (settings_save, api_error, activation, etc.)
8048 * @param string $message The log message
8049 * @param array $data Optional additional data to log
8050 * @return bool Whether the message was logged
8051 */
8052 public static function mxchat_log_debug( $type, $message, $data = array() ) {
8053 $options = get_option( 'mxchat_options', array() );
8054
8055 // Check if debug mode is enabled
8056 if ( ! isset( $options['debug_mode'] ) || $options['debug_mode'] !== 'on' ) {
8057 return false;
8058 }
8059
8060 // Get current log
8061 $log = get_option( 'mxchat_debug_log', array() );
8062 if ( ! is_array( $log ) ) {
8063 $log = array();
8064 }
8065
8066 // Add new entry
8067 $entry = array(
8068 'time' => current_time( 'Y-m-d H:i:s' ),
8069 'type' => sanitize_key( $type ),
8070 'message' => sanitize_text_field( $message ),
8071 );
8072
8073 if ( ! empty( $data ) ) {
8074 $entry['data'] = $data;
8075 }
8076
8077 // Add to beginning of array (newest first)
8078 array_unshift( $log, $entry );
8079
8080 // Keep only last 100 entries
8081 if ( count( $log ) > 100 ) {
8082 $log = array_slice( $log, 0, 100 );
8083 }
8084
8085 // Save log
8086 update_option( 'mxchat_debug_log', $log, false );
8087
8088 return true;
8089 }
8090
8091 /**
8092 * Get the debug log entries
8093 *
8094 * @return array The debug log entries
8095 */
8096 public static function mxchat_get_debug_log() {
8097 $log = get_option( 'mxchat_debug_log', array() );
8098 return is_array( $log ) ? $log : array();
8099 }
8100
8101 /**
8102 * Clear the debug log
8103 *
8104 * @return bool Whether the log was cleared
8105 */
8106 public static function mxchat_clear_debug_log() {
8107 return delete_option( 'mxchat_debug_log' );
8108 }
8109
8110 /**
8111 * Export settings as JSON with masked API keys
8112 *
8113 * @return array The sanitized settings array
8114 */
8115 public static function mxchat_export_settings() {
8116 $options = get_option( 'mxchat_options', array() );
8117
8118 if ( ! is_array( $options ) ) {
8119 return array();
8120 }
8121
8122 // List of API key fields to mask
8123 $api_key_fields = array(
8124 'api_key',
8125 'xai_api_key',
8126 'claude_api_key',
8127 'deepseek_api_key',
8128 'voyage_api_key',
8129 'gemini_api_key',
8130 'openrouter_api_key',
8131 'loops_api_key',
8132 'brave_api_key',
8133 'live_agent_secret_key',
8134 'live_agent_bot_token',
8135 'telegram_bot_token',
8136 'telegram_webhook_secret',
8137 'woocommerce_consumer_key',
8138 'woocommerce_consumer_secret',
8139 );
8140
8141 // Mask API keys (show only last 4 characters)
8142 foreach ( $api_key_fields as $field ) {
8143 if ( isset( $options[ $field ] ) && ! empty( $options[ $field ] ) ) {
8144 $value = $options[ $field ];
8145 if ( strlen( $value ) > 4 ) {
8146 $options[ $field ] = str_repeat( '*', strlen( $value ) - 4 ) . substr( $value, -4 );
8147 } else {
8148 $options[ $field ] = '****';
8149 }
8150 }
8151 }
8152
8153 // Add metadata
8154 $export = array(
8155 'plugin_version' => defined( 'MXCHAT_VERSION' ) ? MXCHAT_VERSION : 'unknown',
8156 'export_date' => current_time( 'Y-m-d H:i:s' ),
8157 'wordpress_version' => get_bloginfo( 'version' ),
8158 'php_version' => phpversion(),
8159 'settings' => $options,
8160 );
8161
8162 return $export;
8163 }
8164
8165 /**
8166 * Reset all settings to defaults
8167 *
8168 * @return bool Whether the reset was successful
8169 */
8170 public static function mxchat_reset_all_settings() {
8171 // Delete main options
8172 $deleted = delete_option( 'mxchat_options' );
8173
8174 // Also clear the debug log
8175 delete_option( 'mxchat_debug_log' );
8176
8177 // Log the reset (will create new log since we just cleared it)
8178 // We need to temporarily enable debug mode to log this
8179 $temp_options = array( 'debug_mode' => 'on' );
8180 update_option( 'mxchat_options', $temp_options );
8181
8182 self::mxchat_log_debug( 'reset', 'All settings have been reset to defaults' );
8183
8184 // Now delete again to trigger re-initialization
8185 delete_option( 'mxchat_options' );
8186
8187 return $deleted;
8188 }
8189
8190 // Method to append the chatbot to the body
8191 public function mxchat_append_chatbot_to_body() {
8192 $options = get_option('mxchat_options');
8193 if (isset($options['append_to_body']) && $options['append_to_body'] === 'on') {
8194 echo do_shortcode('[mxchat_chatbot floating="yes"]');
8195 }
8196 }
8197
8198
8199
8200
8201
8202 private function mxchat_fetch_loops_mailing_lists($api_key) {
8203 $url = 'https://app.loops.so/api/v1/lists';
8204 $response = wp_remote_get($url, array(
8205 'headers' => array(
8206 'Authorization' => 'Bearer ' . $api_key,
8207 'Content-Type' => 'application/json'
8208 )
8209 ));
8210
8211 if (is_wp_error($response)) {
8212 return array();
8213 }
8214
8215 $body = wp_remote_retrieve_body($response);
8216 $lists = json_decode($body, true);
8217
8218 return isset($lists) && is_array($lists) ? $lists : array();
8219 }
8220
8221 function mxchat_calculate_cosine_similarity($vec1, $vec2) {
8222 if (empty($vec1) || empty($vec2)) {
8223 return 0.0;
8224 }
8225
8226 $dot_product = 0.0;
8227 $norm_a = 0.0;
8228 $norm_b = 0.0;
8229
8230 for ($i = 0; $i < count($vec1); $i++) {
8231 $dot_product += $vec1[$i] * $vec2[$i];
8232 $norm_a += pow($vec1[$i], 2);
8233 $norm_b += pow($vec2[$i], 2);
8234 }
8235
8236 if ($norm_a == 0.0 || $norm_b == 0.0) {
8237 return 0.0;
8238 } else {
8239 return $dot_product / (sqrt($norm_a) * sqrt($norm_b));
8240 }
8241 }
8242
8243 /**
8244 * Validates nonce and deletes all chat prompts
8245 */
8246 public function mxchat_handle_delete_all_prompts() {
8247 //error_log('=== DELETE ALL DEBUG START ===');
8248
8249 // Verify nonce
8250 if (!isset($_POST['mxchat_delete_all_prompts_nonce']) || !wp_verify_nonce($_POST['mxchat_delete_all_prompts_nonce'], 'mxchat_delete_all_prompts_action')) {
8251 //error_log('DEBUG: Nonce verification failed');
8252 wp_die(__('Nonce verification failed.', 'mxchat'));
8253 }
8254
8255 // Check permissions
8256 if (!current_user_can('manage_options')) {
8257 //error_log('DEBUG: Permission check failed');
8258 wp_die(__('You do not have sufficient permissions to delete all prompts.', 'mxchat'));
8259 }
8260
8261 // Get bot_id and content type filter from POST data
8262 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
8263 $content_type_filter = isset($_POST['content_type_filter']) ? sanitize_text_field($_POST['content_type_filter']) : '';
8264
8265 $success = true;
8266 $error_messages = array();
8267
8268 // Get bot-specific Pinecone configuration
8269 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
8270 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
8271
8272 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
8273
8274 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
8275 // Delete from Pinecone (with optional content type filter)
8276 $result = $pinecone_manager->mxchat_delete_all_from_pinecone($pinecone_options, $content_type_filter);
8277
8278 if (!$result['success']) {
8279 $success = false;
8280 $error_messages[] = $result['message'];
8281 }
8282
8283 } else {
8284 // Delete from WordPress database
8285 global $wpdb;
8286 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
8287
8288 // Build WHERE conditions
8289 $where_clauses = array();
8290 $where_values = array();
8291
8292 // Bot filter
8293 if ($bot_id !== 'default' && class_exists('MxChat_Multi_Bot_Manager')) {
8294 $where_clauses[] = 'bot_id = %s';
8295 $where_values[] = $bot_id;
8296 }
8297
8298 // Content type filter
8299 if (!empty($content_type_filter)) {
8300 $where_clauses[] = 'content_type = %s';
8301 $where_values[] = $content_type_filter;
8302 }
8303
8304 if (!empty($where_clauses)) {
8305 $where_sql = implode(' AND ', $where_clauses);
8306 $result = $wpdb->query($wpdb->prepare("DELETE FROM {$table_name} WHERE {$where_sql}", $where_values));
8307 } else {
8308 // No filters — delete all
8309 $result = $wpdb->query("DELETE FROM {$table_name}");
8310 }
8311
8312 if ($result === false) {
8313 $success = false;
8314 $error_messages[] = 'Failed to delete from WordPress database';
8315 }
8316 }
8317
8318 // Redirect back with a success message and bot_id
8319 $redirect_url = add_query_arg(array(
8320 'page' => 'mxchat-prompts',
8321 'bot_id' => $bot_id,
8322 'all_deleted' => $success ? 'true' : 'false'
8323 ), admin_url('admin.php'));
8324
8325 wp_safe_redirect($redirect_url);
8326 exit;
8327 }
8328
8329
8330
8331 /**
8332 * Handles deletion prompt with nonce validation
8333 */
8334 public function mxchat_handle_delete_prompt() {
8335 // Sanitize and validate nonce
8336 $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : '';
8337 if (empty($nonce) || !wp_verify_nonce($nonce, 'mxchat_delete_prompt_nonce')) {
8338 wp_die(esc_html__('Nonce verification failed.', 'mxchat'));
8339 }
8340
8341 // Check permissions
8342 if (!current_user_can('manage_options')) {
8343 wp_die(esc_html__('You do not have sufficient permissions to delete prompts.', 'mxchat'));
8344 }
8345
8346 // Get ID and source parameters
8347 $id = isset($_GET['id']) ? sanitize_text_field($_GET['id']) : '';
8348 $source = isset($_GET['source']) ? sanitize_text_field($_GET['source']) : '';
8349
8350 if (empty($id)) {
8351 wp_die(esc_html__('Invalid prompt ID.', 'mxchat'));
8352 }
8353
8354 $success = false;
8355 $error_message = '';
8356
8357 // Check if Pinecone is enabled and determine source automatically if not specified
8358 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
8359 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
8360
8361 // If source is not specified, determine based on Pinecone configuration
8362 if (empty($source)) {
8363 $source = $use_pinecone ? 'pinecone' : 'wordpress';
8364 }
8365
8366 if ($source === 'pinecone' || $use_pinecone) {
8367 //error_log('[MXCHAT-DELETE] Deleting from Pinecone, ID: ' . $id);
8368
8369 // Handle Pinecone deletion
8370 if (empty($pinecone_options['mxchat_pinecone_host']) ||
8371 empty($pinecone_options['mxchat_pinecone_api_key'])) {
8372 wp_die(esc_html__('Pinecone configuration is missing.', 'mxchat'));
8373 }
8374
8375 // Delete from Pinecone using the vector ID directly
8376 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
8377 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
8378 $id,
8379 $pinecone_options['mxchat_pinecone_api_key'],
8380 $pinecone_options['mxchat_pinecone_host']
8381 );
8382 if ($result['success']) {
8383 $success = true;
8384
8385 // Remove from vector cache
8386 $pinecone_manager->mxchat_remove_from_pinecone_vector_cache($id);
8387 $pinecone_manager->mxchat_remove_from_processed_content_caches($id);
8388
8389 set_transient('mxchat_admin_notice_success',
8390 esc_html__('Vector deleted successfully from Pinecone.', 'mxchat'), 30);
8391 } else {
8392 $error_message = $result['message'];
8393 set_transient('mxchat_admin_notice_error',
8394 esc_html__('Failed to delete from Pinecone: ', 'mxchat') . esc_html($error_message), 30);
8395 }
8396 } else {
8397 //error_log('[MXCHAT-DELETE] Deleting from WordPress database, ID: ' . $id);
8398
8399 // Handle WordPress database deletion
8400 global $wpdb;
8401 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
8402
8403 // Clear cache and delete prompt
8404 wp_cache_delete('prompt_' . $id, 'mxchat_prompts');
8405
8406 $result = $wpdb->delete(
8407 $table_name,
8408 array('id' => intval($id)),
8409 array('%d')
8410 );
8411
8412 if ($result !== false) {
8413 $success = true;
8414 set_transient('mxchat_admin_notice_success',
8415 esc_html__('Entry deleted successfully.', 'mxchat'), 30);
8416 } else {
8417 set_transient('mxchat_admin_notice_error',
8418 esc_html__('Failed to delete entry from database.', 'mxchat'), 30);
8419 }
8420 }
8421
8422 // Redirect back to the prompts page
8423 wp_safe_redirect(add_query_arg(
8424 array(
8425 'page' => 'mxchat-prompts',
8426 'deleted' => $success ? 'true' : 'false'
8427 ),
8428 admin_url('admin.php')
8429 ));
8430 exit;
8431 }
8432
8433
8434
8435
8436 /**
8437 * Averages multiple vectors into a single vector
8438 */
8439 private function mxchat_average_vectors($vectors) {
8440 $vector_length = count($vectors[0]);
8441 $sum_vector = array_fill(0, $vector_length, 0);
8442
8443 foreach ($vectors as $vector) {
8444 for ($i = 0; $i < $vector_length; $i++) {
8445 $sum_vector[$i] += $vector[$i];
8446 }
8447 }
8448
8449 // Divide each component by the number of vectors to get the average
8450 $num_vectors = count($vectors);
8451 for ($i = 0; $i < $vector_length; $i++) {
8452 $sum_vector[$i] /= $num_vectors;
8453 }
8454
8455 return $sum_vector;
8456 }
8457
8458
8459
8460
8461 /**
8462 * Generates embeddings from input text for MXChat
8463 */
8464 public function mxchat_generate_embedding($text) {
8465 // Enable detailed logging for debugging
8466 //error_log('[MXCHAT-EMBED] Starting embedding generation. Text length: ' . strlen($text) . ' bytes');
8467 //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
8468
8469 $options = get_option('mxchat_options');
8470 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
8471 //error_log('[MXCHAT-EMBED] Selected embedding model: ' . $selected_model);
8472
8473 // Determine provider and endpoint
8474 if (strpos($selected_model, 'voyage') === 0) {
8475 $api_key = $options['voyage_api_key'] ?? '';
8476 $endpoint = 'https://api.voyageai.com/v1/embeddings';
8477 $provider_name = 'Voyage AI';
8478 //error_log('[MXCHAT-EMBED] Using Voyage AI API');
8479 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
8480 $api_key = $options['gemini_api_key'] ?? '';
8481 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
8482 $provider_name = 'Google Gemini';
8483 //error_log('[MXCHAT-EMBED] Using Google Gemini API');
8484 } else {
8485 $api_key = $options['api_key'] ?? '';
8486 $endpoint = 'https://api.openai.com/v1/embeddings';
8487 $provider_name = 'OpenAI';
8488 //error_log('[MXCHAT-EMBED] Using OpenAI API');
8489 }
8490
8491 //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
8492
8493 if (empty($api_key)) {
8494 $error_message = sprintf('Missing %s API key. Please configure your API key in the MxChat settings.', $provider_name);
8495 //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
8496 return $error_message;
8497 }
8498
8499 // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
8500 $estimated_tokens = ceil(str_word_count($text) / 0.75);
8501 //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
8502
8503 if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
8504 //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
8505 // Consider truncating text here
8506 }
8507
8508 // Prepare request body based on provider
8509 if (strpos($selected_model, 'gemini-embedding') === 0) {
8510 // Gemini API format
8511 $request_body = array(
8512 'model' => 'models/' . $selected_model,
8513 'content' => array(
8514 'parts' => array(
8515 array('text' => $text)
8516 )
8517 )
8518 );
8519
8520 // Set output dimensionality to 1536 for consistency with other models
8521 $request_body['outputDimensionality'] = 1536;
8522 } else {
8523 // OpenAI/Voyage API format
8524 $request_body = array(
8525 'model' => $selected_model,
8526 'input' => $text
8527 );
8528
8529 // Add output_dimension for voyage-3-large model
8530 if ($selected_model === 'voyage-3-large') {
8531 $request_body['output_dimension'] = 2048;
8532 }
8533 }
8534
8535 //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
8536
8537 // Prepare headers based on provider
8538 if (strpos($selected_model, 'gemini-embedding') === 0) {
8539 // Gemini uses API key as query parameter
8540 $endpoint .= '?key=' . $api_key;
8541 $headers = array(
8542 'Content-Type' => 'application/json'
8543 );
8544 } else {
8545 // OpenAI/Voyage use Bearer token
8546 $headers = array(
8547 'Authorization' => 'Bearer ' . $api_key,
8548 'Content-Type' => 'application/json'
8549 );
8550 }
8551
8552 // Make API request
8553 //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
8554 $response = wp_remote_post($endpoint, array(
8555 'body' => wp_json_encode($request_body),
8556 'headers' => $headers,
8557 'timeout' => 60 // Increased timeout for large inputs
8558 ));
8559
8560 // Handle wp_remote_post errors
8561 if (is_wp_error($response)) {
8562 $error_message = $response->get_error_message();
8563 //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
8564 return 'Connection error: ' . $error_message;
8565 }
8566
8567 // Get and check HTTP response code
8568 $http_code = wp_remote_retrieve_response_code($response);
8569 //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
8570
8571 if ($http_code !== 200) {
8572 $error_body = wp_remote_retrieve_body($response);
8573 //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
8574
8575 // Try to parse error for more details
8576 $error_json = json_decode($error_body, true);
8577 if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
8578 $error_type = $error_json['error']['type'] ?? 'unknown';
8579 $error_message = $error_json['error']['message'] ?? 'No message';
8580 //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
8581 //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
8582
8583 // Customize error message for common API errors
8584 if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
8585 $error_message = sprintf('Invalid %s API key. Please check your API key in the MxChat settings.', $provider_name);
8586 } elseif ($error_type === 'authentication_error') {
8587 $error_message = sprintf('%s authentication failed. Please verify your API key in the MxChat settings.', $provider_name);
8588 }
8589
8590 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
8591 return $error_message;
8592 }
8593
8594 $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding", $http_code);
8595 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
8596 return $error_message;
8597 }
8598
8599 // Parse response body
8600 $response_body = wp_remote_retrieve_body($response);
8601 //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
8602
8603 $response_data = json_decode($response_body, true);
8604
8605 if (json_last_error() !== JSON_ERROR_NONE) {
8606 $error = json_last_error_msg();
8607 //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
8608 //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
8609 return "Failed to parse API response: $error";
8610 }
8611
8612 // Handle different response formats based on provider
8613 if (strpos($selected_model, 'gemini-embedding') === 0) {
8614 // Gemini API response format
8615 if (isset($response_data['embedding']['values'])) {
8616 $embedding_dimensions = count($response_data['embedding']['values']);
8617 //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
8618
8619 // Check if embedding dimensions are as expected (should be 1536)
8620 if ($embedding_dimensions !== 1536) {
8621 //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
8622 }
8623
8624 return $response_data['embedding']['values'];
8625 } else {
8626 //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
8627 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
8628
8629 if (isset($response_data['error'])) {
8630 $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
8631 //error_log('[MXCHAT-EMBED] ' . $error_message);
8632 return $error_message;
8633 }
8634
8635 $error_message = "Invalid Gemini API response format: No embedding found";
8636 //error_log('[MXCHAT-EMBED] ' . $error_message);
8637 return $error_message;
8638 }
8639 } else {
8640 // OpenAI/Voyage API response format
8641 if (isset($response_data['data'][0]['embedding'])) {
8642 $embedding_dimensions = count($response_data['data'][0]['embedding']);
8643 //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
8644
8645 // Check if embedding dimensions are as expected
8646 if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
8647 ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
8648 //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
8649 }
8650
8651 return $response_data['data'][0]['embedding'];
8652 } else {
8653 //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
8654 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
8655
8656 if (isset($response_data['error'])) {
8657 $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
8658 //error_log('[MXCHAT-EMBED] ' . $error_message);
8659 return $error_message;
8660 }
8661
8662 $error_message = "Invalid API response format: No embedding found";
8663 //error_log('[MXCHAT-EMBED] ' . $error_message);
8664 return $error_message;
8665 }
8666 }
8667 }
8668
8669
8670
8671 }
8672 ?>
8673