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

8,390 lines 341.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
84 // Slack test connection
85 add_action('wp_ajax_mxchat_test_slack_connection', array($this, 'mxchat_test_slack_connection'));
86
87 // Translation handlers
88 add_action('wp_ajax_mxchat_translate_messages', array($this, 'mxchat_translate_messages'));
89 add_action('wp_ajax_mxchat_get_transcript_translation', array($this, 'mxchat_get_transcript_translation'));
90 }
91
92 private function is_license_active() {
93 // Get the raw value without translation
94 $license_status = get_option('mxchat_license_status', 'inactive');
95
96 // Check against multiple possible values, bypassing translation issues
97 return ($license_status === 'active' || $license_status === esc_html__('active', 'mxchat'));
98 }
99
100 private function initialize_default_options() {
101 $default_options = array(
102 'api_key' => '',
103 'xai_api_key' => '',
104 'claude_api_key' => '',
105 'deepseek_api_key' => '',
106 'voyage_api_key' => '',
107 'gemini_api_key' => '',
108 'enable_streaming_toggle' => 'off',
109 'enable_web_search' => 'off',
110 'embedding_model' => 'text-embedding-ada-002',
111 '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:
112
113 # Response Style - CRITICALLY IMPORTANT
114 - MAXIMUM LENGTH: 1-3 short sentences per response
115 - Ultra-concise: Get straight to the answer with no filler
116 - No introductions like "Sure!" or "I\'d be happy to help"
117 - No phrases like "based on my knowledge" or "according to information"
118 - No explanatory text before giving the answer
119 - No summaries or repetition
120 - Hyperlink all URLs
121 - Respond in user\'s language
122 - Minor chit chat or conversation is okay, but try to keep it focused on [insert topic]
123
124 # Knowledge Base Requirements - PREVENT HALLUCINATIONS
125 - ONLY answer questions using information explicitly provided in OFFICIAL KNOWLEDGE DATABASE CONTENT sections marked with ===== delimiters
126 - If required information is NOT in the knowledge database: "I don\'t have enough information in my knowledge base to answer that question accurately."
127 - NEVER invent or hallucinate URLs, links, product specs, procedures, dates, statistics, names, contacts, or company information
128 - When knowledge base information is unclear or contradictory, acknowledge the limitation rather than guessing
129 - Better to admit insufficient information than provide inaccurate answers',
130 'model' => esc_html__('gpt-5.1-chat-latest', 'mxchat'),
131 'rate_limit_logged_out' => esc_html__('100', 'mxchat'),
132 'role_rate_limits' => array(),
133 'rate_limit_message' => esc_html__('Rate limit exceeded. Please try again later.', 'mxchat'),
134 'enable_email_block' => '',
135 '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'),
136 'email_blocker_button_text' => esc_html__('Start Chat', 'mxchat'),
137 'enable_name_field' => 'off', // NEW
138 'name_field_placeholder' => esc_html__('Enter your name', 'mxchat'), // NEW
139 'top_bar_title' => esc_html__('MxChat', 'mxchat'),
140 'intro_message' => __('Hello! How can I assist you today?', 'mxchat'),
141 'ai_agent_text' => esc_html__('AI Agent', 'mxchat'),
142 'input_copy' => esc_html__('How can I assist?', 'mxchat'),
143 'append_to_body' => esc_html__('off', 'mxchat'),
144 'post_type_visibility_mode' => 'all', // 'all', 'include', 'exclude'
145 'post_type_visibility_list' => array(), // Array of post type slugs
146 'contextual_awareness_toggle' => 'off',
147 'citation_links_toggle' => 'on',
148 'show_frontend_debugger' => '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 // Format actions for response
3856 $formatted_actions = array();
3857 foreach ($actions as $action) {
3858 $callback_data = isset($available_callbacks[$action->callback_function])
3859 ? $available_callbacks[$action->callback_function]
3860 : array('label' => $action->callback_function, 'icon' => 'admin-generic');
3861
3862 $enabled_bots = json_decode($action->enabled_bots, true);
3863 if (!is_array($enabled_bots)) {
3864 $enabled_bots = array('default');
3865 }
3866
3867 $formatted_actions[] = array(
3868 'id' => intval($action->id),
3869 'label' => $action->intent_label,
3870 'phrases' => $action->phrases,
3871 'callback_function' => $action->callback_function,
3872 'callback_label' => $callback_data['label'],
3873 'icon' => isset($callback_data['icon']) ? $callback_data['icon'] : 'admin-generic',
3874 'threshold' => round($action->similarity_threshold * 100),
3875 'enabled' => (bool) $action->enabled,
3876 'enabled_bots' => $enabled_bots,
3877 );
3878 }
3879
3880 $total_pages = ceil($total_actions / $per_page);
3881 $showing_start = $total_actions > 0 ? $offset + 1 : 0;
3882 $showing_end = min($offset + $per_page, $total_actions);
3883
3884 wp_send_json_success(array(
3885 'actions' => $formatted_actions,
3886 'page' => $page,
3887 'per_page' => $per_page,
3888 'total_actions' => intval($total_actions),
3889 'total_pages' => $total_pages,
3890 'showing_start' => $showing_start,
3891 'showing_end' => $showing_end,
3892 ));
3893 }
3894
3895 /**
3896 * AJAX handler to toggle action enabled status
3897 */
3898 public function mxchat_toggle_action_status() {
3899 check_ajax_referer('mxchat_actions_nonce', 'security');
3900
3901 if (!current_user_can('manage_options')) {
3902 wp_send_json_error(__('Unauthorized', 'mxchat'));
3903 }
3904
3905 $action_id = isset($_POST['action_id']) ? intval($_POST['action_id']) : 0;
3906 $enabled = isset($_POST['enabled']) ? intval($_POST['enabled']) : 0;
3907
3908 if (!$action_id) {
3909 wp_send_json_error(__('Invalid action ID', 'mxchat'));
3910 }
3911
3912 global $wpdb;
3913 $table_name = $wpdb->prefix . 'mxchat_intents';
3914
3915 $result = $wpdb->update(
3916 $table_name,
3917 array('enabled' => $enabled ? 1 : 0),
3918 array('id' => $action_id),
3919 array('%d'),
3920 array('%d')
3921 );
3922
3923 if ($result === false) {
3924 wp_send_json_error(__('Failed to update action status', 'mxchat'));
3925 }
3926
3927 wp_send_json_success(array('enabled' => (bool) $enabled));
3928 }
3929
3930 /**
3931 * AJAX handler to bulk delete actions
3932 */
3933 public function mxchat_bulk_delete_actions() {
3934 check_ajax_referer('mxchat_delete_intent_nonce', 'security');
3935
3936 if (!current_user_can('manage_options')) {
3937 wp_send_json_error(__('Unauthorized', 'mxchat'));
3938 }
3939
3940 $action_ids = isset($_POST['action_ids']) ? array_map('intval', (array) $_POST['action_ids']) : array();
3941
3942 if (empty($action_ids)) {
3943 wp_send_json_error(__('No actions selected', 'mxchat'));
3944 }
3945
3946 global $wpdb;
3947 $table_name = $wpdb->prefix . 'mxchat_intents';
3948
3949 $placeholders = implode(',', array_fill(0, count($action_ids), '%d'));
3950 $query = $wpdb->prepare("DELETE FROM $table_name WHERE id IN ($placeholders)", $action_ids);
3951 $result = $wpdb->query($query);
3952
3953 if ($result === false) {
3954 wp_send_json_error(__('Failed to delete actions', 'mxchat'));
3955 }
3956
3957 wp_send_json_success(array('deleted' => $result));
3958 }
3959
3960 /**
3961 * AJAX handler to add a new intent/action
3962 */
3963 public function mxchat_add_intent_ajax() {
3964 check_ajax_referer('mxchat_add_intent_nonce', 'security');
3965
3966 if (!current_user_can('manage_options')) {
3967 wp_send_json_error(__('Unauthorized', 'mxchat'));
3968 }
3969
3970 global $wpdb;
3971 $table_name = $wpdb->prefix . 'mxchat_intents';
3972
3973 // Sanitize input
3974 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
3975 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
3976 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
3977 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
3978 $enabled_bots = isset($_POST['enabled_bots']) ? array_map('sanitize_text_field', (array) $_POST['enabled_bots']) : array('default');
3979
3980 // Validate
3981 if (empty($intent_label) || empty($phrases_input) || empty($callback_function)) {
3982 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
3983 }
3984
3985 // Process phrases
3986 $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
3987 if (empty($phrases_array)) {
3988 wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
3989 }
3990
3991 // Generate embedding (combine phrases into single string for embedding)
3992 $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
3993 if (is_wp_error($embedding_vector)) {
3994 // Fallback: store without embedding
3995 $serialized_vector = null;
3996 } else {
3997 $serialized_vector = maybe_serialize($embedding_vector);
3998 }
3999
4000 // Ensure default bot is included
4001 if (!in_array('default', $enabled_bots)) {
4002 $enabled_bots[] = 'default';
4003 }
4004 $enabled_bots_json = json_encode($enabled_bots);
4005
4006 // Insert
4007 $result = $wpdb->insert(
4008 $table_name,
4009 array(
4010 'intent_label' => $intent_label,
4011 'phrases' => implode(', ', $phrases_array),
4012 'embedding_vector' => $serialized_vector,
4013 'similarity_threshold' => $similarity_threshold,
4014 'callback_function' => $callback_function,
4015 'enabled' => 1,
4016 'enabled_bots' => $enabled_bots_json,
4017 )
4018 );
4019
4020 if ($result === false) {
4021 wp_send_json_error(__('Failed to add action to database.', 'mxchat'));
4022 }
4023
4024 wp_send_json_success(array('id' => $wpdb->insert_id));
4025 }
4026
4027 /**
4028 * AJAX handler to edit an existing intent/action
4029 */
4030 public function mxchat_edit_intent_ajax() {
4031 check_ajax_referer('mxchat_edit_intent', 'security');
4032
4033 if (!current_user_can('manage_options')) {
4034 wp_send_json_error(__('Unauthorized', 'mxchat'));
4035 }
4036
4037 global $wpdb;
4038 $table_name = $wpdb->prefix . 'mxchat_intents';
4039
4040 // Sanitize input
4041 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
4042 $intent_label = isset($_POST['intent_label']) ? sanitize_text_field($_POST['intent_label']) : '';
4043 $phrases_input = isset($_POST['phrases']) ? sanitize_textarea_field($_POST['phrases']) : '';
4044 $callback_function = isset($_POST['callback_function']) ? sanitize_text_field($_POST['callback_function']) : '';
4045 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
4046 $enabled_bots = isset($_POST['enabled_bots']) ? array_map('sanitize_text_field', (array) $_POST['enabled_bots']) : array('default');
4047
4048 // Validate
4049 if (!$intent_id || empty($intent_label) || empty($phrases_input)) {
4050 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
4051 }
4052
4053 // Process phrases
4054 $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
4055 if (empty($phrases_array)) {
4056 wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
4057 }
4058
4059 // Generate new embedding (combine phrases into single string for embedding)
4060 $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
4061 if (is_wp_error($embedding_vector)) {
4062 // Keep existing embedding
4063 $serialized_vector = null;
4064 $update_data = array(
4065 'intent_label' => $intent_label,
4066 'phrases' => implode(', ', $phrases_array),
4067 'similarity_threshold' => $similarity_threshold,
4068 'callback_function' => $callback_function,
4069 'enabled_bots' => json_encode($enabled_bots),
4070 );
4071 } else {
4072 $serialized_vector = maybe_serialize($embedding_vector);
4073 $update_data = array(
4074 'intent_label' => $intent_label,
4075 'phrases' => implode(', ', $phrases_array),
4076 'embedding_vector' => $serialized_vector,
4077 'similarity_threshold' => $similarity_threshold,
4078 'callback_function' => $callback_function,
4079 'enabled_bots' => json_encode($enabled_bots),
4080 );
4081 }
4082
4083 // Ensure default bot is included
4084 if (!in_array('default', $enabled_bots)) {
4085 $enabled_bots[] = 'default';
4086 }
4087 $update_data['enabled_bots'] = json_encode($enabled_bots);
4088
4089 $result = $wpdb->update(
4090 $table_name,
4091 $update_data,
4092 array('id' => $intent_id),
4093 null,
4094 array('%d')
4095 );
4096
4097 if ($result === false) {
4098 wp_send_json_error(__('Failed to update action.', 'mxchat'));
4099 }
4100
4101 wp_send_json_success(array('updated' => true));
4102 }
4103
4104 /**
4105 * AJAX handler to delete a single intent/action
4106 */
4107 public function mxchat_delete_intent_ajax() {
4108 check_ajax_referer('mxchat_delete_intent_nonce', 'security');
4109
4110 if (!current_user_can('manage_options')) {
4111 wp_send_json_error(__('Unauthorized', 'mxchat'));
4112 }
4113
4114 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
4115
4116 if (!$intent_id) {
4117 wp_send_json_error(__('Invalid action ID', 'mxchat'));
4118 }
4119
4120 global $wpdb;
4121 $table_name = $wpdb->prefix . 'mxchat_intents';
4122
4123 $result = $wpdb->delete($table_name, array('id' => $intent_id), array('%d'));
4124
4125 if ($result === false) {
4126 wp_send_json_error(__('Failed to delete action', 'mxchat'));
4127 }
4128
4129 wp_send_json_success(array('deleted' => true));
4130 }
4131
4132
4133 /**
4134 * Enhanced get_available_callbacks function with form action exclusion
4135 *
4136 * @param bool $grouped Whether to return callbacks grouped by category
4137 * @param bool $include_all Whether to include all potential actions (even if add-on not installed)
4138 * @return array Callbacks data with icons, descriptions and availability status
4139 */
4140 private function mxchat_get_available_callbacks($grouped = false, $include_all = true) {
4141 // Load WordPress plugin functions if needed
4142 if (!function_exists('get_plugins')) {
4143 require_once ABSPATH . 'wp-admin/includes/plugin.php';
4144 }
4145
4146 // Get active plugins
4147 $active_plugins = get_option('active_plugins', array());
4148
4149 // Functions to exclude from the action selector only if Pro is activated
4150 // If user doesn't have Pro, show these so they can see what they're missing
4151 $excluded_when_pro_active_functions = array(
4152 'mxchat_handle_form_collection' // Forms add-on action
4153 );
4154
4155 // Always excluded functions (regardless of Pro status)
4156 $always_excluded_functions = array();
4157
4158 // Combine exclusion lists based on Pro activation status
4159 $excluded_functions = $always_excluded_functions;
4160 if ($this->is_activated) {
4161 // Only exclude add-on managed functions if Pro is active
4162 $excluded_functions = array_merge($excluded_functions, $excluded_when_pro_active_functions);
4163 }
4164
4165 // Define add-on plugin files and their corresponding action functions
4166 $addon_plugins = array(
4167 'mxchat-woo/mxchat-woo.php' => array(
4168 'functions' => array(
4169 'mxchat_handle_product_recommendations',
4170 'mxchat_handle_order_history',
4171 'mxchat_show_product_card',
4172 'mxchat_add_to_cart',
4173 'mxchat_checkout_redirect',
4174 'mxchat_handle_featured_products'
4175 ),
4176 'name' => __('WooCommerce Add-on', 'mxchat'),
4177 'pro_required' => true
4178 ),
4179 'mxchat-perplexity/mxchat-perplexity.php' => array(
4180 'functions' => array('mxchat_perplexity_research'),
4181 'name' => __('Perplexity Add-on', 'mxchat'),
4182 'pro_required' => true
4183 ),
4184 'mxchat-forms/mxchat-forms.php' => array(
4185 'functions' => array('mxchat_handle_form_collection'),
4186 'name' => __('Forms Add-on', 'mxchat'),
4187 'pro_required' => true
4188 ),
4189 // Add other add-ons and their functions here
4190 );
4191
4192 // Get the functions that are provided by active add-ons
4193 $addon_provided_functions = array();
4194 $addon_function_mapping = array(); // Maps functions to their add-on info
4195
4196 // Check which add-ons are active
4197 foreach ($addon_plugins as $plugin_file => $addon_info) {
4198 $is_active = in_array($plugin_file, $active_plugins);
4199
4200 // For each function in this addon
4201 foreach ($addon_info['functions'] as $function) {
4202 // Consider a function installed only if:
4203 // 1. The add-on is active AND
4204 // 2. Either it doesn't require Pro OR Pro is activated
4205 $is_installed = $is_active && (!$addon_info['pro_required'] || $this->is_activated);
4206
4207 // If the add-on is installed, mark this function as provided by an add-on
4208 if ($is_installed) {
4209 $addon_provided_functions[] = $function;
4210 }
4211
4212 // Store addon info for this function regardless of installation status
4213 $addon_function_mapping[$function] = array(
4214 'addon' => basename(dirname($plugin_file)),
4215 'addon_name' => $addon_info['name'],
4216 'pro_required' => $addon_info['pro_required'],
4217 'is_active' => $is_active,
4218 'is_installed' => $is_installed
4219 );
4220 }
4221 }
4222
4223 // Core callbacks - always available in the base plugin
4224 $core_callbacks = array(
4225 'mxchat_handle_email_capture' => array(
4226 'label' => __('Loops Email Capture', 'mxchat'),
4227 'pro_only' => false,
4228 'group' => __('Customer Engagement', 'mxchat'),
4229 'icon' => 'email-alt',
4230 'description' => __('Collect visitor emails for your mailing list in Loops', 'mxchat'),
4231 'addon' => false, // Not from an add-on
4232 'installed' => true // Always installed with base plugin
4233 ),
4234 'mxchat_handle_search_request' => array(
4235 'label' => __('Brave Web Search', 'mxchat'),
4236 'pro_only' => false,
4237 'group' => __('Search Features', 'mxchat'),
4238 'icon' => 'search',
4239 'description' => __('Let users search the web directly from the chat', 'mxchat'),
4240 'addon' => false,
4241 'installed' => true
4242 ),
4243 'mxchat_handle_image_search_request' => array(
4244 'label' => __('Brave Image Search', 'mxchat'),
4245 'pro_only' => false,
4246 'group' => __('Search Features', 'mxchat'),
4247 'icon' => 'format-image',
4248 'description' => __('Search and display images in the chat conversation', 'mxchat'),
4249 'addon' => false,
4250 'installed' => true
4251 ),
4252 // Pro core features - check is_activated property
4253 'mxchat_generate_image' => array(
4254 'label' => __('Generate Image (OpenAI)', 'mxchat'),
4255 'pro_only' => false,
4256 'group' => __('Other Features', 'mxchat'),
4257 'icon' => 'art',
4258 'description' => __('Create images with GPT Image from OpenAI (requires OpenAI API key)', 'mxchat'),
4259 'addon' => false,
4260 'installed' => true
4261 ),
4262 'mxchat_generate_gemini_image' => array(
4263 'label' => __('Generate Image (Gemini)', 'mxchat'),
4264 'pro_only' => false,
4265 'group' => __('Other Features', 'mxchat'),
4266 'icon' => 'art',
4267 'description' => __('Create images with Imagen from Google (requires Gemini API key)', 'mxchat'),
4268 'addon' => false,
4269 'installed' => true
4270 ),
4271 'mxchat_handle_pdf_discussion' => array(
4272 'label' => __('Chat with PDF', 'mxchat'),
4273 'pro_only' => false,
4274 'group' => __('Other Features', 'mxchat'),
4275 'icon' => 'media-document',
4276 'description' => __('Answer questions about uploaded PDF documents', 'mxchat'),
4277 'addon' => false,
4278 'installed' => true
4279 ),
4280 'mxchat_live_agent_handover' => array(
4281 'label' => __('Slack Live Agent', 'mxchat'),
4282 'pro_only' => false,
4283 'group' => __('Customer Engagement', 'mxchat'),
4284 'icon' => 'admin-users',
4285 'description' => __('Transfer conversation to a human support agent on Slack', 'mxchat'),
4286 'addon' => false,
4287 'installed' => true
4288 ),
4289 'mxchat_telegram_live_agent_handover' => array(
4290 'label' => __('Telegram Live Agent', 'mxchat'),
4291 'pro_only' => false,
4292 'group' => __('Customer Engagement', 'mxchat'),
4293 'icon' => 'format-chat',
4294 'description' => __('Transfer conversation to a human support agent on Telegram', 'mxchat'),
4295 'addon' => false,
4296 'installed' => true
4297 ),
4298 'mxchat_handle_switch_to_chatbot_intent' => array(
4299 'label' => __('Back to Chatbot', 'mxchat'),
4300 'pro_only' => false,
4301 'group' => __('Customer Engagement', 'mxchat'),
4302 'icon' => 'backup',
4303 'description' => __('Return from live agent mode to AI chatbot', 'mxchat'),
4304 'addon' => false,
4305 'installed' => true
4306 ),
4307 );
4308
4309 // Add-on callbacks with placeholders - only include if the add-on is NOT active
4310 // These are promotional/informational only — not selectable as real actions
4311 $addon_callbacks = array(
4312 // WooCommerce Add-on
4313 'mxchat_handle_product_recommendations' => array(
4314 'label' => __('Product Recommendations', 'mxchat'),
4315 'pro_only' => false,
4316 'addon_promo' => true,
4317 'group' => __('WooCommerce Features', 'mxchat'),
4318 'icon' => 'cart',
4319 'description' => __('Suggest products based on customer preferences', 'mxchat'),
4320 ),
4321 'mxchat_handle_order_history' => array(
4322 'label' => __('Order History', 'mxchat'),
4323 'pro_only' => false,
4324 'addon_promo' => true,
4325 'group' => __('WooCommerce Features', 'mxchat'),
4326 'icon' => 'clipboard',
4327 'description' => __('Allow customers to check their order status', 'mxchat'),
4328 ),
4329 'mxchat_show_product_card' => array(
4330 'label' => __('Show Product Card', 'mxchat'),
4331 'pro_only' => false,
4332 'addon_promo' => true,
4333 'group' => __('WooCommerce Features', 'mxchat'),
4334 'icon' => 'products',
4335 'description' => __('Display product information in the chat', 'mxchat'),
4336 ),
4337 'mxchat_add_to_cart' => array(
4338 'label' => __('Add to Cart', 'mxchat'),
4339 'pro_only' => false,
4340 'addon_promo' => true,
4341 'group' => __('WooCommerce Features', 'mxchat'),
4342 'icon' => 'plus-alt',
4343 'description' => __('Add products to cart directly from chat', 'mxchat'),
4344 ),
4345 'mxchat_checkout_redirect' => array(
4346 'label' => __('Proceed to Checkout', 'mxchat'),
4347 'pro_only' => false,
4348 'addon_promo' => true,
4349 'group' => __('WooCommerce Features', 'mxchat'),
4350 'icon' => 'arrow-right-alt',
4351 'description' => __('Redirect customer to checkout page', 'mxchat'),
4352 ),
4353 'mxchat_handle_featured_products' => array(
4354 'label' => __('Featured Products Showcase', 'mxchat'),
4355 'pro_only' => false,
4356 'addon_promo' => true,
4357 'group' => __('WooCommerce Features', 'mxchat'),
4358 'icon' => 'star-filled',
4359 'description' => __('Display a curated selection of products with an AI-generated message', 'mxchat'),
4360 ),
4361
4362 // Perplexity Add-on
4363 'mxchat_perplexity_research' => array(
4364 'label' => __('Perplexity Research', 'mxchat'),
4365 'pro_only' => false,
4366 'addon_promo' => true,
4367 'group' => __('Search Features', 'mxchat'),
4368 'icon' => 'book-alt',
4369 'description' => __('Allows the chatbot to search the web for accurate, up-to-date answers', 'mxchat'),
4370 ),
4371
4372 // Forms Add-on
4373 'mxchat_handle_form_collection' => array(
4374 'label' => __('Form Collection', 'mxchat'),
4375 'pro_only' => false,
4376 'addon_promo' => true,
4377 'group' => __('Form Features', 'mxchat'),
4378 'icon' => 'feedback',
4379 'description' => __('Collect user information through custom forms in chat', 'mxchat'),
4380 ),
4381 );
4382
4383 // Enhance add-on callbacks with installation status and addon info
4384 foreach ($addon_callbacks as $function => $data) {
4385 if (isset($addon_function_mapping[$function])) {
4386 $addon_info = $addon_function_mapping[$function];
4387
4388 $addon_callbacks[$function]['addon'] = $addon_info['addon'];
4389 $addon_callbacks[$function]['addon_name'] = $addon_info['addon_name'];
4390 $addon_callbacks[$function]['installed'] = $addon_info['is_installed'];
4391
4392 // Set pro_only based on add-on configuration
4393 $addon_callbacks[$function]['pro_only'] = $addon_info['pro_required'];
4394 } else {
4395 $addon_callbacks[$function]['addon'] = 'unknown';
4396 $addon_callbacks[$function]['addon_name'] = __('Unknown Add-on', 'mxchat');
4397 $addon_callbacks[$function]['installed'] = false;
4398 }
4399 }
4400
4401 // Initialize callbacks with core features
4402 $callbacks = $core_callbacks;
4403
4404 // Get callbacks from active add-ons
4405 $active_addon_callbacks = apply_filters('mxchat_available_callbacks', array());
4406
4407 // Add placeholder callbacks only for add-ons that aren't active
4408 if ($include_all) {
4409 foreach ($addon_callbacks as $function => $data) {
4410 // Skip placeholders for functions provided by active add-ons
4411 if (in_array($function, $addon_provided_functions)) {
4412 continue;
4413 }
4414
4415 // Skip excluded functions
4416 if (in_array($function, $excluded_functions)) {
4417 continue;
4418 }
4419
4420 // Add the placeholder
4421 $callbacks[$function] = $data;
4422 }
4423 }
4424
4425 // Add callbacks from active add-ons (will override placeholders)
4426 foreach ($active_addon_callbacks as $function => $data) {
4427 // Skip excluded functions
4428 if (in_array($function, $excluded_functions)) {
4429 continue;
4430 }
4431
4432 // Always include callbacks from add-ons
4433 $callbacks[$function] = $data;
4434
4435 // Ensure they have the proper add-on info
4436 if (isset($addon_function_mapping[$function])) {
4437 $addon_info = $addon_function_mapping[$function];
4438 $callbacks[$function]['addon'] = $addon_info['addon'];
4439 $callbacks[$function]['addon_name'] = $addon_info['addon_name'];
4440 $callbacks[$function]['installed'] = $addon_info['is_installed'];
4441 $callbacks[$function]['pro_only'] = $addon_info['pro_required'];
4442 }
4443 }
4444
4445 // Just before returning callbacks, sort them to prioritize free features
4446 if (!$grouped) {
4447 // Create temporary arrays for sorting
4448 $free_callbacks = array();
4449 $pro_callbacks = array();
4450
4451 // Split callbacks into free and pro
4452 foreach ($callbacks as $key => $data) {
4453 if (isset($data['pro_only']) && $data['pro_only']) {
4454 $pro_callbacks[$key] = $data;
4455 } else {
4456 $free_callbacks[$key] = $data;
4457 }
4458 }
4459
4460 // Merge with free callbacks first
4461 $callbacks = array_merge($free_callbacks, $pro_callbacks);
4462 }
4463
4464 // Return grouped structure if requested
4465 if ($grouped) {
4466 $grouped_callbacks = array();
4467 foreach ($callbacks as $key => $data) {
4468 $group_label = isset($data['group']) ? $data['group'] : __('Other Features', 'mxchat');
4469
4470 // Ensure we carry forward all the new fields in grouped mode
4471 $callback_data = array(
4472 'label' => $data['label'],
4473 'pro_only' => isset($data['pro_only']) ? $data['pro_only'] : false,
4474 'icon' => isset($data['icon']) ? $data['icon'] : 'admin-generic',
4475 'description' => isset($data['description']) ? $data['description'] : __('Custom action for your chatbot', 'mxchat'),
4476 'addon' => isset($data['addon']) ? $data['addon'] : false,
4477 'addon_name' => isset($data['addon_name']) ? $data['addon_name'] : '',
4478 'installed' => isset($data['installed']) ? $data['installed'] : true
4479 );
4480
4481 $grouped_callbacks[$group_label][$key] = $callback_data;
4482 }
4483
4484 // Sort within each group to prioritize free features
4485 foreach ($grouped_callbacks as $group => $items) {
4486 $free_items = array();
4487 $pro_items = array();
4488
4489 foreach ($items as $key => $data) {
4490 if (isset($data['pro_only']) && $data['pro_only']) {
4491 $pro_items[$key] = $data;
4492 } else {
4493 $free_items[$key] = $data;
4494 }
4495 }
4496
4497 $grouped_callbacks[$group] = array_merge($free_items, $pro_items);
4498 }
4499
4500 return $grouped_callbacks;
4501 }
4502
4503 return $callbacks;
4504 }
4505
4506 public function mxchat_page_init() {
4507 register_setting(
4508 'mxchat_option_group',
4509 'mxchat_options',
4510 array($this, 'mxchat_sanitize')
4511 );
4512
4513 register_setting(
4514 'mxchat_option_group',
4515 'mxchat_similarity_threshold',
4516 array(
4517 'type' => 'number',
4518 'sanitize_callback' => function($value) {
4519 $value = absint($value);
4520 return min(max($value, 20), 95);
4521 },
4522 'default' => 80,
4523 )
4524 );
4525
4526 // Chatbot Settings Section
4527 add_settings_section(
4528 'mxchat_chatbot_section',
4529 esc_html__('Chatbot Settings', 'mxchat'),
4530 null,
4531 'mxchat-chatbot'
4532 );
4533
4534 // API Keys Settings Section
4535 add_settings_section(
4536 'mxchat_api_keys_section',
4537 esc_html__('API Keys', 'mxchat'),
4538 array($this, 'mxchat_api_keys_section_callback'),
4539 'mxchat-api-keys'
4540 );
4541
4542 // OpenAI API Key
4543 add_settings_field(
4544 'api_key',
4545 esc_html__('OpenAI API Key', 'mxchat'),
4546 array($this, 'api_key_callback'),
4547 'mxchat-api-keys',
4548 'mxchat_api_keys_section'
4549 );
4550
4551 // X.AI API Key
4552 add_settings_field(
4553 'xai_api_key',
4554 esc_html__('X.AI API Key', 'mxchat'),
4555 array($this, 'xai_api_key_callback'),
4556 'mxchat-api-keys',
4557 'mxchat_api_keys_section'
4558 );
4559
4560 // Claude API Key
4561 add_settings_field(
4562 'claude_api_key',
4563 esc_html__('Claude API Key', 'mxchat'),
4564 array($this, 'claude_api_key_callback'),
4565 'mxchat-api-keys',
4566 'mxchat_api_keys_section'
4567 );
4568
4569 // DeepSeek API Key
4570 add_settings_field(
4571 'deepseek_api_key',
4572 esc_html__('DeepSeek API Key', 'mxchat'),
4573 array($this, 'deepseek_api_key_callback'),
4574 'mxchat-api-keys',
4575 'mxchat_api_keys_section'
4576 );
4577
4578 // Google Gemini API Key
4579 add_settings_field(
4580 'gemini_api_key',
4581 esc_html__('Google Gemini API Key', 'mxchat'),
4582 array($this, 'gemini_api_key_callback'),
4583 'mxchat-api-keys',
4584 'mxchat_api_keys_section'
4585 );
4586
4587 // Voyage AI API Key
4588 add_settings_field(
4589 'voyage_api_key',
4590 esc_html__('Voyage AI API Key', 'mxchat'),
4591 array($this, 'voyage_api_key_callback'),
4592 'mxchat-api-keys',
4593 'mxchat_api_keys_section'
4594 );
4595
4596 // OpenRouter API Key
4597 add_settings_field(
4598 'openrouter_api_key',
4599 esc_html__('OpenRouter API Key', 'mxchat'),
4600 array($this, 'openrouter_api_key_callback'),
4601 'mxchat-api-keys',
4602 'mxchat_api_keys_section'
4603 );
4604
4605 // Loops API Key
4606 add_settings_field(
4607 'loops_api_key',
4608 esc_html__('Loops API Key', 'mxchat'),
4609 array($this, 'mxchat_loops_api_key_callback'),
4610 'mxchat-api-keys',
4611 'mxchat_api_keys_section'
4612 );
4613
4614 // Brave Search API Key
4615 add_settings_field(
4616 'brave_api_key',
4617 __('Brave API Key', 'mxchat'),
4618 array($this, 'mxchat_brave_api_key_callback'),
4619 'mxchat-api-keys',
4620 'mxchat_api_keys_section'
4621 );
4622
4623 // Frontend Debugger Toggle
4624 add_settings_field(
4625 'show_frontend_debugger',
4626 esc_html__('Frontend Debugger', 'mxchat'),
4627 array($this, 'mxchat_frontend_debugger_callback'),
4628 'mxchat-chatbot',
4629 'mxchat_chatbot_section'
4630 );
4631
4632 // Similarity Threshold Slider
4633 add_settings_field(
4634 'similarity_threshold', // Field ID
4635 esc_html__('Similarity Threshold', 'mxchat'), // Field title
4636 array($this, 'mxchat_similarity_threshold_callback'), // Callback function
4637 'mxchat-chatbot', // Page
4638 'mxchat_chatbot_section' // Section
4639 );
4640
4641 // RAG Sources Limit Slider
4642 add_settings_field(
4643 'rag_sources_limit', // Field ID
4644 esc_html__('RAG Sources Limit', 'mxchat'), // Field title
4645 array($this, 'mxchat_rag_sources_limit_callback'), // Callback function
4646 'mxchat-chatbot', // Page
4647 'mxchat_chatbot_section' // Section
4648 );
4649
4650 // RAG Chunks Limit Slider
4651 add_settings_field(
4652 'rag_chunks_limit', // Field ID
4653 esc_html__('RAG Chunks Limit', 'mxchat'), // Field title
4654 array($this, 'mxchat_rag_chunks_limit_callback'), // Callback function
4655 'mxchat-chatbot', // Page
4656 'mxchat_chatbot_section' // Section
4657 );
4658
4659 add_settings_field(
4660 'append_to_body',
4661 esc_html__('Auto-Display Chatbot', 'mxchat'),
4662 array($this, 'mxchat_append_to_body_callback'),
4663 'mxchat-chatbot',
4664 'mxchat_chatbot_section'
4665 );
4666
4667 add_settings_field(
4668 'contextual_awareness_toggle',
4669 esc_html__('Contextual Awareness', 'mxchat'),
4670 array($this, 'mxchat_contextual_awareness_callback'),
4671 'mxchat-chatbot',
4672 'mxchat_chatbot_section'
4673 );
4674
4675 add_settings_field(
4676 'citation_links_toggle',
4677 esc_html__('Citation Links', 'mxchat'),
4678 array($this, 'mxchat_citation_links_toggle_callback'),
4679 'mxchat-chatbot',
4680 'mxchat_chatbot_section'
4681 );
4682
4683 add_settings_field(
4684 'enable_streaming_toggle',
4685 esc_html__('Enable Streaming', 'mxchat'),
4686 array($this, 'enable_streaming_toggle_callback'),
4687 'mxchat-chatbot',
4688 'mxchat_chatbot_section', // Same section as your working toggle
4689 array(
4690 'class' => 'mxchat-setting-row streaming-setting',
4691 'style' => 'display: none;' // Hidden by default, shown when OpenAI/Claude selected
4692 )
4693 );
4694
4695
4696 add_settings_field(
4697 'model',
4698 esc_html__('Chat Model', 'mxchat'),
4699 array($this, 'mxchat_model_callback'),
4700 'mxchat-chatbot',
4701 'mxchat_chatbot_section'
4702 );
4703
4704 add_settings_field(
4705 'embedding_model',
4706 esc_html__('Embedding Model', 'mxchat'),
4707 array($this, 'embedding_model_callback'),
4708 'mxchat-chatbot',
4709 'mxchat_chatbot_section'
4710 );
4711
4712 add_settings_field(
4713 'system_prompt_instructions',
4714 esc_html__('AI Instructions (Behavior)', 'mxchat'),
4715 array($this, 'system_prompt_instructions_callback'),
4716 'mxchat-chatbot',
4717 'mxchat_chatbot_section'
4718 );
4719
4720
4721 add_settings_field(
4722 'top_bar_title',
4723 esc_html__('Top Bar Title', 'mxchat'),
4724 array($this, 'mxchat_top_bar_title_callback'),
4725 'mxchat-chatbot',
4726 'mxchat_chatbot_section'
4727 );
4728
4729 add_settings_field(
4730 'ai_agent_text',
4731 esc_html__('AI Agent Text', 'mxchat'),
4732 array($this, 'mxchat_ai_agent_text_callback'),
4733 'mxchat-chatbot',
4734 'mxchat_chatbot_section'
4735 );
4736
4737 add_settings_field(
4738 'enable_email_block',
4739 esc_html__('Require Email To Chat', 'mxchat'),
4740 array($this, 'enable_email_block_callback'),
4741 'mxchat-chatbot',
4742 'mxchat_chatbot_section'
4743 );
4744
4745 add_settings_field(
4746 'email_blocker_header_content',
4747 esc_html__('Require Email Chat Content', 'mxchat'),
4748 array($this, 'email_blocker_header_content_callback'),
4749 'mxchat-chatbot',
4750 'mxchat_chatbot_section'
4751 );
4752
4753 add_settings_field(
4754 'email_blocker_button_text',
4755 esc_html__('Require Email Chat Button Text', 'mxchat'),
4756 [$this, 'email_blocker_button_text_callback'],
4757 'mxchat-chatbot',
4758 'mxchat_chatbot_section'
4759 );
4760
4761 add_settings_field(
4762 'enable_name_field',
4763 esc_html__('Require Name Field', 'mxchat'),
4764 array($this, 'enable_name_field_callback'),
4765 'mxchat-chatbot',
4766 'mxchat_chatbot_section'
4767 );
4768
4769 add_settings_field(
4770 'name_field_placeholder',
4771 esc_html__('Name Field Placeholder', 'mxchat'),
4772 array($this, 'name_field_placeholder_callback'),
4773 'mxchat-chatbot',
4774 'mxchat_chatbot_section'
4775 );
4776
4777 add_settings_field(
4778 'intro_message',
4779 esc_html__('Introductory Message', 'mxchat'),
4780 array($this, 'mxchat_intro_message_callback'),
4781 'mxchat-chatbot',
4782 'mxchat_chatbot_section'
4783 );
4784
4785 add_settings_field(
4786 'input_copy',
4787 esc_html__('Input Copy', 'mxchat'),
4788 array($this, 'mxchat_input_copy_callback'),
4789 'mxchat-chatbot',
4790 'mxchat_chatbot_section'
4791 );
4792
4793 add_settings_field(
4794 'pre_chat_message',
4795 esc_html__('Chat Teaser Pop-up', 'mxchat'),
4796 array($this, 'mxchat_pre_chat_message_callback'),
4797 'mxchat-chatbot',
4798 'mxchat_chatbot_section'
4799 );
4800
4801 add_settings_field(
4802 'privacy_toggle',
4803 esc_html__('Toggle Privacy Notice', 'mxchat'),
4804 array($this, 'mxchat_privacy_toggle_callback'),
4805 'mxchat-chatbot',
4806 'mxchat_chatbot_section'
4807 );
4808
4809 add_settings_field(
4810 'complianz_toggle',
4811 esc_html__('Enable Complianz', 'mxchat'),
4812 array($this, 'mxchat_complianz_toggle_callback'),
4813 'mxchat-chatbot',
4814 'mxchat_chatbot_section'
4815 );
4816
4817 add_settings_field(
4818 'link_target_toggle',
4819 esc_html__('Open Links in a New Tab', 'mxchat'),
4820 array($this, 'mxchat_link_target_toggle_callback'),
4821 'mxchat-chatbot',
4822 'mxchat_chatbot_section'
4823 );
4824
4825 add_settings_field(
4826 'chat_persistence_toggle',
4827 esc_html__('Enable Chat Persistence', 'mxchat'),
4828 array($this, 'mxchat_chat_persistence_toggle_callback'),
4829 'mxchat-chatbot',
4830 'mxchat_chatbot_section'
4831 );
4832
4833 add_settings_field(
4834 'popular_question_1',
4835 esc_html__('Quick Question 1', 'mxchat'),
4836 array($this, 'mxchat_popular_question_1_callback'),
4837 'mxchat-chatbot',
4838 'mxchat_chatbot_section'
4839 );
4840
4841 add_settings_field(
4842 'popular_question_2',
4843 esc_html__('Quick Question 2', 'mxchat'),
4844 array($this, 'mxchat_popular_question_2_callback'),
4845 'mxchat-chatbot',
4846 'mxchat_chatbot_section'
4847 );
4848
4849 add_settings_field(
4850 'popular_question_3',
4851 esc_html__('Quick Question 3', 'mxchat'),
4852 array($this, 'mxchat_popular_question_3_callback'),
4853 'mxchat-chatbot',
4854 'mxchat_chatbot_section'
4855 );
4856
4857 add_settings_field(
4858 'additional_popular_questions',
4859 esc_html__('Additional Quick Questions', 'mxchat'),
4860 array($this, 'mxchat_additional_popular_questions_callback'),
4861 'mxchat-chatbot',
4862 'mxchat_chatbot_section'
4863 );
4864
4865
4866 add_settings_field(
4867 'rate_limits',
4868 __('Rate Limits Settings', 'mxchat'),
4869 array($this, 'mxchat_rate_limits_callback'),
4870 'mxchat-chatbot',
4871 'mxchat_chatbot_section'
4872 );
4873
4874 // Loops Settings Section
4875 add_settings_section(
4876 'mxchat_loops_section',
4877 esc_html__('Loops Settings', 'mxchat'),
4878 null,
4879 'mxchat-embed'
4880 );
4881
4882 // Loops Settings Fields (API Key moved to API Keys tab)
4883 add_settings_field(
4884 'loops_mailing_list',
4885 esc_html__('Loops Mailing List', 'mxchat'),
4886 array($this, 'mxchat_loops_mailing_list_callback'),
4887 'mxchat-embed',
4888 'mxchat_loops_section'
4889 );
4890
4891 add_settings_field(
4892 'triggered_phrase_response',
4893 esc_html__('Triggered Phrase Response', 'mxchat'),
4894 array($this, 'mxchat_triggered_phrase_response_callback'),
4895 'mxchat-embed',
4896 'mxchat_loops_section'
4897 );
4898
4899 add_settings_field(
4900 'email_capture_response',
4901 esc_html__('Email Capture Response', 'mxchat'),
4902 array($this, 'mxchat_email_capture_response_callback'),
4903 'mxchat-embed',
4904 'mxchat_loops_section'
4905 );
4906
4907 // Brave Search Settings Fields
4908 add_settings_section(
4909 'mxchat_brave_section',
4910 __('Brave Search Settings', 'mxchat'),
4911 array($this, 'mxchat_brave_section_callback'),
4912 'mxchat-embed'
4913 );
4914
4915 // Brave API Key moved to API Keys tab
4916 add_settings_field(
4917 'brave_image_count',
4918 __('Number of Images to Return', 'mxchat'),
4919 array($this, 'mxchat_brave_image_count_callback'),
4920 'mxchat-embed',
4921 'mxchat_brave_section'
4922 );
4923
4924 add_settings_field(
4925 'brave_safe_search',
4926 __('Safe Search', 'mxchat'),
4927 array($this, 'mxchat_brave_safe_search_callback'),
4928 'mxchat-embed',
4929 'mxchat_brave_section'
4930 );
4931
4932 add_settings_field(
4933 'brave_news_count',
4934 __('Number of News Articles', 'mxchat'),
4935 array($this, 'mxchat_brave_news_count_callback'),
4936 'mxchat-embed',
4937 'mxchat_brave_section'
4938 );
4939
4940 add_settings_field(
4941 'brave_country',
4942 __('Country', 'mxchat'),
4943 array($this, 'mxchat_brave_country_callback'),
4944 'mxchat-embed',
4945 'mxchat_brave_section'
4946 );
4947
4948 add_settings_field(
4949 'brave_language',
4950 __('Language', 'mxchat'),
4951 array($this, 'mxchat_brave_language_callback'),
4952 'mxchat-embed',
4953 'mxchat_brave_section'
4954 );
4955
4956 // Chat with PDF Intent Settings Fields
4957 add_settings_section(
4958 'mxchat_pdf_intent_section',
4959 __('Toolbar Settings & Intents', 'mxchat'),
4960 array($this, 'mxchat_pdf_intent_section_callback'),
4961 'mxchat-embed'
4962 );
4963
4964 add_settings_field(
4965 'chat_toolbar_toggle',
4966 __('Show Chat Toolbar', 'mxchat'),
4967 array($this, 'mxchat_chat_toolbar_toggle_callback'),
4968 'mxchat-embed',
4969 'mxchat_pdf_intent_section'
4970 );
4971
4972 // PDF Upload Button Toggle
4973 add_settings_field(
4974 'show_pdf_upload_button',
4975 __('Show PDF Upload Button', 'mxchat'),
4976 array($this, 'mxchat_show_pdf_upload_button_callback'),
4977 'mxchat-embed',
4978 'mxchat_pdf_intent_section'
4979 );
4980
4981 // Word Upload Button Toggle
4982 add_settings_field(
4983 'show_word_upload_button',
4984 __('Show Word Upload Button', 'mxchat'),
4985 array($this, 'mxchat_show_word_upload_button_callback'),
4986 'mxchat-embed',
4987 'mxchat_pdf_intent_section'
4988 );
4989
4990 add_settings_field(
4991 'pdf_intent_trigger_text',
4992 __('Intent Trigger Text', 'mxchat'),
4993 array($this, 'mxchat_pdf_intent_trigger_text_callback'),
4994 'mxchat-embed',
4995 'mxchat_pdf_intent_section'
4996 );
4997
4998 add_settings_field(
4999 'pdf_intent_success_text',
5000 __('Success Text', 'mxchat'),
5001 array($this, 'mxchat_pdf_intent_success_text_callback'),
5002 'mxchat-embed',
5003 'mxchat_pdf_intent_section'
5004 );
5005
5006 add_settings_field(
5007 'pdf_intent_error_text',
5008 __('Error Text', 'mxchat'),
5009 array($this, 'mxchat_pdf_intent_error_text_callback'),
5010 'mxchat-embed',
5011 'mxchat_pdf_intent_section'
5012 );
5013
5014 // Add PDF Maximum Pages Field
5015 add_settings_field(
5016 'pdf_max_pages',
5017 __('Maximum Document Pages', 'mxchat'),
5018 array($this, 'mxchat_pdf_max_pages_callback'),
5019 'mxchat-embed',
5020 'mxchat_pdf_intent_section'
5021 );
5022
5023 // Live Agent Settings Fields
5024 add_settings_section(
5025 'mxchat_live_agent_section',
5026 __('Live Agent Settings', 'mxchat'),
5027 array($this, 'mxchat_live_agent_section_callback'),
5028 'mxchat-embed'
5029 );
5030
5031 // Live Agent Status Fields (add at top of live agent settings)
5032 add_settings_field(
5033 'live_agent_status',
5034 __('Live Agent Status', 'mxchat'),
5035 array($this, 'mxchat_live_agent_status_callback'),
5036 'mxchat-embed',
5037 'mxchat_live_agent_section'
5038 );
5039
5040 add_settings_field(
5041 'live_agent_notification_message',
5042 __('Notification Message', 'mxchat'),
5043 array($this, 'mxchat_live_agent_notification_message_callback'),
5044 'mxchat-embed',
5045 'mxchat_live_agent_section'
5046 );
5047
5048 add_settings_field(
5049 'live_agent_away_message',
5050 __('Away Message', 'mxchat'),
5051 array($this, 'mxchat_live_agent_away_message_callback'),
5052 'mxchat-embed',
5053 'mxchat_live_agent_section'
5054 );
5055
5056 add_settings_field(
5057 'live_agent_user_ids',
5058 __('Slack Agent User IDs', 'mxchat'),
5059 array($this, 'mxchat_live_agent_user_ids_callback'),
5060 'mxchat-embed',
5061 'mxchat_live_agent_section'
5062 );
5063
5064 add_settings_field(
5065 'live_agent_webhook_url',
5066 __('Slack Webhook URL', 'mxchat'),
5067 array($this, 'mxchat_live_agent_webhook_url_callback'),
5068 'mxchat-embed',
5069 'mxchat_live_agent_section'
5070 );
5071
5072 add_settings_field(
5073 'live_agent_secret_key',
5074 __('Slack Secret Key', 'mxchat'),
5075 array($this, 'mxchat_live_agent_secret_key_callback'),
5076 'mxchat-embed',
5077 'mxchat_live_agent_section'
5078 );
5079
5080 // Live Agent Integration Fields
5081 add_settings_field(
5082 'live_agent_bot_token',
5083 __('Slack Bot OAuth Token', 'mxchat'),
5084 array($this, 'mxchat_live_agent_bot_token_callback'),
5085 'mxchat-embed',
5086 'mxchat_live_agent_section'
5087 );
5088
5089 // Telegram Integration Section
5090 add_settings_section(
5091 'mxchat_telegram_section',
5092 __('Telegram Settings', 'mxchat'),
5093 array($this, 'mxchat_telegram_section_callback'),
5094 'mxchat-embed'
5095 );
5096
5097 add_settings_field(
5098 'telegram_status',
5099 __('Live Agent Status', 'mxchat'),
5100 array($this, 'mxchat_telegram_status_callback'),
5101 'mxchat-embed',
5102 'mxchat_telegram_section'
5103 );
5104
5105 add_settings_field(
5106 'telegram_notification_message',
5107 __('Notification Message', 'mxchat'),
5108 array($this, 'mxchat_telegram_notification_message_callback'),
5109 'mxchat-embed',
5110 'mxchat_telegram_section'
5111 );
5112
5113 add_settings_field(
5114 'telegram_away_message',
5115 __('Away Message', 'mxchat'),
5116 array($this, 'mxchat_telegram_away_message_callback'),
5117 'mxchat-embed',
5118 'mxchat_telegram_section'
5119 );
5120
5121 add_settings_field(
5122 'telegram_bot_token',
5123 __('Telegram Bot Token', 'mxchat'),
5124 array($this, 'mxchat_telegram_bot_token_callback'),
5125 'mxchat-embed',
5126 'mxchat_telegram_section'
5127 );
5128
5129 add_settings_field(
5130 'telegram_group_id',
5131 __('Telegram Group ID', 'mxchat'),
5132 array($this, 'mxchat_telegram_group_id_callback'),
5133 'mxchat-embed',
5134 'mxchat_telegram_section'
5135 );
5136
5137 add_settings_field(
5138 'telegram_webhook_secret',
5139 __('Webhook Secret Token', 'mxchat'),
5140 array($this, 'mxchat_telegram_webhook_secret_callback'),
5141 'mxchat-embed',
5142 'mxchat_telegram_section'
5143 );
5144
5145 // General Settings Section
5146 add_settings_section(
5147 'mxchat_general_section',
5148 esc_html__('YouTube Tutorials', 'mxchat'),
5149 null,
5150 'mxchat-general'
5151 );
5152 }
5153
5154 public function mxchat_prompts_page_init() {
5155 register_setting(
5156 'mxchat_prompts_options',
5157 'mxchat_prompts_options',
5158 array(
5159 'type' => 'array',
5160 'description' => __('MXChat Knowledge Base Settings', 'mxchat'),
5161 'default' => array(
5162 'mxchat_auto_sync_posts' => 0,
5163 'mxchat_auto_sync_pages' => 0,
5164 'mxchat_use_pinecone' => 0,
5165 'mxchat_pinecone_api_key' => '',
5166 'mxchat_pinecone_environment' => '',
5167 'mxchat_pinecone_index' => '',
5168 'mxchat_pinecone_host' => '',
5169 ),
5170 'sanitize_callback' => array($this, 'sanitize_prompts_options'),
5171 )
5172 );
5173
5174 add_action('admin_notices', array($this, 'sync_settings_notice'));
5175 }
5176
5177 public function mxchat_transcripts_page_init() {
5178 register_setting(
5179 'mxchat_transcripts_options',
5180 'mxchat_transcripts_options',
5181 array(
5182 'type' => 'array',
5183 'description' => __('MXChat Transcripts Notification Settings', 'mxchat'),
5184 'default' => array(
5185 'mxchat_enable_notifications' => 0,
5186 'mxchat_notification_email' => get_option('admin_email'),
5187 'mxchat_auto_delete_transcripts' => 'never',
5188 ),
5189 'sanitize_callback' => array($this, 'sanitize_transcripts_options'),
5190 )
5191 );
5192
5193 add_settings_section(
5194 'mxchat_transcripts_notification_section',
5195 esc_html__('Chat Notification Settings', 'mxchat'),
5196 array($this, 'mxchat_transcripts_notification_section_callback'),
5197 'mxchat-transcripts'
5198 );
5199
5200 add_settings_field(
5201 'mxchat_enable_notifications',
5202 esc_html__('Enable Chat Notifications', 'mxchat'),
5203 array($this, 'mxchat_enable_notifications_callback'),
5204 'mxchat-transcripts',
5205 'mxchat_transcripts_notification_section'
5206 );
5207
5208 add_settings_field(
5209 'mxchat_notification_email',
5210 esc_html__('Notification Email Address', 'mxchat'),
5211 array($this, 'mxchat_notification_email_callback'),
5212 'mxchat-transcripts',
5213 'mxchat_transcripts_notification_section'
5214 );
5215
5216 add_settings_field(
5217 'mxchat_auto_delete_transcripts',
5218 esc_html__('Auto-Delete Old Transcripts', 'mxchat'),
5219 array($this, 'mxchat_auto_delete_transcripts_callback'),
5220 'mxchat-transcripts',
5221 'mxchat_transcripts_notification_section'
5222 );
5223
5224 add_settings_field(
5225 'mxchat_auto_email_transcript',
5226 esc_html__('Auto-Email Full Transcript', 'mxchat'),
5227 array($this, 'mxchat_auto_email_transcript_callback'),
5228 'mxchat-transcripts',
5229 'mxchat_transcripts_notification_section'
5230 );
5231 }
5232
5233
5234 /**
5235 * Sanitize all prompts options
5236 *
5237 * @param array $input The unsanitized options array
5238 * @return array The sanitized options array
5239 */
5240 public function sanitize_prompts_options($input) {
5241 // Log the incoming input.
5242 //error_log('Sanitizing inputs: ' . print_r($input, true));
5243
5244 $sanitized = array();
5245
5246 // Boolean options
5247 $sanitized['mxchat_auto_sync_posts'] = isset($input['mxchat_auto_sync_posts']) ? 1 : 0;
5248 $sanitized['mxchat_auto_sync_pages'] = isset($input['mxchat_auto_sync_pages']) ? 1 : 0;
5249 $sanitized['mxchat_use_pinecone'] = !empty($input['mxchat_use_pinecone']) ? 1 : 0;
5250
5251 // API Key: if less than 32 characters, flag as invalid.
5252 $api_key = sanitize_text_field($input['mxchat_pinecone_api_key'] ?? '');
5253 if (!empty($api_key) && strlen($api_key) < 32) {
5254 add_settings_error(
5255 'mxchat_prompts_options',
5256 'invalid_api_key',
5257 __('The Pinecone API key appears to be invalid. Please check your API key.', 'mxchat')
5258 );
5259 $existing_options = get_option('mxchat_prompts_options', array());
5260 $sanitized['mxchat_pinecone_api_key'] = $existing_options['mxchat_pinecone_api_key'] ?? '';
5261 } else {
5262 $sanitized['mxchat_pinecone_api_key'] = $api_key;
5263 }
5264
5265 // Environment and Index Name
5266 $sanitized['mxchat_pinecone_environment'] = sanitize_text_field($input['mxchat_pinecone_environment'] ?? '');
5267 $sanitized['mxchat_pinecone_index'] = sanitize_text_field($input['mxchat_pinecone_index'] ?? '');
5268
5269 // Host: Remove protocol and validate format.
5270 $host = sanitize_text_field($input['mxchat_pinecone_host'] ?? '');
5271 $host = preg_replace('#^https?://#', '', $host);
5272 //error_log('Host after removing protocol: ' . $host);
5273 if (!empty($host)) {
5274 if (!preg_match('/^[\w-]+\.svc\.[\w-]+\.pinecone\.io$/', $host)) {
5275 add_settings_error(
5276 'mxchat_prompts_options',
5277 'invalid_host',
5278 __('The Pinecone host appears to be invalid. It should look like "mxchat-vectors-zrmsquq.svc.aped-4627-b74a.pinecone.io"', 'mxchat')
5279 );
5280 $existing_options = get_option('mxchat_prompts_options', array());
5281 $sanitized['mxchat_pinecone_host'] = $existing_options['mxchat_pinecone_host'] ?? '';
5282 } else {
5283 $sanitized['mxchat_pinecone_host'] = $host;
5284 }
5285 } else {
5286 $sanitized['mxchat_pinecone_host'] = '';
5287 }
5288
5289 //error_log('Final sanitized array: ' . print_r($sanitized, true));
5290
5291 return $sanitized;
5292 }
5293
5294 public function sync_settings_notice() {
5295 // Only show notice on our plugin page
5296 if (!isset($_GET['page']) || $_GET['page'] !== 'mxchat-prompts') {
5297 return;
5298 }
5299
5300 // Check if settings were updated
5301 if (isset($_GET['settings-updated'])) {
5302
5303 ?>
5304 <div class="notice notice-success is-dismissible">
5305 <p><?php esc_html_e('Sync settings updated successfully.', 'mxchat'); ?></p>
5306 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php esc_html_e('Dismiss this notice.', 'mxchat'); ?></span></button>
5307 </div>
5308 <?php
5309
5310 }
5311 }
5312 // Add this sanitization function to your class
5313 public function sanitize_sync_setting($input) {
5314 return (bool)$input ? __('1', 'mxchat') : __('', 'mxchat');
5315 }
5316
5317 public function mxchat_rate_limits_callback() {
5318 $all_options = get_option('mxchat_options', []);
5319
5320 // Define available rate limits
5321 $rate_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited');
5322
5323 // Define available timeframes
5324 $timeframes = array(
5325 'hourly' => __('Per Hour', 'mxchat'),
5326 'daily' => __('Per Day', 'mxchat'),
5327 'weekly' => __('Per Week', 'mxchat'),
5328 'monthly' => __('Per Month', 'mxchat')
5329 );
5330
5331 // Get all roles plus a "logged_out" pseudo-role
5332 $roles = wp_roles()->get_names();
5333 $roles['logged_out'] = __('Logged Out Users', 'mxchat');
5334
5335 // Start the wrapper
5336 echo '<div class="pro-feature-wrapper active">';
5337 echo '<div class="mxchat-rate-limits-container">';
5338
5339 echo '<p class="description" style="margin-bottom: 20px;">' .
5340 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') .
5341 '</p>';
5342
5343 // Add markdown link documentation
5344 echo '<div class="notice notice-info inline" style="margin-bottom: 20px; padding: 10px;">';
5345 echo '<p><strong>' . esc_html__('Markdown Links Supported:', 'mxchat') . '</strong></p>';
5346 echo '<p>' . esc_html__('You can include clickable links in your custom messages using markdown syntax:', 'mxchat') . '</p>';
5347 echo '<ul style="margin-left: 20px;">';
5348 echo '<li><code>[Link text](https://example.com)</code> - Creates a clickable link</li>';
5349 echo '<li><code>[Visit our pricing](https://example.com/pricing)</code> - Link with custom text</li>';
5350 echo '<li><code>Plain URLs like https://example.com will also become clickable</code></li>';
5351 echo '</ul>';
5352 echo '</div>';
5353
5354 // Output the controls for each role
5355 foreach ($roles as $role_id => $role_name) {
5356 // Get saved options or defaults
5357 $default_limit = ($role_id === 'logged_out') ? '10' : '100';
5358 $default_timeframe = 'daily';
5359 $default_message = __('Rate limit exceeded. Please try again later.', 'mxchat');
5360
5361 $selected_limit = isset($all_options['rate_limits'][$role_id]['limit'])
5362 ? $all_options['rate_limits'][$role_id]['limit']
5363 : $default_limit;
5364
5365 $selected_timeframe = isset($all_options['rate_limits'][$role_id]['timeframe'])
5366 ? $all_options['rate_limits'][$role_id]['timeframe']
5367 : $default_timeframe;
5368
5369 $custom_message = isset($all_options['rate_limits'][$role_id]['message'])
5370 ? $all_options['rate_limits'][$role_id]['message']
5371 : $default_message;
5372
5373 // Output the row
5374 echo '<div class="mxchat-rate-limit-row mxchat-autosave-section">';
5375
5376 // Role label
5377 echo '<div class="mxchat-rate-limit-role">' . esc_html($role_name) . '</div>';
5378
5379 // Controls section
5380 echo '<div class="mxchat-rate-limit-controls-wrapper">';
5381
5382 // Rate limit and timeframe controls
5383 echo '<div class="mxchat-rate-limit-controls">';
5384
5385 // Limit dropdown
5386 echo '<div>';
5387 echo '<label for="rate_limits_' . esc_attr($role_id) . '_limit">' . esc_html__('Limit:', 'mxchat') . '</label>';
5388 echo '<select
5389 id="rate_limits_' . esc_attr($role_id) . '_limit"
5390 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][limit]"
5391 class="mxchat-autosave-field">';
5392 foreach ($rate_limits as $limit) {
5393 echo '<option value="' . esc_attr($limit) . '" ' . selected($selected_limit, $limit, false) . '>' . esc_html($limit) . '</option>';
5394 }
5395 echo '</select>';
5396 echo '</div>';
5397
5398 // Timeframe dropdown
5399 echo '<div>';
5400 echo '<label for="rate_limits_' . esc_attr($role_id) . '_timeframe">' . esc_html__('Timeframe:', 'mxchat') . '</label>';
5401 echo '<select
5402 id="rate_limits_' . esc_attr($role_id) . '_timeframe"
5403 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][timeframe]"
5404 class="mxchat-autosave-field">';
5405 foreach ($timeframes as $value => $label) {
5406 echo '<option value="' . esc_attr($value) . '" ' . selected($selected_timeframe, $value, false) . '>' . esc_html($label) . '</option>';
5407 }
5408 echo '</select>';
5409 echo '</div>';
5410
5411 echo '</div>'; // End controls
5412
5413 // Custom message textarea
5414 echo '<div class="mxchat-rate-limit-message">';
5415 echo '<label for="rate_limits_' . esc_attr($role_id) . '_message">' . esc_html__('Custom Message:', 'mxchat') . '</label>';
5416 echo '<textarea
5417 id="rate_limits_' . esc_attr($role_id) . '_message"
5418 name="mxchat_options[rate_limits][' . esc_attr($role_id) . '][message]"
5419 class="mxchat-autosave-field"
5420 placeholder="' . esc_attr__('Enter custom message when rate limit is exceeded', 'mxchat') . '">' .
5421 esc_textarea($custom_message) .
5422 '</textarea>';
5423 echo '<p class="description">' .
5424 esc_html__('Example: Rate limit reached! [Visit our pricing page](https://example.com/pricing) to upgrade.', 'mxchat') .
5425 '</p>';
5426 echo '</div>'; // End message
5427
5428 echo '</div>'; // End controls wrapper
5429
5430 echo '</div>'; // End row
5431 }
5432
5433 echo '</div>'; // End container
5434
5435 echo '</div>'; // End pro-feature-wrapper
5436 }
5437
5438 private function mxchat_add_option_field($id, $title, $callback = '') {
5439 add_settings_field(
5440 $id,
5441 __($title, 'mxchat'),
5442 $callback ? array($this, $callback) : array($this, $id . '_callback'),
5443 'mxchat-max',
5444 'mxchat_setting_section_id',
5445 $id === 'model' ? ['label_for' => 'model'] : []
5446 );
5447 }
5448
5449 // API Keys Section Callback
5450 public function mxchat_api_keys_section_callback() {
5451 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>';
5452 }
5453
5454 // OpenAI API Key
5455 public function api_key_callback() {
5456 $apiKey = isset($this->options['api_key']) ? esc_attr($this->options['api_key']) : '';
5457 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5458
5459 echo '<div class="api-key-wrapper">';
5460 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 . '" />';
5461 echo '<button type="button" id="toggleApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
5462 echo '<p class="description">' . esc_html__('Required for OpenAI GPT models and OpenAI embeddings. Get your API key from OpenAI Platform.', 'mxchat') . '</p>';
5463 echo '</div>';
5464 }
5465
5466 // X.AI API Key
5467 public function xai_api_key_callback() {
5468 $xaiApiKey = isset($this->options['xai_api_key']) ? esc_attr($this->options['xai_api_key']) : '';
5469 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5470
5471 echo '<div class="api-key-wrapper">';
5472 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 . '" />';
5473 echo '<button type="button" id="toggleXaiApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
5474 echo '<p class="description">' . esc_html__('Required for X.AI Grok models. Get your API key from X.AI Console.', 'mxchat') . '</p>';
5475 echo '</div>';
5476 }
5477 // Claude API Key
5478 public function claude_api_key_callback() {
5479 $claudeApiKey = isset($this->options['claude_api_key']) ? esc_attr($this->options['claude_api_key']) : '';
5480 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5481
5482 echo '<div class="api-key-wrapper">';
5483 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 . '" />';
5484 echo '<button type="button" id="toggleClaudeApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
5485 echo '<p class="description">' . esc_html__('Required for Anthropic Claude models. Get your API key from Anthropic Console.', 'mxchat') . '</p>';
5486 echo '</div>';
5487 }
5488
5489 // DeepSeek API Key
5490 public function deepseek_api_key_callback() {
5491 $apiKey = isset($this->options['deepseek_api_key']) ? esc_attr($this->options['deepseek_api_key']) : '';
5492 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5493
5494 echo '<div class="api-key-wrapper">';
5495 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 . '" />';
5496 echo '<button type="button" id="toggleDeepSeekApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
5497 echo '<p class="description">' . esc_html__('Required for DeepSeek models. Get your API key from DeepSeek Platform.', 'mxchat') . '</p>';
5498 echo '</div>';
5499 }
5500
5501 // Gemini API Key
5502 public function gemini_api_key_callback() {
5503 $geminiApiKey = isset($this->options['gemini_api_key']) ? esc_attr($this->options['gemini_api_key']) : '';
5504 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5505
5506 echo '<div class="api-key-wrapper">';
5507 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 . '" />';
5508 echo '<button type="button" id="toggleGeminiApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
5509 echo '<p class="description">' . esc_html__('Required for Google Gemini models and embeddings. Get your API key from Google AI Studio.', 'mxchat') . '</p>';
5510 echo '</div>';
5511 }
5512
5513
5514 // OpenRouter API Key
5515 public function openrouter_api_key_callback() {
5516 $openrouterApiKey = isset($this->options['openrouter_api_key']) ? esc_attr($this->options['openrouter_api_key']) : '';
5517 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5518
5519 echo '<div class="api-key-wrapper">';
5520 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 . '" />';
5521 echo '<button type="button" id="toggleOpenRouterApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
5522 echo '<p class="description">' . esc_html__('Required for OpenRouter models. Get your API key from OpenRouter.ai', 'mxchat') . '</p>';
5523 echo '</div>';
5524 }
5525
5526 // Voyage API Key
5527 public function voyage_api_key_callback() {
5528 $apiKey = isset($this->options['voyage_api_key']) ? esc_attr($this->options['voyage_api_key']) : '';
5529 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5530
5531 echo '<div class="api-key-wrapper">';
5532 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 . '" />';
5533 echo '<button type="button" id="toggleVoyageAPIKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
5534 echo '<p class="description">' . esc_html__('Required for Voyage AI embedding models. Get your API key from Voyage AI.', 'mxchat') . '</p>';
5535 echo '</div>';
5536 }
5537
5538 public function mxchat_loops_api_key_callback() {
5539 $loops_api_key = isset($this->options['loops_api_key']) ? esc_attr($this->options['loops_api_key']) : '';
5540 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5541
5542 echo '<div class="api-key-wrapper">';
5543 echo sprintf(
5544 '<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" />',
5545 $loops_api_key,
5546 $nonce
5547 );
5548 echo '<button type="button" id="toggleLoopsApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
5549 echo '</div>';
5550 echo '<p class="description">' . esc_html__('Required for Loops email integration. Get your API key from Loops.so', 'mxchat') . '</p>';
5551 }
5552 public function mxchat_loops_mailing_list_callback() {
5553 // Add error handling and type checking
5554 $loops_api_key = '';
5555 $selected_list = '';
5556 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5557
5558 // Safely get the API key
5559 if (isset($this->options['loops_api_key']) && is_string($this->options['loops_api_key'])) {
5560 $loops_api_key = $this->options['loops_api_key'];
5561 }
5562
5563 // Safely get the selected list
5564 if (isset($this->options['loops_mailing_list']) && is_string($this->options['loops_mailing_list'])) {
5565 $selected_list = $this->options['loops_mailing_list'];
5566 }
5567
5568 if (!empty($loops_api_key)) {
5569 $lists = $this->mxchat_fetch_loops_mailing_lists($loops_api_key);
5570 if (is_array($lists) && !empty($lists)) {
5571 echo '<div class="mxchat-field-wrapper">';
5572 echo '<select id="loops_mailing_list" name="loops_mailing_list" class="mxchat-autosave-field" data-nonce="' . $nonce . '">';
5573
5574 // Add a default "Select a list" option
5575 echo '<option value="" ' . selected($selected_list, '', false) . '>' . esc_html__('Select a list', 'mxchat') . '</option>';
5576
5577 foreach ($lists as $list) {
5578 if (is_array($list) && isset($list['id']) && isset($list['name'])) {
5579 echo sprintf(
5580 '<option value="%s" %s>%s</option>',
5581 esc_attr($list['id']),
5582 selected($selected_list, $list['id'], false),
5583 esc_html($list['name'])
5584 );
5585 }
5586 }
5587 echo '</select>';
5588 echo '</div>';
5589 echo '<p class="description">' . esc_html__('Please select a mailing list to use with Loops.', 'mxchat') . '</p>';
5590 } else {
5591 echo '<p class="description">' . esc_html__('No lists found. Please verify your API Key.', 'mxchat') . '</p>';
5592 }
5593 } else {
5594 echo '<p class="description">' . esc_html__('Enter a valid Loops API Key to load mailing lists.', 'mxchat') . '</p>';
5595 }
5596 }
5597 public function mxchat_triggered_phrase_response_callback() {
5598 $default_response = __('Would you like to join our mailing list? Please provide your email below.', 'mxchat');
5599 $triggered_response = isset($this->options['triggered_phrase_response'])
5600 ? $this->options['triggered_phrase_response']
5601 : $default_response;
5602 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5603
5604 echo '<div class="mxchat-field-wrapper">';
5605 echo sprintf(
5606 '<textarea id="triggered_phrase_response" name="triggered_phrase_response" rows="3" cols="50" class="mxchat-autosave-field" data-nonce="%s">%s</textarea>',
5607 $nonce,
5608 esc_textarea($triggered_response)
5609 );
5610 echo '</div>';
5611 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>';
5612 }
5613
5614 public function mxchat_email_capture_response_callback() {
5615 $default_response = __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
5616 $email_capture_response = isset($this->options['email_capture_response'])
5617 ? $this->options['email_capture_response']
5618 : $default_response;
5619 $nonce = wp_create_nonce('mxchat_autosave_nonce');
5620
5621 echo '<div class="mxchat-field-wrapper">';
5622 echo sprintf(
5623 '<textarea id="email_capture_response" name="email_capture_response" rows="3" cols="50" class="mxchat-autosave-field" data-nonce="%s">%s</textarea>',
5624 $nonce,
5625 esc_textarea($email_capture_response)
5626 );
5627 echo '</div>';
5628 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>';
5629 }
5630 public function mxchat_pre_chat_message_callback() {
5631 // Load the entire 'mxchat_options' array
5632 $all_options = get_option('mxchat_options', []);
5633
5634 // Retrieve the saved message or use the default value
5635 $default_message = __('Hey there! Ask me anything!', 'mxchat');
5636 $pre_chat_message = isset($all_options['pre_chat_message']) ? $all_options['pre_chat_message'] : $default_message;
5637
5638 // Output the textarea
5639 printf(
5640 '<textarea id="pre_chat_message" name="pre_chat_message" rows="5" cols="50">%s</textarea>',
5641 esc_textarea($pre_chat_message)
5642 );
5643 }
5644
5645 // Callback for AI Instructions textarea
5646 public function system_prompt_instructions_callback() {
5647 // Retrieve the current value of the system prompt instructions
5648 $instructions = isset($this->options['system_prompt_instructions']) ? esc_textarea($this->options['system_prompt_instructions']) : '';
5649 // Render the textarea field
5650 printf(
5651 '<textarea id="system_prompt_instructions" name="system_prompt_instructions" rows="5" cols="50">%s</textarea>',
5652 $instructions
5653 );
5654 // Personalization hint
5655 echo '<p class="description" style="margin-top: 8px;">';
5656 echo esc_html__('Use {visitor_name} to personalize AI responses when lead capture is enabled.', 'mxchat') . '<br>';
5657 echo '<code style="font-size: 12px;">' . esc_html__('Example: The visitor\'s name is {visitor_name}. Address them by name.', 'mxchat') . '</code>';
5658 echo '</p>';
5659 // Sample instructions button
5660 echo '<div class="mxchat-instructions-container">';
5661 echo '<button type="button" class="mxchat-instructions-btn" id="mxchatViewSampleBtn">';
5662 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">';
5663 echo '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>';
5664 echo '<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>';
5665 echo '</svg>';
5666 echo esc_html__('View Sample Instructions', 'mxchat');
5667 echo '</button>';
5668 echo '</div>';
5669
5670 // Add modal to WordPress admin footer instead of inline
5671 add_action('admin_footer', array($this, 'render_sample_instructions_modal'));
5672 }
5673
5674 // New method to render modal in admin footer
5675 public function render_sample_instructions_modal() {
5676 static $modal_rendered = false;
5677 if ($modal_rendered) return; // Prevent duplicate modals
5678 $modal_rendered = true;
5679
5680 echo '<div class="mxchat-instructions-modal-overlay" id="mxchatSampleModal">';
5681 echo '<div class="mxchat-instructions-modal-content">';
5682 echo '<div class="mxchat-instructions-modal-header">';
5683 echo '<h3 class="mxchat-instructions-modal-title">';
5684 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">';
5685 echo '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>';
5686 echo '<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>';
5687 echo '</svg>';
5688 echo esc_html__('Sample AI Instructions', 'mxchat');
5689 echo '</h3>';
5690 echo '<button type="button" class="mxchat-instructions-modal-close" id="mxchatModalClose">';
5691 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">';
5692 echo '<line x1="18" y1="6" x2="6" y2="18"/>';
5693 echo '<line x1="6" y1="6" x2="18" y2="18"/>';
5694 echo '</svg>';
5695 echo '</button>';
5696 echo '</div>';
5697 echo '<div class="mxchat-instructions-modal-body">';
5698 echo '<div class="mxchat-instructions-content">';
5699 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:
5700
5701 # Response Style - CRITICALLY IMPORTANT
5702 - MAXIMUM LENGTH: 1-3 short sentences per response
5703 - Ultra-concise: Get straight to the answer with no filler
5704 - No introductions like "Sure!" or "I\'d be happy to help"
5705 - No phrases like "based on my knowledge" or "according to information"
5706 - No explanatory text before giving the answer
5707 - No summaries or repetition
5708 - Hyperlink all URLs
5709 - Respond in user\'s language
5710 - Minor chit chat or conversation is okay, but try to keep it focused on [insert topic]
5711
5712 # Knowledge Base Requirements - PREVENT HALLUCINATIONS
5713 - ONLY answer using information explicitly provided in OFFICIAL KNOWLEDGE DATABASE CONTENT sections marked with ===== delimiters
5714 - If required information is NOT in the knowledge database: "I don\'t have enough information in my knowledge base to answer that question accurately."
5715 - NEVER invent or hallucinate URLs, links, product specs, procedures, dates, statistics, names, contacts, or company information
5716 - When knowledge base information is unclear or contradictory, acknowledge the limitation rather than guessing
5717 - Better to admit insufficient information than provide inaccurate answers');
5718 echo '</div>';
5719 echo '<button type="button" class="mxchat-instructions-copy-btn" id="mxchatCopyBtn">';
5720 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">';
5721 echo '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>';
5722 echo '<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>';
5723 echo '</svg>';
5724 echo esc_html__('Copy Instructions', 'mxchat');
5725 echo '</button>';
5726 echo '</div>';
5727 echo '<div class="mxchat-instructions-modal-footer">';
5728 echo '<button type="button" class="mxchat-instructions-btn-secondary" id="mxchatCloseBtn">' . esc_html__('Close', 'mxchat') . '</button>';
5729 echo '</div>';
5730 echo '</div>';
5731 echo '</div>';
5732 }
5733
5734
5735 public function mxchat_model_callback() {
5736 // Define available models grouped by provider
5737 $models = array(
5738 esc_html__('OpenRouter (100+ Models)', 'mxchat') => array(
5739 'openrouter' => esc_html__('OpenRouter - Select after entering API key', 'mxchat'),
5740 ),
5741 esc_html__('Google Gemini Models', 'mxchat') => array(
5742 'gemini-3-pro-preview' => esc_html__('Gemini 3 Pro (Most Intelligent, Multimodal)', 'mxchat'),
5743 'gemini-3-flash-preview' => esc_html__('Gemini 3 Flash (Balanced Speed & Scale)', 'mxchat'),
5744 'gemini-2.5-pro' => esc_html__('Gemini 2.5 Pro (Advanced Thinking)', 'mxchat'),
5745 'gemini-2.5-flash' => esc_html__('Gemini 2.5 Flash (Best Price-Performance)', 'mxchat'),
5746 'gemini-2.5-flash-lite' => esc_html__('Gemini 2.5 Flash-Lite (Ultra Fast)', 'mxchat'),
5747 'gemini-2.0-flash' => esc_html__('Gemini 2.0 Flash (Deprecated Mar 2026)', 'mxchat'),
5748 'gemini-2.0-flash-lite' => esc_html__('Gemini 2.0 Flash-Lite (Deprecated Mar 2026)', 'mxchat'),
5749 'gemini-1.5-pro' => esc_html__('Gemini 1.5 Pro (Deprecated Sep 2025)', 'mxchat'),
5750 'gemini-1.5-flash' => esc_html__('Gemini 1.5 Flash (Deprecated Sep 2025)', 'mxchat'),
5751 ),
5752 esc_html__('X.AI Models', 'mxchat') => array(
5753 'grok-3-beta' => esc_html__('Grok-3 (Powerful)', 'mxchat'),
5754 'grok-3-fast-beta' => esc_html__('Grok-3 Fast (High Performance)', 'mxchat'),
5755 'grok-3-mini-beta' => esc_html__('Grok-3 Mini (Affordable)', 'mxchat'),
5756 'grok-3-mini-fast-beta' => esc_html__('Grok-3 Mini Fast (Quick Response)', 'mxchat'),
5757 'grok-2' => esc_html__('Grok 2', 'mxchat'),
5758 'grok-4-0709' => esc_html__('Grok 4 (Latest Flagship)', 'mxchat'),
5759 'grok-4-1-fast-reasoning' => esc_html__('Grok 4.1 Fast (Reasoning)', 'mxchat'),
5760 'grok-4-1-fast-non-reasoning' => esc_html__('Grok 4.1 Fast (Non-Reasoning)', 'mxchat'),
5761 ),
5762 esc_html__('DeepSeek Models', 'mxchat') => array(
5763 'deepseek-chat' => esc_html__('DeepSeek-V3', 'mxchat'),
5764 ),
5765 esc_html__('Claude Models', 'mxchat') => array(
5766 'claude-opus-4-6' => esc_html__('Claude Opus 4.6 (Most Capable - Recommended)', 'mxchat'),
5767 'claude-sonnet-4-6' => esc_html__('Claude Sonnet 4.6 (Latest Sonnet - Fast & Capable)', 'mxchat'),
5768 'claude-opus-4-5' => esc_html__('Claude Opus 4.5 (Highly Capable)', 'mxchat'),
5769 'claude-sonnet-4-5-20250929' => esc_html__('Claude Sonnet 4.5 (Best for Agents & Coding)', 'mxchat'),
5770 'claude-opus-4-1-20250805' => esc_html__('Claude Opus 4.1 (Exceptional for Complex Tasks)', 'mxchat'),
5771 'claude-haiku-4-5-20251001' => esc_html__('Claude Haiku 4.5 (Fastest & Most Intelligent)', 'mxchat'),
5772 'claude-opus-4-20250514' => esc_html__('Claude 4 Opus (Complex Tasks)', 'mxchat'),
5773 'claude-sonnet-4-20250514' => esc_html__('Claude 4 Sonnet (High Performance)', 'mxchat'),
5774 ),
5775 esc_html__('OpenAI Models', 'mxchat') => array(
5776 'gpt-5.4' => esc_html__('GPT-5.4 (Flagship Reasoning & Coding)', 'mxchat'),
5777 'gpt-5.4-mini' => esc_html__('GPT-5.4 Mini (Fast, 400K Context)', 'mxchat'),
5778 'gpt-5.4-nano' => esc_html__('GPT-5.4 Nano (Fastest & Cheapest)', 'mxchat'),
5779 'gpt-5.3-chat-latest' => esc_html__('GPT-5.3 Chat (Conversational)', 'mxchat'),
5780 'gpt-5.2' => esc_html__('GPT-5.2 (Best General-Purpose & Agentic Model)', 'mxchat'),
5781 'gpt-5.1-chat-latest' => esc_html__('GPT-5.1 Chat Latest (Recommended)', 'mxchat'),
5782 'gpt-5.1-2025-11-13' => esc_html__('GPT-5.1 (Flagship for Coding & Agentic Tasks)', 'mxchat'),
5783 'gpt-5' => esc_html__('GPT-5 (Flagship for Coding, Reasoning & Agents)', 'mxchat'),
5784 'gpt-5-mini' => esc_html__('GPT-5 Mini (Fast and Lightweight)', 'mxchat'),
5785 'gpt-5-nano' => esc_html__('GPT-5 Nano (Fastest & Cheapest for Summarization/Classification)', 'mxchat'),
5786 ),
5787 );
5788
5789 // Retrieve the currently selected model from saved options
5790 $selected_model = isset($this->options['model']) ? esc_attr($this->options['model']) : 'gpt-5.1-chat-latest';
5791
5792 // Begin the select dropdown
5793 echo '<select id="model" name="model">';
5794
5795 // Iterate over groups of models
5796 foreach ($models as $group_label => $group_models) {
5797 echo '<optgroup label="' . esc_attr($group_label) . '">';
5798
5799 foreach ($group_models as $model_value => $model_label) {
5800 echo '<option value="' . esc_attr($model_value) . '" ' . selected($selected_model, $model_value, false) . '>' . esc_html($model_label) . '</option>';
5801 }
5802
5803 echo '</optgroup>';
5804 }
5805
5806 echo '</select>';
5807
5808 // Add a note for OpenRouter
5809 echo '<p class="description" id="openrouter-model-note" style="display:none; color: #d63638; font-weight: 500;">';
5810 echo '<span class="dashicons dashicons-info" style="font-size: 16px; vertical-align: middle;"></span> ';
5811 echo esc_html__('After entering your OpenRouter API key above, click the button below to load available models.', 'mxchat');
5812 echo '</p>';
5813
5814 // API Key Status Messages (hidden by default, shown by JS based on selected model)
5815 $has_openai_key = !empty($this->options['api_key']);
5816 $has_claude_key = !empty($this->options['claude_api_key']);
5817 $has_xai_key = !empty($this->options['xai_api_key']);
5818 $has_deepseek_key = !empty($this->options['deepseek_api_key']);
5819 $has_gemini_key = !empty($this->options['gemini_api_key']);
5820 $has_openrouter_key = !empty($this->options['openrouter_api_key']);
5821
5822 // OpenAI/GPT models
5823 echo '<p class="mxchat-api-status" data-provider="openai" style="display:none;">';
5824 if ($has_openai_key) {
5825 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenAI detected', 'mxchat') . '</span>';
5826 } else {
5827 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenAI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
5828 }
5829 echo '</p>';
5830
5831 // Claude models
5832 echo '<p class="mxchat-api-status" data-provider="claude" style="display:none;">';
5833 if ($has_claude_key) {
5834 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Anthropic (Claude) detected', 'mxchat') . '</span>';
5835 } else {
5836 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Anthropic (Claude) detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
5837 }
5838 echo '</p>';
5839
5840 // X.AI models
5841 echo '<p class="mxchat-api-status" data-provider="xai" style="display:none;">';
5842 if ($has_xai_key) {
5843 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for X.AI (Grok) detected', 'mxchat') . '</span>';
5844 } else {
5845 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>';
5846 }
5847 echo '</p>';
5848
5849 // DeepSeek models
5850 echo '<p class="mxchat-api-status" data-provider="deepseek" style="display:none;">';
5851 if ($has_deepseek_key) {
5852 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for DeepSeek detected', 'mxchat') . '</span>';
5853 } else {
5854 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for DeepSeek detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
5855 }
5856 echo '</p>';
5857
5858 // Gemini models
5859 echo '<p class="mxchat-api-status" data-provider="gemini" style="display:none;">';
5860 if ($has_gemini_key) {
5861 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Google Gemini detected', 'mxchat') . '</span>';
5862 } else {
5863 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Google Gemini detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
5864 }
5865 echo '</p>';
5866
5867 // OpenRouter models
5868 echo '<p class="mxchat-api-status" data-provider="openrouter" style="display:none;">';
5869 if ($has_openrouter_key) {
5870 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenRouter detected', 'mxchat') . '</span>';
5871 } else {
5872 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenRouter detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
5873 }
5874 echo '</p>';
5875
5876 // ADD THESE HIDDEN FIELDS RIGHT HERE:
5877 $openrouter_model = isset($this->options['openrouter_selected_model']) ? esc_attr($this->options['openrouter_selected_model']) : '';
5878 $openrouter_model_name = isset($this->options['openrouter_selected_model_name']) ? esc_attr($this->options['openrouter_selected_model_name']) : '';
5879
5880 echo '<input type="hidden" id="openrouter_selected_model" name="openrouter_selected_model" value="' . $openrouter_model . '" />';
5881 echo '<input type="hidden" id="openrouter_selected_model_name" name="openrouter_selected_model_name" value="' . $openrouter_model_name . '" />';
5882 }
5883
5884 // Update your existing callback method
5885 public function enable_streaming_toggle_callback() {
5886 // Get value from options array, default to 'on'
5887 $enabled = isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on';
5888 $checked = ($enabled === 'on') ? 'checked' : '';
5889
5890 echo '<label class="toggle-switch">';
5891 echo sprintf(
5892 '<input type="checkbox" id="enable_streaming_toggle" name="enable_streaming_toggle" value="on" %s />',
5893 esc_attr($checked)
5894 );
5895 echo '<span class="slider"></span>';
5896 echo '</label>';
5897
5898 // Test button with better styling
5899 echo '<div style="margin-top: 15px;">';
5900 echo '<button type="button" id="mxchat-test-streaming-btn" class="button button-secondary">';
5901 echo '<span class="dashicons dashicons-admin-tools" style="vertical-align: middle; margin-right: 5px;"></span>';
5902 echo esc_html__('Test Streaming Compatibility', 'mxchat');
5903 echo '</button>';
5904 echo '<p id="mxchat-test-streaming-result" style="margin-top:10px; font-weight: bold;"></p>';
5905 echo '</div>';
5906 }
5907
5908 // Web Search toggle callback
5909 public function enable_web_search_toggle_callback() {
5910 // Get value from options array, default to 'off'
5911 $enabled = isset($this->options['enable_web_search']) ? $this->options['enable_web_search'] : 'off';
5912 $checked = ($enabled === 'on') ? 'checked' : '';
5913
5914 // Get current model to determine if we should show/enable the toggle
5915 $current_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest';
5916
5917 // Models that DON'T support web search (per OpenAI docs)
5918 // gpt-5 with minimal reasoning is handled at API level, gpt-4.1-nano doesn't support it
5919 $unsupported_models = array('gpt-4.1-nano');
5920
5921 // Check if current model is an OpenAI model that supports web search
5922 $openai_models = array(
5923 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.4-nano', 'gpt-5.3-chat-latest',
5924 'gpt-5.2', 'gpt-5.1-chat-latest', 'gpt-5.1-2025-11-13', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano'
5925 );
5926
5927 $is_openai = in_array($current_model, $openai_models);
5928 $is_supported = $is_openai && !in_array($current_model, $unsupported_models);
5929
5930 // Wrapper div with data attributes for JS to show/hide
5931 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;"' : '') . '>';
5932
5933 echo '<label class="toggle-switch">';
5934 echo sprintf(
5935 '<input type="checkbox" id="enable_web_search" name="enable_web_search" value="on" %s />',
5936 esc_attr($checked)
5937 );
5938 echo '<span class="slider"></span>';
5939 echo '</label>';
5940
5941 echo '</div>';
5942
5943 // Message shown when non-OpenAI model is selected
5944 echo '<p id="web-search-unavailable-message" class="description" style="color: #666;' . ($is_supported ? ' display:none;' : '') . '">';
5945 echo '<span class="dashicons dashicons-info" style="font-size: 16px; vertical-align: middle; margin-right: 4px;"></span>';
5946 echo esc_html__('Web search is only available for OpenAI models.', 'mxchat');
5947 echo '</p>';
5948 }
5949
5950 // AJAX handler to fetch OpenRouter models
5951 public function fetch_openrouter_models() {
5952 check_ajax_referer('mxchat_fetch_openrouter_models', 'nonce');
5953
5954 $api_key = isset($_POST['api_key']) ? sanitize_text_field($_POST['api_key']) : '';
5955
5956 if (empty($api_key)) {
5957 wp_send_json_error(array('message' => 'API key is required'));
5958 }
5959
5960 $response = wp_remote_get('https://openrouter.ai/api/v1/models', array(
5961 'headers' => array(
5962 'Authorization' => 'Bearer ' . $api_key,
5963 'Content-Type' => 'application/json',
5964 ),
5965 'timeout' => 15,
5966 ));
5967
5968 if (is_wp_error($response)) {
5969 wp_send_json_error(array('message' => $response->get_error_message()));
5970 }
5971
5972 $body = wp_remote_retrieve_body($response);
5973 $data = json_decode($body, true);
5974
5975 if (isset($data['data']) && is_array($data['data'])) {
5976 // Format the models for the frontend
5977 $models = array_map(function($model) {
5978 return array(
5979 'id' => $model['id'],
5980 'name' => $model['name'] ?? $model['id'],
5981 'description' => $model['description'] ?? '',
5982 'context_length' => $model['context_length'] ?? 0,
5983 'pricing' => array(
5984 'prompt' => $model['pricing']['prompt'] ?? 0,
5985 'completion' => $model['pricing']['completion'] ?? 0,
5986 ),
5987 );
5988 }, $data['data']);
5989
5990 wp_send_json_success(array('models' => $models));
5991 } else {
5992 wp_send_json_error(array('message' => 'Invalid response from OpenRouter'));
5993 }
5994 }
5995
5996 // Callback function for embedding model selection
5997 public function embedding_model_callback() {
5998 $models = array(
5999 esc_html__('OpenAI Embeddings', 'mxchat') => array(
6000 'text-embedding-3-small' => esc_html__('TE3 Small (1536, Efficient)', 'mxchat'),
6001 'text-embedding-ada-002' => esc_html__('Ada 2 (1536, Recommended)', 'mxchat'),
6002 'text-embedding-3-large' => esc_html__('TE3 Large (3072, Powerful)', 'mxchat'),
6003 ),
6004 esc_html__('Voyage AI Embeddings', 'mxchat') => array(
6005 'voyage-3-large' => esc_html__('Voyage-3 Large (2048, Most Capable)', 'mxchat'),
6006 ),
6007 esc_html__('Google Gemini Embeddings', 'mxchat') => array(
6008 'gemini-embedding-001' => esc_html__('Gemini Embedding (1536, Stable)', 'mxchat'),
6009 )
6010 );
6011 $selected_model = isset($this->options['embedding_model']) ? esc_attr($this->options['embedding_model']) : 'text-embedding-ada-002';
6012 echo '<select id="embedding_model" name="embedding_model">';
6013 foreach ($models as $group_label => $group_models) {
6014 echo '<optgroup label="' . esc_attr($group_label) . '">';
6015 foreach ($group_models as $model_value => $model_label) {
6016 echo '<option value="' . esc_attr($model_value) . '" ' . selected($selected_model, $model_value, false) . '>' . esc_html($model_label) . '</option>';
6017 }
6018 echo '</optgroup>';
6019 }
6020 echo '</select>';
6021
6022 // API Key Status Messages for Embedding Models
6023 $has_openai_key = !empty($this->options['api_key']);
6024 $has_voyage_key = !empty($this->options['voyage_api_key']);
6025 $has_gemini_key = !empty($this->options['gemini_api_key']);
6026
6027 // OpenAI Embeddings
6028 echo '<p class="mxchat-embedding-api-status" data-provider="openai" style="display:none;">';
6029 if ($has_openai_key) {
6030 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for OpenAI detected', 'mxchat') . '</span>';
6031 } else {
6032 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for OpenAI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
6033 }
6034 echo '</p>';
6035
6036 // Voyage AI Embeddings
6037 echo '<p class="mxchat-embedding-api-status" data-provider="voyage" style="display:none;">';
6038 if ($has_voyage_key) {
6039 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Voyage AI detected', 'mxchat') . '</span>';
6040 } else {
6041 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Voyage AI detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
6042 }
6043 echo '</p>';
6044
6045 // Gemini Embeddings
6046 echo '<p class="mxchat-embedding-api-status" data-provider="gemini" style="display:none;">';
6047 if ($has_gemini_key) {
6048 echo '<span style="color: #00a32a;">✓ ' . esc_html__('API key for Google Gemini detected', 'mxchat') . '</span>';
6049 } else {
6050 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Google Gemini detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
6051 }
6052 echo '</p>';
6053 }
6054
6055
6056 public function mxchat_top_bar_title_callback() {
6057 // Retrieve the current value of the top bar title from saved options
6058 $top_bar_title = isset($this->options['top_bar_title']) ? esc_attr($this->options['top_bar_title']) : '';
6059
6060 // Render the input field
6061 echo '<input type="text" id="top_bar_title" name="top_bar_title" value="' . $top_bar_title . '" />';
6062 }
6063 public function mxchat_ai_agent_text_callback() {
6064 // Retrieve the current value of the AI agent text from saved options
6065 $ai_agent_text = isset($this->options['ai_agent_text']) ? esc_attr($this->options['ai_agent_text']) : '';
6066 // Render the input field
6067 echo '<input type="text" id="ai_agent_text" name="ai_agent_text" value="' . $ai_agent_text . '" />';
6068 }
6069
6070
6071 public function enable_email_block_callback() {
6072 // Load full plugin options array
6073 $all_options = get_option('mxchat_options', []);
6074
6075 // Get the value, default to 'off'
6076 $enable_email_block = isset($all_options['enable_email_block']) ? $all_options['enable_email_block'] : 'off';
6077
6078 // Check if it's 'on'
6079 $checked = ($enable_email_block === 'on') ? 'checked' : '';
6080
6081 echo '<label class="toggle-switch">';
6082 echo sprintf(
6083 '<input type="checkbox" id="enable_email_block" name="enable_email_block" value="on" %s />',
6084 esc_attr($checked)
6085 );
6086 echo '<span class="slider"></span>';
6087 echo '</label>';
6088 }
6089
6090 public function email_blocker_header_content_callback() {
6091 // Load the entire 'mxchat_options' array
6092 $all_options = get_option('mxchat_options', []);
6093
6094 // Retrieve the saved content or default to empty
6095 $content = isset($all_options['email_blocker_header_content'])
6096 ? $all_options['email_blocker_header_content']
6097 : '';
6098
6099 // Render the textarea - IMPORTANT: name should be just "email_blocker_header_content"
6100 echo '<textarea
6101 id="email_blocker_header_content"
6102 name="email_blocker_header_content"
6103 rows="5"
6104 cols="70"
6105 data-setting="email_blocker_header_content"
6106 >' . esc_textarea($content) . '</textarea>';
6107 }
6108
6109 public function email_blocker_button_text_callback() {
6110 // Load the entire 'mxchat_options' array
6111 $all_options = get_option('mxchat_options', []);
6112
6113 // Retrieve the saved button text or default to empty
6114 $button_text = isset($all_options['email_blocker_button_text'])
6115 ? $all_options['email_blocker_button_text']
6116 : '';
6117
6118 // Use esc_attr to safely render the existing text
6119 echo '<input type="text" id="email_blocker_button_text" name="email_blocker_button_text" value="' . esc_attr($button_text) . '" style="width: 300px;" />';
6120 }
6121
6122 //Enable name field callback
6123 public function enable_name_field_callback() {
6124 // Load full plugin options array
6125 $all_options = get_option('mxchat_options', []);
6126 // Get the value, default to 'off'
6127 $enable_name_field = isset($all_options['enable_name_field']) ? $all_options['enable_name_field'] : 'off';
6128 // Check if it's 'on'
6129 $checked = ($enable_name_field === 'on') ? 'checked' : '';
6130 echo '<label class="toggle-switch">';
6131 echo sprintf(
6132 '<input type="checkbox" id="enable_name_field" name="enable_name_field" value="on" %s />',
6133 esc_attr($checked)
6134 );
6135 echo '<span class="slider"></span>';
6136 echo '</label>';
6137 }
6138 //Name field placeholder callback
6139 public function name_field_placeholder_callback() {
6140 $all_options = get_option('mxchat_options', []);
6141 $placeholder = isset($all_options['name_field_placeholder'])
6142 ? $all_options['name_field_placeholder']
6143 : esc_html__('Enter your name', 'mxchat');
6144
6145 echo '<input type="text" id="name_field_placeholder" name="name_field_placeholder" value="' . esc_attr($placeholder) . '" style="width: 300px;" />';
6146 }
6147
6148
6149
6150 public function mxchat_intro_message_callback() {
6151 // Load the entire 'mxchat_options' array
6152 $all_options = get_option('mxchat_options', []);
6153 // Retrieve the saved intro message or use the default
6154 $default_message = __('Hello! How can I assist you today?', 'mxchat');
6155 $saved_message = isset($all_options['intro_message']) ? $all_options['intro_message'] : $default_message;
6156 // Output the textarea with the saved value without escaping HTML
6157 ?>
6158 <textarea id="intro_message" name="intro_message" rows="5" cols="50"><?php echo $saved_message; ?></textarea>
6159 <p class="description" style="margin-top: 8px;">
6160 <?php esc_html_e('Use {visitor_name} to personalize greetings when lead capture is enabled.', 'mxchat'); ?><br>
6161 <code style="font-size: 12px;"><?php esc_html_e('Example: Hello {visitor_name}! How can I help you today?', 'mxchat'); ?></code>
6162 </p>
6163 <?php
6164 }
6165
6166 public function mxchat_input_copy_callback() {
6167 // Load the entire 'mxchat_options' array
6168 $all_options = get_option('mxchat_options', []);
6169
6170 // Retrieve the saved input copy or use the default value
6171 $default_copy = __('How can I assist?', 'mxchat');
6172 $input_copy = isset($all_options['input_copy']) ? $all_options['input_copy'] : $default_copy;
6173
6174 // Output the input field with the saved value
6175 printf(
6176 '<input type="text" id="input_copy" name="input_copy" value="%s" placeholder="%s" />',
6177 esc_attr($input_copy),
6178 esc_attr__('How can I assist?', 'mxchat')
6179 );
6180 }
6181
6182
6183 public function mxchat_append_to_body_callback() {
6184 // Fetch fresh options to ensure we have the latest saved values
6185 $options = get_option('mxchat_options', array());
6186
6187 // Get value from options array, default to 'off'
6188 $append_to_body = isset($options['append_to_body']) ? $options['append_to_body'] : 'off';
6189 $checked = ($append_to_body === 'on') ? 'checked' : '';
6190
6191 // Get post type visibility settings
6192 $visibility_mode = isset($options['post_type_visibility_mode']) ? $options['post_type_visibility_mode'] : 'all';
6193 $visibility_list = isset($options['post_type_visibility_list']) ? $options['post_type_visibility_list'] : array();
6194 if (!is_array($visibility_list)) {
6195 $visibility_list = array();
6196 }
6197
6198 echo '<div class="mxchat-autosave-section">';
6199
6200 // Main toggle
6201 echo '<label class="toggle-switch">';
6202 echo sprintf(
6203 '<input type="checkbox" id="append_to_body" name="append_to_body" value="on" %s />',
6204 esc_attr($checked)
6205 );
6206 echo '<span class="slider"></span>';
6207 echo '</label>';
6208
6209 // Post Type Visibility Options (only visible when auto-display is ON)
6210 $display_style = ($append_to_body === 'on') ? '' : 'display: none;';
6211 echo '<div id="post-type-visibility-options" class="mxchat-sub-options" style="' . esc_attr($display_style) . '">';
6212
6213 echo '<div class="mxchat-post-type-visibility-header">';
6214 echo '<h4>' . esc_html__('Post Type Visibility', 'mxchat') . '</h4>';
6215 echo '</div>';
6216
6217 // Mode selector (radio buttons)
6218 echo '<div class="mxchat-visibility-mode">';
6219
6220 echo '<label class="mxchat-radio-label">';
6221 echo '<input type="radio" name="post_type_visibility_mode" value="all" ' . checked($visibility_mode, 'all', false) . ' />';
6222 echo '<span>' . esc_html__('Show on all post types', 'mxchat') . '</span>';
6223 echo '</label>';
6224
6225 echo '<label class="mxchat-radio-label">';
6226 echo '<input type="radio" name="post_type_visibility_mode" value="include" ' . checked($visibility_mode, 'include', false) . ' />';
6227 echo '<span>' . esc_html__('Only show on selected post types', 'mxchat') . '</span>';
6228 echo '</label>';
6229
6230 echo '<label class="mxchat-radio-label">';
6231 echo '<input type="radio" name="post_type_visibility_mode" value="exclude" ' . checked($visibility_mode, 'exclude', false) . ' />';
6232 echo '<span>' . esc_html__('Hide on selected post types', 'mxchat') . '</span>';
6233 echo '</label>';
6234
6235 echo '</div>';
6236
6237 // Post type checkboxes (only visible when mode is include or exclude)
6238 $list_display = ($visibility_mode !== 'all') ? '' : 'display: none;';
6239 echo '<div id="post-type-list" class="mxchat-post-type-list" style="' . esc_attr($list_display) . '">';
6240
6241 // Get all public post types
6242 $post_types = get_post_types(array('public' => true), 'objects');
6243
6244 foreach ($post_types as $post_type) {
6245 // Skip attachments
6246 if ($post_type->name === 'attachment') {
6247 continue;
6248 }
6249
6250 $is_checked = in_array($post_type->name, $visibility_list) ? 'checked' : '';
6251
6252 echo '<label class="mxchat-checkbox-label">';
6253 echo '<input type="checkbox" name="post_type_visibility_list[]" value="' . esc_attr($post_type->name) . '" ' . $is_checked . ' />';
6254 echo '<span>' . esc_html($post_type->label) . '</span>';
6255 echo '</label>';
6256 }
6257
6258 echo '</div>'; // End post-type-list
6259 echo '</div>'; // End post-type-visibility-options
6260 echo '</div>'; // End mxchat-autosave-section
6261 }
6262
6263 public function mxchat_frontend_debugger_callback() {
6264 $show_debugger = isset($this->options['show_frontend_debugger']) ? $this->options['show_frontend_debugger'] : 'on';
6265 $checked = ($show_debugger === 'on') ? 'checked' : '';
6266 echo '<label class="toggle-switch">';
6267 echo sprintf(
6268 '<input type="checkbox" id="show_frontend_debugger" name="show_frontend_debugger" value="on" %s />',
6269 esc_attr($checked)
6270 );
6271 echo '<span class="slider"></span>';
6272 echo '</label>';
6273 }
6274
6275 public function mxchat_contextual_awareness_callback() {
6276 // Get value from options array, default to 'off'
6277 $contextual_awareness = isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off';
6278 $checked = ($contextual_awareness === 'on') ? 'checked' : '';
6279 echo '<label class="toggle-switch">';
6280 echo sprintf(
6281 '<input type="checkbox" id="contextual_awareness_toggle" name="contextual_awareness_toggle" value="on" %s />',
6282 esc_attr($checked)
6283 );
6284 echo '<span class="slider"></span>';
6285 echo '</label>';
6286 }
6287
6288 public function mxchat_citation_links_toggle_callback() {
6289 // Get value from options array, default to 'on' (enabled by default)
6290 $citation_links = isset($this->options['citation_links_toggle']) ? $this->options['citation_links_toggle'] : 'on';
6291 $checked = ($citation_links === 'on') ? 'checked' : '';
6292 echo '<label class="toggle-switch">';
6293 echo sprintf(
6294 '<input type="checkbox" id="citation_links_toggle" name="citation_links_toggle" value="on" %s />',
6295 esc_attr($checked)
6296 );
6297 echo '<span class="slider"></span>';
6298 echo '</label>';
6299 }
6300
6301 public function mxchat_privacy_toggle_callback() {
6302 // Load from mxchat_options array
6303 $options = get_option('mxchat_options', []);
6304
6305 // Get privacy toggle value with fallback
6306 $privacy_toggle = isset($options['privacy_toggle']) ? $options['privacy_toggle'] : 'off';
6307 $checked = ($privacy_toggle === 'on') ? 'checked' : '';
6308
6309 // Get privacy text with fallback
6310 $privacy_text = isset($options['privacy_text'])
6311 ? $options['privacy_text']
6312 : __('By chatting, you agree to our <a href="https://example.com/privacy-policy" target="_blank">privacy policy</a>.', 'mxchat');
6313
6314 // Output the toggle switch
6315 echo '<label class="toggle-switch">';
6316 echo sprintf(
6317 '<input type="checkbox" id="privacy_toggle" name="privacy_toggle" value="on" %s />',
6318 esc_attr($checked)
6319 );
6320 echo '<span class="slider"></span>';
6321 echo '</label>';
6322
6323 // Output the custom text input field
6324 echo sprintf(
6325 '<textarea id="privacy_text" name="privacy_text" rows="5" cols="50" class="regular-text">%s</textarea>',
6326 esc_textarea($privacy_text)
6327 );
6328 }
6329
6330
6331 public function mxchat_complianz_toggle_callback() {
6332 // Load from mxchat_options array
6333 $options = get_option('mxchat_options', []);
6334
6335 // Get complianz toggle value with fallback
6336 $complianz_toggle = isset($options['complianz_toggle']) ? $options['complianz_toggle'] : 'off';
6337 $checked = ($complianz_toggle === 'on') ? 'checked' : '';
6338
6339 // Output the toggle switch
6340 echo '<label class="toggle-switch">';
6341 echo sprintf(
6342 '<input type="checkbox" id="complianz_toggle" name="complianz_toggle" value="on" %s />',
6343 esc_attr($checked)
6344 );
6345 echo '<span class="slider"></span>';
6346 echo '</label>';
6347 }
6348
6349 public function mxchat_link_target_toggle_callback() {
6350 // Load from mxchat_options array
6351 $options = get_option('mxchat_options', []);
6352
6353 // Get link target toggle value with fallback
6354 $link_target_toggle = isset($options['link_target_toggle']) ? $options['link_target_toggle'] : 'off';
6355 $checked = ($link_target_toggle === 'on') ? 'checked' : '';
6356
6357 // Output the toggle switch
6358 echo '<label class="toggle-switch">';
6359 echo sprintf(
6360 '<input type="checkbox" id="link_target_toggle" name="link_target_toggle" value="on" %s />',
6361 esc_attr($checked)
6362 );
6363 echo '<span class="slider"></span>';
6364 echo '</label>';
6365 }
6366
6367 public function mxchat_chat_persistence_toggle_callback() {
6368 // Load from mxchat_options array
6369 $options = get_option('mxchat_options', []);
6370
6371 // Get chat persistence toggle value with fallback
6372 $chat_persistence_toggle = isset($options['chat_persistence_toggle']) ? $options['chat_persistence_toggle'] : 'off';
6373 $checked = ($chat_persistence_toggle === 'on') ? 'checked' : '';
6374
6375 // Output the toggle switch
6376 echo '<label class="toggle-switch">';
6377 echo sprintf(
6378 '<input type="checkbox" id="chat_persistence_toggle" name="chat_persistence_toggle" value="on" %s />',
6379 esc_attr($checked)
6380 );
6381 echo '<span class="slider"></span>';
6382 echo '</label>';
6383 }
6384
6385 public function mxchat_popular_question_1_callback() {
6386 // Load the full plugin options array
6387 $all_options = get_option('mxchat_options', []);
6388
6389 // Retrieve the specific option for popular_question_1
6390 $popular_question_1 = isset($all_options['popular_question_1']) ? $all_options['popular_question_1'] : '';
6391
6392 // Render the input field
6393 printf(
6394 '<input type="text" id="popular_question_1" name="popular_question_1" value="%s" placeholder="%s" class="regular-text" />',
6395 esc_attr($popular_question_1),
6396 esc_attr__('Enter Quick Question 1', 'mxchat')
6397 );
6398 }
6399
6400
6401 public function mxchat_popular_question_2_callback() {
6402 // Load the full plugin options array
6403 $all_options = get_option('mxchat_options', []);
6404
6405 // Retrieve the specific option for popular_question_2
6406 $popular_question_2 = isset($all_options['popular_question_2']) ? $all_options['popular_question_2'] : '';
6407
6408 // Render the input field
6409 printf(
6410 '<input type="text" id="popular_question_2" name="popular_question_2" value="%s" placeholder="%s" class="regular-text" />',
6411 esc_attr($popular_question_2),
6412 esc_attr__('Enter Quick Question 2', 'mxchat')
6413 );
6414 }
6415
6416
6417 public function mxchat_popular_question_3_callback() {
6418 // Load the full plugin options array
6419 $all_options = get_option('mxchat_options', []);
6420
6421 // Retrieve the specific option for popular_question_3
6422 $popular_question_3 = isset($all_options['popular_question_3']) ? $all_options['popular_question_3'] : '';
6423
6424 // Render the input field
6425 printf(
6426 '<input type="text" id="popular_question_3" name="popular_question_3" value="%s" placeholder="%s" class="regular-text" />',
6427 esc_attr($popular_question_3),
6428 esc_attr(__('Enter Quick Question 3', 'mxchat'))
6429 );
6430 }
6431
6432 public function mxchat_additional_popular_questions_callback() {
6433 $options = get_option('mxchat_options', []);
6434 $additional_questions = isset($options['additional_popular_questions'])
6435 ? $options['additional_popular_questions']
6436 : get_option('additional_popular_questions', array());
6437
6438 echo '<div id="mxchat-additional-questions-container">';
6439 if (!empty($additional_questions)) {
6440 foreach ($additional_questions as $index => $question) {
6441 printf(
6442 '<div class="mxchat-question-row">
6443 <input type="text" name="additional_popular_questions[]"
6444 value="%s"
6445 placeholder="%s"
6446 class="regular-text mxchat-question-input"
6447 data-question-index="%d" />
6448 <button type="button" class="button mxchat-remove-question"
6449 aria-label="%s">%s</button>
6450 </div>',
6451 esc_attr($question),
6452 esc_attr(sprintf(__('Enter Additional Quick Question %d', 'mxchat'), $index + 4)),
6453 $index,
6454 esc_attr(__('Remove question', 'mxchat')),
6455 esc_html__('Remove', 'mxchat')
6456 );
6457 }
6458 } else {
6459 printf(
6460 '<div class="mxchat-question-row">
6461 <input type="text" name="additional_popular_questions[]"
6462 value=""
6463 placeholder="%s"
6464 class="regular-text mxchat-question-input"
6465 data-question-index="0" />
6466 <button type="button" class="button mxchat-remove-question"
6467 aria-label="%s">%s</button>
6468 </div>',
6469 esc_attr(__('Enter Additional Quick Question 4', 'mxchat')),
6470 esc_attr(__('Remove question', 'mxchat')),
6471 esc_html__('Remove', 'mxchat')
6472 );
6473 }
6474 echo '</div>';
6475 printf(
6476 '<button type="button" class="button mxchat-add-question" aria-label="%s">%s</button>',
6477 esc_attr(__('Add question', 'mxchat')),
6478 esc_html__('Add Question', 'mxchat')
6479 );
6480 }
6481
6482 public function mxchat_brave_api_key_callback() {
6483 $brave_api_key = isset($this->options['brave_api_key']) ? esc_attr($this->options['brave_api_key']) : '';
6484 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6485
6486 echo '<div class="api-key-wrapper">';
6487 echo sprintf(
6488 '<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" />',
6489 $brave_api_key,
6490 $nonce
6491 );
6492 echo '<button type="button" id="toggleBraveApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6493 echo '</div>';
6494 echo '<p class="description">' . __('Required for Brave Search integration. Get your API key from Brave Search API.', 'mxchat') . '</p>';
6495 }
6496
6497 public function mxchat_brave_image_count_callback() {
6498 $brave_image_count = isset($this->options['brave_image_count'])
6499 ? intval($this->options['brave_image_count'])
6500 : 4;
6501 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6502
6503 echo '<div class="mxchat-field-wrapper">';
6504 echo sprintf(
6505 '<input type="number" id="brave_image_count" name="brave_image_count"
6506 value="%d" min="1" max="6" class="small-text mxchat-autosave-field" data-nonce="%s" />',
6507 $brave_image_count,
6508 $nonce
6509 );
6510 echo '</div>';
6511 echo '<p class="description">' . __('Select the number of images to return (1-6).', 'mxchat') . '</p>';
6512 }
6513
6514 public function mxchat_brave_safe_search_callback() {
6515 $brave_safe_search = isset($this->options['brave_safe_search'])
6516 ? esc_attr($this->options['brave_safe_search'])
6517 : 'strict';
6518 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6519
6520 echo '<div class="mxchat-field-wrapper">';
6521 echo '<select id="brave_safe_search" name="brave_safe_search" class="mxchat-autosave-field" data-nonce="' . $nonce . '">';
6522 echo sprintf(
6523 '<option value="strict" %s>%s</option>',
6524 selected($brave_safe_search, 'strict', false),
6525 __('Strict', 'mxchat')
6526 );
6527 echo sprintf(
6528 '<option value="off" %s>%s</option>',
6529 selected($brave_safe_search, 'off', false),
6530 __('Off', 'mxchat')
6531 );
6532 echo '</select>';
6533 echo '</div>';
6534 echo '<p class="description">' .
6535 esc_html__('Set the Safe Search level for image searches. Brave Search only supports "Strict" and "Off" options.', 'mxchat') .
6536 '</p>';
6537 }
6538
6539 public function mxchat_brave_news_count_callback() {
6540 $brave_news_count = isset($this->options['brave_news_count'])
6541 ? intval($this->options['brave_news_count'])
6542 : 3;
6543 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6544
6545 echo '<div class="mxchat-field-wrapper">';
6546 echo sprintf(
6547 '<input type="number" id="brave_news_count" name="brave_news_count"
6548 value="%d" min="1" max="10" class="small-text mxchat-autosave-field" data-nonce="%s" />',
6549 $brave_news_count,
6550 $nonce
6551 );
6552 echo '</div>';
6553 echo '<p class="description">' . esc_html__('Select the number of news articles to retrieve (1-10).', 'mxchat') . '</p>';
6554 }
6555
6556 public function mxchat_brave_country_callback() {
6557 $brave_country = isset($this->options['brave_country'])
6558 ? esc_attr($this->options['brave_country'])
6559 : 'us';
6560 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6561
6562 echo '<div class="mxchat-field-wrapper">';
6563 echo sprintf(
6564 '<input type="text" id="brave_country" name="brave_country"
6565 value="%s" maxlength="2" class="small-text mxchat-autosave-field" data-nonce="%s" />',
6566 $brave_country,
6567 $nonce
6568 );
6569 echo '</div>';
6570 echo '<p class="description">' . esc_html__('Enter the country code (e.g., "us" for United States).', 'mxchat') . '</p>';
6571 }
6572
6573 public function mxchat_brave_language_callback() {
6574 $brave_language = isset($this->options['brave_language'])
6575 ? esc_attr($this->options['brave_language'])
6576 : 'en';
6577 $nonce = wp_create_nonce('mxchat_autosave_nonce');
6578
6579 echo '<div class="mxchat-field-wrapper">';
6580 echo sprintf(
6581 '<input type="text" id="brave_language" name="brave_language"
6582 value="%s" maxlength="2" class="small-text mxchat-autosave-field" data-nonce="%s" />',
6583 $brave_language,
6584 $nonce
6585 );
6586 echo '</div>';
6587 echo '<p class="description">' . esc_html__('Enter the language code (e.g., "en" for English).', 'mxchat') . '</p>';
6588 }
6589
6590
6591
6592
6593
6594 // Section Callback
6595 public function mxchat_pdf_intent_section_callback() {
6596 echo '<p>' . esc_html__('Configure the intent settings for the Chat with PDF feature.', 'mxchat') . '</p>';
6597 }
6598
6599 public function mxchat_chat_toolbar_toggle_callback() {
6600 // Get chat toolbar toggle value with fallback
6601 $chat_toolbar_toggle = isset($this->options['chat_toolbar_toggle']) ? $this->options['chat_toolbar_toggle'] : 'off';
6602 $checked = ($chat_toolbar_toggle === 'on') ? 'checked' : '';
6603
6604 // Output the toggle switch
6605 echo '<label class="toggle-switch">';
6606 echo sprintf(
6607 '<input type="checkbox" id="chat_toolbar_toggle" name="chat_toolbar_toggle" value="on" %s />',
6608 esc_attr($checked)
6609 );
6610 echo '<span class="slider"></span>';
6611 echo '</label>';
6612
6613 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>';
6614 }
6615
6616 /**
6617 * Callback for PDF upload button toggle setting
6618 */
6619 public function mxchat_show_pdf_upload_button_callback() {
6620 // Get toggle value with fallback
6621 $show_pdf_button = isset($this->options['show_pdf_upload_button']) ? $this->options['show_pdf_upload_button'] : 'on';
6622 $checked = ($show_pdf_button === 'on') ? 'checked' : '';
6623
6624 // Output the toggle switch
6625 echo '<label class="toggle-switch">';
6626 echo sprintf(
6627 '<input type="checkbox" id="show_pdf_upload_button" name="show_pdf_upload_button" value="on" %s />',
6628 esc_attr($checked)
6629 );
6630 echo '<span class="slider"></span>';
6631 echo '</label>';
6632
6633 echo '<p class="description">' . esc_html__('Enable to show the PDF upload button in the chatbot toolbar. Disable to hide it.', 'mxchat') . '</p>';
6634 }
6635
6636 /**
6637 * Callback for Word upload button toggle setting
6638 */
6639 public function mxchat_show_word_upload_button_callback() {
6640 // Get toggle value with fallback
6641 $show_word_button = isset($this->options['show_word_upload_button']) ? $this->options['show_word_upload_button'] : 'on';
6642 $checked = ($show_word_button === 'on') ? 'checked' : '';
6643
6644 // Output the toggle switch
6645 echo '<label class="toggle-switch">';
6646 echo sprintf(
6647 '<input type="checkbox" id="show_word_upload_button" name="show_word_upload_button" value="on" %s />',
6648 esc_attr($checked)
6649 );
6650 echo '<span class="slider"></span>';
6651 echo '</label>';
6652
6653 echo '<p class="description">' . esc_html__('Enable to show the Word document upload button in the chatbot toolbar. Disable to hide it.', 'mxchat') . '</p>';
6654 }
6655
6656 public function mxchat_pdf_intent_trigger_text_callback() {
6657 $default_text = __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat');
6658
6659 echo sprintf(
6660 '<textarea id="pdf_intent_trigger_text"
6661 name="pdf_intent_trigger_text"
6662 rows="3"
6663 cols="50"
6664 placeholder="%s">%s</textarea>',
6665 esc_attr__('Enter trigger text', 'mxchat'),
6666 isset($this->options['pdf_intent_trigger_text'])
6667 ? esc_textarea($this->options['pdf_intent_trigger_text'])
6668 : esc_textarea($default_text)
6669 );
6670 echo '<p class="description">' . esc_html__('Text displayed when the intent is triggered.', 'mxchat') . '</p>';
6671 }
6672
6673 public function mxchat_pdf_intent_success_text_callback() {
6674 $default_text = __("I've processed the PDF. What questions do you have about it?", 'mxchat');
6675
6676 echo sprintf(
6677 '<textarea id="pdf_intent_success_text"
6678 name="pdf_intent_success_text"
6679 rows="3"
6680 cols="50"
6681 placeholder="%s">%s</textarea>',
6682 esc_attr__('Enter success text', 'mxchat'),
6683 isset($this->options['pdf_intent_success_text'])
6684 ? esc_textarea($this->options['pdf_intent_success_text'])
6685 : esc_textarea($default_text)
6686 );
6687 echo '<p class="description">' . esc_html__('Text displayed when the intent is successful.', 'mxchat') . '</p>';
6688 }
6689
6690 public function mxchat_pdf_intent_error_text_callback() {
6691 $default_text = __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
6692
6693 echo sprintf(
6694 '<textarea id="pdf_intent_error_text"
6695 name="pdf_intent_error_text"
6696 rows="3"
6697 cols="50"
6698 placeholder="%s">%s</textarea>',
6699 esc_attr__('Enter error text', 'mxchat'),
6700 isset($this->options['pdf_intent_error_text'])
6701 ? esc_textarea($this->options['pdf_intent_error_text'])
6702 : esc_textarea($default_text)
6703 );
6704 echo '<p class="description">' . esc_html__('Text displayed when an error occurs during the intent.', 'mxchat') . '</p>';
6705 }
6706
6707 public function mxchat_pdf_max_pages_callback() {
6708 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
6709
6710 echo sprintf(
6711 '<input type="range"
6712 id="pdf_max_pages"
6713 name="pdf_max_pages"
6714 min="1"
6715 max="69"
6716 value="%d"
6717 class="range-slider" />',
6718 esc_attr($max_pages)
6719 );
6720 echo '<span id="pdf_max_pages_output">' . esc_html($max_pages) . '</span>';
6721 echo '<p class="description">' . esc_html__('Set the maximum number of document pages users can upload for processing. (1-69 pages)', 'mxchat') . '</p>';
6722 }
6723
6724 public function mxchat_live_agent_status_callback() {
6725 // Always get fresh options instead of using cached $this->options
6726 $fresh_options = get_option('mxchat_options');
6727 $status = isset($fresh_options['live_agent_status']) ? $fresh_options['live_agent_status'] : 'off';
6728
6729 echo '<label class="toggle-switch">';
6730 echo sprintf(
6731 '<input type="checkbox" id="live_agent_status" name="live_agent_status" value="on" %s />',
6732 checked($status, 'on', false)
6733 );
6734 echo '<span class="slider"></span>';
6735 echo '</label>';
6736 echo '<label for="live_agent_status" class="mxchat-status-label">';
6737 echo '<span class="status-text">' . ($status === 'on' ? esc_html__('Online', 'mxchat') : esc_html__('Offline', 'mxchat')) . '</span>';
6738 echo '</label>';
6739 }
6740
6741 public function mxchat_live_agent_away_message_callback() {
6742 $message = isset($this->options['live_agent_away_message'])
6743 ? $this->options['live_agent_away_message']
6744 : __('Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.', 'mxchat');
6745
6746 printf(
6747 '<textarea id="live_agent_away_message" name="live_agent_away_message" rows="3" cols="50">%s</textarea>',
6748 esc_textarea($message)
6749 );
6750 echo '<p class="description">' . esc_html__('Message shown when live agents are offline.', 'mxchat') . '</p>';
6751 }
6752
6753 public function mxchat_live_agent_notification_message_callback() {
6754 $message = isset($this->options['live_agent_notification_message'])
6755 ? $this->options['live_agent_notification_message']
6756 : __('Live agent has been notified.', 'mxchat');
6757
6758 printf(
6759 '<textarea id="live_agent_notification_message" name="live_agent_notification_message" rows="3" cols="50">%s</textarea>',
6760 esc_textarea($message)
6761 );
6762 echo '<p class="description">' . esc_html__('Message shown when live transfer activated.', 'mxchat') . '</p>';
6763 }
6764
6765 public function mxchat_live_agent_webhook_url_callback() {
6766 $webhook_url = isset($this->options['live_agent_webhook_url'])
6767 ? esc_url($this->options['live_agent_webhook_url'])
6768 : esc_url(get_option('live_agent_webhook_url', ''));
6769
6770 printf(
6771 '<input type="password" id="live_agent_webhook_url" name="live_agent_webhook_url" value="%s" class="regular-text" />',
6772 $webhook_url
6773 );
6774 echo '<button type="button" id="toggleWebhookUrlVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6775 echo '<p class="description">' . esc_html__('Enter your Slack webhook URL for live agent notifications.', 'mxchat') . '</p>';
6776 }
6777
6778 public function mxchat_live_agent_secret_key_callback() {
6779 printf(
6780 '<input type="password" id="live_agent_secret_key" name="live_agent_secret_key" value="%s" class="regular-text" />',
6781 isset($this->options['live_agent_secret_key']) ? esc_attr($this->options['live_agent_secret_key']) : ''
6782 );
6783 echo '<button type="button" id="toggleSecretKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6784 echo '<p class="description">' . esc_html__('Secret key for validating Slack requests. Keep this secure.', 'mxchat') . '</p>';
6785 }
6786
6787 public function mxchat_live_agent_bot_token_callback() {
6788 printf(
6789 '<input type="password" id="live_agent_bot_token" name="live_agent_bot_token" value="%s" class="regular-text" />',
6790 isset($this->options['live_agent_bot_token']) ? esc_attr($this->options['live_agent_bot_token']) : ''
6791 );
6792 echo '<button type="button" id="toggleBotTokenVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
6793 echo '<p class="description">' . esc_html__('Your Slack Bot OAuth Token (starts with xoxb-). Keep this secure.', 'mxchat') . '</p>';
6794 }
6795
6796 public function mxchat_live_agent_user_ids_callback() {
6797 $user_ids = isset($this->options['live_agent_user_ids'])
6798 ? esc_textarea($this->options['live_agent_user_ids'])
6799 : '';
6800
6801 printf(
6802 '<textarea id="live_agent_user_ids" name="live_agent_user_ids" rows="4" class="large-text">%s</textarea>',
6803 $user_ids
6804 );
6805 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>';
6806 }
6807
6808 /**
6809 * Telegram Integration Callbacks
6810 */
6811 public function mxchat_telegram_section_callback() {
6812 echo '<p>' . esc_html__('Configure Telegram integration for live agent support.', 'mxchat') . '</p>';
6813 }
6814
6815 public function mxchat_telegram_status_callback() {
6816 $fresh_options = get_option('mxchat_options');
6817 $status = isset($fresh_options['telegram_status']) ? $fresh_options['telegram_status'] : 'off';
6818
6819 echo '<label class="toggle-switch">';
6820 echo sprintf(
6821 '<input type="checkbox" id="telegram_status" name="telegram_status" value="on" %s />',
6822 checked($status, 'on', false)
6823 );
6824 echo '<span class="slider"></span>';
6825 echo '</label>';
6826 echo '<label for="telegram_status" class="mxchat-status-label">';
6827 echo '<span class="status-text">' . ($status === 'on' ? esc_html__('Online', 'mxchat') : esc_html__('Offline', 'mxchat')) . '</span>';
6828 echo '</label>';
6829 }
6830
6831 public function mxchat_telegram_notification_message_callback() {
6832 $message = isset($this->options['telegram_notification_message'])
6833 ? $this->options['telegram_notification_message']
6834 : __("I've notified a support agent. Please allow a moment for them to respond.", 'mxchat');
6835
6836 printf(
6837 '<textarea id="telegram_notification_message" name="telegram_notification_message" rows="3" cols="50">%s</textarea>',
6838 esc_textarea($message)
6839 );
6840 echo '<p class="description">' . esc_html__('Message shown when live transfer activated.', 'mxchat') . '</p>';
6841 }
6842
6843 public function mxchat_telegram_away_message_callback() {
6844 $message = isset($this->options['telegram_away_message'])
6845 ? $this->options['telegram_away_message']
6846 : __('Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.', 'mxchat');
6847
6848 printf(
6849 '<textarea id="telegram_away_message" name="telegram_away_message" rows="3" cols="50">%s</textarea>',
6850 esc_textarea($message)
6851 );
6852 echo '<p class="description">' . esc_html__('Message shown when live agents are offline.', 'mxchat') . '</p>';
6853 }
6854
6855 public function mxchat_telegram_bot_token_callback() {
6856 printf(
6857 '<input type="password" id="telegram_bot_token" name="telegram_bot_token" value="%s" class="regular-text" />',
6858 isset($this->options['telegram_bot_token']) ? esc_attr($this->options['telegram_bot_token']) : ''
6859 );
6860 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>';
6861 echo '<p class="description">' . esc_html__('Your Telegram Bot Token from @BotFather (e.g., 123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ).', 'mxchat') . '</p>';
6862 }
6863
6864 public function mxchat_telegram_group_id_callback() {
6865 printf(
6866 '<input type="text" id="telegram_group_id" name="telegram_group_id" value="%s" class="regular-text" />',
6867 isset($this->options['telegram_group_id']) ? esc_attr($this->options['telegram_group_id']) : ''
6868 );
6869 echo '<p class="description">' . esc_html__('Your Telegram supergroup ID (starts with -100). The group must have forum topics enabled.', 'mxchat') . '</p>';
6870 }
6871
6872 public function mxchat_telegram_webhook_secret_callback() {
6873 $secret = isset($this->options['telegram_webhook_secret']) ? $this->options['telegram_webhook_secret'] : '';
6874
6875 // Auto-generate secret if empty
6876 if (empty($secret)) {
6877 $secret = wp_generate_password(64, false, false);
6878 $options = get_option('mxchat_options', []);
6879 $options['telegram_webhook_secret'] = $secret;
6880 update_option('mxchat_options', $options);
6881 $this->options['telegram_webhook_secret'] = $secret;
6882 }
6883
6884 printf(
6885 '<input type="password" id="telegram_webhook_secret" name="telegram_webhook_secret" value="%s" class="regular-text" />',
6886 esc_attr($secret)
6887 );
6888 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>';
6889 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>';
6890 }
6891
6892 public function mxchat_similarity_threshold_callback() {
6893 // Load from mxchat_options array
6894 $options = get_option('mxchat_options', []);
6895
6896 // Get value from options array with default of 35
6897 $threshold = isset($options['similarity_threshold']) ? $options['similarity_threshold'] : 35;
6898
6899 echo '<div class="slider-container">';
6900 echo sprintf(
6901 '<input type="range"
6902 id="similarity_threshold"
6903 name="similarity_threshold"
6904 min="20"
6905 max="85"
6906 step="1"
6907 value="%s"
6908 class="range-slider" />',
6909 esc_attr($threshold)
6910 );
6911 echo sprintf(
6912 '<span id="threshold_value" class="range-value">%s</span>',
6913 esc_html($threshold)
6914 );
6915 echo '</div>';
6916 }
6917
6918 public function mxchat_rag_sources_limit_callback() {
6919 // Load from mxchat_options array
6920 $options = get_option('mxchat_options', []);
6921
6922 // Get value from options array with default of 3
6923 $rag_sources_limit = isset($options['rag_sources_limit']) ? intval($options['rag_sources_limit']) : 3;
6924
6925 echo '<div class="slider-container">';
6926 echo sprintf(
6927 '<input type="range"
6928 id="rag_sources_limit"
6929 name="rag_sources_limit"
6930 min="3"
6931 max="10"
6932 step="1"
6933 value="%s"
6934 class="range-slider" />',
6935 esc_attr($rag_sources_limit)
6936 );
6937 echo sprintf(
6938 '<span id="rag_sources_limit_value" class="range-value">%s</span>',
6939 esc_html($rag_sources_limit)
6940 );
6941 echo '</div>';
6942 }
6943
6944 public function mxchat_rag_chunks_limit_callback() {
6945 // Load from mxchat_options array
6946 $options = get_option('mxchat_options', []);
6947
6948 // Get value from options array with default of 15
6949 $rag_chunks_limit = isset($options['rag_chunks_limit']) ? intval($options['rag_chunks_limit']) : 15;
6950
6951 echo '<div class="slider-container">';
6952 echo sprintf(
6953 '<input type="range"
6954 id="rag_chunks_limit"
6955 name="rag_chunks_limit"
6956 min="8"
6957 max="20"
6958 step="1"
6959 value="%s"
6960 class="range-slider" />',
6961 esc_attr($rag_chunks_limit)
6962 );
6963 echo sprintf(
6964 '<span id="rag_chunks_limit_value" class="range-value">%s</span>',
6965 esc_html($rag_chunks_limit)
6966 );
6967 echo '</div>';
6968 }
6969
6970 public function mxchat_enqueue_admin_assets() {
6971 // Get plugin version
6972 $version = MXCHAT_VERSION;
6973
6974 // Use file modification time for development (remove in production)
6975 if (defined('WP_DEBUG') && WP_DEBUG) {
6976 $version = filemtime(plugin_dir_path(__FILE__) . '../mxchat-basic.php');
6977 }
6978
6979 $current_page = isset($_GET['page']) ? $_GET['page'] : '';
6980 $plugin_url = plugin_dir_url(__FILE__) . '../';
6981
6982 // Only load on MxChat pages
6983 if (strpos($current_page, 'mxchat') === false) {
6984 return;
6985 }
6986
6987 // Always load these on all MxChat pages
6988 $this->enqueue_core_admin_assets($plugin_url, $version);
6989 $this->enqueue_page_specific_assets($current_page, $plugin_url, $version);
6990 $this->localize_admin_scripts($current_page);
6991 }
6992 private function enqueue_core_admin_assets($plugin_url, $version) {
6993 // Core admin styles
6994 wp_enqueue_style('mxchat-admin-css', $plugin_url . 'css/admin-style.css', array(), $version);
6995 wp_enqueue_style('mxchat-knowledge-css', $plugin_url . 'css/knowledge-style.css', array(), $version);
6996
6997 // New sidebar navigation styles (for main settings page)
6998 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
6999
7000 // Core admin scripts
7001 wp_enqueue_script('mxchat-admin-js', $plugin_url . 'js/mxchat-admin.js', array('jquery'), $version, true);
7002 }
7003 private function enqueue_page_specific_assets($current_page, $plugin_url, $version) {
7004 switch ($current_page) {
7005 case 'mxchat-prompts':
7006 // Knowledge processing page assets
7007 wp_enqueue_style('mxchat-content-selector-css', $plugin_url . 'css/content-selector.css', array(), $version);
7008 wp_enqueue_script('mxchat-content-selector-js', $plugin_url . 'js/content-selector.js', array('jquery'), $version, true);
7009 // Add the knowledge processing script (common script needed for WordPress dismiss functionality)
7010 wp_enqueue_script('mxchat-knowledge-processing', $plugin_url . 'js/knowledge-processing.js', array('jquery', 'common'), $version, true);
7011 break;
7012
7013 case 'mxchat-transcripts':
7014 // Load admin sidebar CSS first (shared styles for sidebar navigation)
7015 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
7016 // Load transcripts-specific styles
7017 wp_enqueue_style('mxchat-chat-transcripts-css', $plugin_url . 'css/chat-transcripts.css', array('mxchat-admin-sidebar-css'), $version);
7018 wp_enqueue_script('mxchat-transcripts-js', $plugin_url . 'js/mxchat_transcripts.js', array('jquery'), $version, true);
7019 break;
7020
7021 case 'mxchat-actions':
7022 // Load admin sidebar CSS first (shared styles for sidebar navigation)
7023 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
7024 // Load actions-specific styles
7025 wp_enqueue_style('mxchat-actions-css', $plugin_url . 'css/actions.css', array('mxchat-admin-sidebar-css'), $version);
7026 wp_enqueue_script('mxchat-actions-js', $plugin_url . 'js/mxchat_actions.js', array('jquery'), $version, true);
7027
7028 // Localize script data for actions page
7029 $is_activated = get_option('mxchat_pro_license_status') === 'active';
7030 wp_localize_script('mxchat-actions-js', 'mxchActionsData', array(
7031 'ajaxUrl' => admin_url('admin-ajax.php'),
7032 'nonce' => wp_create_nonce('mxchat_actions_nonce'),
7033 'addNonce' => wp_create_nonce('mxchat_add_intent_nonce'),
7034 'editNonce' => wp_create_nonce('mxchat_edit_intent'),
7035 'deleteNonce' => wp_create_nonce('mxchat_delete_intent_nonce'),
7036 'toggleNonce' => wp_create_nonce('mxchat_actions_nonce'),
7037 'isActivated' => $is_activated,
7038 'i18n' => array(
7039 'confirmDelete' => __('Are you sure you want to delete this action?', 'mxchat'),
7040 'confirmBulkDelete' => __('Are you sure you want to delete the selected actions?', 'mxchat'),
7041 'saving' => __('Saving...', 'mxchat'),
7042 'saved' => __('Saved successfully!', 'mxchat'),
7043 'error' => __('An error occurred. Please try again.', 'mxchat'),
7044 'proRequired' => __('This feature requires MxChat Pro.', 'mxchat'),
7045 'addonRequired' => __('This feature requires an add-on.', 'mxchat')
7046 )
7047 ));
7048 break;
7049
7050 case 'mxchat-activation':
7051 // Load admin sidebar CSS first (shared styles for sidebar navigation)
7052 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
7053 // Load pro page-specific styles
7054 wp_enqueue_style('mxchat-pro-css', $plugin_url . 'css/admin-pro.css', array('mxchat-admin-sidebar-css'), $version);
7055 // Load pro page JavaScript
7056 wp_enqueue_script('mxchat-pro-js', $plugin_url . 'js/mxchat_pro.js', array('jquery'), $version, true);
7057 // Load activation script for license activation/deactivation
7058 wp_enqueue_script('mxchat-activation-js', $plugin_url . 'js/activation-script.js', array('jquery'), $version, true);
7059 break;
7060
7061 case 'mxchat-content':
7062 // Load admin sidebar CSS (shared styles for sidebar navigation)
7063 wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
7064 // Load content page-specific styles
7065 wp_enqueue_style('mxchat-content-css', $plugin_url . 'css/admin-content.css', array('mxchat-admin-sidebar-css'), $version);
7066 // Load content page JavaScript
7067 wp_enqueue_script('mxchat-content-js', $plugin_url . 'js/mxchat-content.js', array('jquery'), $version, true);
7068 break;
7069 default:
7070 wp_enqueue_script(
7071 'mxchat-test-streaming-js',
7072 $plugin_url . 'js/mxchat-test-streaming.js',
7073 ['jquery'],
7074 $version,
7075 true
7076 );
7077
7078 wp_localize_script('mxchat-test-streaming-js', 'mxchatTestStreamingAjax', [
7079 'ajax_url' => admin_url('admin-ajax.php'),
7080 'nonce' => wp_create_nonce('mxchat_test_streaming_nonce'),
7081 'settings_nonce' => wp_create_nonce('mxchat_save_setting_nonce')
7082 ]);
7083 break;
7084 }
7085 }
7086 private function localize_admin_scripts($current_page) {
7087 // Base localization data for main admin script
7088 $base_data = array(
7089 'ajax_url' => admin_url('admin-ajax.php'),
7090 'nonce' => wp_create_nonce('mxchat_admin_nonce'),
7091 'admin_url' => admin_url(),
7092 'license_nonce' => wp_create_nonce('mxchat_activate_license_nonce'),
7093 'inline_edit_nonce' => wp_create_nonce('mxchat_save_inline_nonce'),
7094 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
7095 'export_nonce' => wp_create_nonce('mxchat_export_transcripts'),
7096 'actions_nonce' => wp_create_nonce('mxchat_actions_nonce'),
7097 'add_intent_nonce' => wp_create_nonce('mxchat_add_intent_nonce'),
7098 'edit_intent_nonce' => wp_create_nonce('mxchat_edit_intent'),
7099 'toggle_action_nonce' => wp_create_nonce('mxchat_actions_nonce'),
7100 'fetch_openrouter_models_nonce' => wp_create_nonce('mxchat_fetch_openrouter_models'),
7101 'is_activated' => $this->is_activated ? '1' : '0',
7102 'status_refresh_interval' => 5000,
7103 'prompts_setting_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
7104 'ajaxurl' => admin_url('admin-ajax.php')
7105 );
7106
7107 // Localize main admin script with base data
7108 wp_localize_script('mxchat-admin-js', 'mxchatAdmin', $base_data);
7109
7110 // Page-specific localizations
7111 $this->localize_page_specific_scripts($current_page);
7112 }
7113 private function localize_page_specific_scripts($current_page) {
7114 switch ($current_page) {
7115 case 'mxchat-prompts':
7116 // Status updater localization
7117 wp_localize_script('mxchat-status-updater', 'mxchat_status_data', array(
7118 'ajax_url' => admin_url('admin-ajax.php'),
7119 'nonce' => wp_create_nonce('mxchat_status_nonce')
7120 ));
7121
7122 // Content selector localization
7123 wp_localize_script('mxchat-content-selector-js', 'mxchatSelector', array(
7124 'ajaxurl' => admin_url('admin-ajax.php'),
7125 'nonce' => wp_create_nonce('mxchat_content_selector_nonce'),
7126 'i18n' => array(
7127 'searchPlaceholder' => __('Search posts and pages...', 'mxchat'),
7128 'selectAll' => __('Select All', 'mxchat'),
7129 'process' => __('Process Selected', 'mxchat'),
7130 'cancel' => __('Cancel', 'mxchat'),
7131 'noResults' => __('No content found.', 'mxchat')
7132 )
7133 ));
7134
7135 wp_localize_script('mxchat-knowledge-processing', 'mxchatAdmin', array(
7136 'ajax_url' => admin_url('admin-ajax.php'),
7137 'status_nonce' => wp_create_nonce('mxchat_status_nonce'),
7138 'queue_nonce' => wp_create_nonce('mxchat_queue_nonce'),
7139 'stop_nonce' => wp_create_nonce('mxchat_stop_processing_action'),
7140 'settings_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
7141 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
7142 'entries_nonce' => wp_create_nonce('mxchat_entries_nonce'),
7143 'admin_url' => admin_url(),
7144 'ajaxurl' => admin_url('admin-ajax.php'),
7145 'status_refresh_interval' => 2000,
7146 'bot_id' => isset($_GET['bot_id']) ? sanitize_text_field($_GET['bot_id']) : 'default'
7147 ));
7148 break;
7149
7150 case 'mxchat-activation':
7151 wp_localize_script('mxchat-activation-js', 'mxchatAdmin', array(
7152 'ajax_url' => admin_url('admin-ajax.php'),
7153 'license_nonce' => wp_create_nonce('mxchat_activate_license_nonce')
7154 ));
7155 break;
7156
7157 case 'mxchat-content':
7158 wp_localize_script('mxchat-content-js', 'mxchatContent', array(
7159 'ajaxUrl' => admin_url('admin-ajax.php'),
7160 'nonce' => wp_create_nonce('mxchat_content_nonce'),
7161 'settingNonce' => wp_create_nonce('mxchat_save_setting_nonce'),
7162 'previewUrl' => home_url('/?p='),
7163 'isActivated' => $this->is_activated(),
7164 'hasAdvancedContent' => apply_filters('mxchat_content_pro_feature', false, 'seo_readability'),
7165 'hasGSC' => apply_filters('mxchat_content_pro_feature', false, 'gsc_integration'),
7166 'seoOptimize' => array(
7167 'meta_description' => ($options['seo_optimize_meta_desc'] ?? 'on') === 'on',
7168 'seo_title' => ($options['seo_optimize_seo_title'] ?? 'on') === 'on',
7169 'slug' => ($options['seo_optimize_slug'] ?? 'on') === 'on',
7170 'readability' => ($options['seo_optimize_readability'] ?? 'on') === 'on',
7171 'internal_links' => ($options['seo_optimize_internal_links'] ?? 'on') === 'on',
7172 'img_alt' => ($options['seo_optimize_img_alt'] ?? 'on') === 'on',
7173 'featured_img' => ($options['seo_optimize_featured_img'] ?? 'on') === 'on',
7174 ),
7175 'i18n' => array(
7176 'generating' => __('Generating...', 'mxchat'),
7177 'planning' => __('Planning content structure...', 'mxchat'),
7178 'images' => __('Generating images...', 'mxchat'),
7179 'writing' => __('Writing full content...', 'mxchat'),
7180 'creating' => __('Creating WordPress post...', 'mxchat'),
7181 'done' => __('Content generated successfully!', 'mxchat'),
7182 'error' => __('An error occurred. Please try again.', 'mxchat'),
7183 'editSuccess' => __('Content updated successfully.', 'mxchat'),
7184 'promptEmpty' => __('Please enter a prompt.', 'mxchat'),
7185 )
7186 ));
7187 break;
7188
7189 case 'mxchat-transcripts':
7190 // Get chart data for localization
7191 $chart_data = $this->get_transcripts_chart_data();
7192
7193 wp_localize_script('mxchat-transcripts-js', 'mxchatAdmin', array(
7194 'ajax_url' => admin_url('admin-ajax.php'),
7195 'export_nonce' => wp_create_nonce('mxchat_export_transcripts'),
7196 'delete_nonce' => wp_create_nonce('mxchat_delete_chat_history'),
7197 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'),
7198 'translate_nonce' => wp_create_nonce('mxchat_translate_messages')
7199 ));
7200
7201 // Localize chart data separately - use array_values to ensure proper JSON array encoding
7202 wp_localize_script('mxchat-transcripts-js', 'mxchatChartData', array(
7203 'labels' => array_values($chart_data['labels']),
7204 'chats' => array_values($chart_data['chats']),
7205 'messages' => array_values($chart_data['messages'])
7206 ));
7207 break;
7208
7209 case 'mxchat-settings':
7210 default:
7211 // Color picker and settings localization
7212 wp_localize_script('mxchat-color-picker', 'mxchatStyleSettings', array(
7213 'ajax_url' => admin_url('admin-ajax.php'),
7214 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
7215 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
7216 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
7217 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
7218 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
7219 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
7220 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
7221 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
7222 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
7223 'icon_color' => $this->options['icon_color'] ?? '#fff',
7224 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
7225 'pre_chat_message' => $this->options['pre_chat_message'] ?? esc_html__('Hey there! Ask me anything!', 'mxchat'),
7226 'rate_limit_message' => $this->options['rate_limit_message'] ?? esc_html__('Rate limit exceeded. Please try again later.', 'mxchat'),
7227 'loops_api_key' => $this->options['loops_api_key'] ?? '',
7228 'loops_mailing_list' => $this->options['loops_mailing_list'] ?? '',
7229 'triggered_phrase_response' => $this->options['triggered_phrase_response'] ?? esc_html__('Would you like to join our mailing list? Please provide your email below.', 'mxchat'),
7230 'email_capture_response' => $this->options['email_capture_response'] ?? esc_html__('Thank you for providing your email! You\'ve been added to our list.', 'mxchat'),
7231 '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'),
7232 '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'),
7233 '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'),
7234 'pdf_max_pages' => $this->options['pdf_max_pages'] ?? 69,
7235 'live_agent_webhook_url' => $this->options['live_agent_webhook_url'] ?? '',
7236 'live_agent_secret_key' => $this->options['live_agent_secret_key'] ?? '',
7237 'live_agent_bot_token' => $this->options['live_agent_bot_token'] ?? '',
7238 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
7239 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
7240 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
7241 'show_pdf_upload_button' => $this->options['show_pdf_upload_button'] ?? 'on',
7242 'show_word_upload_button' => $this->options['show_word_upload_button'] ?? 'on',
7243 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
7244 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
7245 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
7246 ));
7247 break;
7248 }
7249
7250 // Additional localization that was in the original code
7251 wp_localize_script('mxchat-admin-js', 'mxchatPromptsAdmin', array(
7252 'ajax_url' => admin_url('admin-ajax.php'),
7253 'prompts_setting_nonce' => wp_create_nonce('mxchat_prompts_setting_nonce'),
7254 ));
7255 }
7256
7257 public function mxchat_sanitize($input) {
7258 $new_input = array();
7259
7260 if (isset($input['api_key'])) {
7261 $new_input['api_key'] = sanitize_text_field($input['api_key']);
7262 }
7263
7264 if (isset($input['similarity_threshold'])) {
7265 $new_input['similarity_threshold'] = absint($input['similarity_threshold']); // Ensure it's an integer
7266 $new_input['similarity_threshold'] = min(max($new_input['similarity_threshold'], 20), 85); // Enforce range
7267 }
7268
7269 if (isset($input['rag_sources_limit'])) {
7270 $new_input['rag_sources_limit'] = absint($input['rag_sources_limit']); // Ensure it's an integer
7271 $new_input['rag_sources_limit'] = min(max($new_input['rag_sources_limit'], 3), 10); // Enforce range 3-10
7272 }
7273
7274 if (isset($input['rag_chunks_limit'])) {
7275 $new_input['rag_chunks_limit'] = absint($input['rag_chunks_limit']); // Ensure it's an integer
7276 $new_input['rag_chunks_limit'] = min(max($new_input['rag_chunks_limit'], 8), 20); // Enforce range 8-20
7277 }
7278
7279 if (isset($input['xai_api_key'])) {
7280 $new_input['xai_api_key'] = sanitize_text_field($input['xai_api_key']);
7281 }
7282
7283 if (isset($input['claude_api_key'])) {
7284 $new_input['claude_api_key'] = sanitize_text_field($input['claude_api_key']);
7285 }
7286
7287 if (isset($input['enable_streaming_toggle'])) {
7288 $new_input['enable_streaming_toggle'] = ($input['enable_streaming_toggle'] === 'on') ? 'on' : 'off';
7289 } else {
7290 // If checkbox not checked, it won't be in $input, so set to 'off'
7291 $new_input['enable_streaming_toggle'] = 'off';
7292 }
7293
7294 if (isset($input['enable_web_search'])) {
7295 $new_input['enable_web_search'] = ($input['enable_web_search'] === 'on') ? 'on' : 'off';
7296 } else {
7297 // If checkbox not checked, it won't be in $input, so set to 'off'
7298 $new_input['enable_web_search'] = 'off';
7299 }
7300
7301 if (isset($input['deepseek_api_key'])) {
7302 $new_input['deepseek_api_key'] = sanitize_text_field($input['deepseek_api_key']);
7303 }
7304
7305 if (isset($input['gemini_api_key'])) {
7306 $new_input['gemini_api_key'] = sanitize_text_field($input['gemini_api_key']);
7307 }
7308
7309 if (isset($input['enable_woocommerce_integration'])) {
7310 $new_input['enable_woocommerce_integration'] = $input['enable_woocommerce_integration'] === 'on' ? 'on' : 'off';
7311 }
7312
7313 if (isset($input['privacy_toggle'])) {
7314 $new_input['privacy_toggle'] = $input['privacy_toggle'];
7315 }
7316
7317 if (isset($input['complianz_toggle'])) {
7318 $new_input['complianz_toggle'] = $input['complianz_toggle'];
7319 }
7320
7321 // Handle custom privacy text input
7322 if (isset($input['privacy_text'])) {
7323 // Allow basic HTML for links
7324 $new_input['privacy_text'] = wp_kses_post($input['privacy_text']);
7325 }
7326
7327 if (isset($input['system_prompt_instructions'])) {
7328 $new_input['system_prompt_instructions'] = sanitize_textarea_field($input['system_prompt_instructions']);
7329 }
7330
7331 if (isset($input['mxchat_pro_email'])) {
7332 $new_input['mxchat_pro_email'] = sanitize_email($input['mxchat_pro_email']);
7333 }
7334
7335 if (isset($input['mxchat_activation_key'])) {
7336 $new_input['mxchat_activation_key'] = sanitize_text_field($input['mxchat_activation_key']);
7337 }
7338
7339 if (isset($input['append_to_body'])) {
7340 $new_input['append_to_body'] = $input['append_to_body'] === 'on' ? 'on' : 'off';
7341 }
7342
7343 // Post type visibility settings
7344 if (isset($input['post_type_visibility_mode'])) {
7345 $allowed_modes = array('all', 'include', 'exclude');
7346 $new_input['post_type_visibility_mode'] = in_array($input['post_type_visibility_mode'], $allowed_modes)
7347 ? $input['post_type_visibility_mode']
7348 : 'all';
7349 }
7350
7351 if (isset($input['post_type_visibility_list'])) {
7352 if (is_array($input['post_type_visibility_list'])) {
7353 $new_input['post_type_visibility_list'] = array_map('sanitize_key', $input['post_type_visibility_list']);
7354 } else {
7355 $new_input['post_type_visibility_list'] = array();
7356 }
7357 }
7358
7359 if (isset($input['contextual_awareness_toggle'])) {
7360 $new_input['contextual_awareness_toggle'] = $input['contextual_awareness_toggle'] === 'on' ? 'on' : 'off';
7361 }
7362
7363 if (isset($input['show_frontend_debugger'])) {
7364 $new_input['show_frontend_debugger'] = $input['show_frontend_debugger'] === 'on' ? 'on' : 'off';
7365 }
7366
7367 if (isset($input['citation_links_toggle'])) {
7368 $new_input['citation_links_toggle'] = $input['citation_links_toggle'] === 'on' ? 'on' : 'off';
7369 }
7370
7371 if (isset($input['top_bar_title'])) {
7372 $new_input['top_bar_title'] = sanitize_text_field($input['top_bar_title']);
7373 }
7374
7375 if (isset($input['ai_agent_text'])) {
7376 $new_input['ai_agent_text'] = sanitize_text_field($input['ai_agent_text']);
7377 }
7378
7379 if (isset($input['enable_email_block'])) {
7380 $new_input['enable_email_block'] = sanitize_text_field($input['enable_email_block']);
7381 }
7382
7383 if (isset($input['email_blocker_header_content'])) {
7384 // wp_kses_post() allows standard HTML tags permitted by WordPress
7385 $new_input['email_blocker_header_content'] = wp_kses_post($input['email_blocker_header_content']);
7386 }
7387 if (isset($input['email_blocker_button_text'])) {
7388 $new_input['email_blocker_button_text'] = sanitize_text_field($input['email_blocker_button_text']);
7389 }
7390 // Sanitize name field toggle
7391 if (isset($input['enable_name_field'])) {
7392 $new_input['enable_name_field'] = ($input['enable_name_field'] === 'on') ? 'on' : 'off';
7393 } else {
7394 $new_input['enable_name_field'] = 'off';
7395 }
7396 // Sanitize name field placeholder
7397 if (isset($input['name_field_placeholder'])) {
7398 $new_input['name_field_placeholder'] = sanitize_text_field($input['name_field_placeholder']);
7399 }
7400 if (isset($input['intro_message'])) {
7401 $new_input['intro_message'] = wp_kses_post($input['intro_message']); // Use wp_kses_post instead
7402 }
7403
7404 if (isset($input['input_copy'])) {
7405 $new_input['input_copy'] = sanitize_text_field($input['input_copy']);
7406 }
7407
7408 if (isset($input['rate_limit_message'])) {
7409 $new_input['rate_limit_message'] = sanitize_text_field($input['rate_limit_message']);
7410 }
7411
7412 // Handle the new rate limits format
7413 if (isset($input['rate_limits']) && is_array($input['rate_limits'])) {
7414 $new_input['rate_limits'] = array();
7415 $allowed_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited');
7416 $allowed_timeframes = array('hourly', 'daily', 'weekly', 'monthly');
7417
7418 foreach ($input['rate_limits'] as $role_id => $settings) {
7419 $new_input['rate_limits'][$role_id] = array();
7420
7421 // Sanitize limit
7422 if (isset($settings['limit'])) {
7423 $limit = sanitize_text_field($settings['limit']);
7424 if (in_array($limit, $allowed_limits, true)) {
7425 $new_input['rate_limits'][$role_id]['limit'] = $limit;
7426 } else {
7427 $new_input['rate_limits'][$role_id]['limit'] = ($role_id === 'logged_out') ? '10' : '100'; // Default
7428 }
7429 }
7430
7431 // Sanitize timeframe
7432 if (isset($settings['timeframe'])) {
7433 $timeframe = sanitize_text_field($settings['timeframe']);
7434 if (in_array($timeframe, $allowed_timeframes, true)) {
7435 $new_input['rate_limits'][$role_id]['timeframe'] = $timeframe;
7436 } else {
7437 $new_input['rate_limits'][$role_id]['timeframe'] = 'daily'; // Default
7438 }
7439 }
7440
7441 // Sanitize message
7442 if (isset($settings['message'])) {
7443 $new_input['rate_limits'][$role_id]['message'] = sanitize_textarea_field($settings['message']);
7444 }
7445 }
7446 }
7447
7448 if (isset($input['pre_chat_message'])) {
7449 $new_input['pre_chat_message'] = sanitize_textarea_field($input['pre_chat_message']);
7450 }
7451
7452 if (isset($input['voyage_api_key'])) {
7453 $new_input['voyage_api_key'] = sanitize_text_field($input['voyage_api_key']);
7454 }
7455
7456 // Add to your sanitize function
7457 if (isset($input['embedding_model'])) {
7458 $allowed_models = array(
7459 'text-embedding-ada-002',
7460 'text-embedding-3-small',
7461 'text-embedding-3-large',
7462 'voyage-3-large',
7463 'gemini-embedding-001'
7464 );
7465 if (in_array($input['embedding_model'], $allowed_models)) {
7466 $new_input['embedding_model'] = sanitize_text_field($input['embedding_model']);
7467 }
7468 }
7469
7470 if (isset($input['model'])) {
7471 if ($input['model'] === 'openrouter') {
7472 $new_input['model'] = 'openrouter';
7473 } else {
7474 $allowed_models = array(
7475 'gemini-3-pro-preview',
7476 'gemini-3-flash-preview',
7477 'gemini-2.5-pro',
7478 'gemini-2.5-flash',
7479 'gemini-2.5-flash-lite',
7480 'gemini-2.0-flash',
7481 'gemini-2.0-flash-lite',
7482 'gemini-1.5-pro',
7483 'gemini-1.5-flash',
7484 'grok-4-0709',
7485 'grok-4-1-fast-reasoning',
7486 'grok-4-1-fast-non-reasoning',
7487 'grok-3-beta',
7488 'grok-3-fast-beta',
7489 'grok-3-mini-beta',
7490 'grok-3-mini-fast-beta',
7491 'grok-2',
7492 'deepseek-chat',
7493 'claude-opus-4-6',
7494 'claude-sonnet-4-6',
7495 'claude-opus-4-5',
7496 'claude-sonnet-4-5-20250929',
7497 'claude-opus-4-1-20250805',
7498 'claude-haiku-4-5-20251001',
7499 'claude-opus-4-20250514',
7500 'claude-sonnet-4-20250514',
7501 'gpt-5.2',
7502 'gpt-5.1-chat-latest',
7503 'gpt-5.1-2025-11-13',
7504 'gpt-5',
7505 'gpt-5-mini',
7506 'gpt-5-nano',
7507 'gpt-5.4',
7508 'gpt-5.4-mini',
7509 'gpt-5.4-nano',
7510 'gpt-5.3-chat-latest',
7511 );
7512
7513 if (in_array($input['model'], $allowed_models)) {
7514 $new_input['model'] = sanitize_text_field($input['model']);
7515 } else {
7516 // Fallback for any deprecated model
7517 $new_input['model'] = 'gpt-5.1-chat-latest';
7518 }
7519 }
7520 }
7521
7522 if (isset($input['openrouter_selected_model'])) {
7523 // Just sanitize it, don't validate against a whitelist
7524 $new_input['openrouter_selected_model'] = sanitize_text_field($input['openrouter_selected_model']);
7525 }
7526
7527 // ADD THIS:
7528 if (isset($input['openrouter_selected_model_name'])) {
7529 $new_input['openrouter_selected_model_name'] = sanitize_text_field($input['openrouter_selected_model_name']);
7530 }
7531
7532 if (isset($input['openrouter_api_key'])) {
7533 $new_input['openrouter_api_key'] = sanitize_text_field($input['openrouter_api_key']);
7534 }
7535
7536
7537
7538 if (isset($input['woocommerce_consumer_key'])) {
7539 $new_input['woocommerce_consumer_key'] = sanitize_text_field($input['woocommerce_consumer_key']);
7540 }
7541
7542 if (isset($input['woocommerce_consumer_secret'])) {
7543 $new_input['woocommerce_consumer_secret'] = sanitize_text_field($input['woocommerce_consumer_secret']);
7544 }
7545
7546
7547 // Sanitize link_target_toggle
7548 if (isset($input['link_target_toggle'])) {
7549 $new_input['link_target_toggle'] = $input['link_target_toggle'] === 'on' ? 'on' : 'off';
7550 }
7551
7552 // Sanitize Loops API Key
7553 if (isset($input['loops_api_key'])) {
7554 $new_input['loops_api_key'] = sanitize_text_field($input['loops_api_key']);
7555 }
7556
7557 if (isset($input['chat_persistence_toggle'])) {
7558 $new_input['chat_persistence_toggle'] = $input['chat_persistence_toggle'] === 'on' ? 'on' : 'off';
7559 }
7560
7561 if (isset($input['popular_question_1'])) {
7562 $new_input['popular_question_1'] = sanitize_text_field($input['popular_question_1']);
7563 }
7564
7565 if (isset($input['popular_question_2'])) {
7566 $new_input['popular_question_2'] = sanitize_text_field($input['popular_question_2']);
7567 }
7568
7569 if (isset($input['popular_question_3'])) {
7570 $new_input['popular_question_3'] = sanitize_text_field($input['popular_question_3']);
7571 }
7572
7573 if (isset($input['additional_popular_questions']) && is_array($input['additional_popular_questions'])) {
7574 $new_input['additional_popular_questions'] = array_map('sanitize_text_field', $input['additional_popular_questions']);
7575 }
7576
7577 // Sanitize Loops Mailing List
7578 if (isset($input['loops_mailing_list'])) {
7579 $new_input['loops_mailing_list'] = sanitize_text_field($input['loops_mailing_list']);
7580 }
7581
7582 // Sanitize Triggered Phrase Response
7583 if (isset($input['triggered_phrase_response'])) {
7584 $new_input['triggered_phrase_response'] = wp_kses_post($input['triggered_phrase_response']);
7585 }
7586 if (isset($input['email_capture_response'])) {
7587 $new_input['email_capture_response'] = wp_kses_post($input['email_capture_response']);
7588 }
7589
7590 // Sanitize Brave Search Settings
7591 if (isset($input['brave_api_key'])) {
7592 $new_input['brave_api_key'] = sanitize_text_field($input['brave_api_key']);
7593 }
7594
7595 if (isset($input['brave_image_count'])) {
7596 $image_count = intval($input['brave_image_count']);
7597 $new_input['brave_image_count'] = ($image_count >=1 && $image_count <=6) ? $image_count : 4;
7598 }
7599
7600 if (isset($input['brave_safe_search'])) {
7601 $allowed = array('strict', 'off');
7602 $new_input['brave_safe_search'] = in_array($input['brave_safe_search'], $allowed, true) ? $input['brave_safe_search'] : 'strict';
7603 }
7604
7605 if (isset($input['brave_news_count'])) {
7606 $news_count = intval($input['brave_news_count']);
7607 $new_input['brave_news_count'] = ($news_count >=1 && $news_count <=10) ? $news_count : 3;
7608 }
7609
7610 if (isset($input['brave_country'])) {
7611 $new_input['brave_country'] = sanitize_text_field($input['brave_country']);
7612 }
7613
7614 if (isset($input['brave_language'])) {
7615 $new_input['brave_language'] = sanitize_text_field($input['brave_language']);
7616 }
7617
7618 if (isset($input['chat_toolbar_toggle'])) {
7619 $new_input['chat_toolbar_toggle'] = $input['chat_toolbar_toggle'] === 'on' ? 'on' : 'off';
7620 }
7621
7622 // Sanitize PDF upload button toggle
7623 if (isset($input['show_pdf_upload_button'])) {
7624 $new_input['show_pdf_upload_button'] = $input['show_pdf_upload_button'] === 'on' ? 'on' : 'off';
7625 } else {
7626 $new_input['show_pdf_upload_button'] = 'off'; // If checkbox is unchecked
7627 }
7628
7629 // Sanitize Word upload button toggle
7630 if (isset($input['show_word_upload_button'])) {
7631 $new_input['show_word_upload_button'] = $input['show_word_upload_button'] === 'on' ? 'on' : 'off';
7632 } else {
7633 $new_input['show_word_upload_button'] = 'off'; // If checkbox is unchecked
7634 }
7635
7636 if (isset($input['pdf_intent_trigger_text'])) {
7637 $new_input['pdf_intent_trigger_text'] = sanitize_text_field($input['pdf_intent_trigger_text']);
7638 }
7639
7640 if (isset($input['pdf_intent_success_text'])) {
7641 $new_input['pdf_intent_success_text'] = sanitize_text_field($input['pdf_intent_success_text']);
7642 }
7643
7644 if (isset($input['pdf_intent_error_text'])) {
7645 $new_input['pdf_intent_error_text'] = sanitize_text_field($input['pdf_intent_error_text']);
7646 }
7647
7648 if (isset($input['pdf_max_pages'])) {
7649 $new_input['pdf_max_pages'] = intval($input['pdf_max_pages']);
7650 if ($new_input['pdf_max_pages'] < 1 || $new_input['pdf_max_pages'] > 69) {
7651 $new_input['pdf_max_pages'] = 69; // Default to 69 if out of range
7652 }
7653 }
7654
7655 if (isset($input['live_agent_webhook_url'])) {
7656 $new_input['live_agent_webhook_url'] = esc_url_raw($input['live_agent_webhook_url']);
7657 }
7658 if (isset($input['live_agent_secret_key'])) {
7659 $new_input['live_agent_secret_key'] = sanitize_text_field($input['live_agent_secret_key']);
7660 }
7661
7662 // Live Agent Integration
7663 if (isset($input['live_agent_bot_token'])) {
7664 $new_input['live_agent_bot_token'] = sanitize_text_field($input['live_agent_bot_token']);
7665 }
7666
7667 if (isset($input['live_agent_user_ids'])) {
7668 $new_input['live_agent_user_ids'] = sanitize_textarea_field($input['live_agent_user_ids']);
7669 }
7670
7671 if (isset($input['live_agent_status'])) {
7672 $new_input['live_agent_status'] = ($input['live_agent_status'] === 'on') ? 'on' : 'off';
7673 }
7674 if (isset($input['live_agent_away_message'])) {
7675 $new_input['live_agent_away_message'] = sanitize_textarea_field($input['live_agent_away_message']);
7676 }
7677 if (isset($input['live_agent_notification_message'])) {
7678 $new_input['live_agent_notification_message'] = sanitize_textarea_field($input['live_agent_notification_message']);
7679 }
7680
7681 // Telegram Integration
7682 if (isset($input['telegram_status'])) {
7683 $new_input['telegram_status'] = ($input['telegram_status'] === 'on') ? 'on' : 'off';
7684 }
7685 if (isset($input['telegram_bot_token'])) {
7686 $new_input['telegram_bot_token'] = sanitize_text_field($input['telegram_bot_token']);
7687 }
7688 if (isset($input['telegram_group_id'])) {
7689 $new_input['telegram_group_id'] = sanitize_text_field($input['telegram_group_id']);
7690 }
7691 if (isset($input['telegram_webhook_secret'])) {
7692 $new_input['telegram_webhook_secret'] = sanitize_text_field($input['telegram_webhook_secret']);
7693 }
7694 if (isset($input['telegram_notification_message'])) {
7695 $new_input['telegram_notification_message'] = sanitize_textarea_field($input['telegram_notification_message']);
7696 }
7697 if (isset($input['telegram_away_message'])) {
7698 $new_input['telegram_away_message'] = sanitize_textarea_field($input['telegram_away_message']);
7699 }
7700
7701 // Sanitize script loading strategy
7702 if (isset($input['script_loading_strategy'])) {
7703 $allowed_strategies = array('default', 'defer', 'delay_1s', 'delay_3s', 'delay_5s', 'on_interaction');
7704 $new_input['script_loading_strategy'] = in_array($input['script_loading_strategy'], $allowed_strategies)
7705 ? $input['script_loading_strategy']
7706 : 'default';
7707 }
7708
7709 // Sanitize debug mode
7710 if (isset($input['debug_mode'])) {
7711 $new_input['debug_mode'] = ($input['debug_mode'] === 'on') ? 'on' : 'off';
7712 }
7713
7714 // Content Generator Settings
7715 if (isset($input['content_model'])) {
7716 $new_input['content_model'] = sanitize_text_field($input['content_model']);
7717 }
7718 if (isset($input['content_image_model'])) {
7719 $new_input['content_image_model'] = sanitize_text_field($input['content_image_model']);
7720 }
7721 if (isset($input['content_enable_images'])) {
7722 $new_input['content_enable_images'] = ($input['content_enable_images'] === 'on') ? 'on' : 'off';
7723 }
7724 if (isset($input['content_use_placeholders'])) {
7725 $new_input['content_use_placeholders'] = ($input['content_use_placeholders'] === 'on') ? 'on' : 'off';
7726 }
7727 if (isset($input['content_internal_linking'])) {
7728 $new_input['content_internal_linking'] = ($input['content_internal_linking'] === 'on') ? 'on' : 'off';
7729 }
7730 if (isset($input['content_tool_use'])) {
7731 $new_input['content_tool_use'] = ($input['content_tool_use'] === 'on') ? 'on' : 'off';
7732 }
7733
7734 // SEO Optimize toggle fields
7735 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) {
7736 if (isset($input[$seo_key])) {
7737 $new_input[$seo_key] = ($input[$seo_key] === 'on') ? 'on' : 'off';
7738 }
7739 }
7740
7741 // Preserve content generator settings when saving from main settings page
7742 // (where content fields are not in the form submission)
7743 $existing = get_option('mxchat_options', array());
7744 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) {
7745 if (!isset($new_input[$key]) && isset($existing[$key])) {
7746 $new_input[$key] = $existing[$key];
7747 }
7748 }
7749
7750 return $new_input;
7751 }
7752
7753 /**
7754 * Log a debug message if debug mode is enabled
7755 *
7756 * @param string $type The type of log entry (settings_save, api_error, activation, etc.)
7757 * @param string $message The log message
7758 * @param array $data Optional additional data to log
7759 * @return bool Whether the message was logged
7760 */
7761 public static function mxchat_log_debug( $type, $message, $data = array() ) {
7762 $options = get_option( 'mxchat_options', array() );
7763
7764 // Check if debug mode is enabled
7765 if ( ! isset( $options['debug_mode'] ) || $options['debug_mode'] !== 'on' ) {
7766 return false;
7767 }
7768
7769 // Get current log
7770 $log = get_option( 'mxchat_debug_log', array() );
7771 if ( ! is_array( $log ) ) {
7772 $log = array();
7773 }
7774
7775 // Add new entry
7776 $entry = array(
7777 'time' => current_time( 'Y-m-d H:i:s' ),
7778 'type' => sanitize_key( $type ),
7779 'message' => sanitize_text_field( $message ),
7780 );
7781
7782 if ( ! empty( $data ) ) {
7783 $entry['data'] = $data;
7784 }
7785
7786 // Add to beginning of array (newest first)
7787 array_unshift( $log, $entry );
7788
7789 // Keep only last 100 entries
7790 if ( count( $log ) > 100 ) {
7791 $log = array_slice( $log, 0, 100 );
7792 }
7793
7794 // Save log
7795 update_option( 'mxchat_debug_log', $log, false );
7796
7797 return true;
7798 }
7799
7800 /**
7801 * Get the debug log entries
7802 *
7803 * @return array The debug log entries
7804 */
7805 public static function mxchat_get_debug_log() {
7806 $log = get_option( 'mxchat_debug_log', array() );
7807 return is_array( $log ) ? $log : array();
7808 }
7809
7810 /**
7811 * Clear the debug log
7812 *
7813 * @return bool Whether the log was cleared
7814 */
7815 public static function mxchat_clear_debug_log() {
7816 return delete_option( 'mxchat_debug_log' );
7817 }
7818
7819 /**
7820 * Export settings as JSON with masked API keys
7821 *
7822 * @return array The sanitized settings array
7823 */
7824 public static function mxchat_export_settings() {
7825 $options = get_option( 'mxchat_options', array() );
7826
7827 if ( ! is_array( $options ) ) {
7828 return array();
7829 }
7830
7831 // List of API key fields to mask
7832 $api_key_fields = array(
7833 'api_key',
7834 'xai_api_key',
7835 'claude_api_key',
7836 'deepseek_api_key',
7837 'voyage_api_key',
7838 'gemini_api_key',
7839 'openrouter_api_key',
7840 'loops_api_key',
7841 'brave_api_key',
7842 'live_agent_secret_key',
7843 'live_agent_bot_token',
7844 'telegram_bot_token',
7845 'telegram_webhook_secret',
7846 'woocommerce_consumer_key',
7847 'woocommerce_consumer_secret',
7848 );
7849
7850 // Mask API keys (show only last 4 characters)
7851 foreach ( $api_key_fields as $field ) {
7852 if ( isset( $options[ $field ] ) && ! empty( $options[ $field ] ) ) {
7853 $value = $options[ $field ];
7854 if ( strlen( $value ) > 4 ) {
7855 $options[ $field ] = str_repeat( '*', strlen( $value ) - 4 ) . substr( $value, -4 );
7856 } else {
7857 $options[ $field ] = '****';
7858 }
7859 }
7860 }
7861
7862 // Add metadata
7863 $export = array(
7864 'plugin_version' => defined( 'MXCHAT_VERSION' ) ? MXCHAT_VERSION : 'unknown',
7865 'export_date' => current_time( 'Y-m-d H:i:s' ),
7866 'wordpress_version' => get_bloginfo( 'version' ),
7867 'php_version' => phpversion(),
7868 'settings' => $options,
7869 );
7870
7871 return $export;
7872 }
7873
7874 /**
7875 * Reset all settings to defaults
7876 *
7877 * @return bool Whether the reset was successful
7878 */
7879 public static function mxchat_reset_all_settings() {
7880 // Delete main options
7881 $deleted = delete_option( 'mxchat_options' );
7882
7883 // Also clear the debug log
7884 delete_option( 'mxchat_debug_log' );
7885
7886 // Log the reset (will create new log since we just cleared it)
7887 // We need to temporarily enable debug mode to log this
7888 $temp_options = array( 'debug_mode' => 'on' );
7889 update_option( 'mxchat_options', $temp_options );
7890
7891 self::mxchat_log_debug( 'reset', 'All settings have been reset to defaults' );
7892
7893 // Now delete again to trigger re-initialization
7894 delete_option( 'mxchat_options' );
7895
7896 return $deleted;
7897 }
7898
7899 // Method to append the chatbot to the body
7900 public function mxchat_append_chatbot_to_body() {
7901 $options = get_option('mxchat_options');
7902 if (isset($options['append_to_body']) && $options['append_to_body'] === 'on') {
7903 echo do_shortcode('[mxchat_chatbot floating="yes"]');
7904 }
7905 }
7906
7907
7908
7909
7910
7911 private function mxchat_fetch_loops_mailing_lists($api_key) {
7912 $url = 'https://app.loops.so/api/v1/lists';
7913 $response = wp_remote_get($url, array(
7914 'headers' => array(
7915 'Authorization' => 'Bearer ' . $api_key,
7916 'Content-Type' => 'application/json'
7917 )
7918 ));
7919
7920 if (is_wp_error($response)) {
7921 return array();
7922 }
7923
7924 $body = wp_remote_retrieve_body($response);
7925 $lists = json_decode($body, true);
7926
7927 return isset($lists) && is_array($lists) ? $lists : array();
7928 }
7929
7930 function mxchat_calculate_cosine_similarity($vec1, $vec2) {
7931 if (empty($vec1) || empty($vec2)) {
7932 return 0.0;
7933 }
7934
7935 $dot_product = 0.0;
7936 $norm_a = 0.0;
7937 $norm_b = 0.0;
7938
7939 for ($i = 0; $i < count($vec1); $i++) {
7940 $dot_product += $vec1[$i] * $vec2[$i];
7941 $norm_a += pow($vec1[$i], 2);
7942 $norm_b += pow($vec2[$i], 2);
7943 }
7944
7945 if ($norm_a == 0.0 || $norm_b == 0.0) {
7946 return 0.0;
7947 } else {
7948 return $dot_product / (sqrt($norm_a) * sqrt($norm_b));
7949 }
7950 }
7951
7952 /**
7953 * Validates nonce and deletes all chat prompts
7954 */
7955 public function mxchat_handle_delete_all_prompts() {
7956 //error_log('=== DELETE ALL DEBUG START ===');
7957
7958 // Verify nonce
7959 if (!isset($_POST['mxchat_delete_all_prompts_nonce']) || !wp_verify_nonce($_POST['mxchat_delete_all_prompts_nonce'], 'mxchat_delete_all_prompts_action')) {
7960 //error_log('DEBUG: Nonce verification failed');
7961 wp_die(__('Nonce verification failed.', 'mxchat'));
7962 }
7963
7964 // Check permissions
7965 if (!current_user_can('manage_options')) {
7966 //error_log('DEBUG: Permission check failed');
7967 wp_die(__('You do not have sufficient permissions to delete all prompts.', 'mxchat'));
7968 }
7969
7970 // Get bot_id from POST data
7971 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
7972 //error_log('DEBUG: Bot ID from POST: ' . $bot_id);
7973
7974 $success = true;
7975 $error_messages = array();
7976
7977 // Get bot-specific Pinecone configuration
7978 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
7979 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
7980
7981 //error_log('DEBUG: Pinecone options retrieved:');
7982 //error_log(' - Use Pinecone: ' . ($pinecone_options['mxchat_use_pinecone'] ?? 'NOT SET'));
7983 //error_log(' - API Key: ' . (empty($pinecone_options['mxchat_pinecone_api_key']) ? 'EMPTY' : 'SET'));
7984 //error_log(' - Host: ' . ($pinecone_options['mxchat_pinecone_host'] ?? 'NOT SET'));
7985
7986 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
7987
7988 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
7989 //error_log('[MXCHAT-DELETE] Pinecone is enabled for bot ' . $bot_id . ' - deleting all from Pinecone');
7990
7991 // Delete from bot-specific Pinecone index
7992 $result = $pinecone_manager->mxchat_delete_all_from_pinecone($pinecone_options);
7993
7994 //error_log('DEBUG: Delete all result: ' . ($result['success'] ? 'SUCCESS' : 'FAILED'));
7995 if (!$result['success']) {
7996 //error_log('DEBUG: Delete all error: ' . $result['message']);
7997 }
7998
7999 if (!$result['success']) {
8000 $success = false;
8001 $error_messages[] = $result['message'];
8002 }
8003
8004 // No cache clearing needed since we removed caching
8005
8006 } else {
8007 //error_log('[MXCHAT-DELETE] Pinecone not enabled for bot ' . $bot_id . ' - deleting from WordPress database');
8008
8009 // Delete from WordPress database
8010 global $wpdb;
8011 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
8012
8013 // If multi-bot is active and we have a specific bot, delete only that bot's content
8014 if ($bot_id !== 'default' && class_exists('MxChat_Multi_Bot_Manager')) {
8015 $result = $wpdb->delete(
8016 $table_name,
8017 array('bot_id' => $bot_id),
8018 array('%s')
8019 );
8020 } else {
8021 // Delete all content for default bot
8022 $result = $wpdb->query("DELETE FROM {$table_name}");
8023 }
8024
8025 if ($result === false) {
8026 $success = false;
8027 $error_messages[] = 'Failed to delete from WordPress database';
8028 }
8029 }
8030
8031 // No cache clearing needed since we removed caching
8032
8033 //error_log('=== DELETE ALL DEBUG END ===');
8034
8035 // Redirect back with a success message and bot_id
8036 $redirect_url = add_query_arg(array(
8037 'page' => 'mxchat-prompts',
8038 'bot_id' => $bot_id,
8039 'all_deleted' => $success ? 'true' : 'false'
8040 ), admin_url('admin.php'));
8041
8042 wp_safe_redirect($redirect_url);
8043 exit;
8044 }
8045
8046
8047
8048 /**
8049 * Handles deletion prompt with nonce validation
8050 */
8051 public function mxchat_handle_delete_prompt() {
8052 // Sanitize and validate nonce
8053 $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : '';
8054 if (empty($nonce) || !wp_verify_nonce($nonce, 'mxchat_delete_prompt_nonce')) {
8055 wp_die(esc_html__('Nonce verification failed.', 'mxchat'));
8056 }
8057
8058 // Check permissions
8059 if (!current_user_can('manage_options')) {
8060 wp_die(esc_html__('You do not have sufficient permissions to delete prompts.', 'mxchat'));
8061 }
8062
8063 // Get ID and source parameters
8064 $id = isset($_GET['id']) ? sanitize_text_field($_GET['id']) : '';
8065 $source = isset($_GET['source']) ? sanitize_text_field($_GET['source']) : '';
8066
8067 if (empty($id)) {
8068 wp_die(esc_html__('Invalid prompt ID.', 'mxchat'));
8069 }
8070
8071 $success = false;
8072 $error_message = '';
8073
8074 // Check if Pinecone is enabled and determine source automatically if not specified
8075 $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
8076 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
8077
8078 // If source is not specified, determine based on Pinecone configuration
8079 if (empty($source)) {
8080 $source = $use_pinecone ? 'pinecone' : 'wordpress';
8081 }
8082
8083 if ($source === 'pinecone' || $use_pinecone) {
8084 //error_log('[MXCHAT-DELETE] Deleting from Pinecone, ID: ' . $id);
8085
8086 // Handle Pinecone deletion
8087 if (empty($pinecone_options['mxchat_pinecone_host']) ||
8088 empty($pinecone_options['mxchat_pinecone_api_key'])) {
8089 wp_die(esc_html__('Pinecone configuration is missing.', 'mxchat'));
8090 }
8091
8092 // Delete from Pinecone using the vector ID directly
8093 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
8094 $result = $pinecone_manager->mxchat_delete_from_pinecone_by_vector_id(
8095 $id,
8096 $pinecone_options['mxchat_pinecone_api_key'],
8097 $pinecone_options['mxchat_pinecone_host']
8098 );
8099 if ($result['success']) {
8100 $success = true;
8101
8102 // Remove from vector cache
8103 $pinecone_manager->mxchat_remove_from_pinecone_vector_cache($id);
8104 $pinecone_manager->mxchat_remove_from_processed_content_caches($id);
8105
8106 set_transient('mxchat_admin_notice_success',
8107 esc_html__('Vector deleted successfully from Pinecone.', 'mxchat'), 30);
8108 } else {
8109 $error_message = $result['message'];
8110 set_transient('mxchat_admin_notice_error',
8111 esc_html__('Failed to delete from Pinecone: ', 'mxchat') . esc_html($error_message), 30);
8112 }
8113 } else {
8114 //error_log('[MXCHAT-DELETE] Deleting from WordPress database, ID: ' . $id);
8115
8116 // Handle WordPress database deletion
8117 global $wpdb;
8118 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
8119
8120 // Clear cache and delete prompt
8121 wp_cache_delete('prompt_' . $id, 'mxchat_prompts');
8122
8123 $result = $wpdb->delete(
8124 $table_name,
8125 array('id' => intval($id)),
8126 array('%d')
8127 );
8128
8129 if ($result !== false) {
8130 $success = true;
8131 set_transient('mxchat_admin_notice_success',
8132 esc_html__('Entry deleted successfully.', 'mxchat'), 30);
8133 } else {
8134 set_transient('mxchat_admin_notice_error',
8135 esc_html__('Failed to delete entry from database.', 'mxchat'), 30);
8136 }
8137 }
8138
8139 // Redirect back to the prompts page
8140 wp_safe_redirect(add_query_arg(
8141 array(
8142 'page' => 'mxchat-prompts',
8143 'deleted' => $success ? 'true' : 'false'
8144 ),
8145 admin_url('admin.php')
8146 ));
8147 exit;
8148 }
8149
8150
8151
8152
8153 /**
8154 * Averages multiple vectors into a single vector
8155 */
8156 private function mxchat_average_vectors($vectors) {
8157 $vector_length = count($vectors[0]);
8158 $sum_vector = array_fill(0, $vector_length, 0);
8159
8160 foreach ($vectors as $vector) {
8161 for ($i = 0; $i < $vector_length; $i++) {
8162 $sum_vector[$i] += $vector[$i];
8163 }
8164 }
8165
8166 // Divide each component by the number of vectors to get the average
8167 $num_vectors = count($vectors);
8168 for ($i = 0; $i < $vector_length; $i++) {
8169 $sum_vector[$i] /= $num_vectors;
8170 }
8171
8172 return $sum_vector;
8173 }
8174
8175
8176
8177
8178 /**
8179 * Generates embeddings from input text for MXChat
8180 */
8181 public function mxchat_generate_embedding($text) {
8182 // Enable detailed logging for debugging
8183 //error_log('[MXCHAT-EMBED] Starting embedding generation. Text length: ' . strlen($text) . ' bytes');
8184 //error_log('[MXCHAT-EMBED] Text preview: ' . substr($text, 0, 100) . '...');
8185
8186 $options = get_option('mxchat_options');
8187 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
8188 //error_log('[MXCHAT-EMBED] Selected embedding model: ' . $selected_model);
8189
8190 // Determine provider and endpoint
8191 if (strpos($selected_model, 'voyage') === 0) {
8192 $api_key = $options['voyage_api_key'] ?? '';
8193 $endpoint = 'https://api.voyageai.com/v1/embeddings';
8194 $provider_name = 'Voyage AI';
8195 //error_log('[MXCHAT-EMBED] Using Voyage AI API');
8196 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
8197 $api_key = $options['gemini_api_key'] ?? '';
8198 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
8199 $provider_name = 'Google Gemini';
8200 //error_log('[MXCHAT-EMBED] Using Google Gemini API');
8201 } else {
8202 $api_key = $options['api_key'] ?? '';
8203 $endpoint = 'https://api.openai.com/v1/embeddings';
8204 $provider_name = 'OpenAI';
8205 //error_log('[MXCHAT-EMBED] Using OpenAI API');
8206 }
8207
8208 //error_log('[MXCHAT-EMBED] Using endpoint: ' . $endpoint);
8209
8210 if (empty($api_key)) {
8211 $error_message = sprintf('Missing %s API key. Please configure your API key in the MxChat settings.', $provider_name);
8212 //error_log('[MXCHAT-EMBED] Error: ' . $error_message);
8213 return $error_message;
8214 }
8215
8216 // Check if text is too long (for OpenAI, roughly estimate tokens as words/0.75)
8217 $estimated_tokens = ceil(str_word_count($text) / 0.75);
8218 //error_log('[MXCHAT-EMBED] Estimated token count: ~' . $estimated_tokens);
8219
8220 if ($estimated_tokens > 8000 && strpos($selected_model, 'voyage') === false && strpos($selected_model, 'gemini-embedding') === false) {
8221 //error_log('[MXCHAT-EMBED] Warning: Text may exceed OpenAI token limits (8K for most models)');
8222 // Consider truncating text here
8223 }
8224
8225 // Prepare request body based on provider
8226 if (strpos($selected_model, 'gemini-embedding') === 0) {
8227 // Gemini API format
8228 $request_body = array(
8229 'model' => 'models/' . $selected_model,
8230 'content' => array(
8231 'parts' => array(
8232 array('text' => $text)
8233 )
8234 )
8235 );
8236
8237 // Set output dimensionality to 1536 for consistency with other models
8238 $request_body['outputDimensionality'] = 1536;
8239 } else {
8240 // OpenAI/Voyage API format
8241 $request_body = array(
8242 'model' => $selected_model,
8243 'input' => $text
8244 );
8245
8246 // Add output_dimension for voyage-3-large model
8247 if ($selected_model === 'voyage-3-large') {
8248 $request_body['output_dimension'] = 2048;
8249 }
8250 }
8251
8252 //error_log('[MXCHAT-EMBED] Request prepared with model: ' . $selected_model);
8253
8254 // Prepare headers based on provider
8255 if (strpos($selected_model, 'gemini-embedding') === 0) {
8256 // Gemini uses API key as query parameter
8257 $endpoint .= '?key=' . $api_key;
8258 $headers = array(
8259 'Content-Type' => 'application/json'
8260 );
8261 } else {
8262 // OpenAI/Voyage use Bearer token
8263 $headers = array(
8264 'Authorization' => 'Bearer ' . $api_key,
8265 'Content-Type' => 'application/json'
8266 );
8267 }
8268
8269 // Make API request
8270 //error_log('[MXCHAT-EMBED] Sending API request to: ' . $endpoint);
8271 $response = wp_remote_post($endpoint, array(
8272 'body' => wp_json_encode($request_body),
8273 'headers' => $headers,
8274 'timeout' => 60 // Increased timeout for large inputs
8275 ));
8276
8277 // Handle wp_remote_post errors
8278 if (is_wp_error($response)) {
8279 $error_message = $response->get_error_message();
8280 //error_log('[MXCHAT-EMBED] WP Remote Post Error: ' . $error_message);
8281 return 'Connection error: ' . $error_message;
8282 }
8283
8284 // Get and check HTTP response code
8285 $http_code = wp_remote_retrieve_response_code($response);
8286 //error_log('[MXCHAT-EMBED] API Response Code: ' . $http_code);
8287
8288 if ($http_code !== 200) {
8289 $error_body = wp_remote_retrieve_body($response);
8290 //error_log('[MXCHAT-EMBED] API Error Response Body: ' . $error_body);
8291
8292 // Try to parse error for more details
8293 $error_json = json_decode($error_body, true);
8294 if (json_last_error() === JSON_ERROR_NONE && isset($error_json['error'])) {
8295 $error_type = $error_json['error']['type'] ?? 'unknown';
8296 $error_message = $error_json['error']['message'] ?? 'No message';
8297 //error_log('[MXCHAT-EMBED] API Error Type: ' . $error_type);
8298 //error_log('[MXCHAT-EMBED] API Error Message: ' . $error_message);
8299
8300 // Customize error message for common API errors
8301 if ($error_type === 'invalid_request_error' && strpos($error_message, 'API key') !== false) {
8302 $error_message = sprintf('Invalid %s API key. Please check your API key in the MxChat settings.', $provider_name);
8303 } elseif ($error_type === 'authentication_error') {
8304 $error_message = sprintf('%s authentication failed. Please verify your API key in the MxChat settings.', $provider_name);
8305 }
8306
8307 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
8308 return $error_message;
8309 }
8310
8311 $error_message = sprintf("API Error (HTTP %d): Unable to generate embedding", $http_code);
8312 //error_log('[MXCHAT-EMBED] Returning error: ' . $error_message);
8313 return $error_message;
8314 }
8315
8316 // Parse response body
8317 $response_body = wp_remote_retrieve_body($response);
8318 //error_log('[MXCHAT-EMBED] Received response length: ' . strlen($response_body) . ' bytes');
8319
8320 $response_data = json_decode($response_body, true);
8321
8322 if (json_last_error() !== JSON_ERROR_NONE) {
8323 $error = json_last_error_msg();
8324 //error_log('[MXCHAT-EMBED] JSON Parse Error: ' . $error);
8325 //error_log('[MXCHAT-EMBED] Response preview: ' . substr($response_body, 0, 200));
8326 return "Failed to parse API response: $error";
8327 }
8328
8329 // Handle different response formats based on provider
8330 if (strpos($selected_model, 'gemini-embedding') === 0) {
8331 // Gemini API response format
8332 if (isset($response_data['embedding']['values'])) {
8333 $embedding_dimensions = count($response_data['embedding']['values']);
8334 //error_log('[MXCHAT-EMBED] Successfully extracted Gemini embedding with ' . $embedding_dimensions . ' dimensions');
8335
8336 // Check if embedding dimensions are as expected (should be 1536)
8337 if ($embedding_dimensions !== 1536) {
8338 //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
8339 }
8340
8341 return $response_data['embedding']['values'];
8342 } else {
8343 //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
8344 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
8345
8346 if (isset($response_data['error'])) {
8347 $error_message = "Gemini API Error in response: " . wp_json_encode($response_data['error']);
8348 //error_log('[MXCHAT-EMBED] ' . $error_message);
8349 return $error_message;
8350 }
8351
8352 $error_message = "Invalid Gemini API response format: No embedding found";
8353 //error_log('[MXCHAT-EMBED] ' . $error_message);
8354 return $error_message;
8355 }
8356 } else {
8357 // OpenAI/Voyage API response format
8358 if (isset($response_data['data'][0]['embedding'])) {
8359 $embedding_dimensions = count($response_data['data'][0]['embedding']);
8360 //error_log('[MXCHAT-EMBED] Successfully extracted embedding with ' . $embedding_dimensions . ' dimensions');
8361
8362 // Check if embedding dimensions are as expected
8363 if (($selected_model === 'text-embedding-ada-002' && $embedding_dimensions !== 1536) ||
8364 ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
8365 //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
8366 }
8367
8368 return $response_data['data'][0]['embedding'];
8369 } else {
8370 //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
8371 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
8372
8373 if (isset($response_data['error'])) {
8374 $error_message = "API Error in response: " . wp_json_encode($response_data['error']);
8375 //error_log('[MXCHAT-EMBED] ' . $error_message);
8376 return $error_message;
8377 }
8378
8379 $error_message = "Invalid API response format: No embedding found";
8380 //error_log('[MXCHAT-EMBED] ' . $error_message);
8381 return $error_message;
8382 }
8383 }
8384 }
8385
8386
8387
8388 }
8389 ?>
8390