PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.3.2
MxChat – AI Chatbot & Content Generation for WordPress v2.3.2
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
← All changes | includes/class-mxchat-integrator.php +3929 -1296 2.0.52.3.2 View file →
@@ -9,50 +9,50 @@
9 9 private $chat_count;
10 10 private $fallbackResponse;
11 11 private $productCardHtml;
12 12 private $word_handler;
13 + private $last_similarity_analysis = null;
13 14
15 +
16 +/**
17 + * Class constructor
18 + */
14 19 public function __construct() {
15 20 $this->options = get_option('mxchat_options');
16 21 $this->prompts_options = get_option('mxchat_prompts_options', array());
17 -
18 22 $this->chat_count = get_option('mxchat_chat_count', 0);
19 23 $this->word_handler = new MXChat_Word_Handler($this->options);
20 -
24 +
25 + // Add all action hooks
21 26 add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
22 27 add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
23 28 add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
24 -
25 29 add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
26 30 add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
31 +
27 32 // Add the AJAX actions for checking if the pre-chat message was dismissed
28 33 add_action('wp_ajax_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
29 34 add_action('wp_ajax_nopriv_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
30 -
31 35 add_action('wp_ajax_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
32 36 add_action('wp_ajax_nopriv_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
33 -
34 37 add_action('wp_ajax_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
35 38 add_action('wp_ajax_nopriv_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
36 -
37 - if (!wp_next_scheduled('mxchat_reset_rate_limits')) {
38 - wp_schedule_event(time(), 'daily', 'mxchat_reset_rate_limits');
39 - }
40 -
39 +
41 40 // Add REST API routes registration
42 41 add_action('rest_api_init', array($this, 'register_routes'));
43 -
44 42 add_action('wp_ajax_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
45 43 add_action('wp_ajax_nopriv_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
46 -
44 +
45 + // Rate limit action - notice we removed the old schedule setup
47 46 add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
48 -
47 +
48 + // File upload and handling actions
49 49 add_action('wp_ajax_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
50 50 add_action('wp_ajax_nopriv_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
51 51 add_action('wp_ajax_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
52 52 add_action('wp_ajax_nopriv_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
53 -
54 - // Add these with your other add_action hooks
53 +
54 + // Word document handling actions
55 55 add_action('wp_ajax_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
56 56 add_action('wp_ajax_nopriv_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
57 57 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
58 58 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
@@ -57,15 +57,39 @@
57 57 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
58 58 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
59 59 add_action('wp_ajax_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
60 60 add_action('wp_ajax_nopriv_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
61 -
61 +
62 + // Email handling actions
62 63 add_action('wp_ajax_nopriv_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
63 64 add_action('wp_ajax_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
64 65 add_action('wp_ajax_nopriv_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
65 66 add_action('wp_ajax_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
67 +
68 + add_action('wp_ajax_mxchat_stream_chat', array($this, 'mxchat_handle_chat_request'));
69 + add_action('wp_ajax_nopriv_mxchat_stream_chat', array($this, 'mxchat_handle_chat_request'));
70 +
71 + // Testing panel AJAX actions
72 + add_action('wp_ajax_mxchat_get_system_info', array($this, 'mxchat_get_system_info'));
73 + add_action('wp_ajax_mxchat_get_similarity_threshold', array($this, 'mxchat_get_similarity_threshold'));
74 + add_action('wp_ajax_mxchat_get_kb_status', array($this, 'mxchat_get_kb_status'));
75 + add_action('wp_ajax_mxchat_start_fresh_session', array($this, 'mxchat_start_fresh_session'));
76 +
77 +add_filter('mxchat_check_actions_only', array($this, 'check_actions_for_addons'), 10, 4);
78 +
79 +
66 80 }
67 81
82 +// In your core plugin's check_actions_for_addons method:
83 +public function check_actions_for_addons($default, $message, $user_id, $session_id) {
84 + error_log('MxChat Core: check_actions_for_addons called with message: ' . $message);
85 +
86 + $result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
87 +
88 + error_log('MxChat Core: Intent check result = ' . ($result === false ? 'false' : 'true'));
89 +
90 + return $result;
91 +}
68 92
69 93 private function mxchat_increment_chat_count() {
70 94 $chat_count = get_option('mxchat_chat_count', 0);
71 95 $chat_count++;
@@ -96,24 +120,9 @@
96 120 'chat_mode' => $chat_mode
97 121 ]);
98 122 wp_die();
99 123 }
100 -private function mxchat_fetch_conversation_history_for_ajax($session_id) {
101 - $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history based on session ID
102 - $formatted_history = [];
103 124
104 - // Format the history to align with the expected structure for OpenAI
105 - foreach ($history as $entry) {
106 - $formatted_history[] = [
107 - 'role' => $entry['role'], // Ensure this matches 'user' or 'assistant'
108 - 'content' => $entry['content']
109 - ];
110 - }
111 -
112 - return $formatted_history;
113 -}
114 -
115 -
116 125 private function mxchat_fetch_conversation_history_for_ai($session_id) {
117 126 $history = get_option("mxchat_history_{$session_id}", []);
118 127 $formatted_history = [];
119 128
@@ -150,9 +159,9 @@
150 159 if (!$has_code && $clean_content !== strip_tags($entry['content'])) {
151 160 continue;
152 161 }
153 162
154 - // More accurate token estimation (1 token ≈ 4 characters)
163 + // More accurate token estimation (1 token ≈ 4 characters)
155 164 $token_estimate = ceil(mb_strlen($clean_content, 'UTF-8') / 4);
156 165
157 166 // Check token budget with the new estimate
158 167 if (($current_token_count + $token_estimate + $reserved_tokens) > $max_tokens) {
@@ -206,8 +215,14 @@
206 215 'methods' => 'POST',
207 216 'callback' => [$this, 'handle_slack_interaction'],
208 217 'permission_callback' => [$this, 'verify_slack_request'],
209 218 ]);
219 +
220 + register_rest_route('mxchat/v1', '/slack-messages', [
221 + 'methods' => 'POST',
222 + 'callback' => [$this, 'handle_slack_messages'],
223 + 'permission_callback' => [$this, 'verify_slack_request'],
224 + ]);
210 225
211 226 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
212 227 }
213 228
@@ -260,8 +275,9 @@
260 275
261 276 // Compare signatures
262 277 return hash_equals($my_signature, $slack_signature);
263 278 }
279 +
264 280 public function mxchat_stream_events(WP_REST_Request $request) {
265 281 header('Content-Type: text/event-stream');
266 282 header('Cache-Control: no-cache');
267 283 header('Connection: keep-alive');
@@ -297,18 +313,26 @@
297 313
298 314
299 315 private function mxchat_save_chat_message($session_id, $role, $message) {
300 316 global $wpdb;
301 -
302 317 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
303 318 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
304 -
319 +
320 + // Check if this is the first message in a new session (before any other database operations)
321 + $is_new_session = false;
322 + if ($role === 'user') { // Only check for user messages, not bot responses
323 + $existing_messages = $wpdb->get_var($wpdb->prepare(
324 + "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
325 + $session_id
326 + ));
327 + $is_new_session = ($existing_messages == 0);
328 + }
329 +
305 330 // 1) Extract agent name if present
306 331 $agent_name = '';
307 332 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
308 333 $agent_name = $matches[1];
309 334 $message = str_replace("Agent: $agent_name - ", '', $message);
310 -
311 335 $session_meta_key = "mxchat_agent_name_{$session_id}";
312 336 if (empty(get_option($session_meta_key))) {
313 337 update_option($session_meta_key, $agent_name);
314 338 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
@@ -313,30 +337,24 @@
313 337 update_option($session_meta_key, $agent_name);
314 338 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
315 339 }
316 340 }
317 -
318 341 // 2) Generate unique message_id
319 342 $message_id = uniqid();
320 343 //error_log("[DEBUG] mxchat_save_chat_message -> Generated message_id: {$message_id}");
321 -
322 344 // 3) Determine user_id
323 345 $user_id = is_user_logged_in() ? get_current_user_id() : 0;
324 -
325 346 // 4) Determine user_identifier
326 347 $user_identifier = $agent_name
327 348 ? $agent_name
328 349 : MxChat_User::mxchat_get_user_identifier();
329 -
330 350 // 5) Determine displayed_name
331 351 $user_email = MxChat_User::mxchat_get_user_email();
332 352 $displayed_name = $agent_name ? $agent_name : ($user_email ?: $user_identifier);
333 -
334 353 // 6) Check for a saved email in wp_options
335 354 $email_option_key = "mxchat_email_{$session_id}";
336 355 $saved_email = get_option($email_option_key);
337 356 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for email_option_key: {$email_option_key}, found: {$saved_email}");
338 -
339 357 // If found, update DB user_email
340 358 if ($saved_email) {
341 359 $update_res = $wpdb->update(
342 360 $table_name,
@@ -346,9 +364,8 @@
346 364 ['%s']
347 365 );
348 366 //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email update for session_id {$session_id}. update_res: {$update_res}");
349 367 }
350 -
351 368 // 7) Save to session history in wp_options
352 369 $history_key = "mxchat_history_{$session_id}";
353 370 $history = get_option($history_key, []);
354 371 $history[] = [
@@ -357,11 +374,10 @@
357 374 'content' => $message,
358 375 'timestamp' => round(microtime(true) * 1000),
359 376 'agent_name' => $displayed_name,
360 377 ];
361 - update_option($history_key, $history);
378 + update_option($history_key, $history, 'no');
362 379 //error_log("[DEBUG] mxchat_save_chat_message -> Updated session history in option: {$history_key}");
363 -
364 380 // 8) Save the message to DB (INSERT)
365 381 $insert_data = [
366 382 'user_id' => $user_id,
367 383 'user_identifier'=> $user_identifier,
@@ -372,12 +388,64 @@
372 388 'timestamp' => current_time('mysql', 1),
373 389 ];
374 390 $wpdb->insert($table_name, $insert_data);
375 391 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
376 -
392 +
393 + // 9) Send notification email if this is the first user message in a new session
394 + if ($wpdb->insert_id && $is_new_session && $role === 'user') {
395 + $this->send_new_chat_notification($session_id, array(
396 + 'identifier' => $user_identifier,
397 + 'email' => $saved_email ?: $user_email,
398 + 'ip' => $_SERVER['REMOTE_ADDR']
399 + ));
400 + }
401 +
377 402 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
378 403 return $message_id;
379 404 }
405 +private function send_new_chat_notification($session_id, $user_info = array()) {
406 + $options = get_option('mxchat_transcripts_options');
407 +
408 + // Check if notifications are enabled
409 + if (empty($options['mxchat_enable_notifications'])) {
410 + return false;
411 + }
412 +
413 + // Get notification email
414 + $to = !empty($options['mxchat_notification_email']) ?
415 + $options['mxchat_notification_email'] :
416 + get_option('admin_email');
417 +
418 + if (!is_email($to)) {
419 + return false;
420 + }
421 +
422 + // Prepare email content
423 + $subject = sprintf('[%s] New Chat Session Started', get_bloginfo('name'));
424 +
425 + $user_identifier = isset($user_info['identifier']) ? $user_info['identifier'] : 'Guest';
426 + $user_email = isset($user_info['email']) ? $user_info['email'] : 'Not provided';
427 + $user_ip = isset($user_info['ip']) ? $user_info['ip'] : $_SERVER['REMOTE_ADDR'];
428 +
429 + $message = sprintf(
430 + "A new chat session has started on your website.\n\n" .
431 + "Session ID: %s\n" .
432 + "User: %s\n" .
433 + "Email: %s\n" .
434 + "IP Address: %s\n" .
435 + "Time: %s\n\n" .
436 + "View transcripts: %s",
437 + $session_id,
438 + $user_identifier,
439 + $user_email,
440 + $user_ip,
441 + current_time('mysql'),
442 + admin_url('admin.php?page=mxchat-transcripts')
443 + );
444 +
445 + // Send email
446 + return wp_mail($to, $subject, $message);
447 +}
380 448
381 449 public function mxchat_handle_save_email_and_response() {
382 450 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
383 451
@@ -382,9 +450,9 @@
382 450 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
383 451
384 452 // Validate nonce
385 453 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
386 - error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
454 + //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
387 455 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
388 456 wp_die();
389 457 }
390 458
@@ -468,96 +536,29 @@
468 536 wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
469 537 }
470 538 }
471 539
540 +public function mxchat_handle_chat_request() {
541 + global $wpdb;
472 542
473 -// First, add this helper function to get the highest rate limit for a user's roles
474 -private function get_user_role_rate_limit($user_id) {
475 - //error_log(esc_html__("Checking rate limit for user ID: ", 'mxchat') . $user_id);
476 -
477 - if (!$user_id) {
478 - //error_log(esc_html__("No user ID provided, using logged-out limit: ", 'mxchat') . ($this->options['rate_limit_logged_out'] ?? '10'));
479 - return $this->options['rate_limit_logged_out'] ?? '10';
480 - }
481 -
482 - $user = get_userdata($user_id);
483 - if (!$user || !$user->roles) {
484 - //error_log(esc_html__("No user data or roles found, using logged-out limit: ", 'mxchat') . ($this->options['rate_limit_logged_out'] ?? '10'));
485 - return $this->options['rate_limit_logged_out'] ?? '10';
486 - }
487 -
488 - //error_log(esc_html__("User roles: ", 'mxchat') . print_r($user->roles, true));
489 - //error_log(esc_html__("Available role rate limits: ", 'mxchat') . print_r($this->options['role_rate_limits'] ?? [], true));
490 -
491 - $max_limit = 0;
492 - foreach ($user->roles as $role) {
493 - //error_log(esc_html__("Checking limit for role: ", 'mxchat') . $role);
494 - if (isset($this->options['role_rate_limits'][$role])) {
495 - $role_limit = $this->options['role_rate_limits'][$role];
496 - //error_log(esc_html__("Found limit for role ", 'mxchat') . $role . esc_html__(": ", 'mxchat') . $role_limit);
497 -
498 - if ($role_limit === 'unlimited') {
499 - //error_log(esc_html__("Returning unlimited for role: ", 'mxchat') . $role);
500 - return 'unlimited';
501 - }
502 -
503 - $max_limit = max($max_limit, (int)$role_limit);
504 - //error_log(esc_html__("Current max limit: ", 'mxchat') . $max_limit);
505 - } else {
506 - //error_log(esc_html__("No limit found for role: ", 'mxchat') . $role);
543 + // NEW: Check if this is a streaming request
544 + $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat';
545 +
546 + // NEW: Set streaming headers if needed
547 + if ($is_streaming) {
548 + // Disable output buffering
549 + while (ob_get_level()) {
550 + ob_end_clean();
507 551 }
552 +
553 + // Set headers for SSE
554 + header('Content-Type: text/event-stream');
555 + header('Cache-Control: no-cache');
556 + header('Connection: keep-alive');
557 + header('X-Accel-Buffering: no');
508 558 }
509 559
510 - $final_limit = $max_limit > 0 ? (string)$max_limit : '100';
511 - //error_log(esc_html__("Final rate limit: ", 'mxchat') . $final_limit);
512 - return $final_limit;
513 -}
514 -
515 -
516 -// Add this to your plugin's main PHP file
517 -public function mxchat_check_new_messages() {
518 - if (!isset($_POST['session_id']) || !isset($_POST['last_seen_id'])) {
519 - wp_send_json_error(['message' => 'Missing required parameters']);
520 - wp_die();
521 - }
522 -
523 - $session_id = sanitize_text_field($_POST['session_id']);
524 - $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
525 -
526 - // Get chat history
527 - $history = get_option("mxchat_history_{$session_id}", []);
528 -
529 - if (empty($history)) {
530 - wp_send_json_success([
531 - 'hasNewMessages' => false,
532 - 'new_messages' => []
533 - ]);
534 - wp_die();
535 - }
536 -
537 - // Filter new messages
538 - $new_messages = array_filter($history, function($message) use ($last_seen_id) {
539 - return isset($message['id']) && $message['id'] > $last_seen_id;
540 - });
541 -
542 - // Sort by ID to ensure proper order
543 - usort($new_messages, function($a, $b) {
544 - return $a['id'] <=> $b['id'];
545 - });
546 -
547 - wp_send_json_success([
548 - 'hasNewMessages' => !empty($new_messages),
549 - 'new_messages' => array_values($new_messages),
550 - 'latestMessageId' => end($new_messages)['id'] ?? $last_seen_id
551 - ]);
552 - wp_die();
553 -}
554 -
555 -public function mxchat_handle_chat_request() {
556 - global $wpdb;
557 -
558 -
559 - // Check if MX Chat Moderation is active
560 + // Check if MX Chat Moderation is active
560 561 if (class_exists('MX_Chat_Moderation')) {
561 562 // Get user email and IP
562 563 $user_email = '';
563 564 $user_ip = $_SERVER['REMOTE_ADDR'];
@@ -591,14 +592,12 @@
591 592 wp_die();
592 593 }
593 594 }
594 595
595 -
596 - // Reset fallback response at the start of each request
597 596 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
598 597 $this->productCardHtml = '';
599 598
600 - // Get the actual WordPress user ID if logged in
599 + // Get the actual WordPress user ID if logged in
601 600 $is_logged_in = is_user_logged_in();
602 601 if ($is_logged_in) {
603 602 $user_id = get_current_user_id(); // This will get the actual WordPress user ID
604 603 } else {
@@ -608,65 +607,24 @@
608 607
609 608 // Get and sanitize the user identifier
610 609 $user_id = sanitize_key($user_id);
611 610
612 - // Determine if user is logged in
613 - $is_logged_in = is_user_logged_in();
614 - //error_log("User logged in status: " . ($is_logged_in ? 'true' : 'false'));
611 + // Check rate limit using new settings structure
612 + $rate_limit_result = $this->check_rate_limit();
615 613
616 - // Get rate limit based on user status
617 - // Get rate limit based on user status
618 - $rate_limit = $is_logged_in
619 - ? $this->get_user_role_rate_limit($user_id)
620 - : ($this->options['rate_limit_logged_out'] ?? '10');
621 -
622 - //error_log("Selected rate limit: " . $rate_limit);
623 -
624 - // Rest of your code remains the same
625 - // If rate limit is 'unlimited', skip rate limiting checks
626 - if ($rate_limit !== 'unlimited') {
627 - // Convert rate limit to integer
628 - $rate_limit = intval($rate_limit);
629 - // Setup rate limiting
630 - $rate_limit_transient_key = 'mxchat_chat_limit_' . $user_id;
631 - $chat_count = get_transient($rate_limit_transient_key);
632 - if ($chat_count === false) {
633 - // Initialize new counter if none exists
634 - $chat_count = 0;
635 - }
636 - // Check if user has exceeded their rate limit
637 - if ($chat_count >= $rate_limit) {
638 - // Get custom rate limit message or use default
639 - $rate_limit_message = isset($this->options['rate_limit_message'])
640 - ? $this->options['rate_limit_message']
641 - : esc_html__('Rate limit exceeded. Please try again later.', 'mxchat');
642 - // Replace placeholder if it exists in the message
643 - $rate_limit_message = str_replace(
644 - array('{limit}', '{count}', '{remaining}'),
645 - array($rate_limit, $chat_count, max(0, $rate_limit - $chat_count)),
646 - $rate_limit_message
647 - );
648 - wp_send_json([
649 - 'success' => false,
650 - 'message' => $rate_limit_message,
651 - 'status' => 'rate_limit_exceeded',
652 - 'limit' => $rate_limit,
653 - 'count' => $chat_count
654 - ]);
655 - wp_die();
656 - }
657 - // Increment the counter
658 - $chat_count++;
659 - // Store the updated count with 24-hour expiration
660 - set_transient($rate_limit_transient_key, $chat_count, DAY_IN_SECONDS);
614 + if ($rate_limit_result !== true) {
615 + wp_send_json([
616 + 'success' => false,
617 + 'message' => $rate_limit_result['message'],
618 + 'status' => 'rate_limit_exceeded'
619 + ]);
620 + wp_die();
661 621 }
662 622
663 623 // Rest of your existing code...
664 624 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
665 - //error_log("Session ID: $session_id");
666 625
667 626 if (empty($session_id)) {
668 - //error_log("Error: Session ID is missing.");
669 627 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
670 628 wp_die();
671 629 }
672 630
@@ -671,57 +629,160 @@
671 629 }
672 630
673 631 // Validate and sanitize the incoming message
674 632 if (empty($_POST['message'])) {
675 - //error_log("Error: No message received.");
676 633 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
677 634 wp_die();
678 635 }
679 636
637 + // NEW: Get page context if provided
638 + $page_context = null;
639 + if (isset($_POST['page_context']) && !empty($_POST['page_context'])) {
640 + $page_context_raw = stripslashes($_POST['page_context']);
641 + $page_context = json_decode($page_context_raw, true);
642 +
643 + // Validate page context structure
644 + if (is_array($page_context) &&
645 + isset($page_context['url']) &&
646 + isset($page_context['title']) &&
647 + isset($page_context['content'])) {
648 +
649 + // Sanitize page context
650 + $page_context['url'] = esc_url_raw($page_context['url']);
651 + $page_context['title'] = sanitize_text_field($page_context['title']);
652 + $page_context['content'] = wp_kses_post($page_context['content']);
653 + } else {
654 + $page_context = null;
655 + }
656 + }
680 657
681 -// Modify the message sanitization to preserve PHP tags in code blocks
682 -$allowed_tags = [
683 - 'pre' => [],
684 - 'code' => ['class' => true],
685 - 'span' => ['class' => true],
686 - 'div' => ['class' => true],
687 -];
658 + // Modify the message sanitization to preserve PHP tags in code blocks
659 + $allowed_tags = [
660 + 'pre' => [],
661 + 'code' => ['class' => true],
662 + 'span' => ['class' => true],
663 + 'div' => ['class' => true],
664 + ];
688 665
689 -// First preserve code blocks
690 -$message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
691 - return htmlspecialchars_decode($matches[0]);
692 -}, $_POST['message']);
666 + // First preserve code blocks
667 + $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
668 + return htmlspecialchars_decode($matches[0]);
669 + }, $_POST['message']);
693 670
694 -// Then apply sanitization
695 -$message = wp_kses($message, $allowed_tags);
671 + // Then apply sanitization
672 + $message = wp_kses($message, $allowed_tags);
696 673
697 -// Decode code blocks
698 -$message = preg_replace_callback('/(&lt;pre&gt;&lt;code.*?&gt;.*?&lt;\/code&gt;&lt;\/pre&gt;)/s', function($matches) {
699 - return htmlspecialchars_decode($matches[1]);
700 -}, $message);
674 + // Preserve code blocks from markdown conversion
675 + $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
676 + $message = apply_filters('mxchat_filter_message', $message, 'prompt', $session_id);
701 677
702 -$message = trim($message);
678 +// ===== SIMPLIFIED TESTING PANEL INITIALIZATION =====
679 + // Always initialize testing data for admins (no toggle needed)
680 + $testing_data = null;
681 + if (current_user_can('administrator')) {
682 + // For vision messages, use the original user message for the query display
683 + $query_for_testing = $message;
684 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
685 + $query_for_testing = sanitize_textarea_field($_POST['original_user_message']);
686 + }
687 +
688 + $testing_data = [
689 + 'query' => $query_for_testing,
690 + 'timestamp' => time(),
691 + 'top_matches' => [],
692 + 'action_matches' => [], // NEW: Initialize action matches array
693 + 'page_context' => $page_context, // NEW: Include page context in testing data
694 + 'is_vision' => isset($_POST['vision_processed']) && $_POST['vision_processed']
695 + ];
696 +
697 + // Get similarity threshold
698 + $similarity_threshold = isset($this->options['similarity_threshold'])
699 + ? ((int) $this->options['similarity_threshold']) / 100
700 + : 0.75;
701 +
702 + $testing_data['similarity_threshold'] = $similarity_threshold;
703 +
704 + // Determine knowledge base type
705 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
706 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
707 + $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
708 + }
709 + // ===== END SIMPLIFIED TESTING INITIALIZATION =====
703 710
704 -// Preserve code blocks from markdown conversion
705 -$message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
711 +// Add debug before and after:
712 +error_log('MxChat Core: About to call mxchat_pre_process_message filter with message: ' . $message);
713 +$pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
714 +error_log('MxChat Core: Filter returned: ' . (is_array($pre_processed_result) ? 'array' : $pre_processed_result));
706 715
707 - // Save the user's message
708 - $this->mxchat_save_chat_message($session_id, 'user', $message);
709 716
717 + // If the pre-processing returned a result (not the original message), use it directly
718 + if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
719 + // Save the AI response
720 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
721 +
722 + // Save HTML content if provided
723 + if (!empty($pre_processed_result['html'])) {
724 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
725 + }
726 +
727 + // Add testing data if admin
728 + $response_data = [
729 + 'text' => $pre_processed_result['text'],
730 + 'html' => $pre_processed_result['html'] ?? '',
731 + 'session_id' => $session_id
732 + ];
733 +
734 + if ($testing_data !== null) {
735 + $response_data['testing_data'] = $testing_data;
736 + }
737 +
738 + wp_send_json($response_data);
739 + wp_die();
740 + }
741 +
742 + // Save the user's message - handle vision processed messages differently
743 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
744 + // For vision messages, save the original user message with image indicator
745 + $original_message = sanitize_textarea_field($_POST['original_user_message']);
746 + if (isset($_POST['vision_images_count']) && $_POST['vision_images_count'] > 0) {
747 + $image_count = intval($_POST['vision_images_count']);
748 + $original_message .= " [{$image_count} image(s)]";
749 + }
750 + $this->mxchat_save_chat_message($session_id, 'user', $original_message);
751 + } else {
752 + // Regular message - save as normal
753 + $this->mxchat_save_chat_message($session_id, 'user', $message);
754 + }
755 +
710 756 // Check if the message is an email address
711 757 if (is_email($message)) {
712 758 // Add the email to Loops
713 759 $this->add_email_to_loops($message);
714 -
760 +
715 761 // Send success response
716 762 $response_message = $this->options['email_capture_response'] ??
717 763 esc_html__('Thank you! Your coupon is on the way!', 'mxchat');
718 -
719 - wp_send_json([
764 +
765 + // Clear streaming headers if they were set
766 + if ($is_streaming) {
767 + header_remove('Content-Type');
768 + header_remove('Cache-Control');
769 + header_remove('Connection');
770 + header_remove('X-Accel-Buffering');
771 + header('Content-Type: application/json');
772 + }
773 +
774 + $email_response = [
720 775 'success' => true,
721 776 'status' => 'email_captured',
722 777 'message' => $response_message
723 - ]);
778 + ];
779 +
780 + if ($testing_data !== null) {
781 + $email_response['testing_data'] = $testing_data;
782 + }
783 +
784 + wp_send_json($email_response);
724 785 wp_die();
725 786 }
726 787
727 788 $intent_info = '';
@@ -727,19 +788,22 @@
727 788 $intent_info = '';
728 789
729 790 // Check chat mode
730 791 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
731 - //error_log("Chat Mode: $chat_mode");
732 792
733 793 // Handle agent mode
794 +// Handle agent mode
734 795 if ($chat_mode === 'agent') {
735 796 // First, check for switch intent before doing anything else
736 797 $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
737 798
799 + // NEW: Capture action analysis for testing panel after intent check
800 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
801 + $testing_data['action_matches'] = $this->last_action_analysis;
802 + }
803 +
738 804 // If we matched an intent and it's the switch intent, handle it
739 805 if ($intent_matched && !empty($this->fallbackResponse['text'])) {
740 - //error_log("Switch to chatbot intent detected");
741 -
742 806 // Update chat mode first
743 807 update_option("mxchat_mode_{$session_id}", 'ai');
744 808
745 809 // Clear any existing PDF context to start fresh
@@ -752,8 +816,12 @@
752 816 'session_id' => $session_id,
753 817 'chat_mode' => 'ai'
754 818 ];
755 819
820 + if ($testing_data !== null) {
821 + $response_data['testing_data'] = $testing_data;
822 + }
823 +
756 824 // Save the mode switch message
757 825 $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
758 826 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
759 827
@@ -763,16 +831,20 @@
763 831 } elseif (!$intent_matched) {
764 832 // No intent matched, handle live agent message
765 833 try {
766 834 $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
767 - //error_log("Message sent to agent.");
768 835
769 - wp_send_json_success([
836 + $agent_response = [
770 837 'status' => 'waiting_for_agent',
771 838 'message' => esc_html__('Message sent to live agent.', 'mxchat')
772 - ]);
839 + ];
840 +
841 + if ($testing_data !== null) {
842 + $agent_response['testing_data'] = $testing_data;
843 + }
844 +
845 + wp_send_json_success($agent_response);
773 846 } catch (\Exception $e) {
774 - //error_log("Error sending message to agent: " . $e->getMessage());
775 847 wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
776 848 }
777 849 wp_die();
778 850 }
@@ -777,160 +849,283 @@
777 849 wp_die();
778 850 }
779 851 }
780 852
781 - // Step 1: Check for new PDF URL in the message
782 - if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
783 - $new_pdf_url = $matches[0];
853 + // Step 1: Check for new PDF URL in the message
854 + if (!isset($_POST['vision_processed']) && preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
855 + $new_pdf_url = $matches[0];
784 856
785 - // Check if this is likely a PDF-related request
786 - $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
787 - $is_pdf_request = false;
857 + // Check if this is likely a PDF-related request
858 + $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
859 + $is_pdf_request = false;
788 860
789 - foreach ($pdf_keywords as $keyword) {
790 - if (stripos($message, $keyword) !== false) {
791 - $is_pdf_request = true;
792 - break;
793 - }
861 + foreach ($pdf_keywords as $keyword) {
862 + if (stripos($message, $keyword) !== false) {
863 + $is_pdf_request = true;
864 + break;
794 865 }
866 + }
795 867
796 - // If it looks like a PDF request or we're waiting for a PDF URL
797 - if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
798 - // Validate HTTPS
799 - if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
800 - // Extract filename from URL
801 - $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
868 + // If it looks like a PDF request or we're waiting for a PDF URL
869 + if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
870 + // Validate HTTPS
871 + if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
872 + // Extract filename from URL
873 + $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
802 874
803 - // Clear previous PDF transients
804 - $this->clear_pdf_transients($session_id);
875 + // Clear previous PDF transients
876 + $this->clear_pdf_transients($session_id);
805 877
806 - // Process new PDF
807 - $max_pages = $this->options['pdf_max_pages'] ?? 69;
808 - $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
878 + // Process new PDF
879 + $max_pages = $this->options['pdf_max_pages'] ?? 69;
880 + $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
809 881
810 - if ($embeddings === 'too_many_pages') {
811 - $error_text = sprintf(
812 - $this->options['pdf_intent_error_text'] ??
813 - esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
814 - $max_pages
815 - );
816 - $this->fallbackResponse['text'] = $error_text;
817 - } elseif ($embeddings) {
818 - // Store new PDF information
819 - // Create a more meaningful filename from URL
820 - $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
882 + if ($embeddings === 'too_many_pages') {
883 + $error_text = sprintf(
884 + $this->options['pdf_intent_error_text'] ??
885 + esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
886 + $max_pages
887 + );
888 + $this->fallbackResponse['text'] = $error_text;
889 + } elseif ($embeddings) {
890 + // Store new PDF information
891 + $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
821 892
822 - // If the filename is generic (like results_download.php), create a more descriptive one
823 - if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
824 - strpos($pdf_filename, '.php') !== false) {
825 - // Create a timestamp-based name
826 - $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
827 - }
893 + // If the filename is generic, create a more descriptive one
894 + if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
895 + strpos($pdf_filename, '.php') !== false) {
896 + $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
897 + }
828 898
829 - set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
830 - set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
831 - set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
832 - set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
899 + set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
900 + set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
901 + set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
902 + set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
833 903
834 - $success_text = $this->options['pdf_intent_success_text'] ??
835 - esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
904 + $success_text = $this->options['pdf_intent_success_text'] ??
905 + esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
836 906
837 - // Return success with filename for UI update
838 - wp_send_json([
839 - 'success' => true,
840 - 'message' => $success_text,
841 - 'data' => [
842 - 'filename' => $pdf_filename
843 - ]
844 - ]);
845 - wp_die();
846 - } else {
847 - $error_text = $this->options['pdf_intent_error_text'] ??
848 - esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
849 - $this->fallbackResponse['text'] = $error_text;
907 + $pdf_response = [
908 + 'success' => true,
909 + 'message' => $success_text,
910 + 'data' => [
911 + 'filename' => $pdf_filename
912 + ]
913 + ];
914 +
915 + if ($testing_data !== null) {
916 + $pdf_response['testing_data'] = $testing_data;
850 917 }
851 918
852 - wp_send_json([
853 - 'success' => false,
854 - 'message' => $this->fallbackResponse['text']
855 - ]);
919 + wp_send_json($pdf_response);
856 920 wp_die();
921 + } else {
922 + $error_text = $this->options['pdf_intent_error_text'] ??
923 + esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
924 + $this->fallbackResponse['text'] = $error_text;
857 925 }
926 +
927 + $pdf_error_response = [
928 + 'success' => false,
929 + 'message' => $this->fallbackResponse['text']
930 + ];
931 +
932 + if ($testing_data !== null) {
933 + $pdf_error_response['testing_data'] = $testing_data;
934 + }
935 +
936 + wp_send_json($pdf_error_response);
937 + wp_die();
858 938 }
859 939 }
940 + }
860 941
942 + // Check if there's an active recommendation flow session
943 + $flow_state = get_option("mxchat_sr_flow_state_{$session_id}", array());
944 + if (!empty($flow_state) && isset($flow_state['flow_id'])) {
945 + // Create a dummy intent object that matches the original intent
946 + $dummy_intent = new stdClass();
947 + $dummy_intent->intent_label = 'Recommendation Flow ' . $flow_state['flow_id'];
948 + $dummy_intent->phrases = ''; // Empty phrases to avoid matching the original trigger
949 +
950 + // Call the recommendation flow handler directly
951 + $response_data = apply_filters('mxchat_sr_recommendation_flow', false, $message, $user_id, $session_id, $dummy_intent);
952 +
953 + // If the handler returned a response, send it
954 + if (is_array($response_data) && (isset($response_data['text']) || isset($response_data['html']))) {
955 + // Save the bot's response to the chat history
956 + if (!empty($response_data['text'])) {
957 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['text']);
958 + }
959 + if (!empty($response_data['html'])) {
960 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['html']);
961 + }
962 +
963 + if ($testing_data !== null) {
964 + $response_data['testing_data'] = $testing_data;
965 + }
966 +
967 + // Send the response
968 + wp_send_json($response_data);
969 + wp_die();
970 + }
971 + }
972 +
861 973 // Step 2: Detect intent and handle intent-based responses
862 - $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
863 - //error_log("Intent Matched: " . ($intent_matched ? "Yes" : "No"));
974 + $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
864 975
865 - // Step 3: If intent is matched and handled, respond immediately
866 - if ($intent_matched && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
867 - //error_log("Intent response triggered.");
868 - $response_data = [
869 - 'text' => $this->fallbackResponse['text'],
870 - 'html' => $this->fallbackResponse['html'],
871 - 'session_id' => $session_id
872 - ];
873 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text'] . $this->fallbackResponse['html']);
874 - wp_send_json($response_data);
875 - wp_die();
976 + // NEW: Capture action analysis for testing panel after intent check
977 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
978 + $testing_data['action_matches'] = $this->last_action_analysis;
876 979 }
877 980
878 - // If no intent matched or product not found, proceed with AI response
879 - //error_log("No matching intent or fallback. Generating AI response.");
981 + // Step 3: Handle the intent result appropriately
982 + if ($intent_result !== false) {
983 + // Intent was matched - ALWAYS send as JSON response, never streaming
984 +
985 + if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
986 + // Intent returned a direct response array
987 + $response_data = [
988 + 'text' => $intent_result['text'] ?? '',
989 + 'html' => $intent_result['html'] ?? '',
990 + 'session_id' => $session_id
991 + ];
992 +
993 + if ($testing_data !== null) {
994 + $response_data['testing_data'] = $testing_data;
995 + }
996 +
997 + // Clear streaming headers if they were set
998 + if ($is_streaming) {
999 + header_remove('Content-Type');
1000 + header_remove('Cache-Control');
1001 + header_remove('Connection');
1002 + header_remove('X-Accel-Buffering');
1003 + header('Content-Type: application/json');
1004 + }
1005 +
1006 + wp_send_json($response_data);
1007 + wp_die();
1008 + }
1009 + else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1010 + // Intent returned true and set fallbackResponse
1011 + $response_data = [
1012 + 'text' => $this->fallbackResponse['text'] ?? '',
1013 + 'html' => $this->fallbackResponse['html'] ?? '',
1014 + 'session_id' => $session_id
1015 + ];
1016 +
1017 + if ($testing_data !== null) {
1018 + $response_data['testing_data'] = $testing_data;
1019 + }
1020 +
1021 + // Clear streaming headers if they were set
1022 + if ($is_streaming) {
1023 + header_remove('Content-Type');
1024 + header_remove('Cache-Control');
1025 + header_remove('Connection');
1026 + header_remove('X-Accel-Buffering');
1027 + header('Content-Type: application/json');
1028 + }
1029 +
1030 + wp_send_json($response_data);
1031 + wp_die();
1032 + }
1033 + }
880 1034
1035 + // If we get here, no intent matched OR the intent didn't provide a usable response
1036 +
881 1037 // Step 4: Generate AI response
882 1038 $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
883 1039 $this->mxchat_increment_chat_count();
884 -
1040 +
885 1041 // Generate embedding for the user's query
886 1042 $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
887 - if (!is_array($user_message_embedding)) {
888 - //error_log("Failed to generate message embedding for session $session_id");
889 - wp_send_json_error(esc_html__('Error processing your message.', 'mxchat'));
1043 +
1044 + // Check if the embedding generation returned an error
1045 + if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1046 + $error_message = $user_message_embedding['error'];
1047 + $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
1048 +
1049 + wp_send_json_error([
1050 + 'error_message' => $error_message,
1051 + 'error_code' => $error_code
1052 + ]);
890 1053 wp_die();
891 1054 }
1055 +
1056 + // Check if the embedding is valid
1057 + if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1058 + wp_send_json_error([
1059 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1060 + 'error_code' => 'invalid_embedding'
1061 + ]);
1062 + wp_die();
1063 + }
892 1064
893 1065 // Build context with both knowledge base and PDF content if available
894 1066 $context_content = "User asked: '{$message}'\n\n";
895 1067
896 - // Get relevant content from knowledge base
1068 + // NEW: Add page context if available and contextual awareness is enabled
1069 + if ($page_context && isset($this->options['contextual_awareness_toggle']) && $this->options['contextual_awareness_toggle'] === 'on') {
1070 + $context_content .= "===== CURRENT PAGE CONTEXT =====\n";
1071 + $context_content .= "Page URL: " . $page_context['url'] . "\n";
1072 + $context_content .= "Page Title: " . $page_context['title'] . "\n";
1073 + $context_content .= "Page Content: " . $page_context['content'] . "\n";
1074 + $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1075 + }
1076 +
1077 + // Get relevant content from knowledge base - THIS IS WHERE THE SIMILARITY ANALYSIS HAPPENS
897 1078 $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
1079 +
1080 + // ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1081 + if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1082 + // Update testing data with the REAL similarity analysis
1083 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1084 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1085 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1086 + }
1087 + // ===== END SIMILARITY DATA CAPTURE =====
1088 +
898 1089 if (!empty($relevant_content)) {
899 - $context_content .= "Relevant content from knowledge database:\n" . $relevant_content . "\n\n";
1090 + $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1091 + } else {
1092 + $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
900 1093 }
901 1094
902 -
903 - // Check for and include PDF content
904 - $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
905 - $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
906 - $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
907 - if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
908 - $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
909 - if (!empty($relevant_pdf_pages)) {
910 - $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
911 - foreach ($relevant_pdf_pages as $page_data) {
912 - $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
913 - }
914 - $context_content .= "\n";
1095 + // Check for and include PDF content
1096 + $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
1097 + $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
1098 + $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
1099 + if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
1100 + $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
1101 + if (!empty($relevant_pdf_pages)) {
1102 + $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
1103 + foreach ($relevant_pdf_pages as $page_data) {
1104 + $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
915 1105 }
1106 + $context_content .= "\n";
916 1107 }
1108 + }
917 1109
918 - // Check for and include Word content
919 - $word_url = get_transient('mxchat_word_url_' . $session_id);
920 - $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
921 - $word_filename = get_transient('mxchat_word_filename_' . $session_id);
922 - if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
923 - $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
924 - if (!empty($relevant_word_chunks)) {
925 - $context_content .= "Relevant content from Word document '{$word_filename}':\n";
926 - foreach ($relevant_word_chunks as $chunk_data) {
927 - $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
928 - }
929 - $context_content .= "\n";
1110 + // Check for and include Word content
1111 + $word_url = get_transient('mxchat_word_url_' . $session_id);
1112 + $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
1113 + $word_filename = get_transient('mxchat_word_filename_' . $session_id);
1114 + if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
1115 + $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
1116 + if (!empty($relevant_word_chunks)) {
1117 + $context_content .= "Relevant content from Word document '{$word_filename}':\n";
1118 + foreach ($relevant_word_chunks as $chunk_data) {
1119 + $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
930 1120 }
1121 + $context_content .= "\n";
931 1122 }
932 - // Generate the response using the full context
1123 + }
1124 +
1125 + $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
1126 +
1127 + // Generate response
933 1128 $response = $this->mxchat_generate_response(
934 1129 $context_content,
935 1130 $this->options['api_key'],
936 1131 $this->options['xai_api_key'],
@@ -935,11 +1130,34 @@
935 1130 $this->options['api_key'],
936 1131 $this->options['xai_api_key'],
937 1132 $this->options['claude_api_key'],
938 1133 $this->options['deepseek_api_key'],
939 - $conversation_history
1134 + $this->options['gemini_api_key'],
1135 + $conversation_history,
1136 + $is_streaming,
1137 + $session_id,
1138 + $testing_data
940 1139 );
941 -
1140 +
1141 + // Handle streaming vs non-streaming responses
1142 + if ($is_streaming) {
1143 + // Check if streaming actually happened or if it fell back to regular response
1144 + if ($response === true) {
1145 + wp_die();
1146 + }
1147 + // If we get here, streaming fell back to regular response, continue
1148 + }
1149 +
1150 + // Check if the response is an error array
1151 + if (is_array($response) && isset($response['error'])) {
1152 + wp_send_json_error([
1153 + 'error_message' => $response['error'],
1154 + 'error_code' => $response['error_code'] ?? 'api_error'
1155 + ]);
1156 + wp_die();
1157 + }
1158 +
1159 + // If we get here, the response is valid text
942 1160 $this->mxchat_save_chat_message($session_id, 'bot', $response);
943 1161
944 1162 // Step 5: Save additional content if available
945 1163 if (!empty($this->productCardHtml)) {
@@ -956,48 +1174,59 @@
956 1174 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
957 1175 'session_id' => $session_id
958 1176 ];
959 1177
1178 + // Always add testing data for admins (no toggle needed)
1179 + if ($testing_data !== null) {
1180 + $response_data['testing_data'] = $testing_data;
1181 + }
1182 +
960 1183 wp_send_json($response_data);
961 1184 wp_die();
962 1185 }
963 1186
964 -// New function to check intents and invoke the callback function
1187 +// Updated function to check intents and invoke the callback function
965 1188 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
966 1189 global $wpdb;
967 1190 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
968 1191
969 - //error_log('🔍 MXCHAT DEBUG: Intent Check Started ==================');
970 - //error_log("🔍 MXCHAT DEBUG: Message: '$message'");
971 - //error_log("🔍 MXCHAT DEBUG: Chat Mode: $chat_mode");
972 -
973 1192 // Generate the user embedding
974 - //error_log('🔄 MXCHAT DEBUG: Generating user embedding');
975 1193 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
976 - if (!is_array($user_embedding)) {
977 - //error_log('❌ MXCHAT DEBUG: Failed to generate user embedding');
978 - return false;
1194 +
1195 + // Check if embedding generation returned an error
1196 + if (is_array($user_embedding) && isset($user_embedding['error'])) {
1197 + $error_message = $user_embedding['error'];
1198 + $error_code = $user_embedding['error_code'] ?? 'embedding_error';
1199 +
1200 + wp_send_json_error([
1201 + 'error_message' => $error_message,
1202 + 'error_code' => $error_code
1203 + ]);
1204 + wp_die();
979 1205 }
980 - //error_log('✅ MXCHAT DEBUG: User embedding generated successfully');
981 1206
1207 + // Check if embedding is valid
1208 + if (!is_array($user_embedding) || empty($user_embedding)) {
1209 + wp_send_json_error([
1210 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1211 + 'error_code' => 'invalid_embedding'
1212 + ]);
1213 + wp_die();
1214 + }
1215 +
982 1216 // Fetch intents from the database
983 1217 $table_name = $wpdb->prefix . 'mxchat_intents';
984 1218 if ($chat_mode === 'agent') {
985 - //error_log('🔍 MXCHAT DEBUG: Agent mode - fetching only chatbot switch intent');
986 1219 $query = $wpdb->prepare(
987 - "SELECT * FROM $table_name WHERE callback_function = %s",
1220 + "SELECT * FROM $table_name WHERE callback_function = %s AND (enabled = 1 OR enabled IS NULL)",
988 1221 'mxchat_handle_switch_to_chatbot_intent'
989 1222 );
990 1223 $intents = $wpdb->get_results($query);
991 1224 } else {
992 - //error_log('🔍 MXCHAT DEBUG: AI mode - fetching all intents');
993 - $intents = $wpdb->get_results("SELECT * FROM $table_name");
1225 + $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
994 1226 }
995 1227
996 - //error_log('🔍 MXCHAT DEBUG: Found ' . count($intents) . ' intents to check');
997 -
998 1228 if (empty($intents)) {
999 - //error_log('❌ MXCHAT DEBUG: No intents found in database');
1000 1229 return false;
1001 1230 }
1002 1231
1003 1232 $highest_similarity = -INF;
@@ -1002,11 +1231,17 @@
1002 1231
1003 1232 $highest_similarity = -INF;
1004 1233 $matched_intent = null;
1005 1234
1006 - //error_log('📊 MXCHAT DEBUG: Intent Similarity Scores ==================');
1235 + // NEW: Array to store action analysis for testing panel
1236 + $action_analysis = [];
1237 +
1007 1238 foreach ($intents as $intent) {
1008 - //error_log("🔄 MXCHAT DEBUG: Checking intent: '{$intent->intent_label}' (callback: {$intent->callback_function})");
1239 + // Additional check for enabled state
1240 + $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
1241 + if (!$is_enabled) {
1242 + continue;
1243 + }
1009 1244
1010 1245 $intent_embedding_serialized = $intent->embedding_vector;
1011 1246 $intent_embedding = $intent_embedding_serialized
1012 1247 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
@@ -1012,9 +1247,8 @@
1012 1247 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
1013 1248 : null;
1014 1249
1015 1250 if (!is_array($intent_embedding)) {
1016 - //error_log("❌ MXCHAT DEBUG: Invalid embedding for intent: {$intent->intent_label}");
1017 1251 continue;
1018 1252 }
1019 1253
1020 1254 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
@@ -1019,34 +1253,56 @@
1019 1253
1020 1254 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
1021 1255 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
1022 1256
1257 + // NEW: Store action analysis data for testing panel
1258 + $action_analysis[] = [
1259 + 'intent_label' => $intent->intent_label,
1260 + 'callback_function' => $intent->callback_function,
1261 + 'similarity' => round($similarity, 4),
1262 + 'similarity_percentage' => round($similarity * 100, 2),
1263 + 'threshold' => $intent_threshold,
1264 + 'threshold_percentage' => round($intent_threshold * 100, 2),
1265 + 'above_threshold' => $similarity >= $intent_threshold,
1266 + 'triggered' => false // Will be updated below if this intent is triggered
1267 + ];
1023 1268
1024 1269 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
1025 1270 $highest_similarity = $similarity;
1026 1271 $matched_intent = $intent;
1027 - //error_log("✅ MXCHAT DEBUG: New best match: '{$intent->intent_label}' with similarity {$similarity}");
1028 1272 }
1029 1273 }
1030 - //error_log('📊 MXCHAT DEBUG: End Intent Scores ==================');
1031 1274
1275 + // NEW: Mark the triggered action if any
1032 1276 if ($matched_intent) {
1033 - //error_log("🎯 MXCHAT DEBUG: Final Intent Match: '{$matched_intent->intent_label}'");
1034 - //error_log("🔄 MXCHAT DEBUG: Invoking callback: {$matched_intent->callback_function}");
1035 -
1277 + foreach ($action_analysis as &$action) {
1278 + if ($action['intent_label'] === $matched_intent->intent_label) {
1279 + $action['triggered'] = true;
1280 + break;
1281 + }
1282 + }
1283 + }
1284 +
1285 + // NEW: Sort actions by similarity (highest first) and store for testing panel
1286 + usort($action_analysis, function($a, $b) {
1287 + return $b['similarity'] <=> $a['similarity'];
1288 + });
1289 +
1290 + // Store action analysis for testing panel capture
1291 + $this->last_action_analysis = $action_analysis;
1292 +
1293 + if ($matched_intent) {
1036 1294 // If the callback is a method on this instance (core callback), call it directly
1037 1295 if (method_exists($this, $matched_intent->callback_function)) {
1038 - //error_log('🔍 MXCHAT DEBUG: Using direct method call for core callback');
1039 1296 $callback_result = call_user_func(
1040 - [$this, $matched_intent->callback_function],
1041 - $message,
1042 - $user_id,
1043 - $session_id,
1044 - $matched_intent,
1045 - $user_context // Add user context
1046 - );
1297 + [$this, $matched_intent->callback_function],
1298 + $message,
1299 + $user_id,
1300 + $session_id,
1301 + $matched_intent,
1302 + $user_context ?? null
1303 + );
1047 1304 } else {
1048 - //error_log('🔍 MXCHAT DEBUG: Using apply_filters for add-on callback');
1049 1305 // Otherwise, use apply_filters for add-on callbacks
1050 1306 $callback_result = apply_filters(
1051 1307 $matched_intent->callback_function,
1052 1308 false, // default return value
@@ -1056,24 +1312,17 @@
1056 1312 $matched_intent
1057 1313 );
1058 1314 }
1059 1315
1060 - //error_log('🔍 MXCHAT DEBUG: Callback result type: ' . gettype($callback_result));
1061 1316 if ($callback_result !== false) {
1062 - //error_log('✅ MXCHAT DEBUG: Intent handled successfully');
1063 1317 $this->fallbackResponse = $callback_result;
1064 1318 return true;
1065 1319 }
1066 - //error_log('❌ MXCHAT DEBUG: Callback returned false');
1067 - } else {
1068 - //error_log('❌ MXCHAT DEBUG: No matching intent found');
1069 1320 }
1070 1321
1071 - //error_log('🔍 MXCHAT DEBUG: Intent Check Completed ==================');
1072 1322 return false;
1073 1323 }
1074 1324
1075 -
1076 1325 // Helper function to clear PDF and Word document related transients
1077 1326 private function clear_pdf_transients($session_id) {
1078 1327 // PDF transients
1079 1328 delete_transient('mxchat_pdf_url_' . $session_id);
@@ -1097,56 +1346,75 @@
1097 1346 //error_log("Triggered email capture intent for message: " . sanitize_text_field($message));
1098 1347
1099 1348 // Initiate email capture flow
1100 1349 $response = esc_html($this->options['triggered_phrase_response'] ?? esc_html__("Would you like to join our mailing list? Please provide your email below.", 'mxchat'));
1101 -
1102 1350 set_transient('mxchat_email_capture_' . $user_id, true, 5 * MINUTE_IN_SECONDS);
1103 1351 $this->mxchat_save_chat_message($session_id, 'bot', $response);
1104 -
1105 - // Respond to the user
1106 - wp_send_json(['message' => $response]);
1107 - wp_die();
1352 +
1353 + // FIXED: Return response data instead of sending JSON directly
1354 + // This allows the main chat handler to add testing data before sending
1355 + return [
1356 + 'text' => $response,
1357 + 'html' => '',
1358 + 'session_id' => $session_id
1359 + ];
1108 1360 }
1109 1361
1110 -//very good
1111 1362 public function mxchat_generate_image($message, $user_id, $session_id) {
1363 + //error_log("Starting image generation for message: " . $message);
1364 +
1112 1365 // Prepare a prompt for DALL-E
1113 1366 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
1114 -
1367 +
1115 1368 // Use the existing OpenAI API key
1116 1369 $openai_api_key = sanitize_text_field($this->options['api_key']);
1117 -
1370 +
1118 1371 // Call DALL-E to generate an image
1119 1372 $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1120 -
1373 +
1121 1374 // Check if the response contains an image URL
1122 1375 if (isset($image_response['imageUrl'])) {
1123 1376 $image_url = esc_url_raw($image_response['imageUrl']);
1124 -
1377 +
1125 1378 // Construct the HTML with a CSS class instead of inline styles
1126 1379 $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
1380 + $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1381 +
1382 + // Save the bot message with both text and HTML
1383 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1384 + $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
1385 +
1386 + // Set the fallback response for the chat handler
1387 + $this->fallbackResponse = [
1388 + 'text' => $response_text,
1389 + 'html' => $response_html,
1390 + 'images' => [$image_url]
1391 + ];
1392 +
1393 + // For debugging/verification - Use json_encode to verify what's being set
1394 + //error_log("Image generation successful - fallbackResponse set: " . json_encode($this->fallbackResponse));
1127 1395
1128 - $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1396 + // Return the response directly instead of relying on the property
1397 + return $this->fallbackResponse;
1129 1398 } else {
1130 1399 $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
1131 - $response_html = '';
1400 +
1401 + // Save the error message
1402 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1403 +
1404 + // Set the fallback response for the chat handler
1405 + $this->fallbackResponse = [
1406 + 'text' => $response_text,
1407 + 'html' => '',
1408 + 'images' => []
1409 + ];
1410 +
1132 1411 //error_log("DALL-E image generation error: " . esc_html($image_response['error'] ?? 'Unknown error.'));
1412 + //error_log("Error fallbackResponse set: " . json_encode($this->fallbackResponse));
1413 +
1414 + // Return the response directly instead of relying on the property
1415 + return $this->fallbackResponse;
1133 1416 }
1134 -
1135 - // Save both text and HTML responses
1136 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text . "\n" . $response_html);
1137 -
1138 - // Prepare the response data
1139 - $response_data = [
1140 - 'message' => $response_text,
1141 - 'html' => $response_html,
1142 - 'image_url' => $image_url ?? '',
1143 - ];
1144 -
1145 - // Send the JSON response
1146 - header('Content-Type: application/json; charset=' . get_option('blog_charset'));
1147 - echo json_encode($response_data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
1148 - wp_die();
1149 1417 }
1150 1418 private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
1151 1419 $api_url = 'https://api.openai.com/v1/images/generations';
1152 1420 $body = json_encode([
@@ -1185,44 +1453,43 @@
1185 1453
1186 1454 /**
1187 1455 * Handle web search requests.
1188 1456 *
1189 - * Sends the refined search query to the Brave Search API and displays neatly formatted,
1190 - * styled search results. Results are cached for performance.
1457 + * Sends the refined search query to the Brave Search API and uses the
1458 + * results to generate a conversational response with the AI model.
1191 1459 *
1192 1460 * @since 1.0.0
1193 1461 * @param string $message The user's search query.
1194 1462 * @param string $user_id The user identifier.
1195 1463 * @param string $session_id The current session ID.
1196 - * @return void
1464 + * @return array Response array containing text with embedded HTML links
1197 1465 */
1198 -public function mxchat_handle_search_request( $message, $user_id, $session_id ) {
1466 +public function mxchat_handle_search_request($message, $user_id, $session_id) {
1199 1467 // Step 1: Interpret and refine the search query
1200 - $refined_search_query = $this->mxchat_interpret_search_query( $message );
1201 -
1202 - if ( empty( $refined_search_query ) ) {
1203 - $this->fallbackResponse = array(
1204 - 'text' => esc_html__( 'I apologize, but could you please rephrase your search request?', 'mxchat' ),
1468 + $refined_search_query = $this->mxchat_interpret_search_query($message);
1469 + if (empty($refined_search_query)) {
1470 + return array(
1471 + 'text' => esc_html__('I apologize, but could you please rephrase your search request?', 'mxchat'),
1472 + 'html' => ''
1205 1473 );
1206 - return;
1207 1474 }
1208 -
1475 +
1209 1476 // Retrieve and validate API settings
1210 - $options = get_option( 'mxchat_options' );
1211 - $api_key = isset( $options['brave_api_key'] ) ? sanitize_text_field( $options['brave_api_key'] ) : '';
1212 - $results_count = isset( $options['brave_results_count'] ) ? absint( $options['brave_results_count'] ) : 5;
1213 -
1214 - if ( empty( $api_key ) ) {
1215 - $this->fallbackResponse = array(
1216 - 'text' => esc_html__( 'Search functionality is temporarily unavailable. Please try again later.', 'mxchat' ),
1477 + $options = get_option('mxchat_options');
1478 + $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1479 + $results_count = isset($options['brave_results_count']) ? absint($options['brave_results_count']) : 5;
1480 +
1481 + if (empty($api_key)) {
1482 + return array(
1483 + 'text' => esc_html__('Search functionality is temporarily unavailable. Please try again later.', 'mxchat'),
1484 + 'html' => ''
1217 1485 );
1218 - return;
1219 1486 }
1220 -
1487 +
1221 1488 // Build the API request URL
1222 1489 $api_url = add_query_arg(
1223 1490 array(
1224 - 'q' => rawurlencode( $refined_search_query ),
1491 + 'q' => rawurlencode($refined_search_query),
1225 1492 'count' => $results_count,
1226 1493 'text_decorations' => 'true',
1227 1494 'rich_data' => 'true',
1228 1495 ),
@@ -1227,14 +1494,14 @@
1227 1494 'rich_data' => 'true',
1228 1495 ),
1229 1496 'https://api.search.brave.com/res/v1/web/search'
1230 1497 );
1231 -
1498 +
1232 1499 // Attempt to retrieve cached results first
1233 - $transient_key = 'mxchat_search_' . md5( $refined_search_query );
1234 - $results = get_transient( $transient_key );
1235 -
1236 - if ( false === $results ) {
1500 + $transient_key = 'mxchat_search_' . md5($refined_search_query);
1501 + $results = get_transient($transient_key);
1502 +
1503 + if (false === $results) {
1237 1504 // Fetch new results from the Brave Search API
1238 1505 $response = wp_remote_get(
1239 1506 $api_url,
1240 1507 array(
@@ -1245,162 +1512,98 @@
1245 1512 ),
1246 1513 'timeout' => 10,
1247 1514 )
1248 1515 );
1249 -
1250 - if ( is_wp_error( $response ) ) {
1251 - $this->fallbackResponse = array(
1252 - 'text' => esc_html__( 'I encountered an error while searching. Please try again.', 'mxchat' ),
1516 +
1517 + if (is_wp_error($response)) {
1518 + return array(
1519 + 'text' => esc_html__('I encountered an error while searching. Please try again.', 'mxchat'),
1520 + 'html' => ''
1253 1521 );
1254 - return;
1255 1522 }
1256 -
1257 - $results = json_decode( wp_remote_retrieve_body( $response ), true );
1258 -
1259 - if ( json_last_error() !== JSON_ERROR_NONE ) {
1260 - $this->fallbackResponse = array(
1261 - 'text' => esc_html__( 'I received an invalid response from the search service.', 'mxchat' ),
1523 +
1524 + $results = json_decode(wp_remote_retrieve_body($response), true);
1525 +
1526 + if (json_last_error() !== JSON_ERROR_NONE) {
1527 + return array(
1528 + 'text' => esc_html__('I received an invalid response from the search service.', 'mxchat'),
1529 + 'html' => ''
1262 1530 );
1263 - return;
1264 1531 }
1265 -
1532 +
1266 1533 // Cache results for one hour
1267 - set_transient( $transient_key, $results, HOUR_IN_SECONDS );
1534 + set_transient($transient_key, $results, HOUR_IN_SECONDS);
1268 1535 }
1269 -
1270 - // Process and display results
1271 - if ( ! empty( $results['web']['results'] ) && is_array( $results['web']['results'] ) ) {
1272 - $html = $this->generate_search_results_html( $results['web']['results'], $refined_search_query );
1273 -
1274 - // Only return HTML (no large text summary)
1275 - $this->fallbackResponse = array(
1276 - 'html' => $html,
1536 +
1537 + // Process results
1538 + if (!empty($results['web']['results']) && is_array($results['web']['results'])) {
1539 + // Create a more straightforward summary with HTML links
1540 + $search_results_text = '';
1541 +
1542 + // Add a simple intro
1543 + $search_results_text .= sprintf(
1544 + esc_html__("Here's what I found about '%s':", 'mxchat'),
1545 + esc_html($refined_search_query)
1277 1546 );
1278 -
1547 +
1548 + // Add the top results with HTML links
1549 + foreach (array_slice($results['web']['results'], 0, 5) as $result) {
1550 + $title = isset($result['title']) ? wp_strip_all_tags($result['title']) : '';
1551 + $url = isset($result['url']) ? esc_url($result['url']) : '';
1552 + $description = isset($result['description']) ? wp_strip_all_tags($result['description']) : '';
1553 +
1554 + // Add a line break after the intro
1555 + $search_results_text .= '<br><br>';
1556 +
1557 + // Add title as a link
1558 + $search_results_text .= sprintf(
1559 + '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a><br>',
1560 + $url,
1561 + $title
1562 + );
1563 +
1564 + // Add a condensed description
1565 + $search_results_text .= sprintf("%s", $description);
1566 + }
1567 +
1279 1568 // Save to chat history
1280 - $this->mxchat_save_chat_message( $session_id, 'bot', $html );
1569 + $this->mxchat_save_chat_message($session_id, 'bot', $search_results_text);
1570 +
1571 + // Return the formatted text with embedded HTML links
1572 + return array(
1573 + 'text' => $search_results_text,
1574 + 'html' => ''
1575 + );
1281 1576 } else {
1282 - $this->fallbackResponse = array(
1577 + return array(
1283 1578 'text' => sprintf(
1284 - esc_html__( 'I couldn\'t find any relevant results for "%s". Would you like to try different search terms?', 'mxchat' ),
1285 - esc_html( $refined_search_query )
1579 + esc_html__('I searched for "%s" but couldn\'t find any relevant results. Would you like to try different search terms?', 'mxchat'),
1580 + esc_html($refined_search_query)
1286 1581 ),
1582 + 'html' => ''
1287 1583 );
1288 1584 }
1289 1585 }
1290 1586
1291 -
1587 +//very good
1292 1588 /**
1293 - * Format search results into a natural text summary.
1589 + * Handle image search requests from the chatbot
1294 1590 *
1295 - * @since 1.0.0
1296 - * @param array $results The search results from the API.
1297 - * @param string $query The original search query.
1298 - * @return string The text summary of the top results.
1591 + * @param string $message The user's search query
1592 + * @param int $user_id The user's ID
1593 + * @param string $session_id The chat session ID
1594 + * @return array Response array with text and HTML content
1299 1595 */
1300 -private function format_search_results( $results, $query ) {
1301 - $summary = sprintf(
1302 - esc_html__( 'Here are the most relevant results for "%s":', 'mxchat' ),
1303 - esc_html( $query )
1304 - ) . "\n\n";
1305 -
1306 - $max_results = min( count( $results ), 3 );
1307 - for ( $i = 0; $i < $max_results; $i++ ) {
1308 - $result = $results[ $i ];
1309 - $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1310 - $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1311 -
1312 - // Append title and description to the summary
1313 - $summary .= sprintf(
1314 - "%s\n%s\n\n",
1315 - esc_html( $title ),
1316 - esc_html( $description )
1317 - );
1318 - }
1319 -
1320 - return $summary;
1321 -}
1322 -
1323 -/**
1324 - * Generate HTML markup for search results.
1325 - *
1326 - * @since 1.0.0
1327 - * @param array $results The search results from the API.
1328 - * @param string $query The user-refined query.
1329 - * @return string The HTML markup for displaying the results.
1330 - */
1331 -private function generate_search_results_html( $results, $query ) {
1332 - ob_start();
1333 - ?>
1334 - <div class="mxchat-search-results">
1335 - <?php foreach ( $results as $result ) :
1336 - $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1337 - $url = isset( $result['url'] ) ? esc_url( $result['url'] ) : '#';
1338 - $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1339 - $favicon = isset( $result['meta_url']['favicon'] ) ? esc_url( $result['meta_url']['favicon'] ) : '';
1340 - $thumbnail = isset( $result['thumbnail']['src'] ) ? esc_url( $result['thumbnail']['src'] ) : '';
1341 - $domain = parse_url( $url, PHP_URL_HOST );
1342 - ?>
1343 - <div class="mxchat-search-item">
1344 - <div class="mxchat-search-header">
1345 - <?php if ( $favicon ) : ?>
1346 - <img
1347 - src="<?php echo esc_url( $favicon ); ?>"
1348 - class="mxchat-site-icon"
1349 - alt="<?php echo esc_attr__( 'Site icon', 'mxchat' ); ?>"
1350 - width="16"
1351 - height="16"
1352 - />
1353 - <?php endif; ?>
1354 - <div class="mxchat-site-url"><?php echo esc_html( $domain ); ?></div>
1355 - </div>
1356 -
1357 - <div class="mxchat-search-content">
1358 - <h3 class="mxchat-search-title">
1359 - <a href="<?php echo esc_url( $url ); ?>"
1360 - target="_blank"
1361 - rel="noopener noreferrer"
1362 - >
1363 - <?php echo esc_html( $title ); ?>
1364 - </a>
1365 - </h3>
1366 -
1367 - <?php if ( $thumbnail ) : ?>
1368 - <div class="mxchat-search-thumbnail">
1369 - <img
1370 - src="<?php echo esc_url( $thumbnail ); ?>"
1371 - alt="<?php echo esc_attr__( 'Thumbnail image', 'mxchat' ); ?>"
1372 - loading="lazy"
1373 - />
1374 - </div>
1375 - <?php endif; ?>
1376 -
1377 - <div class="mxchat-search-description">
1378 - <?php echo esc_html( $description ); ?>
1379 - </div>
1380 - </div>
1381 - </div>
1382 - <?php endforeach; ?>
1383 - </div>
1384 - <?php
1385 - return ob_get_clean();
1386 -}
1387 -
1388 -
1389 -//very good
1390 1596 public function mxchat_handle_image_search_request($message, $user_id, $session_id) {
1391 -
1392 - // Step 1: Interpret the search query for better results
1597 + // Step 1: Interpret the search query using the user's selected AI model
1393 1598 $refined_search_query = $this->mxchat_interpret_search_query($message);
1394 1599
1395 -
1396 1600 // If no query was interpreted, return a fallback message
1397 1601 if (empty($refined_search_query)) {
1398 - $this->fallbackResponse = [
1602 + return array(
1399 1603 'text' => __("I'm sorry, I couldn't interpret your search query. Please specify what you'd like to see images of.", 'mxchat'),
1400 1604 'html' => "",
1401 - ];
1402 - return;
1605 + );
1403 1606 }
1404 1607
1405 1608 // Brave API URL
1406 1609 $api_url = 'https://api.search.brave.com/res/v1/images/search';
@@ -1409,19 +1612,12 @@
1409 1612 $options = get_option('mxchat_options');
1410 1613 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1411 1614
1412 1615 if (empty($api_key)) {
1413 -/*
1414 - if (defined('WP_DEBUG') && WP_DEBUG) {
1415 - error_log("Brave API key is missing.");
1416 - }
1417 -*/
1418 -
1419 - $this->fallbackResponse = [
1616 + return array(
1420 1617 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'),
1421 1618 'html' => "",
1422 - ];
1423 - return;
1619 + );
1424 1620 }
1425 1621
1426 1622 $image_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1427 1623 $safe_search = isset($options['brave_safe_search']) ? sanitize_text_field($options['brave_safe_search']) : 'strict';
@@ -1432,16 +1628,8 @@
1432 1628 'count' => $image_count,
1433 1629 'safesearch' => $safe_search,
1434 1630 ], $api_url);
1435 1631
1436 -/*
1437 - // Log the final API URL for the search
1438 - if (defined('WP_DEBUG') && WP_DEBUG) {
1439 - error_log("Final API URL for Brave Image Search: " . esc_url_raw($api_url));
1440 - }
1441 -*/
1442 -
1443 -
1444 1632 // Implement caching
1445 1633 $transient_key = 'mxchat_image_search_' . md5($refined_search_query);
1446 1634 $body = get_transient($transient_key);
1447 1635
@@ -1457,19 +1645,12 @@
1457 1645
1458 1646 $response = wp_remote_get($api_url, $args);
1459 1647
1460 1648 if (is_wp_error($response)) {
1461 -/*
1462 - if (defined('WP_DEBUG') && WP_DEBUG) {
1463 - error_log("Brave Image API request failed: " . $response->get_error_message());
1464 - }
1465 -*/
1466 -
1467 - $this->fallbackResponse = [
1649 + return array(
1468 1650 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1469 1651 'html' => "",
1470 - ];
1471 - return;
1652 + );
1472 1653 }
1473 1654
1474 1655 $body = json_decode(wp_remote_retrieve_body($response), true);
1475 1656 set_transient($transient_key, $body, HOUR_IN_SECONDS);
@@ -1477,10 +1658,16 @@
1477 1658
1478 1659 // Process the API response
1479 1660 if (isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) {
1480 1661 $html_output = '<div class="mxchat-image-gallery">';
1481 -
1482 - foreach ($body['results'] as $image) {
1662 +
1663 + // Get the configured image count (1-6)
1664 + $display_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1665 + $display_count = min($display_count, count($body['results'])); // Make sure we don't exceed available images
1666 +
1667 + // Use only the requested number of images
1668 + for ($i = 0; $i < $display_count; $i++) {
1669 + $image = $body['results'][$i];
1483 1670 $image_url = isset($image['url']) ? esc_url($image['url']) : '';
1484 1671 $thumbnail_url = isset($image['thumbnail']['src']) ? esc_url($image['thumbnail']['src']) : '';
1485 1672 $title = isset($image['title']) ? esc_html($image['title']) : esc_html__('Image', 'mxchat');
1486 1673
@@ -1494,47 +1681,95 @@
1494 1681 }
1495 1682
1496 1683 $html_output .= '</div>';
1497 1684
1498 - $this->fallbackResponse = [
1499 - 'text' => "",
1500 - 'html' => $html_output,
1501 - ];
1502 -
1503 - // Save response in chat history
1685 + // Create response text
1686 + $response_text = sprintf(__("Here are some images of %s:", 'mxchat'), $refined_search_query);
1687 +
1688 + // Save both response text and HTML to chat history
1689 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1504 1690 $this->mxchat_save_chat_message($session_id, 'bot', $html_output);
1505 1691
1692 + // Return the combined response
1693 + return array(
1694 + 'text' => $response_text,
1695 + 'html' => $html_output,
1696 + );
1506 1697 } else {
1507 -/*
1508 - if (defined('WP_DEBUG') && WP_DEBUG) {
1509 - error_log("Brave Image API response did not contain expected data structure or was empty: " . print_r($body, true));
1510 - }
1511 -*/
1512 -
1513 - $this->fallbackResponse = [
1514 - 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1698 + $response_text = __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat');
1699 +
1700 + // Save the error message to chat history
1701 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1702 +
1703 + return array(
1704 + 'text' => $response_text,
1515 1705 'html' => "",
1516 - ];
1706 + );
1517 1707 }
1518 1708 }
1709 +
1710 +/**
1711 + * Interpret the search query using the user's selected AI model
1712 + *
1713 + * @param string $user_query The original query from the user
1714 + * @return string The refined search query
1715 + */
1519 1716 public function mxchat_interpret_search_query($user_query) {
1520 1717 $system_prompt = esc_html__("Interpret the user's request to provide only the essential keywords or phrases for image searching. Remove conversational language, politeness, or extra context. Return a concise search query that doesn't lose any of the original meaning.", 'mxchat');
1521 -
1522 - // Retrieve OpenAI API key using 'api_key' as the option key
1523 - $api_key = isset($this->options['api_key']) ? sanitize_text_field($this->options['api_key']) : sanitize_text_field(get_option('mxchat_options')['api_key']);
1524 -
1525 - /*
1526 - // Log the API key check, without exposing the key
1527 - if (defined('WP_DEBUG') && WP_DEBUG) {
1528 - error_log("Retrieved OpenAI API Key: " . ($api_key ? "Present" : "Missing"));
1718 +
1719 + // Get options and determine the selected model
1720 + $options = $this->options ?? get_option('mxchat_options');
1721 + $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
1722 +
1723 + // Extract model prefix to determine the provider
1724 + $model_parts = explode('-', $selected_model);
1725 + $provider = strtolower($model_parts[0]);
1726 +
1727 + // Determine which API key to use based on the provider
1728 + switch ($provider) {
1729 + case 'gemini':
1730 + $api_key = isset($options['gemini_api_key']) ? sanitize_text_field($options['gemini_api_key']) : '';
1731 + if (empty($api_key)) {
1732 + return sanitize_text_field($user_query); // Default to original query if API key missing
1733 + }
1734 + return $this->interpret_query_with_gemini($user_query, $system_prompt, $api_key, $selected_model);
1735 +
1736 + case 'claude':
1737 + $api_key = isset($options['claude_api_key']) ? sanitize_text_field($options['claude_api_key']) : '';
1738 + if (empty($api_key)) {
1739 + return sanitize_text_field($user_query);
1740 + }
1741 + return $this->interpret_query_with_claude($user_query, $system_prompt, $api_key, $selected_model);
1742 +
1743 + case 'grok':
1744 + $api_key = isset($options['xai_api_key']) ? sanitize_text_field($options['xai_api_key']) : '';
1745 + if (empty($api_key)) {
1746 + return sanitize_text_field($user_query);
1747 + }
1748 + return $this->interpret_query_with_xai($user_query, $system_prompt, $api_key, $selected_model);
1749 +
1750 + case 'deepseek':
1751 + $api_key = isset($options['deepseek_api_key']) ? sanitize_text_field($options['deepseek_api_key']) : '';
1752 + if (empty($api_key)) {
1753 + return sanitize_text_field($user_query);
1754 + }
1755 + return $this->interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $selected_model);
1756 +
1757 + case 'gpt':
1758 + default:
1759 + // Default to OpenAI for custom models or unrecognized prefixes
1760 + $api_key = isset($options['api_key']) ? sanitize_text_field($options['api_key']) : '';
1761 + if (empty($api_key)) {
1762 + return sanitize_text_field($user_query);
1763 + }
1764 + return $this->interpret_query_with_openai($user_query, $system_prompt, $api_key, $selected_model);
1529 1765 }
1530 - */
1766 +}
1531 1767
1532 - if (empty($api_key)) {
1533 - //error_log("OpenAI API key is missing.");
1534 - return sanitize_text_field($user_query); // Default to the original query if API key is missing
1535 - }
1536 -
1768 +/**
1769 + * Interpret query using OpenAI models
1770 + */
1771 +private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-4o') {
1537 1772 $url = 'https://api.openai.com/v1/chat/completions';
1538 1773 $args = [
1539 1774 'headers' => [
1540 1775 'Authorization' => 'Bearer ' . $api_key,
@@ -1540,9 +1775,9 @@
1540 1775 'Authorization' => 'Bearer ' . $api_key,
1541 1776 'Content-Type' => 'application/json',
1542 1777 ],
1543 1778 'body' => wp_json_encode([
1544 - 'model' => 'gpt-3.5-turbo',
1779 + 'model' => $model,
1545 1780 'messages' => [
1546 1781 ['role' => 'system', 'content' => $system_prompt],
1547 1782 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1548 1783 ],
@@ -1549,166 +1784,178 @@
1549 1784 'temperature' => 0.2,
1550 1785 'max_tokens' => 20,
1551 1786 ]),
1552 1787 'method' => 'POST',
1788 + 'timeout' => 15,
1553 1789 ];
1554 1790
1555 1791 $response = wp_remote_post($url, $args);
1556 -
1557 1792 if (is_wp_error($response)) {
1558 - //error_log("OpenAI request failed: " . $response->get_error_message());
1559 - return sanitize_text_field($user_query); // Fallback to the original query if there's an error
1793 + return sanitize_text_field($user_query);
1560 1794 }
1561 1795
1562 1796 $body = json_decode(wp_remote_retrieve_body($response), true);
1797 + return isset($body['choices'][0]['message']['content'])
1798 + ? sanitize_text_field(trim($body['choices'][0]['message']['content']))
1799 + : sanitize_text_field($user_query);
1800 +}
1563 1801
1564 - // Check for a valid response and sanitize output
1565 - if (isset($body['choices'][0]['message']['content'])) {
1566 - $interpreted_query = sanitize_text_field(trim($body['choices'][0]['message']['content']));
1802 +/**
1803 + * Interpret query using Claude models
1804 + */
1805 +private function interpret_query_with_claude($user_query, $system_prompt, $api_key, $model) {
1806 + $url = 'https://api.anthropic.com/v1/messages';
1807 +
1808 + $args = [
1809 + 'headers' => [
1810 + 'Content-Type' => 'application/json',
1811 + 'x-api-key' => $api_key,
1812 + 'anthropic-version' => '2023-06-01',
1813 + ],
1814 + 'body' => wp_json_encode([
1815 + 'model' => $model,
1816 + 'system' => $system_prompt,
1817 + 'messages' => [
1818 + ['role' => 'user', 'content' => sanitize_text_field($user_query)]
1819 + ],
1820 + 'max_tokens' => 20,
1821 + 'temperature' => 0.2,
1822 + ]),
1823 + 'method' => 'POST',
1824 + 'timeout' => 15,
1825 + ];
1567 1826
1568 - /*
1569 - // Log the interpreted query for debugging
1570 - if (defined('WP_DEBUG') && WP_DEBUG) {
1571 - error_log("Interpreted search query: " . $interpreted_query);
1572 - }
1573 - */
1827 + $response = wp_remote_post($url, $args);
1828 + if (is_wp_error($response)) {
1829 + return sanitize_text_field($user_query);
1830 + }
1574 1831
1575 - return $interpreted_query;
1576 - } else {
1577 - //error_log("Unexpected API response format: " . print_r($body, true));
1578 - return sanitize_text_field($user_query);
1832 + $body = json_decode(wp_remote_retrieve_body($response), true);
1833 + if (!empty($body['content'][0]['text'])) {
1834 + return sanitize_text_field(trim($body['content'][0]['text']));
1579 1835 }
1836 +
1837 + return sanitize_text_field($user_query);
1580 1838 }
1581 1839
1582 -
1583 -
1584 -private function find_product_in_message($message) {
1585 - global $wpdb;
1586 -
1587 - // Get embedding for the search query
1588 - $query_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1589 - if (!is_array($query_embedding)) {
1590 - return null;
1840 +/**
1841 + * Interpret query using Gemini models
1842 + */
1843 +private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
1844 + // Strip "gemini-" prefix for the API
1845 + $model_version = str_replace('gemini-', '', $model);
1846 +
1847 + $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
1848 +
1849 + $args = [
1850 + 'headers' => [
1851 + 'Content-Type' => 'application/json',
1852 + ],
1853 + 'body' => wp_json_encode([
1854 + 'contents' => [
1855 + [
1856 + 'role' => 'user',
1857 + 'parts' => [
1858 + ['text' => $system_prompt . "\n\nQuery: " . sanitize_text_field($user_query)]
1859 + ]
1860 + ]
1861 + ],
1862 + 'generationConfig' => [
1863 + 'temperature' => 0.2,
1864 + 'maxOutputTokens' => 20,
1865 + ],
1866 + ]),
1867 + 'method' => 'POST',
1868 + 'timeout' => 15,
1869 + ];
1870 +
1871 + $response = wp_remote_post($url, $args);
1872 + if (is_wp_error($response)) {
1873 + return sanitize_text_field($user_query);
1591 1874 }
1592 -
1593 - // Get relevant content as string
1594 - $relevant_content = $this->mxchat_find_relevant_products($query_embedding);
1595 - if (empty($relevant_content)) {
1596 - // Return null to indicate no results and set fallback response
1597 - $this->fallbackResponse['text'] = esc_html__("I couldn't find any relevant products based on your query. Could you please be more specific about the product you're looking for?", 'mxchat');
1598 - return null;
1875 +
1876 + $body = json_decode(wp_remote_retrieve_body($response), true);
1877 + if (!empty($body['candidates'][0]['content']['parts'][0]['text'])) {
1878 + return sanitize_text_field(trim($body['candidates'][0]['content']['parts'][0]['text']));
1599 1879 }
1880 +
1881 + return sanitize_text_field($user_query);
1882 +}
1600 1883
1601 - // Extract product URLs from the content
1602 - preg_match_all('/https?:\/\/[^\s<>"\']+?\/product\/[^\s<>"\']+/', $relevant_content, $matches);
1603 -
1604 - if (!empty($matches[0])) {
1605 - // Try each URL found
1606 - foreach ($matches[0] as $url) {
1607 - // Clean the URL
1608 - $url = rtrim($url, '/."\']');
1609 -
1610 - // Get the product slug
1611 - $path = parse_url($url, PHP_URL_PATH);
1612 - $slug = basename(rtrim($path, '/'));
1613 -
1614 - // Find product by slug
1615 - $args = array(
1616 - 'post_type' => 'product',
1617 - 'post_status' => 'publish',
1618 - 'name' => $slug,
1619 - 'posts_per_page' => 1
1620 - );
1621 -
1622 - $products = get_posts($args);
1623 -
1624 - if (!empty($products)) {
1625 - $product_id = $products[0]->ID;
1626 - $product = wc_get_product($product_id);
1627 -
1628 - if ($product && $product->is_purchasable()) {
1629 - return $product_id;
1630 - }
1631 - }
1632 - }
1884 +/**
1885 + * Interpret query using X.AI (Grok) models
1886 + */
1887 +private function interpret_query_with_xai($user_query, $system_prompt, $api_key, $model) {
1888 + $url = 'https://api.xai.com/v1/chat/completions';
1889 +
1890 + $args = [
1891 + 'headers' => [
1892 + 'Content-Type' => 'application/json',
1893 + 'Authorization' => 'Bearer ' . $api_key,
1894 + ],
1895 + 'body' => wp_json_encode([
1896 + 'model' => $model,
1897 + 'messages' => [
1898 + ['role' => 'system', 'content' => $system_prompt],
1899 + ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1900 + ],
1901 + 'temperature' => 0.2,
1902 + 'max_tokens' => 20,
1903 + ]),
1904 + 'method' => 'POST',
1905 + 'timeout' => 15,
1906 + ];
1907 +
1908 + $response = wp_remote_post($url, $args);
1909 + if (is_wp_error($response)) {
1910 + return sanitize_text_field($user_query);
1633 1911 }
1634 -
1635 - // Fallback: Look for product names in the content
1636 - $products = wc_get_products([
1637 - 'status' => 'publish',
1638 - 'limit' => -1,
1639 - 'return' => 'all'
1640 - ]);
1641 -
1642 - foreach ($products as $product) {
1643 - $name = $product->get_name();
1644 - if (stripos($relevant_content, $name) !== false) {
1645 - if ($product->is_purchasable()) {
1646 - return $product->get_id();
1647 - }
1648 - }
1912 +
1913 + $body = json_decode(wp_remote_retrieve_body($response), true);
1914 + if (isset($body['choices'][0]['message']['content'])) {
1915 + return sanitize_text_field(trim($body['choices'][0]['message']['content']));
1649 1916 }
1650 -
1651 - // If no product is found after all checks, set the fallback response
1652 - $this->fallbackResponse['text'] = esc_html__("I couldn't find any relevant products based on your query. Try to be more specific", 'mxchat');
1653 - return null;
1917 +
1918 + return sanitize_text_field($user_query);
1654 1919 }
1655 1920
1656 -// New method to handle intent responses
1657 -private function generate_intent_response($context_content, $session_id) {
1658 - // Convert the context array to a structured string for the AI
1659 - $context_string = $this->format_intent_context($context_content);
1660 -
1661 - // Generate AI response using the context
1662 - $response = $this->mxchat_generate_response(
1663 - $context_string,
1664 - $this->options['api_key'],
1665 - $this->options['xai_api_key'],
1666 - $this->options['claude_api_key'],
1667 - $this->options['deepseek_api_key'],
1668 - $this->mxchat_fetch_conversation_history_for_ai($session_id)
1669 - );
1670 -
1671 - $this->fallbackResponse['text'] = $response;
1672 - return true;
1673 -}
1674 -// Helper method to format intent context
1675 -private function format_intent_context($context) {
1676 - $context_string = esc_html__("INTENT CONTEXT:\n", 'mxchat');
1677 -
1678 - switch ($context['intent']) {
1679 - case 'add_to_cart':
1680 - if ($context['status'] === 'success') {
1681 - $context_string .= esc_html__("Action: Successfully added product to cart\n", 'mxchat');
1682 - $context_string .= sprintf(esc_html__("Product: %s\n", 'mxchat'), $context['product']['name']);
1683 - $context_string .= esc_html__("Available actions: ", 'mxchat') . implode(', ', $context['available_actions']) . "\n";
1684 - $context_string .= sprintf(esc_html__("Cart URL: %s\n", 'mxchat'), $context['cart_url']);
1685 - $context_string .= esc_html__("\nPlease inform the user of the successful addition and their available options.", 'mxchat');
1686 - } else {
1687 - $context_string .= esc_html__("Action: Failed to add product to cart\n", 'mxchat');
1688 - $context_string .= sprintf(esc_html__("Reason: %s\n", 'mxchat'), $context['reason']);
1689 - switch ($context['reason']) {
1690 - case 'woocommerce_not_available':
1691 - $context_string .= esc_html__("\nPlease inform the user that shopping features are not available.", 'mxchat');
1692 - break;
1693 - case 'no_product_context':
1694 - $context_string .= esc_html__("\nPlease ask the user to specify which product they want to add.", 'mxchat');
1695 - break;
1696 - case 'product_not_found':
1697 - $context_string .= esc_html__("\nPlease inform the user that the product couldn't be found and ask them to try again.", 'mxchat');
1698 - break;
1699 - case 'add_to_cart_failed':
1700 - $context_string .= esc_html__("\nPlease apologize to the user and suggest they try again or ask for assistance.", 'mxchat');
1701 - break;
1702 - }
1703 - }
1704 - break;
1921 +/**
1922 + * Interpret query using DeepSeek models
1923 + */
1924 +private function interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $model) {
1925 + $url = 'https://api.deepseek.com/v1/chat/completions';
1926 +
1927 + $args = [
1928 + 'headers' => [
1929 + 'Content-Type' => 'application/json',
1930 + 'Authorization' => 'Bearer ' . $api_key,
1931 + ],
1932 + 'body' => wp_json_encode([
1933 + 'model' => $model,
1934 + 'messages' => [
1935 + ['role' => 'system', 'content' => $system_prompt],
1936 + ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1937 + ],
1938 + 'temperature' => 0.2,
1939 + 'max_tokens' => 20,
1940 + ]),
1941 + 'method' => 'POST',
1942 + 'timeout' => 15,
1943 + ];
1944 +
1945 + $response = wp_remote_post($url, $args);
1946 + if (is_wp_error($response)) {
1947 + return sanitize_text_field($user_query);
1705 1948 }
1706 -
1707 - return $context_string;
1949 +
1950 + $body = json_decode(wp_remote_retrieve_body($response), true);
1951 + if (isset($body['choices'][0]['message']['content'])) {
1952 + return sanitize_text_field(trim($body['choices'][0]['message']['content']));
1953 + }
1954 +
1955 + return sanitize_text_field($user_query);
1708 1956 }
1709 1957
1710 -
1711 1958 //very good
1712 1959 private function add_email_to_loops($email) {
1713 1960 // Sanitize the email
1714 1961 $email = sanitize_email($email);
@@ -1792,95 +2039,169 @@
1792 2039
1793 2040 // Default to proceeding with conversation if no specific PDF action is needed
1794 2041 $this->fallbackResponse['text'] = '';
1795 2042 }
2043 +
2044 +
2045 +/**
2046 + * Enhanced fetch_and_split_pdf_pages with detailed debugging
2047 + */
1796 2048 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
2049 + // CLEAR DEBUG LOGGING
2050 + error_log("=== MXCHAT PDF PROCESSING START ===");
2051 + error_log("PDF Source: " . $pdf_source);
2052 + error_log("Max Pages: " . $max_pages);
2053 + error_log("Session ID: " . ($this->session_id ?? 'not set'));
2054 +
2055 + // Check if Advanced Claude Toolbar is available and enabled
2056 + $claude_available = function_exists('mxchatACT_is_advanced_claude_enabled');
2057 + $claude_enabled = $claude_available ? mxchatACT_is_advanced_claude_enabled() : false;
2058 +
2059 + error_log("Claude Function Available: " . ($claude_available ? 'YES' : 'NO'));
2060 + error_log("Claude Enabled: " . ($claude_enabled ? 'YES' : 'NO'));
2061 +
2062 + if ($claude_available && $claude_enabled) {
2063 + error_log("🚀 ATTEMPTING CLAUDE PROCESSING...");
2064 +
2065 + // Attempt Claude processing first
2066 + $claude_result = apply_filters('mxchat_process_pdf_advanced', false, $pdf_source, $max_pages, $this->session_id);
2067 +
2068 + if ($claude_result !== false && is_array($claude_result) && !empty($claude_result)) {
2069 + error_log("✅ CLAUDE PROCESSING SUCCESSFUL!");
2070 + error_log("Claude returned " . count($claude_result) . " processed pages");
2071 +
2072 + // Log first page details for verification
2073 + if (isset($claude_result[0])) {
2074 + $first_page = $claude_result[0];
2075 + error_log("First page enhanced: " . (isset($first_page['enhanced']) && $first_page['enhanced'] ? 'YES' : 'NO'));
2076 + error_log("Processing method: " . ($first_page['processing_method'] ?? 'not set'));
2077 + error_log("First page text preview: " . substr($first_page['text'] ?? '', 0, 100) . "...");
2078 + }
2079 +
2080 + error_log("=== MXCHAT PDF PROCESSING END (CLAUDE) ===");
2081 + return $claude_result;
2082 + } else {
2083 + error_log("❌ CLAUDE PROCESSING FAILED or returned invalid result");
2084 + error_log("Claude result type: " . gettype($claude_result));
2085 + if (is_array($claude_result)) {
2086 + error_log("Claude result count: " . count($claude_result));
2087 + }
2088 + }
2089 + }
2090 +
2091 + // Fallback to basic processing
2092 + error_log("🔄 FALLING BACK TO BASIC PDF PROCESSING...");
2093 +
1797 2094 $upload_dir = wp_upload_dir();
1798 2095 $temp_file = null;
1799 -
2096 +
1800 2097 try {
1801 - // Handle URL vs local file
2098 + // Your existing basic processing code here...
2099 + // (I'll include the key parts with debug logging)
2100 +
1802 2101 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
1803 - // Validate and download the file from URL
1804 - $temp_file = wp_tempnam($pdf_source); // Safe temporary file name
1805 - $response = wp_remote_get($pdf_source, ['timeout' => 60]);
1806 -
2102 + error_log("Downloading PDF from URL...");
2103 + $temp_file = wp_tempnam($pdf_source);
2104 + $response = wp_remote_get($pdf_source, [
2105 + 'timeout' => 60,
2106 + 'headers' => ['User-Agent' => 'MxChat PDF Processor']
2107 + ]);
2108 +
1807 2109 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1808 - //error_log(esc_html__("Failed to download PDF. Error: ", 'mxchat') . print_r($response, true));
2110 + $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code($response);
2111 + error_log("❌ BASIC PROCESSING: Failed to download PDF: " . $error_message);
1809 2112 return false;
1810 2113 }
1811 -
2114 +
1812 2115 file_put_contents($temp_file, wp_remote_retrieve_body($response));
1813 -
1814 - // Validate that the downloaded file is a PDF
1815 - $mime_type = mime_content_type($temp_file);
1816 - if ($mime_type !== 'application/pdf') {
1817 - //error_log(esc_html__("Invalid MIME type detected for PDF: ", 'mxchat') . $mime_type);
1818 - unlink($temp_file);
1819 - return false;
1820 - }
2116 + error_log("✅ PDF downloaded successfully");
1821 2117 } else {
1822 - // For local files, use the provided path directly
1823 2118 $temp_file = $pdf_source;
2119 + error_log("Using local PDF file: " . $temp_file);
1824 2120 }
1825 -
1826 - // Parse and process the PDF
2121 +
2122 + // Parse PDF
2123 + error_log("Parsing PDF with basic parser...");
1827 2124 $parser = new \Smalot\PdfParser\Parser();
1828 2125 $pdf = $parser->parseFile($temp_file);
1829 2126 $pages = $pdf->getPages();
1830 -
2127 +
2128 + error_log("PDF contains " . count($pages) . " pages");
2129 +
1831 2130 if (count($pages) > $max_pages) {
1832 - //error_log(esc_html__("PDF exceeds the maximum allowed pages: ", 'mxchat') . count($pages));
1833 - if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
2131 + error_log("❌ BASIC PROCESSING: Too many pages (" . count($pages) . " > " . $max_pages . ")");
2132 + if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
1834 2133 unlink($temp_file);
1835 2134 }
1836 - return esc_html__('too_many_pages', 'mxchat');
2135 + return 'too_many_pages';
1837 2136 }
1838 -
2137 +
1839 2138 $embeddings = [];
2139 + $processed_pages = 0;
2140 +
1840 2141 foreach ($pages as $page_number => $page) {
1841 2142 $text = $page->getText();
1842 -
1843 - // Ensure text is non-empty before generating embeddings
2143 +
1844 2144 if (empty(trim($text))) {
1845 - //error_log(esc_html__("Skipping empty page: ", 'mxchat') . ($page_number + 1));
2145 + error_log("Skipping empty page: " . ($page_number + 1));
1846 2146 continue;
1847 2147 }
1848 -
2148 +
2149 + $text = $this->mxchat_clean_text($text);
2150 +
1849 2151 $embedding = $this->mxchat_generate_embedding(
1850 - esc_html__("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
2152 + __("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
1851 2153 $this->options['api_key']
1852 2154 );
1853 -
2155 +
1854 2156 if ($embedding) {
1855 2157 $embeddings[] = [
1856 2158 'page_number' => $page_number + 1,
1857 2159 'embedding' => $embedding,
1858 2160 'text' => $text,
2161 + 'enhanced' => false, // CLEARLY MARK AS BASIC
2162 + 'processing_method' => 'basic_pdf_parser'
1859 2163 ];
1860 - } else {
1861 - //error_log(esc_html__("Failed to generate embedding for page ", 'mxchat') . ($page_number + 1));
2164 + $processed_pages++;
1862 2165 }
1863 2166 }
1864 -
1865 - // Clean up downloaded file if it was from URL
1866 - if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
2167 +
2168 + error_log("✅ BASIC PROCESSING COMPLETE: " . $processed_pages . " pages processed");
2169 +
2170 + // Cleanup
2171 + if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
1867 2172 unlink($temp_file);
1868 2173 }
1869 -
2174 +
2175 + error_log("=== MXCHAT PDF PROCESSING END (BASIC) ===");
1870 2176 return $embeddings;
1871 -
2177 +
1872 2178 } catch (\Exception $e) {
1873 - // error_log(esc_html__("Error parsing or processing PDF: ", 'mxchat') . $e->getMessage());
1874 -
1875 - // Cleanup in case of exception
2179 + error_log("❌ BASIC PROCESSING ERROR: " . $e->getMessage());
1876 2180 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
1877 2181 unlink($temp_file);
1878 2182 }
1879 -
2183 + error_log("=== MXCHAT PDF PROCESSING END (ERROR) ===");
1880 2184 return false;
1881 2185 }
1882 2186 }
2187 +
2188 +private function mxchat_clean_text($text) {
2189 + // Remove excessive whitespace
2190 + $text = preg_replace('/\s+/', ' ', $text);
2191 +
2192 + // Remove control characters except newlines and tabs
2193 + $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text);
2194 +
2195 + // Normalize line endings
2196 + $text = str_replace(["\r\n", "\r"], "\n", $text);
2197 +
2198 + // Trim whitespace
2199 + $text = trim($text);
2200 +
2201 + return $text;
2202 +}
2203 +
1883 2204 private function find_relevant_pdf_pages($query_embedding, $embeddings) {
1884 2205 //error_log(esc_html__("find_relevant_pdf_pages called.", 'mxchat'));
1885 2206
1886 2207 $most_relevant = null;
@@ -2038,10 +2359,8 @@
2038 2359 'new_messages' => array_values($new_messages)
2039 2360 ]);
2040 2361 wp_die();
2041 2362 }
2042 -
2043 -
2044 2363 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
2045 2364 // First check if live agents are available
2046 2365 $live_agent_available = $this->options['live_agent_status'] ?? 'off';
2047 2366 if ($live_agent_available !== 'on') {
@@ -2060,18 +2379,101 @@
2060 2379 ]);
2061 2380 wp_die();
2062 2381 }
2063 2382
2064 - $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2065 - if (empty($slack_webhook_url)) {
2383 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2384 +
2385 + if (empty($slack_bot_token)) {
2066 2386 return false;
2067 2387 }
2068 2388
2069 - // Get recent chat history (last 5 messages)
2389 + // Check if channel already exists for this session
2390 + $channel_id = get_option("mxchat_channel_{$session_id}", '');
2391 +
2392 + if (empty($channel_id)) {
2393 + // Create new channel with session ID as name
2394 + $channel_name = 'chat-' . strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $session_id));
2395 +
2396 + //error_log("Attempting to create channel: $channel_name");
2397 +
2398 + $response = wp_remote_post('https://slack.com/api/conversations.create', [
2399 + 'headers' => [
2400 + 'Content-Type' => 'application/json',
2401 + 'Authorization' => 'Bearer ' . $slack_bot_token
2402 + ],
2403 + 'body' => json_encode([
2404 + 'name' => $channel_name,
2405 + 'is_private' => false // Public channel - anyone in workspace can join
2406 + ])
2407 + ]);
2408 +
2409 + if (!is_wp_error($response)) {
2410 + $response_body = wp_remote_retrieve_body($response);
2411 + $response_data = json_decode($response_body, true);
2412 +
2413 + //error_log("Channel creation response: " . $response_body);
2414 +
2415 + if (isset($response_data['ok']) && $response_data['ok']) {
2416 + $channel_id = $response_data['channel']['id'];
2417 + $actual_channel_name = $response_data['channel']['name'] ?? 'unknown';
2418 + //error_log("Channel created successfully: ID=$channel_id, Name=$actual_channel_name");
2419 + update_option("mxchat_channel_{$session_id}", $channel_id);
2420 +
2421 + // Auto-invite agents to the channel
2422 + $agent_user_ids = $this->options['live_agent_user_ids'] ?? '';
2423 +
2424 + if (!empty($agent_user_ids)) {
2425 + // Parse user IDs (one per line)
2426 + $user_ids = array_filter(array_map('trim', explode("\n", $agent_user_ids)));
2427 +
2428 + foreach ($user_ids as $user_id_to_invite) {
2429 + //error_log("Inviting user to channel: $user_id_to_invite");
2430 +
2431 + $invite_response = wp_remote_post('https://slack.com/api/conversations.invite', [
2432 + 'headers' => [
2433 + 'Content-Type' => 'application/json',
2434 + 'Authorization' => 'Bearer ' . $slack_bot_token
2435 + ],
2436 + 'body' => json_encode([
2437 + 'channel' => $channel_id,
2438 + 'users' => $user_id_to_invite
2439 + ])
2440 + ]);
2441 +
2442 + if (!is_wp_error($invite_response)) {
2443 + $invite_body = wp_remote_retrieve_body($invite_response);
2444 + $invite_data = json_decode($invite_body, true);
2445 + //error_log("Invite response for $user_id_to_invite: " . $invite_body);
2446 +
2447 + if (isset($invite_data['ok']) && $invite_data['ok']) {
2448 + //error_log("Successfully invited user $user_id_to_invite to channel");
2449 + } else {
2450 + //error_log("Failed to invite user $user_id_to_invite: " . ($invite_data['error'] ?? 'Unknown error'));
2451 + }
2452 + } else {
2453 + //error_log("WP Error inviting user $user_id_to_invite: " . $invite_response->get_error_message());
2454 + }
2455 + }
2456 + } else {
2457 + //error_log("No agent user IDs configured for auto-invite");
2458 + }
2459 + } else {
2460 + //error_log("Channel creation failed: " . ($response_data['error'] ?? 'Unknown error'));
2461 + }
2462 + } else {
2463 + //error_log("WP Error creating channel: " . $response->get_error_message());
2464 + }
2465 +
2466 + if (empty($channel_id)) {
2467 + return false; // Failed to create channel
2468 + }
2469 + }
2470 +
2471 + // Get recent chat history
2070 2472 $history = get_option("mxchat_history_{$session_id}", []);
2071 - $recent_history = array_slice($history, -5); // Get last 5 messages
2473 + $recent_history = array_slice($history, -5);
2072 2474
2073 - // Format conversation history
2475 + // Format conversation context
2074 2476 $conversation_context = "";
2075 2477 if (!empty($recent_history)) {
2076 2478 $conversation_context = "*Recent Conversation:*\n";
2077 2479 foreach ($recent_history as $hist_message) {
@@ -2082,83 +2484,32 @@
2082 2484 }
2083 2485
2084 2486 update_option("mxchat_mode_{$session_id}", 'agent');
2085 2487
2086 - $webhook_data = [
2087 - 'blocks' => [
2088 - [
2089 - 'type' => 'header',
2090 - 'text' => [
2091 - 'type' => 'plain_text',
2092 - 'text' => '🔔 New Live Agent Request',
2093 - 'emoji' => true
2094 - ]
2095 - ],
2096 - [
2097 - 'type' => 'section',
2098 - 'fields' => [
2099 - [
2100 - 'type' => 'mrkdwn',
2101 - 'text' => sprintf('*User ID:*\n`%s`', $user_id)
2102 - ],
2103 - [
2104 - 'type' => 'mrkdwn',
2105 - 'text' => sprintf('*Session ID:*\n`%s`', $session_id)
2106 - ]
2107 - ]
2108 - ]
2109 - ]
2110 - ];
2111 -
2112 - // Add conversation history if exists
2488 + // Send message to channel
2489 + $channel_message = "🔔 *New Live Agent Request*\n\n";
2490 + $channel_message .= "*Session ID:* `{$session_id}`\n";
2491 + $channel_message .= "*User ID:* `{$user_id}`\n\n";
2492 +
2113 2493 if (!empty($conversation_context)) {
2114 - $webhook_data['blocks'][] = [
2115 - 'type' => 'section',
2116 - 'text' => [
2117 - 'type' => 'mrkdwn',
2118 - 'text' => $conversation_context
2119 - ]
2120 - ];
2494 + $channel_message .= $conversation_context;
2121 2495 }
2496 +
2497 + $channel_message .= "*Current Message:*\n{$message}\n\n";
2498 + $channel_message .= "_Reply directly in this channel - all messages will go to the user_";
2122 2499
2123 - // Add the current message
2124 - $webhook_data['blocks'][] = [
2125 - 'type' => 'section',
2126 - 'text' => [
2127 - 'type' => 'mrkdwn',
2128 - 'text' => sprintf('*Current Message:*\n%s', $message)
2129 - ]
2130 - ];
2131 -
2132 - // Add the reply button
2133 - $webhook_data['blocks'][] = [
2134 - 'type' => 'actions',
2135 - 'elements' => [
2136 - [
2137 - 'type' => 'button',
2138 - 'text' => [
2139 - 'type' => 'plain_text',
2140 - 'text' => '✍️ Reply',
2141 - 'emoji' => true
2142 - ],
2143 - 'value' => $session_id,
2144 - 'action_id' => 'reply_to_user',
2145 - 'style' => 'primary'
2146 - ]
2147 - ]
2148 - ];
2149 -
2150 - $response = wp_remote_post($slack_webhook_url, [
2151 - 'body' => json_encode($webhook_data),
2500 + wp_remote_post('https://slack.com/api/chat.postMessage', [
2152 2501 'headers' => [
2153 2502 'Content-Type' => 'application/json',
2503 + 'Authorization' => 'Bearer ' . $slack_bot_token
2154 2504 ],
2505 + 'body' => json_encode([
2506 + 'channel' => $channel_id,
2507 + 'text' => $channel_message,
2508 + 'mrkdwn' => true
2509 + ])
2155 2510 ]);
2156 2511
2157 - if (is_wp_error($response)) {
2158 - return false;
2159 - }
2160 -
2161 2512 $success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
2162 2513 $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
2163 2514
2164 2515 $this->fallbackResponse = [
@@ -2178,78 +2529,30 @@
2178 2529 ]);
2179 2530 wp_die();
2180 2531 }
2181 2532 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
2182 - $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2533 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2534 + $channel_id = get_option("mxchat_channel_{$session_id}", '');
2183 2535
2184 - if (empty($slack_webhook_url)) {
2185 - //error_log(esc_html__('Slack Webhook URL is not configured.', 'mxchat'));
2536 + if (empty($slack_bot_token) || empty($channel_id)) {
2186 2537 return false;
2187 2538 }
2188 2539
2189 - $webhook_data = [
2190 - 'blocks' => [
2191 - [
2192 - 'type' => 'header',
2193 - 'text' => [
2194 - 'type' => 'plain_text',
2195 - 'text' => esc_html__('📩 New Chat Message', 'mxchat'),
2196 - 'emoji' => true
2197 - ]
2198 - ],
2199 - [
2200 - 'type' => 'section',
2201 - 'fields' => [
2202 - [
2203 - 'type' => 'mrkdwn',
2204 - 'text' => sprintf(esc_html__('*User ID:*\n`%s`', 'mxchat'), $user_id)
2205 - ],
2206 - [
2207 - 'type' => 'mrkdwn',
2208 - 'text' => sprintf(esc_html__('*Session ID:*\n`%s`', 'mxchat'), $session_id)
2209 - ]
2210 - ]
2211 - ],
2212 - [
2213 - 'type' => 'section',
2214 - 'text' => [
2215 - 'type' => 'mrkdwn',
2216 - 'text' => sprintf(esc_html__('*Message:*\n%s', 'mxchat'), $message)
2217 - ]
2218 - ],
2219 - [
2220 - 'type' => 'actions',
2221 - 'elements' => [
2222 - [
2223 - 'type' => 'button',
2224 - 'text' => [
2225 - 'type' => 'plain_text',
2226 - 'text' => esc_html__('✍️ Reply', 'mxchat'),
2227 - 'emoji' => true
2228 - ],
2229 - 'value' => $session_id,
2230 - 'action_id' => 'reply_to_user',
2231 - 'style' => 'primary'
2232 - ]
2233 - ]
2234 - ]
2235 - ]
2236 - ];
2540 + $user_message = "💬 *User:* {$message}";
2237 2541
2238 - $response = wp_remote_post($slack_webhook_url, [
2239 - 'body' => json_encode($webhook_data),
2542 + $response = wp_remote_post('https://slack.com/api/chat.postMessage', [
2240 2543 'headers' => [
2241 2544 'Content-Type' => 'application/json',
2545 + 'Authorization' => 'Bearer ' . $slack_bot_token
2242 2546 ],
2547 + 'body' => json_encode([
2548 + 'channel' => $channel_id,
2549 + 'text' => $user_message,
2550 + 'mrkdwn' => true
2551 + ])
2243 2552 ]);
2244 2553
2245 - if (is_wp_error($response)) {
2246 - //error_log(esc_html__('Error sending message to Slack: ', 'mxchat') . $response->get_error_message());
2247 - return false;
2248 - }
2249 -
2250 - //error_log(esc_html__('Message sent to Slack successfully.', 'mxchat'));
2251 - return true;
2554 + return !is_wp_error($response);
2252 2555 }
2253 2556 public function handle_slack_interaction(WP_REST_Request $request) {
2254 2557 //error_log('Received Slack interaction');
2255 2558
@@ -2337,17 +2640,16 @@
2337 2640
2338 2641 // Default acknowledgment
2339 2642 return new WP_REST_Response(['ok' => true]);
2340 2643 }
2341 -
2342 2644 public function mxchat_handle_agent_response(WP_REST_Request $request) {
2343 2645 //error_log('Received agent response request');
2344 2646 //error_log('Request data: ' . print_r($request->get_params(), true));
2345 - // error_log('Raw body: ' . file_get_contents('php://input'));
2647 + // //error_log('Raw body: ' . file_get_contents('php://input'));
2346 2648
2347 2649 // Get the data from Slack's slash command format
2348 2650 $command_text = $request->get_param('text');
2349 - // error_log('Command text: ' . $command_text);
2651 + // //error_log('Command text: ' . $command_text);
2350 2652
2351 2653 if (empty($command_text)) {
2352 2654 //error_log(esc_html__('Agent response error: No command text received', 'mxchat'));
2353 2655 return new WP_REST_Response([
@@ -2372,9 +2674,9 @@
2372 2674 // Save the message
2373 2675 $message_id = $this->mxchat_save_chat_message($session_id, 'agent', $message);
2374 2676
2375 2677 if (!$message_id) {
2376 - // error_log('Failed to save agent message');
2678 + // //error_log('Failed to save agent message');
2377 2679 return new WP_REST_Response([
2378 2680 'error' => esc_html__('Failed to save message', 'mxchat')
2379 2681 ], 500);
2380 2682 }
@@ -2384,10 +2686,8 @@
2384 2686 'response_type' => 'in_channel',
2385 2687 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
2386 2688 ], 200);
2387 2689 }
2388 -
2389 -
2390 2690 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
2391 2691 //error_log(esc_html__("Switching back to chatbot mode via intent.", 'mxchat'));
2392 2692
2393 2693 // Just update mode to AI
@@ -2401,12 +2701,122 @@
2401 2701 $this->fallbackResponse['text'] = esc_html__('You are now chatting with the AI chatbot.', 'mxchat');
2402 2702
2403 2703 return true; // Intent was handled
2404 2704 }
2705 +public function handle_slack_messages(WP_REST_Request $request) {
2706 + // Log the incoming request for debugging
2707 + //error_log('Slack events request received: ' . $request->get_body());
2708 +
2709 + $body = $request->get_body();
2710 + $data = json_decode($body, true);
2711 +
2712 + // Handle Slack URL verification
2713 + if (isset($data['type']) && $data['type'] === 'url_verification') {
2714 + //error_log('Slack URL verification challenge: ' . $data['challenge']);
2715 + return new WP_REST_Response($data['challenge'], 200, ['Content-Type' => 'text/plain']);
2716 + }
2717 +
2718 + // IMPORTANT: Handle Slack's event deduplication
2719 + if (isset($data['event_id'])) {
2720 + $event_id = $data['event_id'];
2721 + $processed_events = get_transient('mxchat_slack_events') ?: [];
2722 +
2723 + // Check if we've already processed this event
2724 + if (in_array($event_id, $processed_events)) {
2725 + //error_log("Duplicate event detected: $event_id");
2726 + return new WP_REST_Response(['ok' => true]);
2727 + }
2728 +
2729 + // Add this event to processed list
2730 + $processed_events[] = $event_id;
2731 + // Keep only last 100 events to prevent memory issues
2732 + if (count($processed_events) > 100) {
2733 + $processed_events = array_slice($processed_events, -100);
2734 + }
2735 + // Store for 1 hour
2736 + set_transient('mxchat_slack_events', $processed_events, HOUR_IN_SECONDS);
2737 + }
2738 +
2739 + // Handle message events
2740 + if (isset($data['event']) && $data['event']['type'] === 'message') {
2741 + $event = $data['event'];
2742 +
2743 + // Skip bot messages and messages with subtypes (like bot_message)
2744 + if (isset($event['bot_id']) || isset($event['subtype'])) {
2745 + return new WP_REST_Response(['ok' => true]);
2746 + }
2747 +
2748 + // Additional check: Skip if this is a threaded reply to our confirmation
2749 + if (isset($event['thread_ts']) && $event['thread_ts'] !== $event['ts']) {
2750 + return new WP_REST_Response(['ok' => true]);
2751 + }
2752 +
2753 + $channel_id = $event['channel'];
2754 + $message_text = $event['text'] ?? '';
2755 + $message_ts = $event['ts'] ?? '';
2756 +
2757 + // Find session ID by looking for matching channel
2758 + global $wpdb;
2759 + $session_option = $wpdb->get_var(
2760 + $wpdb->prepare(
2761 + "SELECT option_name FROM {$wpdb->options}
2762 + WHERE option_name LIKE 'mxchat_channel_%'
2763 + AND option_value = %s",
2764 + $channel_id
2765 + )
2766 + );
2767 +
2768 + if ($session_option) {
2769 + $session_id = str_replace('mxchat_channel_', '', $session_option);
2770 +
2771 + // Create a unique key for this specific message
2772 + $message_key = md5($session_id . $message_ts . $message_text);
2773 + $processed_messages = get_transient('mxchat_processed_messages_' . $session_id) ?: [];
2774 +
2775 + // Check if we've already processed this exact message
2776 + if (in_array($message_key, $processed_messages)) {
2777 + //error_log("Duplicate message detected for session $session_id");
2778 + return new WP_REST_Response(['ok' => true]);
2779 + }
2780 +
2781 + // Add to processed messages
2782 + $processed_messages[] = $message_key;
2783 + // Keep only last 50 messages per session
2784 + if (count($processed_messages) > 50) {
2785 + $processed_messages = array_slice($processed_messages, -50);
2786 + }
2787 + set_transient('mxchat_processed_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
2788 +
2789 + // Save the agent message
2790 + $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
2791 +
2792 + // Send confirmation back to Slack (only once)
2793 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2794 + if (!empty($slack_bot_token)) {
2795 + // Use a transient to prevent duplicate confirmations
2796 + $confirm_key = 'mxchat_confirm_' . $message_key;
2797 + if (!get_transient($confirm_key)) {
2798 + wp_remote_post('https://slack.com/api/chat.postMessage', [
2799 + 'headers' => [
2800 + 'Content-Type' => 'application/json',
2801 + 'Authorization' => 'Bearer ' . $slack_bot_token
2802 + ],
2803 + 'body' => json_encode([
2804 + 'channel' => $channel_id,
2805 + 'text' => "✅ _Message sent to user_",
2806 + 'thread_ts' => $event['ts'] // Reply in thread
2807 + ])
2808 + ]);
2809 + // Set transient to prevent duplicate confirmations
2810 + set_transient($confirm_key, true, 300); // 5 minutes
2811 + }
2812 + }
2813 + }
2814 + }
2815 +
2816 + return new WP_REST_Response(['ok' => true]);
2817 +}
2405 2818
2406 -
2407 -
2408 -
2409 2819 // For the word upload handler
2410 2820 public function mxchat_handle_word_upload() {
2411 2821 // Delegate to word handler
2412 2822 $this->word_handler->mxchat_handle_word_upload();
@@ -2429,21 +2839,102 @@
2429 2839 return MxChat_User::mxchat_get_user_identifier();
2430 2840 }
2431 2841
2432 2842 private function mxchat_generate_embedding($text, $api_key) {
2433 - $endpoint = 'https://api.openai.com/v1/embeddings';
2434 -
2435 - $body = wp_json_encode([
2436 - 'input' => $text,
2437 - 'model' => 'text-embedding-ada-002'
2438 - ]);
2439 -
2843 + try {
2844 + // Get options and selected model
2845 + $options = get_option('mxchat_options');
2846 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
2847 +
2848 + // Determine endpoint and API key based on model
2849 + if (strpos($selected_model, 'voyage') === 0) {
2850 + $endpoint = 'https://api.voyageai.com/v1/embeddings';
2851 + $api_key = $options['voyage_api_key'] ?? '';
2852 +
2853 + // Check if Voyage API key is missing
2854 + if (empty($api_key)) {
2855 + //error_log('Voyage API key is missing');
2856 + return [
2857 + 'error' => esc_html__('Voyage AI API key is not configured', 'mxchat'),
2858 + 'error_code' => 'missing_voyage_api_key'
2859 + ];
2860 + }
2861 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2862 + $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
2863 + $api_key = $options['gemini_api_key'] ?? '';
2864 +
2865 + // Check if Gemini API key is missing
2866 + if (empty($api_key)) {
2867 + //error_log('Gemini API key is missing');
2868 + return [
2869 + 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
2870 + 'error_code' => 'missing_gemini_api_key'
2871 + ];
2872 + }
2873 + } else {
2874 + $endpoint = 'https://api.openai.com/v1/embeddings';
2875 + // Use the passed API key for OpenAI
2876 +
2877 + // Check if OpenAI API key is missing
2878 + if (empty($api_key)) {
2879 + //error_log('OpenAI API key is missing');
2880 + return [
2881 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
2882 + 'error_code' => 'missing_openai_api_key'
2883 + ];
2884 + }
2885 + }
2886 +
2887 + // Check if text is empty
2888 + if (empty($text)) {
2889 + //error_log('Empty text provided for embedding generation');
2890 + return [
2891 + 'error' => esc_html__('No text provided for embedding generation', 'mxchat'),
2892 + 'error_code' => 'empty_embedding_text'
2893 + ];
2894 + }
2895 +
2896 + // Prepare request body based on provider
2897 + if (strpos($selected_model, 'gemini-embedding') === 0) {
2898 + // Gemini API format
2899 + $request_body = [
2900 + 'model' => 'models/' . $selected_model,
2901 + 'content' => [
2902 + 'parts' => [
2903 + ['text' => $text]
2904 + ]
2905 + ],
2906 + 'outputDimensionality' => 1536
2907 + ];
2908 +
2909 + // Prepare headers for Gemini (API key as query parameter)
2910 + $endpoint .= '?key=' . $api_key;
2911 + $headers = [
2912 + 'Content-Type' => 'application/json'
2913 + ];
2914 + } else {
2915 + // OpenAI/Voyage API format
2916 + $request_body = [
2917 + 'input' => $text,
2918 + 'model' => $selected_model
2919 + ];
2920 +
2921 + // Add output_dimension for voyage-3-large
2922 + if ($selected_model === 'voyage-3-large') {
2923 + $request_body['output_dimension'] = 2048;
2924 + }
2925 +
2926 + // Prepare headers for OpenAI/Voyage
2927 + $headers = [
2928 + 'Content-Type' => 'application/json',
2929 + 'Authorization' => 'Bearer ' . $api_key
2930 + ];
2931 + }
2932 +
2933 + // Prepare request arguments
2440 2934 $args = [
2441 - 'body' => $body,
2442 - 'headers' => [
2443 - 'Content-Type' => 'application/json',
2444 - 'Authorization' => 'Bearer ' . $api_key,
2445 - ],
2935 + 'body' => wp_json_encode($request_body),
2936 + 'headers' => $headers,
2446 2937 'timeout' => 60,
2447 2938 'redirection' => 5,
2448 2939 'blocking' => true,
2449 2940 'httpversion' => '1.0',
@@ -2448,25 +2939,109 @@
2448 2939 'blocking' => true,
2449 2940 'httpversion' => '1.0',
2450 2941 'sslverify' => true,
2451 2942 ];
2452 -
2943 +
2944 + // Make the request
2453 2945 $response = wp_remote_post($endpoint, $args);
2454 -
2946 +
2947 + // Handle WordPress errors
2455 2948 if (is_wp_error($response)) {
2456 - return null;
2949 + $error_message = $response->get_error_message();
2950 + //error_log('Embedding Generation Error: ' . $error_message);
2951 + return [
2952 + 'error' => esc_html__('Connection error when generating embeddings: ', 'mxchat') . esc_html($error_message),
2953 + 'error_code' => 'embedding_connection_error'
2954 + ];
2457 2955 }
2458 -
2956 +
2957 + // Check HTTP status code
2958 + $status_code = wp_remote_retrieve_response_code($response);
2959 + if ($status_code !== 200) {
2960 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
2961 +
2962 + $error_message = isset($response_body['error']['message'])
2963 + ? $response_body['error']['message']
2964 + : 'HTTP Error ' . $status_code;
2965 +
2966 + $error_type = isset($response_body['error']['type'])
2967 + ? $response_body['error']['type']
2968 + : 'unknown';
2969 +
2970 + //error_log('Embedding API HTTP Error: ' . $status_code . ' - ' . $error_message);
2971 +
2972 + // Handle specific error types
2973 + switch ($error_type) {
2974 + case 'invalid_request_error':
2975 + if (strpos($error_message, 'API key') !== false) {
2976 + return [
2977 + 'error' => esc_html__('Invalid API key for embedding generation. Please check your API key configuration.', 'mxchat'),
2978 + 'error_code' => 'embedding_invalid_api_key'
2979 + ];
2980 + }
2981 + break;
2982 +
2983 + case 'authentication_error':
2984 + return [
2985 + 'error' => esc_html__('Authentication failed for embedding generation. Please check your API key.', 'mxchat'),
2986 + 'error_code' => 'embedding_auth_error'
2987 + ];
2988 +
2989 + case 'rate_limit_exceeded':
2990 + return [
2991 + 'error' => esc_html__('Rate limit exceeded for embedding generation. Please try again later.', 'mxchat'),
2992 + 'error_code' => 'embedding_rate_limit'
2993 + ];
2994 +
2995 + case 'quota_exceeded':
2996 + return [
2997 + 'error' => esc_html__('API quota exceeded for embedding generation. Please check your billing details.', 'mxchat'),
2998 + 'error_code' => 'embedding_quota_exceeded'
2999 + ];
3000 + }
3001 +
3002 + // Generic error fallback
3003 + return [
3004 + 'error' => esc_html__('Embedding API error - check embedding API key.: ', 'mxchat') . esc_html($error_message),
3005 + 'error_code' => 'embedding_api_error',
3006 + 'status_code' => $status_code
3007 + ];
3008 + }
3009 +
2459 3010 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2460 -
2461 - if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
2462 - return $response_body['data'][0]['embedding'];
3011 +
3012 + // Handle different response formats based on provider
3013 + if (strpos($selected_model, 'gemini-embedding') === 0) {
3014 + // Gemini API response format
3015 + if (isset($response_body['embedding']['values']) && is_array($response_body['embedding']['values'])) {
3016 + return $response_body['embedding']['values'];
3017 + } else {
3018 + //error_log('Invalid Gemini embedding response: ' . wp_json_encode($response_body));
3019 + return [
3020 + 'error' => esc_html__('Received invalid embedding data from the Gemini API.', 'mxchat'),
3021 + 'error_code' => 'invalid_gemini_embedding_response'
3022 + ];
3023 + }
2463 3024 } else {
2464 - return null;
3025 + // OpenAI/Voyage API response format
3026 + if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
3027 + return $response_body['data'][0]['embedding'];
3028 + } else {
3029 + //error_log('Invalid embedding response: ' . wp_json_encode($response_body));
3030 + return [
3031 + 'error' => esc_html__('Received invalid embedding data from the API.', 'mxchat'),
3032 + 'error_code' => 'invalid_embedding_response'
3033 + ];
3034 + }
2465 3035 }
3036 + } catch (Exception $e) {
3037 + //error_log('Embedding Exception: ' . $e->getMessage());
3038 + return [
3039 + 'error' => esc_html__('System error when generating embeddings: ', 'mxchat') . esc_html($e->getMessage()),
3040 + 'error_code' => 'embedding_exception'
3041 + ];
2466 3042 }
2467 -
2468 -
3043 +}
2469 3044 private function mxchat_find_relevant_content($user_embedding) {
2470 3045 //error_log('MXChat Vector Search: Starting content search...');
2471 3046
2472 3047 // Retrieve the add-on settings from the database.
@@ -2472,9 +3047,8 @@
2472 3047 // Retrieve the add-on settings from the database.
2473 3048 $addon_options = get_option('mxchat_pinecone_addon_options', array());
2474 3049
2475 3050 // Determine whether Pinecone is enabled.
2476 - // We expect the sanitized setting to be a string '1' if enabled, otherwise '0'.
2477 3051 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
2478 3052
2479 3053 //error_log('Pinecone enabled flag: ' . $use_pinecone);
2480 3054
@@ -2492,18 +3066,26 @@
2492 3066 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2493 3067 $cache_key = 'mxchat_system_prompt_embeddings';
2494 3068 $batch_size = 500;
2495 3069
3070 + // Initialize similarity analysis storage
3071 + $this->last_similarity_analysis = [
3072 + 'knowledge_base_type' => 'WordPress Database',
3073 + 'top_matches' => [],
3074 + 'threshold_used' => 0,
3075 + 'total_checked' => 0
3076 + ];
3077 +
2496 3078 // Retrieve embeddings from cache or database
2497 3079 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
2498 3080 if ($embeddings === false) {
3081 + // Cache miss - load embeddings from database WITH CONTENT for testing
2499 3082 $embeddings = [];
2500 3083 $offset = 0;
2501 3084
2502 - // Load in batches and build cache
2503 3085 do {
2504 3086 $query = $wpdb->prepare(
2505 - "SELECT id, embedding_vector
3087 + "SELECT id, embedding_vector, article_content, source_url
2506 3088 FROM {$system_prompt_table}
2507 3089 LIMIT %d OFFSET %d",
2508 3090 $batch_size,
2509 3091 $offset
@@ -2515,62 +3097,125 @@
2515 3097 }
2516 3098
2517 3099 $embeddings = array_merge($embeddings, $batch);
2518 3100 $offset += $batch_size;
2519 -
2520 - // Free memory
2521 3101 unset($batch);
2522 -
2523 3102 } while (true);
2524 3103
2525 3104 if (empty($embeddings)) {
2526 - return ''; // Return an empty string if no embeddings found
3105 + return '';
2527 3106 }
3107 +
3108 + // Cache embeddings for future use (but note: this now includes content)
2528 3109 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
2529 3110 }
2530 3111
2531 - // Initialize array to store relevant results with similarity scores
3112 + // Get configuration options
3113 + $main_options = get_option('mxchat_options', []);
3114 +
3115 + // Get base similarity threshold (default 75%)
3116 + $similarity_threshold = isset($main_options['similarity_threshold'])
3117 + ? ((int) $main_options['similarity_threshold']) / 100
3118 + : 0.75;
3119 +
3120 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3121 +
3122 + // Calculate similarities and build results array
3123 + $all_similarities = [];
2532 3124 $relevant_results = [];
2533 - // Iterate through embeddings to calculate similarity
3125 +
2534 3126 foreach ($embeddings as $embedding) {
2535 3127 $database_embedding = $embedding->embedding_vector
2536 3128 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
2537 3129 : null;
3130 +
2538 3131 if (is_array($database_embedding) && is_array($user_embedding)) {
2539 3132 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
2540 - $relevant_results[] = [
2541 - 'id' => $embedding->id,
2542 - 'similarity' => $similarity
3133 +
3134 + // Store ALL similarities for testing (top 10)
3135 + $source_display = '';
3136 + if (!empty($embedding->source_url) && $embedding->source_url !== '#') {
3137 + $source_display = $embedding->source_url;
3138 + } else {
3139 + $content_preview = strip_tags($embedding->article_content ?? '');
3140 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3141 + $source_display = substr(trim($content_preview), 0, 50) . '...';
3142 + }
3143 +
3144 + $all_similarities[] = [
3145 + 'document_id' => $embedding->id,
3146 + 'similarity' => $similarity,
3147 + 'similarity_percentage' => round($similarity * 100, 2),
3148 + 'above_threshold' => $similarity >= $similarity_threshold,
3149 + 'source_display' => $source_display,
3150 + 'content_preview' => substr(strip_tags($embedding->article_content ?? ''), 0, 100) . '...',
3151 + 'used_for_context' => false // Initialize as false, we'll update this later
2543 3152 ];
3153 +
3154 + // Only consider results above threshold for actual content retrieval
3155 + if ($similarity >= $similarity_threshold) {
3156 + $relevant_results[] = [
3157 + 'id' => $embedding->id,
3158 + 'similarity' => $similarity
3159 + ];
3160 + }
2544 3161 }
2545 - // Free memory
3162 +
2546 3163 unset($database_embedding);
2547 3164 }
2548 3165
2549 - // Retrieve the similarity threshold
2550 - $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
2551 -
2552 - // Filter and sort relevant results by similarity
2553 - $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
2554 - return $result['similarity'] >= $similarity_threshold;
3166 + // Sort ALL similarities for testing display (highest first)
3167 + usort($all_similarities, function ($a, $b) {
3168 + return $b['similarity'] <=> $a['similarity'];
2555 3169 });
3170 +
3171 + // Sort relevant results by similarity (highest first)
2556 3172 usort($relevant_results, function ($a, $b) {
2557 3173 return $b['similarity'] <=> $a['similarity'];
2558 3174 });
2559 -
2560 - // Limit to the top 5 results
3175 +
3176 + // Get top 5 results for actual content (standard approach)
2561 3177 $top_results = array_slice($relevant_results, 0, 5);
2562 -
2563 - // Initialize the final content
3178 +
3179 + // NOW mark which documents are actually used for context
3180 + $used_document_ids = [];
3181 + foreach ($top_results as $result) {
3182 + $used_document_ids[] = $result['id'];
3183 + }
3184 +
3185 + // Update the all_similarities array to mark which were actually used
3186 + foreach ($all_similarities as &$similarity_item) {
3187 + $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
3188 + }
3189 +
3190 + // Store top 10 for testing panel (now with correct used_for_context flags)
3191 + $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
3192 + $this->last_similarity_analysis['total_checked'] = count($embeddings);
3193 +
3194 + //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " top matches for testing");
3195 +
3196 + // Initialize final content
2564 3197 $content = '';
2565 -
2566 - // Fetch and combine content for the top results
2567 - foreach ($top_results as $result) {
3198 +
3199 + // Track document IDs to avoid duplicates
3200 + $added_document_ids = [];
3201 +
3202 + // Fetch and format content for each selected result
3203 + foreach ($top_results as $index => $result) {
3204 + if (in_array($result['id'], $added_document_ids)) {
3205 + continue;
3206 + }
3207 +
2568 3208 $chunk_content = $this->fetch_content_with_product_links($result['id']);
2569 - // Check if the content is PDF-related and add surrounding pages
3209 + $added_document_ids[] = $result['id'];
3210 +
3211 + $content .= "## Reference " . ($index + 1) . " ##\n";
3212 + $content .= $chunk_content . "\n\n";
3213 +
3214 + // PDF surrounding pages logic (unchanged)
2570 3215 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
2571 3216 $surrounding_content = $wpdb->get_results($wpdb->prepare(
2572 - "SELECT article_content FROM {$system_prompt_table}
3217 + "SELECT id, article_content FROM {$system_prompt_table}
2573 3218 WHERE id IN (
2574 3219 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
2575 3220 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
2576 3221 )",
@@ -2576,52 +3221,73 @@
2576 3221 )",
2577 3222 $result['id'],
2578 3223 $result['id']
2579 3224 ));
2580 - // Add previous content if it exists
3225 +
2581 3226 if (!empty($surrounding_content[0])) {
3227 + $content .= "## Related Content ##\n";
2582 3228 $content .= $surrounding_content[0]->article_content . "\n\n";
3229 + $added_document_ids[] = $surrounding_content[0]->id;
2583 3230 }
2584 - // Add the main chunk content
2585 - $content .= $chunk_content . "\n\n";
2586 - // Add next content if it exists
3231 +
2587 3232 if (!empty($surrounding_content[1])) {
3233 + $content .= "## Related Content ##\n";
2588 3234 $content .= $surrounding_content[1]->article_content . "\n\n";
3235 + $added_document_ids[] = $surrounding_content[1]->id;
2589 3236 }
3237 + }
3238 + }
3239 +
3240 + // Add response guidelines
3241 + if (empty($top_results)) {
3242 + $content = "No reference information was found for this query.\n\n";
2590 3243 } else {
2591 - // For non-PDF content, add directly
2592 - $content .= $chunk_content . "\n\n";
3244 + $content .= "\n## Response Guidelines ##\n" .
3245 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3246 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
3247 + "If you don't have specific information or are uncertain about any details, it's always " .
3248 + "better to honestly say you don't know rather than making up or guessing at answers. " .
3249 + "When information is incomplete, let them know you are unsure.";
2593 3250 }
2594 - }
2595 -
3251 +
2596 3252 return trim($content);
2597 3253 }
2598 -/**
2599 - * Find relevant content in Pinecone vector database
2600 - */
3254 +
2601 3255 private function find_relevant_content_pinecone($user_embedding) {
2602 3256 $options = get_option('mxchat_pinecone_addon_options', array());
2603 3257 $api_key = $options['mxchat_pinecone_api_key'] ?? '';
2604 3258 $host = $options['mxchat_pinecone_host'] ?? '';
2605 -
3259 +
3260 + // Initialize similarity analysis storage
3261 + $this->last_similarity_analysis = [
3262 + 'knowledge_base_type' => 'Pinecone',
3263 + 'top_matches' => [],
3264 + 'threshold_used' => 0,
3265 + 'total_checked' => 0
3266 + ];
3267 +
2606 3268 if (empty($host) || empty($api_key)) {
2607 - //error_log('Pinecone credentials not properly configured');
2608 3269 return '';
2609 3270 }
2610 -
2611 - // Get similarity threshold from WordPress settings
2612 - $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
2613 -
2614 - // Prepare the query request for Pinecone
3271 +
3272 + // Get the similarity threshold from the main options
3273 + $main_options = get_option('mxchat_options', []);
3274 + $similarity_threshold = isset($main_options['similarity_threshold'])
3275 + ? ((int) $main_options['similarity_threshold']) / 100
3276 + : 0.75;
3277 +
3278 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3279 +
3280 + // Prepare the query request for Pinecone (request more for testing)
2615 3281 $api_endpoint = "https://{$host}/query";
2616 -
3282 +
2617 3283 $request_body = array(
2618 3284 'vector' => $user_embedding,
2619 - 'topK' => 5,
3285 + 'topK' => 20, // Request more to get good testing data
2620 3286 'includeMetadata' => true,
2621 3287 'includeValues' => true
2622 3288 );
2623 -
3289 +
2624 3290 $response = wp_remote_post($api_endpoint, array(
2625 3291 'headers' => array(
2626 3292 'Api-Key' => $api_key,
2627 3293 'accept' => 'application/json',
@@ -2629,46 +3295,120 @@
2629 3295 ),
2630 3296 'body' => wp_json_encode($request_body),
2631 3297 'timeout' => 30
2632 3298 ));
2633 -
3299 +
2634 3300 if (is_wp_error($response)) {
2635 - //error_log('Pinecone query error: ' . $response->get_error_message());
2636 3301 return '';
2637 3302 }
2638 -
3303 +
2639 3304 $response_code = wp_remote_retrieve_response_code($response);
2640 3305 if ($response_code !== 200) {
2641 - //error_log('Pinecone API error: ' . wp_remote_retrieve_body($response));
2642 3306 return '';
2643 3307 }
2644 -
3308 +
2645 3309 $results = json_decode(wp_remote_retrieve_body($response), true);
2646 3310 if (empty($results['matches'])) {
2647 3311 return '';
2648 3312 }
2649 -
3313 +
3314 + // First, determine which matches will actually be used for content
3315 + $matches_used_for_context = [];
3316 + $matches_used = 0;
3317 +
3318 + foreach ($results['matches'] as $index => $match) {
3319 + // Skip if similarity is below threshold
3320 + if ($match['score'] < $similarity_threshold) {
3321 + continue;
3322 + }
3323 +
3324 + // Limit to top 5 matches above threshold
3325 + if ($matches_used >= 5) {
3326 + break;
3327 + }
3328 +
3329 + if (!empty($match['metadata']['text'])) {
3330 + $matches_used_for_context[] = $match['id'] ?? $index;
3331 + $matches_used++;
3332 + }
3333 + }
3334 +
3335 + // Process ALL matches for testing data (top 10)
3336 + $all_matches = [];
3337 + foreach ($results['matches'] as $index => $match) {
3338 + if ($index >= 10) break; // Limit to top 10 for testing
3339 +
3340 + $source_display = '';
3341 + if (!empty($match['metadata']['source_url'])) {
3342 + $source_display = $match['metadata']['source_url'];
3343 + } else {
3344 + $content_preview = strip_tags($match['metadata']['text'] ?? '');
3345 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3346 + $source_display = substr(trim($content_preview), 0, 50) . '...';
3347 + }
3348 +
3349 + $match_id = $match['id'] ?? $index;
3350 +
3351 + $all_matches[] = [
3352 + 'document_id' => $match_id,
3353 + 'similarity' => $match['score'],
3354 + 'similarity_percentage' => round($match['score'] * 100, 2),
3355 + 'above_threshold' => $match['score'] >= $similarity_threshold,
3356 + 'source_display' => $source_display,
3357 + 'content_preview' => substr(strip_tags($match['metadata']['text'] ?? ''), 0, 100) . '...',
3358 + 'used_for_context' => in_array($match_id, $matches_used_for_context) // Correct usage flag
3359 + ];
3360 + }
3361 +
3362 + // Store for testing panel
3363 + $this->last_similarity_analysis['top_matches'] = $all_matches;
3364 + $this->last_similarity_analysis['total_checked'] = count($results['matches']);
3365 +
3366 + //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " Pinecone matches for testing");
3367 +
2650 3368 // Initialize the final content
2651 3369 $content = '';
2652 -
2653 - // Process each match
2654 - foreach ($results['matches'] as $match) {
3370 + $matches_used = 0;
3371 +
3372 + // Process each match for actual content (this is the real content generation)
3373 + foreach ($results['matches'] as $index => $match) {
2655 3374 // Skip if similarity is below threshold
2656 3375 if ($match['score'] < $similarity_threshold) {
2657 3376 continue;
2658 3377 }
2659 -
2660 - if (!empty($match['metadata']['text']) && !empty($match['metadata']['source_url'])) {
2661 - // Add content with citation
2662 - $content .= $match['metadata']['text'] . "\n";
2663 - $content .= "Source: " . $match['metadata']['source_url'] . "\n\n";
3378 +
3379 + // Limit to top 5 matches above threshold
3380 + if ($matches_used >= 5) {
3381 + break;
2664 3382 }
3383 +
3384 + if (!empty($match['metadata']['text'])) {
3385 + $content .= "## Reference " . ($matches_used + 1) . " ##\n";
3386 + $content .= $match['metadata']['text'] . "\n\n";
3387 +
3388 + if (!empty($match['metadata']['source_url'])) {
3389 + $content .= "URL: " . $match['metadata']['source_url'] . "\n\n";
3390 + }
3391 +
3392 + $matches_used++;
3393 + }
2665 3394 }
2666 -
3395 +
3396 + // Add response guidelines
3397 + if ($matches_used === 0) {
3398 + $content = "No reference information was found for this query.\n\n";
3399 + } else {
3400 + $content .= "\n## Response Guidelines ##\n" .
3401 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3402 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
3403 + "If you don't have specific information or are uncertain about any details, it's always " .
3404 + "better to honestly say you don't know rather than making up or guessing at answers. " .
3405 + "When information is incomplete, let them know you are unsure.";
3406 + }
3407 +
2667 3408 return trim($content);
2668 3409 }
2669 3410
2670 -
2671 3411 private function mxchat_find_relevant_products($user_embedding) {
2672 3412 //error_log('MXChat Vector Search: Starting product search...');
2673 3413
2674 3414 // Retrieve the add-on settings from the database
@@ -2686,9 +3426,8 @@
2686 3426 //error_log('MXChat Vector Search: Using WordPress database for products');
2687 3427 return $this->find_relevant_products_wordpress($user_embedding);
2688 3428 }
2689 3429 }
2690 -
2691 3430 private function find_relevant_products_wordpress($user_embedding) {
2692 3431 global $wpdb;
2693 3432 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2694 3433 $cache_key = 'mxchat_system_prompt_embeddings';
@@ -2762,10 +3501,8 @@
2762 3501 }
2763 3502
2764 3503 return trim($content);
2765 3504 }
2766 -
2767 -// Modified search function with correct filter syntax
2768 3505 private function find_relevant_products_pinecone($user_embedding) {
2769 3506 //error_log('Starting Pinecone product search...');
2770 3507
2771 3508 $options = get_option('mxchat_pinecone_addon_options', array());
@@ -2840,10 +3577,8 @@
2840 3577 }
2841 3578
2842 3579 return trim($content);
2843 3580 }
2844 -
2845 -
2846 3581 private function fetch_content_with_product_links($most_relevant_id) {
2847 3582 global $wpdb;
2848 3583 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2849 3584
@@ -2862,315 +3597,657 @@
2862 3597
2863 3598 return null;
2864 3599 }
2865 3600
2866 -// Function definition
2867 -private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $conversation_history) {
3601 +/**
3602 + * Modified streaming functions to include testing data
3603 + */
3604 +
3605 +// 1. Update the main handler to pass testing data to streaming functions
3606 +private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $gemini_api_key, $conversation_history, $streaming = false, $session_id = '', $testing_data = null) {
2868 3607 try {
2869 3608 if (!$relevant_content) {
2870 - return esc_html__("I'm sorry, I couldn't find relevant information on that topic.", 'mxchat');
3609 + $error_response = [
3610 + 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
3611 + 'error_code' => 'no_relevant_content'
3612 + ];
3613 +
3614 + // Add testing data to error response if available
3615 + if ($testing_data !== null) {
3616 + $error_response['testing_data'] = $testing_data;
3617 + //error_log("MxChat Testing: Added testing data to no_relevant_content error");
3618 + }
3619 +
3620 + return $error_response;
2871 3621 }
2872 -
3622 +
2873 3623 // Ensure conversation_history is an array
2874 3624 if (!is_array($conversation_history)) {
2875 3625 $conversation_history = array();
2876 3626 }
2877 -
3627 +
2878 3628 // Get selected model with default fallback
2879 3629 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
2880 -
3630 +
2881 3631 // Extract model prefix to determine the provider
2882 3632 $model_parts = explode('-', $selected_model);
2883 3633 $provider = strtolower($model_parts[0]);
2884 -
3634 +
2885 3635 // Handle model selection based on provider prefix
2886 3636 switch ($provider) {
2887 - case 'claude':
2888 - if (empty($claude_api_key)) {
2889 - throw new Exception(esc_html__('Claude API key is not configured', 'mxchat'));
3637 + case 'gemini':
3638 + if (empty($gemini_api_key)) {
3639 + $error_response = [
3640 + 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
3641 + 'error_code' => 'missing_gemini_api_key'
3642 + ];
3643 + if ($testing_data !== null) {
3644 + $error_response['testing_data'] = $testing_data;
3645 + }
3646 + return $error_response;
2890 3647 }
2891 - return $this->mxchat_generate_response_claude(
3648 + $response = $this->mxchat_generate_response_gemini(
2892 3649 $selected_model,
2893 - $claude_api_key,
3650 + $gemini_api_key,
2894 3651 $conversation_history,
2895 3652 $relevant_content
2896 3653 );
2897 -
3654 + break;
3655 +
3656 + case 'claude':
3657 + if (empty($claude_api_key)) {
3658 + $error_response = [
3659 + 'error' => esc_html__('Claude API key is not configured', 'mxchat'),
3660 + 'error_code' => 'missing_claude_api_key'
3661 + ];
3662 + if ($testing_data !== null) {
3663 + $error_response['testing_data'] = $testing_data;
3664 + }
3665 + return $error_response;
3666 + }
3667 + if ($streaming) {
3668 + return $this->mxchat_generate_response_claude_stream(
3669 + $selected_model,
3670 + $claude_api_key,
3671 + $conversation_history,
3672 + $relevant_content,
3673 + $session_id,
3674 + $testing_data // Pass testing data
3675 + );
3676 + } else {
3677 + $response = $this->mxchat_generate_response_claude(
3678 + $selected_model,
3679 + $claude_api_key,
3680 + $conversation_history,
3681 + $relevant_content
3682 + );
3683 + }
3684 + break;
3685 +
2898 3686 case 'grok':
2899 3687 if (empty($xai_api_key)) {
2900 - throw new Exception(esc_html__('X.AI API key is not configured', 'mxchat'));
3688 + $error_response = [
3689 + 'error' => esc_html__('X.AI API key is not configured', 'mxchat'),
3690 + 'error_code' => 'missing_xai_api_key'
3691 + ];
3692 + if ($testing_data !== null) {
3693 + $error_response['testing_data'] = $testing_data;
3694 + }
3695 + return $error_response;
2901 3696 }
2902 - return $this->mxchat_generate_response_xai(
2903 - $selected_model,
2904 - $xai_api_key,
2905 - $conversation_history,
2906 - $relevant_content
2907 - );
2908 -
3697 + if ($streaming) {
3698 + return $this->mxchat_generate_response_xai_stream(
3699 + $selected_model,
3700 + $xai_api_key,
3701 + $conversation_history,
3702 + $relevant_content,
3703 + $session_id,
3704 + $testing_data // Pass testing data
3705 + );
3706 + } else {
3707 + $response = $this->mxchat_generate_response_xai(
3708 + $selected_model,
3709 + $xai_api_key,
3710 + $conversation_history,
3711 + $relevant_content
3712 + );
3713 + }
3714 + break;
3715 +
2909 3716 case 'deepseek':
2910 3717 if (empty($deepseek_api_key)) {
2911 - throw new Exception(esc_html__('DeepSeek API key is not configured', 'mxchat'));
3718 + $error_response = [
3719 + 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
3720 + 'error_code' => 'missing_deepseek_api_key'
3721 + ];
3722 + if ($testing_data !== null) {
3723 + $error_response['testing_data'] = $testing_data;
3724 + }
3725 + return $error_response;
2912 3726 }
2913 - return $this->mxchat_generate_response_deepseek(
3727 + $response = $this->mxchat_generate_response_deepseek(
2914 3728 $selected_model,
2915 3729 $deepseek_api_key,
2916 3730 $conversation_history,
2917 3731 $relevant_content
2918 3732 );
2919 -
3733 + break;
3734 +
2920 3735 case 'gpt':
3736 + case 'o1':
2921 3737 if (empty($api_key)) {
2922 - throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
3738 + $error_response = [
3739 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
3740 + 'error_code' => 'missing_openai_api_key'
3741 + ];
3742 + if ($testing_data !== null) {
3743 + $error_response['testing_data'] = $testing_data;
3744 + }
3745 + return $error_response;
2923 3746 }
2924 - return $this->mxchat_generate_response_openai(
2925 - $selected_model,
2926 - $api_key,
2927 - $conversation_history,
2928 - $relevant_content
2929 - );
2930 -
3747 + if ($streaming) {
3748 + return $this->mxchat_generate_response_openai_stream(
3749 + $selected_model,
3750 + $api_key,
3751 + $conversation_history,
3752 + $relevant_content,
3753 + $session_id,
3754 + $testing_data // Pass testing data
3755 + );
3756 + } else {
3757 + $response = $this->mxchat_generate_response_openai(
3758 + $selected_model,
3759 + $api_key,
3760 + $conversation_history,
3761 + $relevant_content
3762 + );
3763 + }
3764 + break;
3765 +
2931 3766 default:
2932 3767 // Default to OpenAI for custom models or unrecognized prefixes
2933 3768 if (empty($api_key)) {
2934 - throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
3769 + $error_response = [
3770 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
3771 + 'error_code' => 'missing_openai_api_key'
3772 + ];
3773 + if ($testing_data !== null) {
3774 + $error_response['testing_data'] = $testing_data;
3775 + }
3776 + return $error_response;
2935 3777 }
2936 - return $this->mxchat_generate_response_openai(
2937 - $selected_model,
2938 - $api_key,
2939 - $conversation_history,
2940 - $relevant_content
2941 - );
3778 + if ($streaming) {
3779 + return $this->mxchat_generate_response_openai_stream(
3780 + $selected_model,
3781 + $api_key,
3782 + $conversation_history,
3783 + $relevant_content,
3784 + $session_id,
3785 + $testing_data // Pass testing data
3786 + );
3787 + } else {
3788 + $response = $this->mxchat_generate_response_openai(
3789 + $selected_model,
3790 + $api_key,
3791 + $conversation_history,
3792 + $relevant_content
3793 + );
3794 + }
3795 + break;
2942 3796 }
3797 +
3798 + // Check if the response is an error array from the provider-specific function
3799 + if (is_array($response) && isset($response['error'])) {
3800 + // Add testing data to error response if available
3801 + if ($testing_data !== null) {
3802 + $response['testing_data'] = $testing_data;
3803 + //error_log("MxChat Testing: Added testing data to provider error response");
3804 + }
3805 + return $response; // Pass through the error with testing data
3806 + }
3807 +
3808 + // For successful non-streaming responses, we don't add testing data here
3809 + // because it will be added in the main handler
3810 + return $response;
3811 +
2943 3812 } catch (Exception $e) {
2944 3813 //error_log('MXChat Error: ' . $e->getMessage());
2945 - return sprintf(
2946 - esc_html__('An error occurred: %s', 'mxchat'),
2947 - esc_html($e->getMessage())
2948 - );
3814 + $error_response = [
3815 + 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
3816 + 'error_code' => 'system_exception',
3817 + 'exception_details' => $e->getMessage()
3818 + ];
3819 +
3820 + // Add testing data to exception response if available
3821 + if ($testing_data !== null) {
3822 + $error_response['testing_data'] = $testing_data;
3823 + //error_log("MxChat Testing: Added testing data to exception response");
3824 + }
3825 +
3826 + return $error_response;
2949 3827 }
2950 3828 }
2951 3829
3830 +// 2. Update Claude streaming function
3831 +private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
3832 + try {
3833 + // Get system prompt instructions from options
3834 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
2952 3835
2953 -private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
2954 - // Ensure conversation_history is an array
2955 - if (!is_array($conversation_history)) {
2956 - $conversation_history = array();
2957 - }
3836 + // Ensure conversation_history is an array
3837 + if (!is_array($conversation_history)) {
3838 + $conversation_history = array();
3839 + }
2958 3840
2959 - // Get system prompt instructions from options
2960 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3841 + // Clean and validate conversation history
3842 + foreach ($conversation_history as &$message) {
3843 + // Convert bot and agent roles to assistant
3844 + if ($message['role'] === 'bot' || $message['role'] === 'agent') {
3845 + $message['role'] = 'assistant';
3846 + }
3847 +
3848 + // Remove unsupported roles - Claude only supports 'assistant' and 'user'
3849 + if (!in_array($message['role'], ['assistant', 'user'])) {
3850 + $message['role'] = 'user';
3851 + }
2961 3852
2962 - // Create a new array for the formatted conversation
2963 - $formatted_conversation = array();
3853 + // Ensure content field exists
3854 + if (!isset($message['content']) || empty($message['content'])) {
3855 + $message['content'] = '';
3856 + }
2964 3857
2965 - // Add system message first
2966 - $formatted_conversation[] = array(
2967 - 'role' => 'system',
2968 - 'content' => $system_prompt_instructions . " " . $relevant_content
2969 - );
3858 + // Remove any unsupported fields
3859 + $message = array_intersect_key($message, array_flip(['role', 'content']));
3860 + }
2970 3861
2971 - // Add the rest of the conversation history
2972 - foreach ($conversation_history as $message) {
2973 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
2974 - $role = $message['role'];
3862 + // Add relevant content as the latest user message
3863 + $conversation_history[] = [
3864 + 'role' => 'user',
3865 + 'content' => $relevant_content
3866 + ];
2975 3867
2976 - // Convert roles to supported format
2977 - if ($role === 'bot' || $role === 'agent') {
2978 - $role = 'assistant';
3868 + // Prepare the request body with stream: true
3869 + $body = json_encode([
3870 + 'model' => $selected_model,
3871 + 'messages' => $conversation_history,
3872 + 'max_tokens' => 1000,
3873 + 'temperature' => 0.8,
3874 + 'system' => $system_prompt_instructions,
3875 + 'stream' => true
3876 + ]);
3877 +
3878 + // Check if we can actually stream (headers not sent, etc.)
3879 + if (headers_sent() || !function_exists('curl_init')) {
3880 + // Fallback to regular response with testing data
3881 + //error_log("MxChat: Streaming not possible, falling back to regular response");
3882 + $regular_response = $this->mxchat_generate_response_claude(
3883 + $selected_model,
3884 + $claude_api_key,
3885 + array_slice($conversation_history, 0, -1), // Remove the added content
3886 + $relevant_content
3887 + );
3888 +
3889 + // Return as JSON with testing data
3890 + $response_data = [
3891 + 'text' => $regular_response,
3892 + 'html' => '',
3893 + 'session_id' => $session_id
3894 + ];
3895 +
3896 + if ($testing_data !== null) {
3897 + $response_data['testing_data'] = $testing_data;
3898 + //error_log("MxChat Testing: Added testing data to Claude fallback response");
2979 3899 }
2980 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
2981 - $role = 'user';
3900 +
3901 + // Clear any streaming headers and send JSON
3902 + if (headers_sent() === false) {
3903 + header('Content-Type: application/json');
2982 3904 }
2983 -
2984 - $formatted_conversation[] = array(
2985 - 'role' => $role,
2986 - 'content' => $message['content']
2987 - );
3905 + echo json_encode($response_data);
3906 + return true; // Indicate we handled the response
2988 3907 }
2989 - }
2990 3908
2991 - $body = json_encode([
2992 - 'model' => $selected_model,
2993 - 'messages' => $formatted_conversation,
2994 - 'temperature' => 0.8,
2995 - 'stream' => false
2996 - ]);
3909 + // Use cURL for streaming support
3910 + $ch = curl_init();
3911 + curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
3912 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
3913 + curl_setopt($ch, CURLOPT_POST, true);
3914 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
3915 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
3916 + 'Content-Type: application/json',
3917 + 'x-api-key: ' . $claude_api_key,
3918 + 'anthropic-version: 2023-06-01'
3919 + ));
3920 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
3921 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
2997 3922
2998 - $args = [
2999 - 'body' => $body,
3000 - 'headers' => [
3001 - 'Content-Type' => 'application/json',
3002 - 'Authorization' => 'Bearer ' . $deepseek_api_key,
3003 - ],
3004 - 'timeout' => 60,
3005 - 'redirection' => 5,
3006 - 'blocking' => true,
3007 - 'httpversion' => '1.0',
3008 - 'sslverify' => true,
3009 - ];
3923 + $full_response = ''; // Accumulate full response for saving
3924 + $stream_started = false;
3010 3925
3011 - $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
3926 + // Buffer control for real-time streaming
3927 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
3928 + // Send testing data as the first event if available
3929 + if (!$stream_started && $testing_data !== null) {
3930 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
3931 + flush();
3932 + $stream_started = true;
3933 + //error_log("MxChat Testing: Sent testing data in Claude stream");
3934 + }
3935 +
3936 + // Process each chunk of data
3937 + $lines = explode("\n", $data);
3012 3938
3013 - if (is_wp_error($response)) {
3014 - //error_log('DeepSeek API Error: ' . $response->get_error_message());
3015 - return "Sorry, there was an error processing your request.";
3016 - }
3939 + foreach ($lines as $line) {
3940 + if (trim($line) === '') {
3941 + continue;
3942 + }
3017 3943
3018 - $response_body = wp_remote_retrieve_body($response);
3019 - $decoded_response = json_decode($response_body, true);
3944 + // Claude uses event: and data: format
3945 + if (strpos($line, 'event: ') === 0) {
3946 + // Store the event type for the next data line
3947 + continue;
3948 + }
3020 3949
3021 - if (isset($decoded_response['choices'][0]['message']['content'])) {
3022 - return trim($decoded_response['choices'][0]['message']['content']);
3023 - } else {
3024 - //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
3025 - return "Sorry, I couldn't process that request.";
3026 - }
3027 -}
3950 + if (strpos($line, 'data: ') === 0) {
3951 + $json_str = substr($line, 6); // Remove 'data: ' prefix
3028 3952
3029 -private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
3030 - // Ensure conversation_history is an array
3031 - if (!is_array($conversation_history)) {
3032 - $conversation_history = array();
3033 - }
3953 + $json = json_decode($json_str, true);
3954 + if (json_last_error() !== JSON_ERROR_NONE) {
3955 + continue;
3956 + }
3034 3957
3035 - // Get system prompt instructions from options
3036 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3958 + // Handle different event types
3959 + if (isset($json['type'])) {
3960 + switch ($json['type']) {
3961 + case 'content_block_delta':
3962 + if (isset($json['delta']['text'])) {
3963 + $content = $json['delta']['text'];
3964 + $full_response .= $content; // Accumulate
3965 + // Send as SSE format compatible with your frontend
3966 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
3967 + flush();
3968 + }
3969 + break;
3037 3970
3038 - // Create a new array for the formatted conversation
3039 - $formatted_conversation = array();
3971 + case 'message_stop':
3972 + echo "data: [DONE]\n\n";
3973 + flush();
3974 + break;
3040 3975
3041 - // Add system message first
3042 - $formatted_conversation[] = array(
3043 - 'role' => 'system',
3044 - 'content' => $system_prompt_instructions . " " . $relevant_content
3045 - );
3976 + case 'error':
3977 + echo "data: " . json_encode(['error' => $json['error']['message'] ?? 'Unknown error']) . "\n\n";
3978 + flush();
3979 + break;
3980 + }
3981 + }
3982 + }
3983 + }
3046 3984
3047 - // Add the rest of the conversation history
3048 - foreach ($conversation_history as $message) {
3049 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3050 - $role = $message['role'];
3985 + return strlen($data);
3986 + });
3051 3987
3052 - // Convert roles to supported format
3053 - if ($role === 'bot' || $role === 'agent') {
3054 - $role = 'assistant';
3055 - }
3056 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3057 - $role = 'user';
3058 - }
3988 + $response = curl_exec($ch);
3989 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
3059 3990
3060 - $formatted_conversation[] = array(
3061 - 'role' => $role,
3062 - 'content' => $message['content']
3063 - );
3991 + if (curl_errno($ch)) {
3992 + curl_close($ch);
3993 + throw new Exception('cURL Error: ' . curl_error($ch));
3064 3994 }
3065 - }
3066 3995
3067 - $body = json_encode([
3068 - 'model' => $selected_model,
3069 - 'messages' => $formatted_conversation,
3070 - 'temperature' => 0.8,
3071 - 'stream' => false
3072 - ]);
3996 + curl_close($ch);
3073 3997
3074 - $args = [
3075 - 'body' => $body,
3076 - 'headers' => [
3077 - 'Content-Type' => 'application/json',
3078 - 'Authorization' => 'Bearer ' . $api_key,
3079 - ],
3080 - 'timeout' => 60,
3081 - 'redirection' => 5,
3082 - 'blocking' => true,
3083 - 'httpversion' => '1.0',
3084 - 'sslverify' => true,
3085 - ];
3998 + if ($http_code !== 200) {
3999 + // Fallback to regular response
4000 + //error_log("MxChat: Claude streaming failed with HTTP $http_code, falling back");
4001 + $regular_response = $this->mxchat_generate_response_claude(
4002 + $selected_model,
4003 + $claude_api_key,
4004 + array_slice($conversation_history, 0, -1), // Remove the added content
4005 + $relevant_content
4006 + );
4007 +
4008 + $response_data = [
4009 + 'text' => $regular_response,
4010 + 'html' => '',
4011 + 'session_id' => $session_id
4012 + ];
4013 +
4014 + if ($testing_data !== null) {
4015 + $response_data['testing_data'] = $testing_data;
4016 + //error_log("MxChat Testing: Added testing data to Claude error fallback");
4017 + }
4018 +
4019 + header('Content-Type: application/json');
4020 + echo json_encode($response_data);
4021 + return true;
4022 + }
3086 4023
3087 - $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
4024 + // Save the complete response to maintain chat persistence
4025 + if (!empty($full_response) && !empty($session_id)) {
4026 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
4027 + }
3088 4028
3089 - if (is_wp_error($response)) {
3090 - //error_log('OpenAI API Error: ' . $response->get_error_message());
3091 - return "Sorry, there was an error processing your request.";
3092 - }
4029 + return true; // Indicate streaming completed successfully
3093 4030
3094 - $response_body = wp_remote_retrieve_body($response);
3095 - $decoded_response = json_decode($response_body, true);
3096 -
3097 - if (isset($decoded_response['choices'][0]['message']['content'])) {
3098 - return trim($decoded_response['choices'][0]['message']['content']);
3099 - } else {
3100 - //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
3101 - return "Sorry, I couldn't process that request.";
4031 + } catch (Exception $e) {
4032 + //error_log("MxChat Claude streaming exception: " . $e->getMessage());
4033 +
4034 + // Fallback to regular response on exception
4035 + $regular_response = $this->mxchat_generate_response_claude(
4036 + $selected_model,
4037 + $claude_api_key,
4038 + $conversation_history,
4039 + $relevant_content
4040 + );
4041 +
4042 + $response_data = [
4043 + 'text' => $regular_response,
4044 + 'html' => '',
4045 + 'session_id' => $session_id
4046 + ];
4047 +
4048 + if ($testing_data !== null) {
4049 + $response_data['testing_data'] = $testing_data;
4050 + //error_log("MxChat Testing: Added testing data to Claude exception fallback");
4051 + }
4052 +
4053 + header('Content-Type: application/json');
4054 + echo json_encode($response_data);
4055 + return true;
3102 4056 }
3103 4057 }
3104 -private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
3105 - // Get system prompt instructions from options
3106 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3107 4058
3108 - // Add system prompt to relevant content
3109 - $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
4059 +// 3. Update OpenAI streaming function similarly
4060 +private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4061 + try {
4062 + // Get system prompt instructions from options
4063 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4064 +
4065 + // Ensure conversation_history is an array
4066 + if (!is_array($conversation_history)) {
4067 + $conversation_history = array();
4068 + }
3110 4069
3111 - // Prepend system instructions to the conversation history
3112 - array_unshift($conversation_history, [
3113 - 'role' => 'system',
3114 - 'content' => "Here are your instructions: " . $content_with_instructions
3115 - ]);
4070 + // Format conversation history for OpenAI
4071 + $formatted_conversation = array();
3116 4072
3117 - // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3118 - foreach ($conversation_history as &$message) {
3119 - if ($message['role'] === 'bot') {
3120 - $message['role'] = 'assistant';
3121 - } elseif ($message['role'] === 'agent') {
3122 - // Tag the message as coming from a live agent
3123 - $message['role'] = 'assistant';
3124 - if (!isset($message['metadata'])) {
3125 - $message['metadata'] = ['source' => 'live_agent'];
4073 + $formatted_conversation[] = array(
4074 + 'role' => 'system',
4075 + 'content' => $system_prompt_instructions . " " . $relevant_content
4076 + );
4077 +
4078 + foreach ($conversation_history as $message) {
4079 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4080 + $role = $message['role'];
4081 + if ($role === 'bot' || $role === 'agent') {
4082 + $role = 'assistant';
4083 + }
4084 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4085 + $role = 'user';
4086 + }
4087 + $formatted_conversation[] = array(
4088 + 'role' => $role,
4089 + 'content' => $message['content']
4090 + );
3126 4091 }
3127 4092 }
3128 4093
3129 - // Ensure all roles are valid
3130 - if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3131 - $message['role'] = 'user'; // Default to 'user'
4094 + // Check if we can actually stream
4095 + if (headers_sent() || !function_exists('curl_init')) {
4096 + // Fallback to regular response with testing data
4097 + //error_log("MxChat: OpenAI streaming not possible, falling back to regular response");
4098 + $regular_response = $this->mxchat_generate_response_openai(
4099 + $selected_model,
4100 + $api_key,
4101 + $conversation_history,
4102 + $relevant_content
4103 + );
4104 +
4105 + $response_data = [
4106 + 'text' => $regular_response,
4107 + 'html' => '',
4108 + 'session_id' => $session_id
4109 + ];
4110 +
4111 + if ($testing_data !== null) {
4112 + $response_data['testing_data'] = $testing_data;
4113 + //error_log("MxChat Testing: Added testing data to OpenAI fallback response");
4114 + }
4115 +
4116 + header('Content-Type: application/json');
4117 + echo json_encode($response_data);
4118 + return true;
3132 4119 }
3133 - }
3134 4120
4121 + // Prepare the request body with stream: true
4122 + $body = json_encode([
4123 + 'model' => $selected_model,
4124 + 'messages' => $formatted_conversation,
4125 + 'temperature' => 0.8,
4126 + 'stream' => true
4127 + ]);
3135 4128
3136 - // Build the request body
3137 - $body = json_encode([
3138 - 'model' => $selected_model,
3139 - 'messages' => $conversation_history,
3140 - 'temperature' => 0.8,
3141 - 'stream' => false
3142 - ]);
3143 -
3144 - // Set up the API request
3145 - $args = [
3146 - 'body' => $body,
3147 - 'headers' => [
3148 - 'Content-Type' => 'application/json',
3149 - 'Authorization' => 'Bearer ' . $xai_api_key,
3150 - ],
3151 - 'timeout' => 60,
3152 - 'redirection' => 5,
3153 - 'blocking' => true,
3154 - 'httpversion' => '1.0',
3155 - 'sslverify' => true,
3156 - ];
3157 -
3158 - // Make the API request
3159 - $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
3160 -
3161 - // Process the response
3162 - if (is_wp_error($response)) {
3163 - return "Sorry, there was an error processing your request.";
4129 + // Use cURL for streaming support
4130 + $ch = curl_init();
4131 + curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
4132 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4133 + curl_setopt($ch, CURLOPT_POST, true);
4134 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4135 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4136 + 'Content-Type: application/json',
4137 + 'Authorization: Bearer ' . $api_key
4138 + ));
4139 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4140 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
4141 +
4142 + $full_response = ''; // Accumulate full response for saving
4143 + $stream_started = false;
4144 +
4145 + // Buffer control for real-time streaming
4146 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
4147 + // Send testing data as the first event if available
4148 + if (!$stream_started && $testing_data !== null) {
4149 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4150 + flush();
4151 + $stream_started = true;
4152 + //error_log("MxChat Testing: Sent testing data in OpenAI stream");
4153 + }
4154 +
4155 + // Process each chunk of data
4156 + $lines = explode("\n", $data);
4157 +
4158 + foreach ($lines as $line) {
4159 + if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
4160 + continue;
4161 + }
4162 +
4163 + $json_str = substr($line, 6); // Remove 'data: ' prefix
4164 +
4165 + if ($json_str === '[DONE]') {
4166 + echo "data: [DONE]\n\n";
4167 + flush();
4168 + continue;
4169 + }
4170 +
4171 + $json = json_decode($json_str, true);
4172 + if (isset($json['choices'][0]['delta']['content'])) {
4173 + $content = $json['choices'][0]['delta']['content'];
4174 + $full_response .= $content; // Accumulate
4175 + // Send as SSE format
4176 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
4177 + flush();
4178 + }
4179 + }
4180 +
4181 + return strlen($data);
4182 + });
4183 +
4184 + $response = curl_exec($ch);
4185 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4186 +
4187 + if (curl_errno($ch) || $http_code !== 200) {
4188 + curl_close($ch);
4189 +
4190 + // Fallback to regular response
4191 + //error_log("MxChat: OpenAI streaming failed, falling back");
4192 + $regular_response = $this->mxchat_generate_response_openai(
4193 + $selected_model,
4194 + $api_key,
4195 + $conversation_history,
4196 + $relevant_content
4197 + );
4198 +
4199 + $response_data = [
4200 + 'text' => $regular_response,
4201 + 'html' => '',
4202 + 'session_id' => $session_id
4203 + ];
4204 +
4205 + if ($testing_data !== null) {
4206 + $response_data['testing_data'] = $testing_data;
4207 + //error_log("MxChat Testing: Added testing data to OpenAI error fallback");
4208 + }
4209 +
4210 + header('Content-Type: application/json');
4211 + echo json_encode($response_data);
4212 + return true;
4213 + }
4214 +
4215 + curl_close($ch);
4216 +
4217 + // Save the complete response to maintain chat persistence
4218 + if (!empty($full_response) && !empty($session_id)) {
4219 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
4220 + }
4221 +
4222 + return true; // Indicate streaming completed successfully
4223 +
4224 + } catch (Exception $e) {
4225 + //error_log("MxChat OpenAI streaming exception: " . $e->getMessage());
4226 +
4227 + // Fallback to regular response
4228 + $regular_response = $this->mxchat_generate_response_openai(
4229 + $selected_model,
4230 + $api_key,
4231 + $conversation_history,
4232 + $relevant_content
4233 + );
4234 +
4235 + $response_data = [
4236 + 'text' => $regular_response,
4237 + 'html' => '',
4238 + 'session_id' => $session_id
4239 + ];
4240 +
4241 + if ($testing_data !== null) {
4242 + $response_data['testing_data'] = $testing_data;
4243 + //error_log("MxChat Testing: Added testing data to OpenAI exception fallback");
4244 + }
4245 +
4246 + header('Content-Type: application/json');
4247 + echo json_encode($response_data);
4248 + return true;
3164 4249 }
3165 -
3166 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
3167 -
3168 - if (isset($response_body['choices'][0]['message']['content'])) {
3169 - return trim($response_body['choices'][0]['message']['content']);
3170 - } else {
3171 - return "Sorry, I couldn't process that request.";
3172 - }
3173 4250 }
3174 4251
3175 4252 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
3176 4253 // Get system prompt instructions from options
@@ -3271,8 +4348,846 @@
3271 4348 // Log unexpected response format
3272 4349 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
3273 4350 return "Sorry, I received an unexpected response format from the API.";
3274 4351 }
4352 +private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
4353 + try {
4354 + // Ensure conversation_history is an array
4355 + if (!is_array($conversation_history)) {
4356 + $conversation_history = array();
4357 + }
4358 +
4359 + // Get system prompt instructions from options
4360 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4361 +
4362 + // Create a new array for the formatted conversation
4363 + $formatted_conversation = array();
4364 +
4365 + // Add system message first
4366 + $formatted_conversation[] = array(
4367 + 'role' => 'system',
4368 + 'content' => $system_prompt_instructions . " " . $relevant_content
4369 + );
4370 +
4371 + // Add the rest of the conversation history
4372 + foreach ($conversation_history as $message) {
4373 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4374 + $role = $message['role'];
4375 +
4376 + // Convert roles to supported format
4377 + if ($role === 'bot' || $role === 'agent') {
4378 + $role = 'assistant';
4379 + }
4380 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4381 + $role = 'user';
4382 + }
4383 +
4384 + $formatted_conversation[] = array(
4385 + 'role' => $role,
4386 + 'content' => $message['content']
4387 + );
4388 + }
4389 + }
4390 +
4391 + $body = json_encode([
4392 + 'model' => $selected_model,
4393 + 'messages' => $formatted_conversation,
4394 + 'temperature' => 0.8,
4395 + 'stream' => false
4396 + ]);
4397 +
4398 + $args = [
4399 + 'body' => $body,
4400 + 'headers' => [
4401 + 'Content-Type' => 'application/json',
4402 + 'Authorization' => 'Bearer ' . $api_key,
4403 + ],
4404 + 'timeout' => 60,
4405 + 'redirection' => 5,
4406 + 'blocking' => true,
4407 + 'httpversion' => '1.0',
4408 + 'sslverify' => true,
4409 + ];
4410 +
4411 + $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
4412 +
4413 + if (is_wp_error($response)) {
4414 + $error_message = $response->get_error_message();
4415 + //error_log('OpenAI API Error: ' . $error_message);
4416 + return [
4417 + 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
4418 + 'error_code' => 'openai_connection_error',
4419 + 'provider' => 'openai'
4420 + ];
4421 + }
4422 +
4423 + $status_code = wp_remote_retrieve_response_code($response);
4424 + if ($status_code !== 200) {
4425 + $response_body = wp_remote_retrieve_body($response);
4426 + $decoded_response = json_decode($response_body, true);
4427 +
4428 + $error_message = isset($decoded_response['error']['message'])
4429 + ? $decoded_response['error']['message']
4430 + : 'HTTP Error ' . $status_code;
4431 +
4432 + $error_type = isset($decoded_response['error']['type'])
4433 + ? $decoded_response['error']['type']
4434 + : 'unknown';
4435 +
4436 + //error_log('OpenAI API HTTP Error: ' . $status_code . ' - ' . $error_message);
4437 +
4438 + // Handle specific error types
4439 + switch ($error_type) {
4440 + case 'invalid_request_error':
4441 + if (strpos($error_message, 'API key') !== false) {
4442 + return [
4443 + 'error' => esc_html__('Invalid OpenAI API key. Please check your API key configuration.', 'mxchat'),
4444 + 'error_code' => 'openai_invalid_api_key',
4445 + 'provider' => 'openai'
4446 + ];
4447 + }
4448 + break;
4449 +
4450 + case 'authentication_error':
4451 + return [
4452 + 'error' => esc_html__('Authentication failed with OpenAI. Please check your API key.', 'mxchat'),
4453 + 'error_code' => 'openai_auth_error',
4454 + 'provider' => 'openai'
4455 + ];
4456 +
4457 + case 'rate_limit_exceeded':
4458 + return [
4459 + 'error' => esc_html__('OpenAI rate limit exceeded. Please try again later.', 'mxchat'),
4460 + 'error_code' => 'openai_rate_limit',
4461 + 'provider' => 'openai'
4462 + ];
4463 +
4464 + case 'quota_exceeded':
4465 + return [
4466 + 'error' => esc_html__('OpenAI API quota exceeded. Please check your billing details.', 'mxchat'),
4467 + 'error_code' => 'openai_quota_exceeded',
4468 + 'provider' => 'openai'
4469 + ];
4470 + }
4471 +
4472 + // Generic error fallback
4473 + return [
4474 + 'error' => esc_html__('OpenAI API error: ', 'mxchat') . esc_html($error_message),
4475 + 'error_code' => 'openai_api_error',
4476 + 'provider' => 'openai',
4477 + 'status_code' => $status_code
4478 + ];
4479 + }
4480 +
4481 + $response_body = wp_remote_retrieve_body($response);
4482 + $decoded_response = json_decode($response_body, true);
4483 +
4484 + if (isset($decoded_response['choices'][0]['message']['content'])) {
4485 + return trim($decoded_response['choices'][0]['message']['content']);
4486 + } else {
4487 + //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
4488 + return [
4489 + 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
4490 + 'error_code' => 'openai_response_format_error',
4491 + 'provider' => 'openai'
4492 + ];
4493 + }
4494 + } catch (Exception $e) {
4495 + //error_log('OpenAI Exception: ' . $e->getMessage());
4496 + return [
4497 + 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
4498 + 'error_code' => 'openai_exception',
4499 + 'provider' => 'openai'
4500 + ];
4501 + }
4502 +}
4503 +private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
4504 + try {
4505 + // Get system prompt instructions from options
4506 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4507 +
4508 + // Add system prompt to relevant content
4509 + $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
4510 +
4511 + // Prepend system instructions to the conversation history
4512 + array_unshift($conversation_history, [
4513 + 'role' => 'system',
4514 + 'content' => "Here are your instructions: " . $content_with_instructions
4515 + ]);
4516 +
4517 + // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
4518 + foreach ($conversation_history as &$message) {
4519 + if ($message['role'] === 'bot') {
4520 + $message['role'] = 'assistant';
4521 + } elseif ($message['role'] === 'agent') {
4522 + // Tag the message as coming from a live agent
4523 + $message['role'] = 'assistant';
4524 + if (!isset($message['metadata'])) {
4525 + $message['metadata'] = ['source' => 'live_agent'];
4526 + }
4527 + }
4528 +
4529 + // Ensure all roles are valid
4530 + if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
4531 + $message['role'] = 'user'; // Default to 'user'
4532 + }
4533 + }
4534 +
4535 + // Build the request body
4536 + $body = json_encode([
4537 + 'model' => $selected_model,
4538 + 'messages' => $conversation_history,
4539 + 'temperature' => 0.8,
4540 + 'stream' => false
4541 + ]);
4542 +
4543 + // Set up the API request
4544 + $args = [
4545 + 'body' => $body,
4546 + 'headers' => [
4547 + 'Content-Type' => 'application/json',
4548 + 'Authorization' => 'Bearer ' . $xai_api_key,
4549 + ],
4550 + 'timeout' => 60,
4551 + 'redirection' => 5,
4552 + 'blocking' => true,
4553 + 'httpversion' => '1.0',
4554 + 'sslverify' => true,
4555 + ];
4556 +
4557 + // Make the API request
4558 + $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
4559 +
4560 + // Process the response
4561 + if (is_wp_error($response)) {
4562 + $error_message = $response->get_error_message();
4563 + //error_log('X.AI API Error: ' . $error_message);
4564 + return [
4565 + 'error' => esc_html__('Connection error when contacting X.AI: ', 'mxchat') . esc_html($error_message),
4566 + 'error_code' => 'xai_connection_error',
4567 + 'provider' => 'xai'
4568 + ];
4569 + }
4570 +
4571 + $status_code = wp_remote_retrieve_response_code($response);
4572 + if ($status_code !== 200) {
4573 + $response_body = wp_remote_retrieve_body($response);
4574 + $decoded_response = json_decode($response_body, true);
4575 +
4576 + // Log the full response for debugging
4577 + //error_log('X.AI Error Response: ' . print_r($decoded_response, true));
4578 +
4579 + // Extract error message from X.AI's specific format
4580 + $error_message = '';
4581 +
4582 + // Check for direct error string (as seen in your logs)
4583 + if (isset($decoded_response['error']) && is_string($decoded_response['error'])) {
4584 + $error_message = $decoded_response['error'];
4585 + }
4586 + // Check for nested error object (OpenAI style)
4587 + elseif (isset($decoded_response['error']['message'])) {
4588 + $error_message = $decoded_response['error']['message'];
4589 + }
4590 + // Check for top-level message
4591 + elseif (isset($decoded_response['message'])) {
4592 + $error_message = $decoded_response['message'];
4593 + }
4594 + // Fallback
4595 + else {
4596 + $error_message = 'HTTP Error ' . $status_code;
4597 + }
4598 +
4599 + //error_log('X.AI API HTTP Error: ' . $status_code . ' - ' . $error_message);
4600 +
4601 + // Check for API key errors using string matching
4602 + if (stripos($error_message, 'api key') !== false ||
4603 + stripos($error_message, 'incorrect api key') !== false ||
4604 + stripos($error_message, 'invalid api key') !== false) {
4605 + return [
4606 + 'error' => esc_html__('Invalid X.AI API key. Please check your API key configuration.', 'mxchat'),
4607 + 'error_code' => 'xai_invalid_api_key',
4608 + 'provider' => 'xai'
4609 + ];
4610 + }
4611 +
4612 + // Authentication errors
4613 + if ($status_code === 401 || $status_code === 403 ||
4614 + stripos($error_message, 'auth') !== false) {
4615 + return [
4616 + 'error' => esc_html__('Authentication failed with X.AI. Please check your API key.', 'mxchat'),
4617 + 'error_code' => 'xai_auth_error',
4618 + 'provider' => 'xai'
4619 + ];
4620 + }
4621 +
4622 + // Model errors
4623 + if (stripos($error_message, 'model') !== false) {
4624 + return [
4625 + 'error' => esc_html__('Invalid model specified for X.AI. Please check your model configuration.', 'mxchat'),
4626 + 'error_code' => 'xai_invalid_model',
4627 + 'provider' => 'xai'
4628 + ];
4629 + }
4630 +
4631 + // Rate limit errors
4632 + if ($status_code === 429 ||
4633 + stripos($error_message, 'rate') !== false ||
4634 + stripos($error_message, 'limit') !== false) {
4635 + return [
4636 + 'error' => esc_html__('X.AI rate limit exceeded. Please try again later.', 'mxchat'),
4637 + 'error_code' => 'xai_rate_limit',
4638 + 'provider' => 'xai'
4639 + ];
4640 + }
4641 +
4642 + // Quota errors
4643 + if (stripos($error_message, 'quota') !== false ||
4644 + stripos($error_message, 'billing') !== false) {
4645 + return [
4646 + 'error' => esc_html__('X.AI API quota exceeded. Please check your billing details.', 'mxchat'),
4647 + 'error_code' => 'xai_quota_exceeded',
4648 + 'provider' => 'xai'
4649 + ];
4650 + }
4651 +
4652 + // Server errors
4653 + if ($status_code >= 500) {
4654 + return [
4655 + 'error' => esc_html__('X.AI service is currently unavailable. Please try again later.', 'mxchat'),
4656 + 'error_code' => 'xai_service_unavailable',
4657 + 'provider' => 'xai'
4658 + ];
4659 + }
4660 +
4661 + // Generic error fallback with the actual error message
4662 + return [
4663 + 'error' => esc_html__('X.AI API error: ', 'mxchat') . esc_html($error_message),
4664 + 'error_code' => 'xai_api_error',
4665 + 'provider' => 'xai',
4666 + 'status_code' => $status_code
4667 + ];
4668 + }
4669 +
4670 + $response_body = wp_remote_retrieve_body($response);
4671 + $decoded_response = json_decode($response_body, true);
4672 +
4673 + if (isset($decoded_response['choices'][0]['message']['content'])) {
4674 + return trim($decoded_response['choices'][0]['message']['content']);
4675 + } else {
4676 + //error_log('X.AI API Response Format Error: ' . print_r($decoded_response, true));
4677 + return [
4678 + 'error' => esc_html__('Unexpected response format from X.AI.', 'mxchat'),
4679 + 'error_code' => 'xai_response_format_error',
4680 + 'provider' => 'xai'
4681 + ];
4682 + }
4683 +} catch (Exception $e) {
4684 + //error_log('X.AI Exception: ' . $e->getMessage());
4685 + return [
4686 + 'error' => esc_html__('System error when processing X.AI request: ', 'mxchat') . esc_html($e->getMessage()),
4687 + 'error_code' => 'xai_exception',
4688 + 'provider' => 'xai'
4689 + ];
4690 +}
4691 +}
4692 +private function mxchat_generate_response_xai_stream($selected_model, $xai_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4693 + try {
4694 + // Get system prompt instructions from options
4695 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4696 +
4697 + // Ensure conversation_history is an array
4698 + if (!is_array($conversation_history)) {
4699 + $conversation_history = array();
4700 + }
4701 +
4702 + // Format conversation history for X.AI (same as OpenAI format)
4703 + $formatted_conversation = array();
4704 +
4705 + $formatted_conversation[] = array(
4706 + 'role' => 'system',
4707 + 'content' => $system_prompt_instructions . " " . $relevant_content
4708 + );
4709 +
4710 + foreach ($conversation_history as $message) {
4711 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4712 + $role = $message['role'];
4713 + if ($role === 'bot' || $role === 'agent') {
4714 + $role = 'assistant';
4715 + }
4716 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4717 + $role = 'user';
4718 + }
4719 + $formatted_conversation[] = array(
4720 + 'role' => $role,
4721 + 'content' => $message['content']
4722 + );
4723 + }
4724 + }
4725 +
4726 + // Check if we can actually stream
4727 + if (headers_sent() || !function_exists('curl_init')) {
4728 + // Fallback to regular response with testing data
4729 + //error_log("MxChat: X.AI streaming not possible, falling back to regular response");
4730 + $regular_response = $this->mxchat_generate_response_xai(
4731 + $selected_model,
4732 + $xai_api_key,
4733 + $conversation_history,
4734 + $relevant_content
4735 + );
4736 +
4737 + $response_data = [
4738 + 'text' => $regular_response,
4739 + 'html' => '',
4740 + 'session_id' => $session_id
4741 + ];
4742 +
4743 + if ($testing_data !== null) {
4744 + $response_data['testing_data'] = $testing_data;
4745 + //error_log("MxChat Testing: Added testing data to X.AI fallback response");
4746 + }
4747 +
4748 + header('Content-Type: application/json');
4749 + echo json_encode($response_data);
4750 + return true;
4751 + }
4752 +
4753 + // Prepare the request body with stream: true
4754 + $body = json_encode([
4755 + 'model' => $selected_model,
4756 + 'messages' => $formatted_conversation,
4757 + 'temperature' => 0.8,
4758 + 'stream' => true
4759 + ]);
4760 +
4761 + // Use cURL for streaming support
4762 + $ch = curl_init();
4763 + curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
4764 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4765 + curl_setopt($ch, CURLOPT_POST, true);
4766 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4767 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4768 + 'Content-Type: application/json',
4769 + 'Authorization: Bearer ' . $xai_api_key
4770 + ));
4771 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4772 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
4773 +
4774 + $full_response = ''; // Accumulate full response for saving
4775 + $stream_started = false;
4776 +
4777 + // Buffer control for real-time streaming
4778 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
4779 + // Send testing data as the first event if available
4780 + if (!$stream_started && $testing_data !== null) {
4781 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4782 + flush();
4783 + $stream_started = true;
4784 + //error_log("MxChat Testing: Sent testing data in X.AI stream");
4785 + }
4786 +
4787 + // Process each chunk of data
4788 + $lines = explode("\n", $data);
4789 +
4790 + foreach ($lines as $line) {
4791 + if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
4792 + continue;
4793 + }
4794 +
4795 + $json_str = substr($line, 6); // Remove 'data: ' prefix
4796 +
4797 + if ($json_str === '[DONE]') {
4798 + echo "data: [DONE]\n\n";
4799 + flush();
4800 + continue;
4801 + }
4802 +
4803 + $json = json_decode($json_str, true);
4804 + if (isset($json['choices'][0]['delta']['content'])) {
4805 + $content = $json['choices'][0]['delta']['content'];
4806 + $full_response .= $content; // Accumulate
4807 + // Send as SSE format
4808 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
4809 + flush();
4810 + }
4811 + }
4812 +
4813 + return strlen($data);
4814 + });
4815 +
4816 + $response = curl_exec($ch);
4817 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4818 +
4819 + if (curl_errno($ch) || $http_code !== 200) {
4820 + curl_close($ch);
4821 +
4822 + // Fallback to regular response
4823 + //error_log("MxChat: X.AI streaming failed, falling back");
4824 + $regular_response = $this->mxchat_generate_response_xai(
4825 + $selected_model,
4826 + $xai_api_key,
4827 + $conversation_history,
4828 + $relevant_content
4829 + );
4830 +
4831 + $response_data = [
4832 + 'text' => $regular_response,
4833 + 'html' => '',
4834 + 'session_id' => $session_id
4835 + ];
4836 +
4837 + if ($testing_data !== null) {
4838 + $response_data['testing_data'] = $testing_data;
4839 + //error_log("MxChat Testing: Added testing data to X.AI error fallback");
4840 + }
4841 +
4842 + header('Content-Type: application/json');
4843 + echo json_encode($response_data);
4844 + return true;
4845 + }
4846 +
4847 + curl_close($ch);
4848 +
4849 + // Save the complete response to maintain chat persistence
4850 + if (!empty($full_response) && !empty($session_id)) {
4851 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
4852 + }
4853 +
4854 + return true; // Indicate streaming completed successfully
4855 +
4856 + } catch (Exception $e) {
4857 + //error_log("MxChat X.AI streaming exception: " . $e->getMessage());
4858 +
4859 + // Fallback to regular response
4860 + $regular_response = $this->mxchat_generate_response_xai(
4861 + $selected_model,
4862 + $xai_api_key,
4863 + $conversation_history,
4864 + $relevant_content
4865 + );
4866 +
4867 + $response_data = [
4868 + 'text' => $regular_response,
4869 + 'html' => '',
4870 + 'session_id' => $session_id
4871 + ];
4872 +
4873 + if ($testing_data !== null) {
4874 + $response_data['testing_data'] = $testing_data;
4875 + //error_log("MxChat Testing: Added testing data to X.AI exception fallback");
4876 + }
4877 +
4878 + header('Content-Type: application/json');
4879 + echo json_encode($response_data);
4880 + return true;
4881 + }
4882 +}
4883 +
4884 +private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
4885 + try {
4886 + // Ensure conversation_history is an array
4887 + if (!is_array($conversation_history)) {
4888 + $conversation_history = array();
4889 + }
4890 +
4891 + // Get system prompt instructions from options
4892 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4893 +
4894 + // Create a new array for the formatted conversation
4895 + $formatted_conversation = array();
4896 +
4897 + // Add system message first
4898 + $formatted_conversation[] = array(
4899 + 'role' => 'system',
4900 + 'content' => $system_prompt_instructions . " " . $relevant_content
4901 + );
4902 +
4903 + // Add the rest of the conversation history
4904 + foreach ($conversation_history as $message) {
4905 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4906 + $role = $message['role'];
4907 +
4908 + // Convert roles to supported format
4909 + if ($role === 'bot' || $role === 'agent') {
4910 + $role = 'assistant';
4911 + }
4912 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4913 + $role = 'user';
4914 + }
4915 +
4916 + $formatted_conversation[] = array(
4917 + 'role' => $role,
4918 + 'content' => $message['content']
4919 + );
4920 + }
4921 + }
4922 +
4923 + $body = json_encode([
4924 + 'model' => $selected_model,
4925 + 'messages' => $formatted_conversation,
4926 + 'temperature' => 0.8,
4927 + 'stream' => false
4928 + ]);
4929 +
4930 + $args = [
4931 + 'body' => $body,
4932 + 'headers' => [
4933 + 'Content-Type' => 'application/json',
4934 + 'Authorization' => 'Bearer ' . $deepseek_api_key,
4935 + ],
4936 + 'timeout' => 60,
4937 + 'redirection' => 5,
4938 + 'blocking' => true,
4939 + 'httpversion' => '1.0',
4940 + 'sslverify' => true,
4941 + ];
4942 +
4943 + $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
4944 +
4945 + if (is_wp_error($response)) {
4946 + $error_message = $response->get_error_message();
4947 + //error_log('DeepSeek API Error: ' . $error_message);
4948 + return [
4949 + 'error' => esc_html__('Connection error when contacting DeepSeek: ', 'mxchat') . esc_html($error_message),
4950 + 'error_code' => 'deepseek_connection_error',
4951 + 'provider' => 'deepseek'
4952 + ];
4953 + }
4954 +
4955 + $status_code = wp_remote_retrieve_response_code($response);
4956 + if ($status_code !== 200) {
4957 + $response_body = wp_remote_retrieve_body($response);
4958 + $decoded_response = json_decode($response_body, true);
4959 +
4960 + $error_message = isset($decoded_response['error']['message'])
4961 + ? $decoded_response['error']['message']
4962 + : 'HTTP Error ' . $status_code;
4963 +
4964 + $error_type = isset($decoded_response['error']['type'])
4965 + ? $decoded_response['error']['type']
4966 + : 'unknown';
4967 +
4968 + //error_log('DeepSeek API HTTP Error: ' . $status_code . ' - ' . $error_message);
4969 +
4970 + // Handle specific error types
4971 + switch ($status_code) {
4972 + case 401:
4973 + return [
4974 + 'error' => esc_html__('Authentication failed with DeepSeek. Please check your API key.', 'mxchat'),
4975 + 'error_code' => 'deepseek_auth_error',
4976 + 'provider' => 'deepseek'
4977 + ];
4978 +
4979 + case 400:
4980 + if (strpos($error_message, 'API key') !== false) {
4981 + return [
4982 + 'error' => esc_html__('Invalid DeepSeek API key. Please check your API key configuration.', 'mxchat'),
4983 + 'error_code' => 'deepseek_invalid_api_key',
4984 + 'provider' => 'deepseek'
4985 + ];
4986 + }
4987 + break;
4988 +
4989 + case 429:
4990 + if (strpos($error_message, 'quota') !== false) {
4991 + return [
4992 + 'error' => esc_html__('DeepSeek API quota exceeded. Please check your billing details.', 'mxchat'),
4993 + 'error_code' => 'deepseek_quota_exceeded',
4994 + 'provider' => 'deepseek'
4995 + ];
4996 + } else {
4997 + return [
4998 + 'error' => esc_html__('DeepSeek rate limit exceeded. Please try again later.', 'mxchat'),
4999 + 'error_code' => 'deepseek_rate_limit',
5000 + 'provider' => 'deepseek'
5001 + ];
5002 + }
5003 +
5004 + case 500:
5005 + case 502:
5006 + case 503:
5007 + case 504:
5008 + return [
5009 + 'error' => esc_html__('DeepSeek service is currently unavailable. Please try again later.', 'mxchat'),
5010 + 'error_code' => 'deepseek_service_unavailable',
5011 + 'provider' => 'deepseek'
5012 + ];
5013 + }
5014 +
5015 + // Generic error fallback
5016 + return [
5017 + 'error' => esc_html__('DeepSeek API error: ', 'mxchat') . esc_html($error_message),
5018 + 'error_code' => 'deepseek_api_error',
5019 + 'provider' => 'deepseek',
5020 + 'status_code' => $status_code
5021 + ];
5022 + }
5023 +
5024 + $response_body = wp_remote_retrieve_body($response);
5025 + $decoded_response = json_decode($response_body, true);
5026 +
5027 + if (isset($decoded_response['choices'][0]['message']['content'])) {
5028 + return trim($decoded_response['choices'][0]['message']['content']);
5029 + } else {
5030 + //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
5031 + return [
5032 + 'error' => esc_html__('Unexpected response format from DeepSeek.', 'mxchat'),
5033 + 'error_code' => 'deepseek_response_format_error',
5034 + 'provider' => 'deepseek'
5035 + ];
5036 + }
5037 + } catch (Exception $e) {
5038 + //error_log('DeepSeek Exception: ' . $e->getMessage());
5039 + return [
5040 + 'error' => esc_html__('System error when processing DeepSeek request: ', 'mxchat') . esc_html($e->getMessage()),
5041 + 'error_code' => 'deepseek_exception',
5042 + 'provider' => 'deepseek'
5043 + ];
5044 + }
5045 +}
5046 +
5047 +private function mxchat_generate_response_gemini($selected_model, $gemini_api_key, $conversation_history, $relevant_content) {
5048 + // Get system prompt instructions from options
5049 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5050 +
5051 + // Add system prompt to relevant content
5052 + $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
5053 +
5054 + // Format messages for Gemini API
5055 + $formatted_messages = [];
5056 +
5057 + // Add system message as the first user message with role prefix
5058 + // Note: Gemini doesn't have a dedicated system role, so we use a prefixed user message
5059 + $formatted_messages[] = [
5060 + 'role' => 'user',
5061 + 'parts' => [
5062 + ['text' => "[System Instructions] " . $content_with_instructions]
5063 + ]
5064 + ];
5065 +
5066 + // Add model response to acknowledge system instructions
5067 + $formatted_messages[] = [
5068 + 'role' => 'model',
5069 + 'parts' => [
5070 + ['text' => "I understand and will follow these instructions."]
5071 + ]
5072 + ];
5073 +
5074 + // Process the rest of the conversation history
5075 + $current_role = null;
5076 + $current_parts = [];
5077 +
5078 + foreach ($conversation_history as $message) {
5079 + // Skip the first system message as we already handled it
5080 + if ($message['role'] === 'system') {
5081 + continue;
5082 + }
5083 +
5084 + // Map roles to Gemini format
5085 + $gemini_role = '';
5086 + if ($message['role'] === 'user') {
5087 + $gemini_role = 'user';
5088 + } else if (in_array($message['role'], ['assistant', 'bot', 'agent'])) {
5089 + $gemini_role = 'model';
5090 + } else {
5091 + // Skip unsupported roles
5092 + continue;
5093 + }
5094 +
5095 + // If we have a new role, add the previous message
5096 + if ($current_role !== null && $current_role !== $gemini_role && !empty($current_parts)) {
5097 + $formatted_messages[] = [
5098 + 'role' => $current_role,
5099 + 'parts' => $current_parts
5100 + ];
5101 + $current_parts = [];
5102 + }
5103 +
5104 + // Set current role and add text to parts
5105 + $current_role = $gemini_role;
5106 + $current_parts[] = ['text' => $message['content']];
5107 + }
5108 +
5109 + // Add the last message if there's content
5110 + if ($current_role !== null && !empty($current_parts)) {
5111 + $formatted_messages[] = [
5112 + 'role' => $current_role,
5113 + 'parts' => $current_parts
5114 + ];
5115 + }
5116 +
5117 + // Build the request body
5118 + $body = json_encode([
5119 + 'contents' => $formatted_messages,
5120 + 'generationConfig' => [
5121 + 'temperature' => 0.7,
5122 + 'topP' => 0.95,
5123 + 'topK' => 40,
5124 + 'maxOutputTokens' => 8192,
5125 + ],
5126 + 'safetySettings' => [
5127 + [
5128 + 'category' => 'HARM_CATEGORY_HARASSMENT',
5129 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
5130 + ],
5131 + [
5132 + 'category' => 'HARM_CATEGORY_HATE_SPEECH',
5133 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
5134 + ],
5135 + [
5136 + 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
5137 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
5138 + ],
5139 + [
5140 + 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT',
5141 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
5142 + ]
5143 + ]
5144 + ]);
5145 +
5146 + // Prepare the API endpoint
5147 + $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
5148 +
5149 + // Set up the API request
5150 + $args = [
5151 + 'body' => $body,
5152 + 'headers' => [
5153 + 'Content-Type' => 'application/json',
5154 + ],
5155 + 'timeout' => 60,
5156 + 'redirection' => 5,
5157 + 'blocking' => true,
5158 + 'httpversion' => '1.0',
5159 + 'sslverify' => true,
5160 + ];
5161 +
5162 + // Make the API request
5163 + $response = wp_remote_post($api_endpoint, $args);
5164 +
5165 + // Process the response
5166 + if (is_wp_error($response)) {
5167 + return "Sorry, there was an error processing your request: " . $response->get_error_message();
5168 + }
5169 +
5170 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
5171 +
5172 + // Handle potential errors in the response
5173 + if (isset($response_body['error'])) {
5174 + //error_log('Gemini API Error: ' . json_encode($response_body['error']));
5175 + return "Sorry, there was an error with the Gemini API: " .
5176 + (isset($response_body['error']['message']) ? $response_body['error']['message'] : 'Unknown error');
5177 + }
5178 +
5179 + // Extract the response text
5180 + if (isset($response_body['candidates'][0]['content']['parts'][0]['text'])) {
5181 + return trim($response_body['candidates'][0]['content']['parts'][0]['text']);
5182 + } else {
5183 + //error_log('Unexpected Gemini API response format: ' . json_encode($response_body));
5184 + return "Sorry, I couldn't process that request. The response format was unexpected.";
5185 + }
5186 +}
5187 +
5188 +
5189 +
3275 5190 public function mxchat_dismiss_pre_chat_message() {
3276 5191 // Get and sanitize the user identifier
3277 5192 $user_id = $this->mxchat_get_user_identifier();
3278 5193 $user_id = sanitize_key($user_id);
@@ -3328,11 +5243,10 @@
3328 5243 }
3329 5244
3330 5245 public function mxchat_enqueue_scripts_styles() {
3331 5246 // Define version numbers for the styles and scripts
3332 - $chat_style_version = '2.0.5'; // Replace with your actual version
3333 - $chat_script_version = '2.0.5'; // Replace with your actual version
3334 -
5247 + $chat_style_version = '2.3.2';
5248 + $chat_script_version = '2.3.2';
3335 5249 // Enqueue the script
3336 5250 wp_enqueue_script(
3337 5251 'mxchat-chat-js',
3338 5252 plugin_dir_url(__FILE__) . '../js/chat-script.js',
@@ -3339,9 +5253,8 @@
3339 5253 array('jquery'),
3340 5254 $chat_script_version,
3341 5255 true
3342 5256 );
3343 -
3344 5257 // Enqueue the CSS
3345 5258 wp_enqueue_style(
3346 5259 'mxchat-chat-css',
3347 5260 plugin_dir_url(__FILE__) . '../css/chat-style.css',
@@ -3347,17 +5260,19 @@
3347 5260 plugin_dir_url(__FILE__) . '../css/chat-style.css',
3348 5261 array(),
3349 5262 $chat_style_version
3350 5263 );
3351 -
3352 5264 // Fetch options from the database
3353 5265 $this->options = get_option('mxchat_options');
3354 5266 $prompts_options = get_option('mxchat_prompts_options', array());
3355 -
5267 +
3356 5268 // Prepare settings for JavaScript
3357 5269 $style_settings = array(
3358 5270 'ajax_url' => admin_url('admin-ajax.php'),
3359 5271 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
5272 + 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-4o',
5273 + 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
5274 + 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off', // ADD THIS LINE
3360 5275 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
3361 5276 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
3362 5277 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
3363 5278 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
@@ -3371,10 +5286,9 @@
3371 5286 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
3372 5287 'icon_color' => $this->options['icon_color'] ?? '#fff',
3373 5288 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
3374 5289 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
3375 - 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off', // Example for consistency
3376 -
5290 + 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
3377 5291 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
3378 5292 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
3379 5293 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
3380 5294 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
@@ -3379,76 +5293,795 @@
3379 5293 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
3380 5294 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
3381 5295 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
3382 5296 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
3383 -
3384 5297 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
3385 5298 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
3386 5299 );
3387 -
3388 5300 // Pass the settings to the script
3389 5301 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
3390 5302 }
3391 5303
3392 5304
5305 +/**
5306 + * Setup the cron jobs for rate limits with guard against multiple calls
5307 + */
5308 +public function setup_rate_limit_cron_jobs() {
5309 + // Add a guard to prevent multiple rapid calls
5310 + $last_setup = get_transient('mxchat_cron_setup_guard');
5311 + if ($last_setup && (time() - $last_setup) < 60) {
5312 + // Don't run again if we ran less than 60 seconds ago
5313 + return;
5314 + }
5315 +
5316 + // Set the guard
5317 + set_transient('mxchat_cron_setup_guard', time(), 300); // 5 minutes
5318 +
5319 + try {
5320 + // First, check if WordPress cron is disabled
5321 + if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
5322 + error_log('MxChat: WordPress cron is disabled (DISABLE_WP_CRON = true), using fallback system');
5323 + $this->setup_fallback_rate_limit_system();
5324 + return;
5325 + }
5326 +
5327 + // Check if cron is already scheduled - if so, don't mess with it
5328 + if (wp_next_scheduled('mxchat_reset_rate_limits')) {
5329 + error_log('MxChat: Rate limit cron already scheduled, skipping setup');
5330 + return;
5331 + }
5332 +
5333 + // Clear any orphaned hooks (but don't loop indefinitely)
5334 + $hooks_to_clear = [
5335 + 'mxchat_reset_rate_limits',
5336 + 'mxchat_reset_hourly_rate_limits',
5337 + 'mxchat_reset_daily_rate_limits',
5338 + 'mxchat_reset_weekly_rate_limits',
5339 + 'mxchat_reset_monthly_rate_limits'
5340 + ];
5341 +
5342 + foreach ($hooks_to_clear as $hook) {
5343 + // Only clear a maximum of 3 instances to prevent infinite loops
5344 + $cleared = 0;
5345 + while (wp_next_scheduled($hook) && $cleared < 3) {
5346 + wp_clear_scheduled_hook($hook);
5347 + $cleared++;
5348 + }
5349 + }
5350 +
5351 + // Small delay after clearing
5352 + usleep(100000); // 0.1 seconds
5353 +
5354 + // Try to schedule the event
5355 + $initial_time = time() + 300; // Start in 5 minutes
5356 + $result = wp_schedule_event($initial_time, 'hourly', 'mxchat_reset_rate_limits');
5357 +
5358 + if ($result === false) {
5359 + error_log('MxChat: Failed to schedule cron, using fallback system');
5360 + $this->setup_fallback_rate_limit_system();
5361 + } else {
5362 + error_log('MxChat: Successfully scheduled rate limit reset cron');
5363 + }
5364 +
5365 + } catch (Exception $e) {
5366 + error_log('MxChat: Cron setup exception: ' . $e->getMessage());
5367 + $this->setup_fallback_rate_limit_system();
5368 + }
5369 +}
5370 +
5371 +/**
5372 + * Try alternative cron scheduling methods
5373 + */
5374 +private function try_alternative_cron_scheduling($initial_time) {
5375 + try {
5376 + // Method 1: Try with current time instead of future time
5377 + $result1 = wp_schedule_event(time(), 'hourly', 'mxchat_reset_rate_limits');
5378 + if ($result1 !== false) {
5379 + error_log('MxChat: Alternative method 1 (current time) succeeded');
5380 + return true;
5381 + }
5382 +
5383 + // Method 2: Try with a different interval
5384 + $result2 = wp_schedule_event($initial_time, 'daily', 'mxchat_reset_rate_limits');
5385 + if ($result2 !== false) {
5386 + error_log('MxChat: Alternative method 2 (daily interval) succeeded');
5387 + return true;
5388 + }
5389 +
5390 + // Method 3: Try wp_schedule_single_event first, then recurring
5391 + $result3 = wp_schedule_single_event($initial_time, 'mxchat_reset_rate_limits');
5392 + if ($result3 !== false) {
5393 + error_log('MxChat: Alternative method 3 (single event) succeeded');
5394 + // Schedule the next one manually in the handler
5395 + return true;
5396 + }
5397 +
5398 + return false;
5399 +
5400 + } catch (Exception $e) {
5401 + error_log('MxChat: Alternative cron scheduling exception: ' . $e->getMessage());
5402 + return false;
5403 + }
5404 +}
5405 +
5406 +/**
5407 + * Enhanced fallback rate limit system
5408 + */
5409 +private function setup_fallback_rate_limit_system() {
5410 + // Set a flag to use database-based rate limit cleanup
5411 + update_option('mxchat_use_fallback_rate_limits', true);
5412 +
5413 + // Schedule a one-time check to happen on the next plugin load
5414 + update_option('mxchat_next_rate_limit_check', time() + 3600);
5415 +
5416 + // Also set up a more frequent fallback check (every 4 hours)
5417 + update_option('mxchat_fallback_check_interval', 4 * 3600);
5418 +
5419 + error_log('MxChat: Fallback rate limit system activated');
5420 +}
5421 +
5422 +/**
5423 + * Enhanced fallback check method
5424 + */
5425 +public function check_fallback_rate_limits() {
5426 + $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
5427 +
5428 + if (!$use_fallback) {
5429 + return; // Regular cron is working
5430 + }
5431 +
5432 + $next_check = get_option('mxchat_next_rate_limit_check', 0);
5433 + $check_interval = get_option('mxchat_fallback_check_interval', 3600);
5434 +
5435 + if (time() >= $next_check) {
5436 + error_log('MxChat: Running fallback rate limit cleanup');
5437 + $this->mxchat_reset_rate_limits();
5438 +
5439 + // Schedule next check
5440 + update_option('mxchat_next_rate_limit_check', time() + $check_interval);
5441 + }
5442 +}
5443 +/**
5444 + * Enhanced rate limit check that includes fallback cleanup
5445 + */
5446 +public function check_rate_limit() {
5447 + // Check if we need to run fallback cleanup
5448 + $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
5449 + $next_check = get_option('mxchat_next_rate_limit_check', 0);
5450 +
5451 + if ($use_fallback && time() >= $next_check) {
5452 + $this->mxchat_reset_rate_limits();
5453 + update_option('mxchat_next_rate_limit_check', time() + 3600); // Next hour
5454 + }
5455 +
5456 + // Continue with your existing rate limit logic...
5457 + $all_options = get_option('mxchat_options', []);
5458 +
5459 + // Determine user role or if logged out
5460 + if (is_user_logged_in()) {
5461 + $user = wp_get_current_user();
5462 + $user_id = $user->ID;
5463 +
5464 + // Get the user's primary role using reset() to safely get the first element
5465 + $user_roles = $user->roles;
5466 +
5467 + // Safely get the first role regardless of array key structure
5468 + if (!empty($user_roles) && is_array($user_roles)) {
5469 + $role = reset($user_roles); // This safely gets the first element regardless of key
5470 + } else {
5471 + $role = 'subscriber'; // Default to subscriber if no role found
5472 + }
5473 + } else {
5474 + $role = 'logged_out';
5475 + // Use IP address for non-logged-in users
5476 + $user_id = $this->get_client_ip();
5477 + }
5478 +
5479 + // Check if rate limits are configured for this role
5480 + if (!isset($all_options['rate_limits'][$role])) {
5481 + return true; // No limit set for this role
5482 + }
5483 +
5484 + $limit = $all_options['rate_limits'][$role]['limit'];
5485 +
5486 + // If unlimited, return true immediately
5487 + if ($limit === 'unlimited') {
5488 + return true;
5489 + }
5490 +
5491 + // Get the option name for this user/role with safer naming
5492 + $safe_role = preg_replace('/[^a-zA-Z0-9_]/', '_', $role);
5493 + $safe_user_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $user_id);
5494 + $option_name = 'mxchat_chat_limit_' . $safe_role . '_' . $safe_user_id;
5495 +
5496 + // Get the counter data
5497 + $limit_data = get_option($option_name, ['count' => 0, 'timestamp' => time()]);
5498 +
5499 + // If first request or counter reset needed, set the initial timestamp
5500 + if ($limit_data['count'] === 0) {
5501 + $limit_data['timestamp'] = time();
5502 + update_option($option_name, $limit_data);
5503 + }
5504 +
5505 + // Get the timeframe
5506 + $timeframe = isset($all_options['rate_limits'][$role]['timeframe']) ?
5507 + $all_options['rate_limits'][$role]['timeframe'] : 'daily';
5508 +
5509 + // Check if the counter needs to be reset based on timeframe
5510 + $current_time = time();
5511 + $timestamp = $limit_data['timestamp'];
5512 + $should_reset = false;
5513 +
5514 + switch ($timeframe) {
5515 + case 'hourly':
5516 + $should_reset = ($current_time - $timestamp) >= 3600; // 1 hour
5517 + break;
5518 + case 'daily':
5519 + $should_reset = ($current_time - $timestamp) >= 86400; // 24 hours
5520 + break;
5521 + case 'weekly':
5522 + $should_reset = ($current_time - $timestamp) >= 604800; // 7 days
5523 + break;
5524 + case 'monthly':
5525 + $should_reset = ($current_time - $timestamp) >= 2592000; // 30 days
5526 + break;
5527 + }
5528 +
5529 + // Reset the counter if the timeframe has passed
5530 + if ($should_reset) {
5531 + $limit_data = ['count' => 0, 'timestamp' => $current_time];
5532 + update_option($option_name, $limit_data);
5533 + }
5534 +
5535 + // Check if user has exceeded their limit
5536 + if ($limit_data['count'] >= intval($limit)) {
5537 + // Get the custom message for this role
5538 + $message = !empty($all_options['rate_limits'][$role]['message'])
5539 + ? $all_options['rate_limits'][$role]['message']
5540 + : __('Rate limit exceeded. Please try again later.', 'mxchat');
5541 +
5542 + // Add timeframe information to the message if placeholders exist
5543 + $timeframe_label = '';
5544 + switch ($timeframe) {
5545 + case 'hourly':
5546 + $timeframe_label = __('hour', 'mxchat');
5547 + break;
5548 + case 'daily':
5549 + $timeframe_label = __('day', 'mxchat');
5550 + break;
5551 + case 'weekly':
5552 + $timeframe_label = __('week', 'mxchat');
5553 + break;
5554 + case 'monthly':
5555 + $timeframe_label = __('month', 'mxchat');
5556 + break;
5557 + }
5558 +
5559 + // Replace placeholders in the message
5560 + $message = str_replace(
5561 + ['{limit}', '{count}', '{remaining}', '{timeframe}'],
5562 + [intval($limit), $limit_data['count'], max(0, intval($limit) - $limit_data['count']), $timeframe_label],
5563 + $message
5564 + );
5565 +
5566 + // Process HTML links in the message
5567 + $message = $this->process_rate_limit_message_html($message);
5568 +
5569 + // Return error with the processed message
5570 + return [
5571 + 'error' => true,
5572 + 'message' => $message
5573 + ];
5574 + }
5575 +
5576 + // Increment the counter
5577 + $limit_data['count']++;
5578 + update_option($option_name, $limit_data);
5579 +
5580 + return true;
5581 +}
5582 +
5583 +/**
5584 + * Enhanced rate limit reset with better error handling
5585 + */
3393 5586 public function mxchat_reset_rate_limits() {
5587 + try {
3394 5588 global $wpdb;
5589 + $all_options = get_option('mxchat_options', []);
5590 + $current_time = time();
5591 +
5592 + // Get rate limit options with a safer query and limit
5593 + $option_names = $wpdb->get_col(
5594 + $wpdb->prepare(
5595 + "SELECT option_name FROM {$wpdb->options}
5596 + WHERE option_name LIKE %s
5597 + LIMIT 1000",
5598 + 'mxchat_chat_limit_%'
5599 + )
5600 + );
5601 +
5602 + if (empty($option_names)) {
5603 + return;
5604 + }
5605 +
5606 + $processed_count = 0;
5607 + $max_processing_time = 30; // Maximum 30 seconds
5608 + $start_time = time();
5609 +
5610 + foreach ($option_names as $option_name) {
5611 + // Check processing time limit
5612 + if ((time() - $start_time) > $max_processing_time) {
5613 + error_log('MxChat: Rate limit reset timeout after processing ' . $processed_count . ' entries');
5614 + break;
5615 + }
5616 +
5617 + // Parse the option name more safely
5618 + if (!preg_match('/^mxchat_chat_limit_(.+)_(.+)$/', $option_name, $matches)) {
5619 + continue;
5620 + }
5621 +
5622 + $role_and_user = $matches[1] . '_' . $matches[2];
5623 + $parts = explode('_', $role_and_user);
5624 +
5625 + if (count($parts) < 2) {
5626 + continue;
5627 + }
5628 +
5629 + // Extract role (everything except the last part which is user ID)
5630 + $user_id_part = array_pop($parts);
5631 + $role = implode('_', $parts);
5632 +
5633 + // Skip if role doesn't exist in our settings
5634 + if (!isset($all_options['rate_limits'][$role])) {
5635 + // Clean up orphaned entries
5636 + delete_option($option_name);
5637 + continue;
5638 + }
5639 +
5640 + $timeframe = $all_options['rate_limits'][$role]['timeframe'] ?? 'daily';
5641 + $limit_data = get_option($option_name);
5642 +
5643 + if (!$limit_data || !is_array($limit_data) || !isset($limit_data['timestamp'])) {
5644 + // Clean up invalid entries
5645 + delete_option($option_name);
5646 + continue;
5647 + }
5648 +
5649 + $timestamp = $limit_data['timestamp'];
5650 + $should_reset = false;
5651 +
5652 + // Determine if we should reset based on the timeframe
5653 + switch ($timeframe) {
5654 + case 'hourly':
5655 + $should_reset = ($current_time - $timestamp) >= 3600;
5656 + break;
5657 + case 'daily':
5658 + $should_reset = ($current_time - $timestamp) >= 86400;
5659 + break;
5660 + case 'weekly':
5661 + $should_reset = ($current_time - $timestamp) >= 604800;
5662 + break;
5663 + case 'monthly':
5664 + $should_reset = ($current_time - $timestamp) >= 2592000;
5665 + break;
5666 + }
5667 +
5668 + // Reset the counter if the timeframe has passed
5669 + if ($should_reset) {
5670 + delete_option($option_name);
5671 + wp_cache_delete($option_name, 'options');
5672 + $processed_count++;
5673 + }
5674 + }
5675 +
5676 + // Clean up any orphaned cache entries
5677 + wp_cache_delete('mxchat_all_chat_limits', 'options');
5678 +
5679 + error_log("MxChat: Rate limit reset completed. Processed {$processed_count} entries.");
5680 +
5681 + } catch (Exception $e) {
5682 + error_log('MxChat: Rate limit reset error: ' . $e->getMessage());
5683 + }
5684 +}
3395 5685
3396 - // Define a cache key pattern for rate limits
3397 - $cache_key_pattern = 'mxchat_chat_limit_%';
3398 5686
3399 - // Retrieve all option names matching the pattern
3400 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
3401 - $option_names = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
5687 +/**
5688 + * Process HTML links in rate limit messages
5689 + *
5690 + * @param string $message The rate limit message
5691 + * @return string The processed message with safe HTML links
5692 + */
5693 +private function process_rate_limit_message_html($message) {
5694 + // Return original message if empty
5695 + if (empty($message)) {
5696 + return $message;
5697 + }
5698 +
5699 + // First, convert markdown links to HTML
5700 + $message = $this->convert_markdown_links($message);
5701 +
5702 + // Then, auto-convert any remaining plain URLs to links
5703 + $message = $this->auto_link_urls($message);
5704 +
5705 + // Allow basic HTML tags for links and formatting
5706 + $allowed_tags = [
5707 + 'a' => [
5708 + 'href' => true,
5709 + 'target' => true,
5710 + 'rel' => true,
5711 + 'title' => true,
5712 + 'class' => true
5713 + ],
5714 + 'strong' => [],
5715 + 'em' => [],
5716 + 'br' => [],
5717 + 'b' => [],
5718 + 'i' => [],
5719 + 'span' => ['class' => true]
5720 + ];
5721 +
5722 + // Sanitize but allow the specified HTML tags
5723 + $processed_message = wp_kses($message, $allowed_tags);
5724 +
5725 + // If wp_kses stripped everything, return the original message as plain text
5726 + if (empty($processed_message) && !empty($message)) {
5727 + // Strip all HTML and return plain text as fallback
5728 + return wp_strip_all_tags($message);
5729 + }
5730 +
5731 + return $processed_message;
5732 +}
3402 5733
3403 - // db call ok; no-cache ok
3404 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- db call ok
3405 - $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
5734 +/**
5735 + * Convert markdown links to HTML
5736 + *
5737 + * @param string $text The text to process
5738 + * @return string The text with markdown links converted to HTML
5739 + */
5740 +private function convert_markdown_links($text) {
5741 + // Return original text if empty
5742 + if (empty($text)) {
5743 + return $text;
5744 + }
5745 +
5746 + // Pattern to match markdown links: [text](url)
5747 + $pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
5748 +
5749 + $processed_text = preg_replace_callback($pattern, function($matches) {
5750 + $link_text = $matches[1];
5751 + $url = $matches[2];
5752 +
5753 + // Clean up any trailing punctuation from the URL
5754 + $url = rtrim($url, '.,;:!?');
5755 +
5756 + // Sanitize the link text and URL
5757 + $safe_text = esc_html($link_text);
5758 + $safe_url = esc_url($url);
5759 +
5760 + // Create the HTML link
5761 + return '<a href="' . $safe_url . '" target="_blank" rel="noopener noreferrer">' . $safe_text . '</a>';
5762 + }, $text);
5763 +
5764 + // If preg_replace_callback failed, return original text
5765 + if ($processed_text === null) {
5766 + return $text;
5767 + }
5768 +
5769 + return $processed_text;
5770 +}
3406 5771
3407 - // Clear the relevant cache entries
3408 - foreach ($option_names as $option_name) {
3409 - wp_cache_delete($option_name, 'options');
3410 - }
5772 +/**
5773 + * Auto-convert plain URLs to clickable links
5774 + *
5775 + * @param string $text The text to process
5776 + * @return string The text with URLs converted to links
5777 + */
5778 +private function auto_link_urls($text) {
5779 + // Return original text if empty
5780 + if (empty($text)) {
5781 + return $text;
5782 + }
5783 +
5784 + // Simple pattern that avoids complex lookbehinds
5785 + // This will match URLs that are not already inside href attributes or markdown links
5786 + $pattern = '/(?<!href=["\'])(?<!\]\()https?:\/\/[^\s<>"\')\]]+/i';
5787 +
5788 + $processed_text = preg_replace_callback($pattern, function($matches) {
5789 + $url = $matches[0];
5790 + // Clean up any trailing punctuation that might have been captured
5791 + $url = rtrim($url, '.,;:!?');
5792 +
5793 + // Add target="_blank" and rel="noopener noreferrer" for security
5794 + return '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($url) . '</a>';
5795 + }, $text);
5796 +
5797 + // If preg_replace_callback failed, return original text
5798 + if ($processed_text === null) {
5799 + return $text;
5800 + }
5801 +
5802 + return $processed_text;
5803 +}
3411 5804
3412 - // Optionally, clear a general cache if you have one
3413 - wp_cache_delete('mxchat_all_chat_limits', 'options');
5805 +
5806 +// Helper function to get client IP address
5807 +private function get_client_ip() {
5808 + // Check for shared internet/ISP IP
5809 + if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
5810 + return sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
3414 5811 }
5812 +
5813 + // Check for IPs passing through proxies
5814 + if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
5815 + // Use the first value in the comma-separated list
5816 + $forwarded_for = explode(',', sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']));
5817 + return trim($forwarded_for[0]);
5818 + }
5819 +
5820 + if (!empty($_SERVER['REMOTE_ADDR'])) {
5821 + return sanitize_text_field($_SERVER['REMOTE_ADDR']);
5822 + }
5823 +
5824 + // Fallback
5825 + return 'unknown';
5826 +}
3415 5827
3416 -private function mxchat_fetch_woocommerce_products() {
3417 - // Ensure WooCommerce is active
3418 - if (!class_exists('WooCommerce')) {
3419 - return [];
5828 +/**
5829 + * AJAX handler to get system information for testing panel
5830 + */
5831 +public function mxchat_get_system_info() {
5832 + // Verify nonce for security
5833 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
5834 + wp_send_json_error(['message' => 'Invalid nonce']);
5835 + return;
3420 5836 }
5837 +
5838 + // Only allow admin users
5839 + if (!current_user_can('administrator')) {
5840 + wp_send_json_error(['message' => 'Unauthorized']);
5841 + return;
5842 + }
5843 +
5844 + // Get system prompt from options
5845 + $system_prompt = isset($this->options['system_prompt_instructions'])
5846 + ? $this->options['system_prompt_instructions']
5847 + : 'No system prompt configured';
5848 +
5849 + // Get selected model
5850 + $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
5851 +
5852 + // Get API key status (just check if they exist, don't expose the keys)
5853 + $api_status = [];
5854 + $api_status['openai'] = !empty($this->options['api_key']);
5855 + $api_status['claude'] = !empty($this->options['claude_api_key']);
5856 + $api_status['gemini'] = !empty($this->options['gemini_api_key']);
5857 + $api_status['xai'] = !empty($this->options['xai_api_key']);
5858 + $api_status['deepseek'] = !empty($this->options['deepseek_api_key']);
5859 +
5860 + wp_send_json_success([
5861 + 'system_prompt' => $system_prompt,
5862 + 'selected_model' => $selected_model,
5863 + 'api_status' => $api_status
5864 + ]);
5865 +}
3421 5866
3422 - $args = array(
3423 - 'post_type' => 'product',
3424 - 'post_status' => 'publish',
3425 - 'posts_per_page' => -1,
3426 - );
5867 +/**
5868 + * AJAX handler to get similarity threshold
5869 + */
5870 +public function mxchat_get_similarity_threshold() {
5871 + // Verify nonce for security
5872 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
5873 + wp_send_json_error(['message' => 'Invalid nonce']);
5874 + return;
5875 + }
5876 +
5877 + // Only allow admin users
5878 + if (!current_user_can('administrator')) {
5879 + wp_send_json_error(['message' => 'Unauthorized']);
5880 + return;
5881 + }
5882 +
5883 + // Get similarity threshold from main options (default 75%)
5884 + $similarity_threshold = isset($this->options['similarity_threshold'])
5885 + ? ((int) $this->options['similarity_threshold']) / 100
5886 + : 0.75;
5887 +
5888 + wp_send_json_success([
5889 + 'threshold' => $similarity_threshold,
5890 + 'threshold_percentage' => ($similarity_threshold * 100) . '%'
5891 + ]);
5892 +}
3427 5893
3428 - $products = get_posts($args);
3429 - $product_data = [];
5894 +/**
5895 + * AJAX handler to get knowledge base status
5896 + */
5897 +public function mxchat_get_kb_status() {
5898 + // Verify nonce for security
5899 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
5900 + wp_send_json_error(['message' => 'Invalid nonce']);
5901 + return;
5902 + }
5903 +
5904 + // Only allow admin users
5905 + if (!current_user_can('administrator')) {
5906 + wp_send_json_error(['message' => 'Unauthorized']);
5907 + return;
5908 + }
5909 +
5910 + // Check Pinecone vs WordPress
5911 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
5912 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
5913 +
5914 + $kb_info = [
5915 + 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
5916 + 'status' => 'Active'
5917 + ];
5918 +
5919 + // Get document count
5920 + if ($use_pinecone) {
5921 + $kb_info['documents'] = 'Connected to Pinecone';
5922 + $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
5923 + } else {
5924 + // Count documents in WordPress database
5925 + global $wpdb;
5926 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5927 + $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
5928 + $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
5929 + }
5930 +
5931 + wp_send_json_success($kb_info);
5932 +}
3430 5933
3431 - foreach ($products as $product) {
3432 - $product_id = $product->ID;
3433 - $product_obj = wc_get_product($product_id);
5934 +/**
5935 + * AJAX handler to start a completely fresh session (NEW - replaces old clear session)
5936 + */
5937 +public function mxchat_start_fresh_session() {
5938 + // Verify nonce for security
5939 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
5940 + wp_send_json_error(['message' => 'Invalid nonce']);
5941 + return;
5942 + }
5943 +
5944 + // Only allow admin users
5945 + if (!current_user_can('administrator')) {
5946 + wp_send_json_error(['message' => 'Unauthorized']);
5947 + return;
5948 + }
5949 +
5950 + $old_session_id = isset($_POST['old_session_id']) ? sanitize_text_field($_POST['old_session_id']) : '';
5951 + $new_session_id = isset($_POST['new_session_id']) ? sanitize_text_field($_POST['new_session_id']) : '';
5952 +
5953 + if (empty($old_session_id)) {
5954 + wp_send_json_error(['message' => 'Old session ID required']);
5955 + return;
5956 + }
5957 +
5958 + // If no new session ID provided, generate one
5959 + if (empty($new_session_id)) {
5960 + $new_session_id = 'mxchat_chat_' . substr(md5(uniqid()), 0, 9);
5961 + }
5962 +
5963 + // Clear ALL data associated with the old session
5964 + $this->clear_complete_session_data($old_session_id);
5965 +
5966 + // Initialize the new session
5967 + $this->initialize_fresh_session($new_session_id);
5968 +
5969 + wp_send_json_success([
5970 + 'message' => 'Fresh session started successfully',
5971 + 'new_session_id' => $new_session_id,
5972 + 'old_session_id' => $old_session_id
5973 + ]);
5974 +}
3434 5975
3435 - $product_data[] = array(
3436 - 'id' => $product_id,
3437 - 'name' => $product_obj->get_name(),
3438 - 'description' => $product_obj->get_description(),
3439 - 'short_description' => $product_obj->get_short_description(),
3440 - 'url' => get_permalink($product_id),
3441 - 'price' => $product_obj->get_regular_price(),
3442 - 'sale_price' => $product_obj->get_sale_price(),
3443 - 'stock_status' => $product_obj->get_stock_status(),
3444 - 'sku' => $product_obj->get_sku(),
3445 - 'in_stock' => $product_obj->is_in_stock(),
3446 - 'total_sales' => $product_obj->get_total_sales(),
3447 - );
5976 +/**
5977 + * Clear ALL data associated with a session (ENHANCED)
5978 + */
5979 +private function clear_complete_session_data($session_id) {
5980 + // Clear chat history
5981 + delete_option("mxchat_history_{$session_id}");
5982 +
5983 + // Clear chat mode
5984 + delete_option("mxchat_mode_{$session_id}");
5985 +
5986 + // Clear any PDF/Word transients
5987 + $this->clear_pdf_transients($session_id);
5988 + if (method_exists($this, 'clear_word_transients')) {
5989 + $this->clear_word_transients($session_id);
3448 5990 }
5991 +
5992 + // Clear agent-related data
5993 + delete_option("mxchat_channel_{$session_id}");
5994 + delete_option("mxchat_agent_name_{$session_id}");
5995 + delete_option("mxchat_email_{$session_id}");
5996 +
5997 + // Clear any recommendation flow state
5998 + delete_option("mxchat_sr_flow_state_{$session_id}");
5999 +
6000 + // Clear any cached embeddings or context
6001 + delete_transient("mxchat_context_{$session_id}");
6002 + delete_transient("mxchat_last_query_{$session_id}");
6003 +
6004 + // Clear any testing data
6005 + delete_transient("mxchat_testing_data_{$session_id}");
6006 +
6007 + // Clear any rate limiting data for this session
6008 + delete_transient("mxchat_rate_limit_{$session_id}");
6009 +
6010 + // Clear any other session-specific transients
6011 + delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
6012 + delete_transient("mxchat_include_pdf_in_context_{$session_id}");
6013 + delete_transient("mxchat_include_word_in_context_{$session_id}");
6014 +
6015 + //error_log("MxChat: Cleared all data for session: {$session_id}");
6016 +}
3449 6017
3450 - return $product_data;
6018 +/**
6019 + * Initialize a fresh session with default data
6020 + */
6021 +private function initialize_fresh_session($session_id) {
6022 + // Set default chat mode
6023 + update_option("mxchat_mode_{$session_id}", 'ai');
6024 +
6025 + //error_log("MxChat: Initialized fresh session: {$session_id}");
3451 6026 }
6027 +
6028 +/**
6029 + * Helper method to clear Word document transients (if you have Word support)
6030 + */
6031 +private function clear_word_transients($session_id) {
6032 + delete_transient('mxchat_word_url_' . $session_id);
6033 + delete_transient('mxchat_word_filename_' . $session_id);
6034 + delete_transient('mxchat_word_embeddings_' . $session_id);
6035 + delete_transient('mxchat_include_word_in_context_' . $session_id);
6036 +}
6037 +
6038 +/**
6039 + * Simplified testing data capture method (CLEANED UP)
6040 + */
6041 +private function capture_testing_data($user_embedding, $message, $session_id) {
6042 + // Only capture for admin users
6043 + if (!current_user_can('administrator')) {
6044 + return null;
6045 + }
6046 +
6047 + $testing_data = [
6048 + 'query' => $message,
6049 + 'timestamp' => time(),
6050 + 'top_matches' => [],
6051 + 'action_matches' => [] // NEW: Add action matches
6052 + ];
6053 +
6054 + // Get similarity threshold
6055 + $similarity_threshold = isset($this->options['similarity_threshold'])
6056 + ? ((int) $this->options['similarity_threshold']) / 100
6057 + : 0.75;
6058 +
6059 + $testing_data['similarity_threshold'] = $similarity_threshold;
6060 +
6061 + // Use the real similarity analysis if available
6062 + if ($this->last_similarity_analysis !== null) {
6063 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
6064 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
6065 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
6066 + } else {
6067 + // Fallback: determine knowledge base type
6068 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
6069 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
6070 +
6071 + $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
6072 + }
6073 +
6074 + // NEW: Include action analysis if available
6075 + if (isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
6076 + $testing_data['action_matches'] = $this->last_action_analysis;
6077 +
6078 + // Clear it after capturing to avoid stale data
6079 + $this->last_action_analysis = null;
6080 + }
6081 +
6082 + return $testing_data;
6083 +}
6084 +
3452 6085
3453 6086 }
3454 6087 ?>