PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.2.3
MxChat – AI Chatbot & Content Generation for WordPress v2.2.3
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | includes/class-mxchat-integrator.php +2107 -751 2.0.42.2.3 View file →
@@ -10,49 +10,72 @@
10 10 private $fallbackResponse;
11 11 private $productCardHtml;
12 12 private $word_handler;
13 13
14 +/**
15 + * Setup the cron jobs for rate limits with error handling
16 + */
17 +public function setup_rate_limit_cron_jobs() {
18 + // Clear previous schedules
19 + wp_clear_scheduled_hook('mxchat_reset_rate_limits');
20 + wp_clear_scheduled_hook('mxchat_reset_hourly_rate_limits');
21 + wp_clear_scheduled_hook('mxchat_reset_daily_rate_limits');
22 + wp_clear_scheduled_hook('mxchat_reset_weekly_rate_limits');
23 + wp_clear_scheduled_hook('mxchat_reset_monthly_rate_limits');
24 +
25 + // Schedule the main rate limit reset check (runs hourly) with error handling
26 + if (!wp_next_scheduled('mxchat_reset_rate_limits')) {
27 + $result = wp_schedule_event(time(), 'hourly', 'mxchat_reset_rate_limits');
28 +
29 + // Log error but don't break the plugin
30 + if ($result === false) {
31 + //error_log('MxChat: Failed to schedule rate limit reset cron. Rate limits will still work but may not auto-reset.');
32 + }
33 + }
34 +}
35 +
36 +/**
37 + * Class constructor
38 + */
14 39 public function __construct() {
15 40 $this->options = get_option('mxchat_options');
16 41 $this->prompts_options = get_option('mxchat_prompts_options', array());
17 -
18 42 $this->chat_count = get_option('mxchat_chat_count', 0);
19 43 $this->word_handler = new MXChat_Word_Handler($this->options);
20 -
44 +
45 + // Setup the cron jobs for rate limits
46 + $this->setup_rate_limit_cron_jobs();
47 +
48 + // Add all action hooks
21 49 add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
22 50 add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
23 51 add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
24 -
25 52 add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
26 53 add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
54 +
27 55 // Add the AJAX actions for checking if the pre-chat message was dismissed
28 56 add_action('wp_ajax_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
29 57 add_action('wp_ajax_nopriv_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
30 -
31 58 add_action('wp_ajax_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
32 59 add_action('wp_ajax_nopriv_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
33 -
34 60 add_action('wp_ajax_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
35 61 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 -
62 +
41 63 // Add REST API routes registration
42 64 add_action('rest_api_init', array($this, 'register_routes'));
43 -
44 65 add_action('wp_ajax_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
45 66 add_action('wp_ajax_nopriv_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
46 -
67 +
68 + // Rate limit action - notice we removed the old schedule setup
47 69 add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
48 -
70 +
71 + // File upload and handling actions
49 72 add_action('wp_ajax_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
50 73 add_action('wp_ajax_nopriv_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
51 74 add_action('wp_ajax_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
52 75 add_action('wp_ajax_nopriv_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
53 -
54 - // Add these with your other add_action hooks
76 +
77 + // Word document handling actions
55 78 add_action('wp_ajax_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
56 79 add_action('wp_ajax_nopriv_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
57 80 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
58 81 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
@@ -57,9 +80,10 @@
57 80 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
58 81 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
59 82 add_action('wp_ajax_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
60 83 add_action('wp_ajax_nopriv_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
61 -
84 +
85 + // Email handling actions
62 86 add_action('wp_ajax_nopriv_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
63 87 add_action('wp_ajax_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
64 88 add_action('wp_ajax_nopriv_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
65 89 add_action('wp_ajax_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
@@ -206,8 +230,14 @@
206 230 'methods' => 'POST',
207 231 'callback' => [$this, 'handle_slack_interaction'],
208 232 'permission_callback' => [$this, 'verify_slack_request'],
209 233 ]);
234 +
235 + register_rest_route('mxchat/v1', '/slack-messages', [
236 + 'methods' => 'POST',
237 + 'callback' => [$this, 'handle_slack_messages'],
238 + 'permission_callback' => [$this, 'verify_slack_request'],
239 + ]);
210 240
211 241 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
212 242 }
213 243
@@ -260,8 +290,9 @@
260 290
261 291 // Compare signatures
262 292 return hash_equals($my_signature, $slack_signature);
263 293 }
294 +
264 295 public function mxchat_stream_events(WP_REST_Request $request) {
265 296 header('Content-Type: text/event-stream');
266 297 header('Cache-Control: no-cache');
267 298 header('Connection: keep-alive');
@@ -297,18 +328,26 @@
297 328
298 329
299 330 private function mxchat_save_chat_message($session_id, $role, $message) {
300 331 global $wpdb;
301 -
302 332 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
303 333 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
304 -
334 +
335 + // Check if this is the first message in a new session (before any other database operations)
336 + $is_new_session = false;
337 + if ($role === 'user') { // Only check for user messages, not bot responses
338 + $existing_messages = $wpdb->get_var($wpdb->prepare(
339 + "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
340 + $session_id
341 + ));
342 + $is_new_session = ($existing_messages == 0);
343 + }
344 +
305 345 // 1) Extract agent name if present
306 346 $agent_name = '';
307 347 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
308 348 $agent_name = $matches[1];
309 349 $message = str_replace("Agent: $agent_name - ", '', $message);
310 -
311 350 $session_meta_key = "mxchat_agent_name_{$session_id}";
312 351 if (empty(get_option($session_meta_key))) {
313 352 update_option($session_meta_key, $agent_name);
314 353 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
@@ -313,30 +352,24 @@
313 352 update_option($session_meta_key, $agent_name);
314 353 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
315 354 }
316 355 }
317 -
318 356 // 2) Generate unique message_id
319 357 $message_id = uniqid();
320 358 //error_log("[DEBUG] mxchat_save_chat_message -> Generated message_id: {$message_id}");
321 -
322 359 // 3) Determine user_id
323 360 $user_id = is_user_logged_in() ? get_current_user_id() : 0;
324 -
325 361 // 4) Determine user_identifier
326 362 $user_identifier = $agent_name
327 363 ? $agent_name
328 364 : MxChat_User::mxchat_get_user_identifier();
329 -
330 365 // 5) Determine displayed_name
331 366 $user_email = MxChat_User::mxchat_get_user_email();
332 367 $displayed_name = $agent_name ? $agent_name : ($user_email ?: $user_identifier);
333 -
334 368 // 6) Check for a saved email in wp_options
335 369 $email_option_key = "mxchat_email_{$session_id}";
336 370 $saved_email = get_option($email_option_key);
337 371 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for email_option_key: {$email_option_key}, found: {$saved_email}");
338 -
339 372 // If found, update DB user_email
340 373 if ($saved_email) {
341 374 $update_res = $wpdb->update(
342 375 $table_name,
@@ -346,9 +379,8 @@
346 379 ['%s']
347 380 );
348 381 //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email update for session_id {$session_id}. update_res: {$update_res}");
349 382 }
350 -
351 383 // 7) Save to session history in wp_options
352 384 $history_key = "mxchat_history_{$session_id}";
353 385 $history = get_option($history_key, []);
354 386 $history[] = [
@@ -357,11 +389,10 @@
357 389 'content' => $message,
358 390 'timestamp' => round(microtime(true) * 1000),
359 391 'agent_name' => $displayed_name,
360 392 ];
361 - update_option($history_key, $history);
393 + update_option($history_key, $history, 'no');
362 394 //error_log("[DEBUG] mxchat_save_chat_message -> Updated session history in option: {$history_key}");
363 -
364 395 // 8) Save the message to DB (INSERT)
365 396 $insert_data = [
366 397 'user_id' => $user_id,
367 398 'user_identifier'=> $user_identifier,
@@ -372,12 +403,64 @@
372 403 'timestamp' => current_time('mysql', 1),
373 404 ];
374 405 $wpdb->insert($table_name, $insert_data);
375 406 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
376 -
407 +
408 + // 9) Send notification email if this is the first user message in a new session
409 + if ($wpdb->insert_id && $is_new_session && $role === 'user') {
410 + $this->send_new_chat_notification($session_id, array(
411 + 'identifier' => $user_identifier,
412 + 'email' => $saved_email ?: $user_email,
413 + 'ip' => $_SERVER['REMOTE_ADDR']
414 + ));
415 + }
416 +
377 417 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
378 418 return $message_id;
379 419 }
420 +private function send_new_chat_notification($session_id, $user_info = array()) {
421 + $options = get_option('mxchat_transcripts_options');
422 +
423 + // Check if notifications are enabled
424 + if (empty($options['mxchat_enable_notifications'])) {
425 + return false;
426 + }
427 +
428 + // Get notification email
429 + $to = !empty($options['mxchat_notification_email']) ?
430 + $options['mxchat_notification_email'] :
431 + get_option('admin_email');
432 +
433 + if (!is_email($to)) {
434 + return false;
435 + }
436 +
437 + // Prepare email content
438 + $subject = sprintf('[%s] New Chat Session Started', get_bloginfo('name'));
439 +
440 + $user_identifier = isset($user_info['identifier']) ? $user_info['identifier'] : 'Guest';
441 + $user_email = isset($user_info['email']) ? $user_info['email'] : 'Not provided';
442 + $user_ip = isset($user_info['ip']) ? $user_info['ip'] : $_SERVER['REMOTE_ADDR'];
443 +
444 + $message = sprintf(
445 + "A new chat session has started on your website.\n\n" .
446 + "Session ID: %s\n" .
447 + "User: %s\n" .
448 + "Email: %s\n" .
449 + "IP Address: %s\n" .
450 + "Time: %s\n\n" .
451 + "View transcripts: %s",
452 + $session_id,
453 + $user_identifier,
454 + $user_email,
455 + $user_ip,
456 + current_time('mysql'),
457 + admin_url('admin.php?page=mxchat-transcripts')
458 + );
459 +
460 + // Send email
461 + return wp_mail($to, $subject, $message);
462 +}
380 463
381 464 public function mxchat_handle_save_email_and_response() {
382 465 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
383 466
@@ -382,9 +465,9 @@
382 465 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
383 466
384 467 // Validate nonce
385 468 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'));
469 + //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
387 470 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
388 471 wp_die();
389 472 }
390 473
@@ -468,52 +551,8 @@
468 551 wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
469 552 }
470 553 }
471 554
472 -
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);
507 - }
508 - }
509 -
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 555 // Add this to your plugin's main PHP file
517 556 public function mxchat_check_new_messages() {
518 557 if (!isset($_POST['session_id']) || !isset($_POST['last_seen_id'])) {
519 558 wp_send_json_error(['message' => 'Missing required parameters']);
@@ -591,76 +630,42 @@
591 630 wp_die();
592 631 }
593 632 }
594 633
634 +$this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
635 +$this->productCardHtml = '';
595 636
596 - // Reset fallback response at the start of each request
597 - $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
598 - $this->productCardHtml = '';
637 +// Get the actual WordPress user ID if logged in
638 +$is_logged_in = is_user_logged_in();
639 +if ($is_logged_in) {
640 + $user_id = get_current_user_id(); // This will get the actual WordPress user ID
641 +} else {
642 + // For logged-out users, use your existing identifier method
643 + $user_id = $this->mxchat_get_user_identifier();
644 +}
599 645
600 - // Get the actual WordPress user ID if logged in
601 - $is_logged_in = is_user_logged_in();
602 - if ($is_logged_in) {
603 - $user_id = get_current_user_id(); // This will get the actual WordPress user ID
604 - } else {
605 - // For logged-out users, use your existing identifier method
606 - $user_id = $this->mxchat_get_user_identifier();
607 - }
646 +// Get and sanitize the user identifier
647 +$user_id = sanitize_key($user_id);
608 648
609 - // Get and sanitize the user identifier
610 - $user_id = sanitize_key($user_id);
649 +// Check rate limit using new settings structure
650 +$rate_limit_result = $this->check_rate_limit();
611 651
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'));
652 +// Add this at the start of your rate limit checking in mxchat_handle_chat_request()
653 +//error_log('MXChat Rate Limit: Starting rate limit check in handle_chat_request()');
615 654
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');
655 +// Then right after checking the result:
656 +if ($rate_limit_result !== true) {
657 + //error_log('MXChat Rate Limit: Rate limit exceeded, returning error');
658 + wp_send_json([
659 + 'success' => false,
660 + 'message' => $rate_limit_result['message'],
661 + 'status' => 'rate_limit_exceeded'
662 + ]);
663 + wp_die();
664 +} else {
665 + //error_log('MXChat Rate Limit: Check passed successfully');
666 +}
621 667
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);
661 - }
662 -
663 668 // Rest of your existing code...
664 669 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
665 670 //error_log("Session ID: $session_id");
666 671
@@ -703,8 +708,30 @@
703 708
704 709 // Preserve code blocks from markdown conversion
705 710 $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
706 711
712 +// Check if any add-ons want to pre-process this message (for web search etc.)
713 +$pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
714 +
715 +// If the pre-processing returned a result (not the original message), use it directly
716 +if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
717 + // Save the AI response
718 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
719 +
720 + // Save HTML content if provided
721 + if (!empty($pre_processed_result['html'])) {
722 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
723 + }
724 +
725 + // Return the response
726 + wp_send_json([
727 + 'text' => $pre_processed_result['text'],
728 + 'html' => $pre_processed_result['html'] ?? '',
729 + 'session_id' => $session_id
730 + ]);
731 + wp_die();
732 +}
733 +
707 734 // Save the user's message
708 735 $this->mxchat_save_chat_message($session_id, 'user', $message);
709 736
710 737 // Check if the message is an email address
@@ -856,51 +883,130 @@
856 883 wp_die();
857 884 }
858 885 }
859 886 }
887 +
888 +
889 +// Add this before the intent check section (before Step 2) in mxchat_handle_chat_request
890 +// Check if there's an active recommendation flow session
891 +$flow_state = get_option("mxchat_sr_flow_state_{$session_id}", array());
892 +if (!empty($flow_state) && isset($flow_state['flow_id'])) {
893 + //error_log('MXCHAT DEBUG: Detected active recommendation flow, routing directly');
894 +
895 + // Create a dummy intent object that matches the original intent
896 + $dummy_intent = new stdClass();
897 + $dummy_intent->intent_label = 'Recommendation Flow ' . $flow_state['flow_id'];
898 + $dummy_intent->phrases = ''; // Empty phrases to avoid matching the original trigger
899 +
900 + // Call the recommendation flow handler directly
901 + $response_data = apply_filters('mxchat_sr_recommendation_flow', false, $message, $user_id, $session_id, $dummy_intent);
902 +
903 + // If the handler returned a response, send it
904 + if (is_array($response_data) && (isset($response_data['text']) || isset($response_data['html']))) {
905 + // Save the bot's response to the chat history
906 + if (!empty($response_data['text'])) {
907 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['text']);
908 + }
909 + if (!empty($response_data['html'])) {
910 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['html']);
911 + }
912 +
913 + // Send the response
914 + wp_send_json($response_data);
915 + wp_die();
916 + }
917 +
918 + // If we reach here, the flow handler didn't provide a usable response
919 + // We'll continue with regular processing
920 + //error_log('MXCHAT DEBUG: Recommendation flow handler did not provide a usable response');
921 +}
860 922
861 - // 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"));
923 + // Step 2: Detect intent and handle intent-based responses
924 +$intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
925 +//error_log("Intent Result Type: " . gettype($intent_result));
864 926
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.");
927 +// Step 3: Handle the intent result appropriately
928 +if ($intent_result !== false) {
929 + // The intent was matched and handled
930 + //error_log("Intent was matched and handled.");
931 +
932 + if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
933 + // Intent returned a direct response array
934 + //error_log("Intent returned a direct response.");
868 935 $response_data = [
869 - 'text' => $this->fallbackResponse['text'],
870 - 'html' => $this->fallbackResponse['html'],
936 + 'text' => $intent_result['text'] ?? '',
937 + 'html' => $intent_result['html'] ?? '',
871 938 'session_id' => $session_id
872 939 ];
873 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text'] . $this->fallbackResponse['html']);
940 +
874 941 wp_send_json($response_data);
875 942 wp_die();
943 + }
944 + else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
945 + // Intent returned true and set fallbackResponse
946 + //error_log("Intent returned true with fallbackResponse set.");
947 + $response_data = [
948 + 'text' => $this->fallbackResponse['text'] ?? '',
949 + 'html' => $this->fallbackResponse['html'] ?? '',
950 + 'session_id' => $session_id
951 + ];
952 +
953 + wp_send_json($response_data);
954 + wp_die();
876 955 }
956 +
957 + // Intent was matched but no usable response was provided
958 + // This shouldn't happen with proper intent implementation
959 + //error_log("Warning: Intent matched but no response provided.");
960 +}
877 961
878 - // If no intent matched or product not found, proceed with AI response
879 - //error_log("No matching intent or fallback. Generating AI response.");
962 + // If we get here, no intent matched OR the intent didn't provide a usable response
963 + //error_log("No matching intent or usable response. Generating AI response.");
964 +
965 + // Step 4: Generate AI response
966 + $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
967 + $this->mxchat_increment_chat_count();
968 +
969 + // Generate embedding for the user's query
970 + $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
971 +
972 + // Check if the embedding generation returned an error
973 + if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
974 + $error_message = $user_message_embedding['error'];
975 + $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
976 +
977 + //error_log("Embedding error for session $session_id: $error_message (Code: $error_code)");
978 +
979 + // Important: Structure the error data correctly for wp_send_json_error
980 + wp_send_json_error([
981 + 'error_message' => $error_message,
982 + 'error_code' => $error_code
983 + ]);
984 + wp_die();
985 + }
986 +
987 + // Check if the embedding is valid
988 + if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
989 + //error_log("Failed to generate message embedding for session $session_id");
990 + wp_send_json_error([
991 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
992 + 'error_code' => 'invalid_embedding'
993 + ]);
994 + wp_die();
995 + }
880 996
881 - // Step 4: Generate AI response
882 - $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
883 - $this->mxchat_increment_chat_count();
997 + // Build context with both knowledge base and PDF content if available
998 + $context_content = "User asked: '{$message}'\n\n";
884 999
885 - // Generate embedding for the user's query
886 - $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'));
890 - wp_die();
891 - }
892 1000
893 - // Build context with both knowledge base and PDF content if available
894 - $context_content = "User asked: '{$message}'\n\n";
1001 + // Get relevant content from knowledge base
1002 + $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
1003 + if (!empty($relevant_content)) {
1004 + $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1005 + } else {
1006 + $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
1007 + }
895 1008
896 - // Get relevant content from knowledge base
897 - $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
898 - if (!empty($relevant_content)) {
899 - $context_content .= "Relevant content from knowledge database:\n" . $relevant_content . "\n\n";
900 - }
901 -
902 -
903 1009 // Check for and include PDF content
904 1010 $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
905 1011 $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
906 1012 $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
@@ -928,19 +1034,36 @@
928 1034 }
929 1035 $context_content .= "\n";
930 1036 }
931 1037 }
932 - // Generate the response using the full context
933 - $response = $this->mxchat_generate_response(
934 - $context_content,
935 - $this->options['api_key'],
936 - $this->options['xai_api_key'],
937 - $this->options['claude_api_key'],
938 - $this->options['deepseek_api_key'],
939 - $conversation_history
940 - );
1038 +
1039 + $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
941 1040
942 - $this->mxchat_save_chat_message($session_id, 'bot', $response);
1041 + // Generate the response using the full context
1042 + $response = $this->mxchat_generate_response(
1043 + $context_content,
1044 + $this->options['api_key'],
1045 + $this->options['xai_api_key'],
1046 + $this->options['claude_api_key'],
1047 + $this->options['deepseek_api_key'],
1048 + $this->options['gemini_api_key'],
1049 + $conversation_history
1050 + );
1051 +
1052 + // Check if the response is an error array
1053 + if (is_array($response) && isset($response['error'])) {
1054 + //error_log("AI Response Error: " . $response['error'] . " (Code: " . ($response['error_code'] ?? 'unknown') . ")");
1055 +
1056 + // Send a user-friendly error message
1057 + wp_send_json_error([
1058 + 'error_message' => $response['error'],
1059 + 'error_code' => $response['error_code'] ?? 'api_error'
1060 + ]);
1061 + wp_die();
1062 + }
1063 +
1064 + // If we get here, the response is valid text
1065 + $this->mxchat_save_chat_message($session_id, 'bot', $response);
943 1066
944 1067 // Step 5: Save additional content if available
945 1068 if (!empty($this->productCardHtml)) {
946 1069 $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
@@ -960,9 +1083,9 @@
960 1083 wp_send_json($response_data);
961 1084 wp_die();
962 1085 }
963 1086
964 -// New function to check intents and invoke the callback function
1087 +// Updated function to check intents and invoke the callback function
965 1088 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
966 1089 global $wpdb;
967 1090 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
968 1091
@@ -972,12 +1095,36 @@
972 1095
973 1096 // Generate the user embedding
974 1097 //error_log('🔄 MXCHAT DEBUG: Generating user embedding');
975 1098 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
976 - if (!is_array($user_embedding)) {
1099 +
1100 + // Check if embedding generation returned an error
1101 + if (is_array($user_embedding) && isset($user_embedding['error'])) {
1102 + $error_message = $user_embedding['error'];
1103 + $error_code = $user_embedding['error_code'] ?? 'embedding_error';
1104 +
1105 + //error_log("❌ MXCHAT DEBUG: Embedding error: $error_message (Code: $error_code)");
1106 +
1107 + // Send the error to the frontend
1108 + wp_send_json_error([
1109 + 'error_message' => $error_message,
1110 + 'error_code' => $error_code
1111 + ]);
1112 + wp_die();
1113 + }
1114 +
1115 + // Check if embedding is valid (not an error and is an array)
1116 + if (!is_array($user_embedding) || empty($user_embedding)) {
977 1117 //error_log('❌ MXCHAT DEBUG: Failed to generate user embedding');
978 - return false;
1118 +
1119 + // Send a generic error to the frontend
1120 + wp_send_json_error([
1121 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1122 + 'error_code' => 'invalid_embedding'
1123 + ]);
1124 + wp_die();
979 1125 }
1126 +
980 1127 //error_log('✅ MXCHAT DEBUG: User embedding generated successfully');
981 1128
982 1129 // Fetch intents from the database
983 1130 $table_name = $wpdb->prefix . 'mxchat_intents';
@@ -983,21 +1130,22 @@
983 1130 $table_name = $wpdb->prefix . 'mxchat_intents';
984 1131 if ($chat_mode === 'agent') {
985 1132 //error_log('🔍 MXCHAT DEBUG: Agent mode - fetching only chatbot switch intent');
986 1133 $query = $wpdb->prepare(
987 - "SELECT * FROM $table_name WHERE callback_function = %s",
1134 + "SELECT * FROM $table_name WHERE callback_function = %s AND (enabled = 1 OR enabled IS NULL)",
988 1135 'mxchat_handle_switch_to_chatbot_intent'
989 1136 );
990 1137 $intents = $wpdb->get_results($query);
991 1138 } else {
992 - //error_log('🔍 MXCHAT DEBUG: AI mode - fetching all intents');
993 - $intents = $wpdb->get_results("SELECT * FROM $table_name");
1139 + //error_log('🔍 MXCHAT DEBUG: AI mode - fetching all enabled intents');
1140 + // Only fetch enabled intents (either explicitly enabled with 1 or implicitly enabled with NULL for backward compatibility)
1141 + $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
994 1142 }
995 1143
996 - //error_log('🔍 MXCHAT DEBUG: Found ' . count($intents) . ' intents to check');
1144 + //error_log('🔍 MXCHAT DEBUG: Found ' . count($intents) . ' enabled intents to check');
997 1145
998 1146 if (empty($intents)) {
999 - //error_log('❌ MXCHAT DEBUG: No intents found in database');
1147 + //error_log('❌ MXCHAT DEBUG: No enabled intents found in database');
1000 1148 return false;
1001 1149 }
1002 1150
1003 1151 $highest_similarity = -INF;
@@ -1006,8 +1154,15 @@
1006 1154 //error_log('📊 MXCHAT DEBUG: Intent Similarity Scores ==================');
1007 1155 foreach ($intents as $intent) {
1008 1156 //error_log("🔄 MXCHAT DEBUG: Checking intent: '{$intent->intent_label}' (callback: {$intent->callback_function})");
1009 1157
1158 + // Additional check for enabled state in case database structure was modified
1159 + $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
1160 + if (!$is_enabled) {
1161 + //error_log("⚠️ MXCHAT DEBUG: Skipping disabled intent: {$intent->intent_label}");
1162 + continue;
1163 + }
1164 +
1010 1165 $intent_embedding_serialized = $intent->embedding_vector;
1011 1166 $intent_embedding = $intent_embedding_serialized
1012 1167 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
1013 1168 : null;
@@ -1019,8 +1174,9 @@
1019 1174
1020 1175 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
1021 1176 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
1022 1177
1178 + //error_log("📊 MXCHAT DEBUG: Intent '{$intent->intent_label}' similarity: {$similarity}, threshold: {$intent_threshold}");
1023 1179
1024 1180 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
1025 1181 $highest_similarity = $similarity;
1026 1182 $matched_intent = $intent;
@@ -1041,9 +1197,9 @@
1041 1197 $message,
1042 1198 $user_id,
1043 1199 $session_id,
1044 1200 $matched_intent,
1045 - $user_context // Add user context
1201 + $user_context
1046 1202 );
1047 1203 } else {
1048 1204 //error_log('🔍 MXCHAT DEBUG: Using apply_filters for add-on callback');
1049 1205 // Otherwise, use apply_filters for add-on callbacks
@@ -1071,9 +1227,8 @@
1071 1227 //error_log('🔍 MXCHAT DEBUG: Intent Check Completed ==================');
1072 1228 return false;
1073 1229 }
1074 1230
1075 -
1076 1231 // Helper function to clear PDF and Word document related transients
1077 1232 private function clear_pdf_transients($session_id) {
1078 1233 // PDF transients
1079 1234 delete_transient('mxchat_pdf_url_' . $session_id);
@@ -1106,47 +1261,63 @@
1106 1261 wp_send_json(['message' => $response]);
1107 1262 wp_die();
1108 1263 }
1109 1264
1110 -//very good
1111 1265 public function mxchat_generate_image($message, $user_id, $session_id) {
1266 + //error_log("Starting image generation for message: " . $message);
1267 +
1112 1268 // Prepare a prompt for DALL-E
1113 1269 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
1114 -
1270 +
1115 1271 // Use the existing OpenAI API key
1116 1272 $openai_api_key = sanitize_text_field($this->options['api_key']);
1117 -
1273 +
1118 1274 // Call DALL-E to generate an image
1119 1275 $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1120 -
1276 +
1121 1277 // Check if the response contains an image URL
1122 1278 if (isset($image_response['imageUrl'])) {
1123 1279 $image_url = esc_url_raw($image_response['imageUrl']);
1124 -
1280 +
1125 1281 // Construct the HTML with a CSS class instead of inline styles
1126 1282 $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
1283 + $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1284 +
1285 + // Save the bot message with both text and HTML
1286 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1287 + $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
1288 +
1289 + // Set the fallback response for the chat handler
1290 + $this->fallbackResponse = [
1291 + 'text' => $response_text,
1292 + 'html' => $response_html,
1293 + 'images' => [$image_url]
1294 + ];
1295 +
1296 + // For debugging/verification - Use json_encode to verify what's being set
1297 + //error_log("Image generation successful - fallbackResponse set: " . json_encode($this->fallbackResponse));
1127 1298
1128 - $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1299 + // Return the response directly instead of relying on the property
1300 + return $this->fallbackResponse;
1129 1301 } else {
1130 1302 $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
1131 - $response_html = '';
1303 +
1304 + // Save the error message
1305 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1306 +
1307 + // Set the fallback response for the chat handler
1308 + $this->fallbackResponse = [
1309 + 'text' => $response_text,
1310 + 'html' => '',
1311 + 'images' => []
1312 + ];
1313 +
1132 1314 //error_log("DALL-E image generation error: " . esc_html($image_response['error'] ?? 'Unknown error.'));
1315 + //error_log("Error fallbackResponse set: " . json_encode($this->fallbackResponse));
1316 +
1317 + // Return the response directly instead of relying on the property
1318 + return $this->fallbackResponse;
1133 1319 }
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 1320 }
1150 1321 private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
1151 1322 $api_url = 'https://api.openai.com/v1/images/generations';
1152 1323 $body = json_encode([
@@ -1185,44 +1356,43 @@
1185 1356
1186 1357 /**
1187 1358 * Handle web search requests.
1188 1359 *
1189 - * Sends the refined search query to the Brave Search API and displays neatly formatted,
1190 - * styled search results. Results are cached for performance.
1360 + * Sends the refined search query to the Brave Search API and uses the
1361 + * results to generate a conversational response with the AI model.
1191 1362 *
1192 1363 * @since 1.0.0
1193 1364 * @param string $message The user's search query.
1194 1365 * @param string $user_id The user identifier.
1195 1366 * @param string $session_id The current session ID.
1196 - * @return void
1367 + * @return array Response array containing text with embedded HTML links
1197 1368 */
1198 -public function mxchat_handle_search_request( $message, $user_id, $session_id ) {
1369 +public function mxchat_handle_search_request($message, $user_id, $session_id) {
1199 1370 // 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' ),
1371 + $refined_search_query = $this->mxchat_interpret_search_query($message);
1372 + if (empty($refined_search_query)) {
1373 + return array(
1374 + 'text' => esc_html__('I apologize, but could you please rephrase your search request?', 'mxchat'),
1375 + 'html' => ''
1205 1376 );
1206 - return;
1207 1377 }
1208 -
1378 +
1209 1379 // 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' ),
1380 + $options = get_option('mxchat_options');
1381 + $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1382 + $results_count = isset($options['brave_results_count']) ? absint($options['brave_results_count']) : 5;
1383 +
1384 + if (empty($api_key)) {
1385 + return array(
1386 + 'text' => esc_html__('Search functionality is temporarily unavailable. Please try again later.', 'mxchat'),
1387 + 'html' => ''
1217 1388 );
1218 - return;
1219 1389 }
1220 -
1390 +
1221 1391 // Build the API request URL
1222 1392 $api_url = add_query_arg(
1223 1393 array(
1224 - 'q' => rawurlencode( $refined_search_query ),
1394 + 'q' => rawurlencode($refined_search_query),
1225 1395 'count' => $results_count,
1226 1396 'text_decorations' => 'true',
1227 1397 'rich_data' => 'true',
1228 1398 ),
@@ -1227,14 +1397,14 @@
1227 1397 'rich_data' => 'true',
1228 1398 ),
1229 1399 'https://api.search.brave.com/res/v1/web/search'
1230 1400 );
1231 -
1401 +
1232 1402 // 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 ) {
1403 + $transient_key = 'mxchat_search_' . md5($refined_search_query);
1404 + $results = get_transient($transient_key);
1405 +
1406 + if (false === $results) {
1237 1407 // Fetch new results from the Brave Search API
1238 1408 $response = wp_remote_get(
1239 1409 $api_url,
1240 1410 array(
@@ -1245,51 +1415,78 @@
1245 1415 ),
1246 1416 'timeout' => 10,
1247 1417 )
1248 1418 );
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' ),
1419 +
1420 + if (is_wp_error($response)) {
1421 + return array(
1422 + 'text' => esc_html__('I encountered an error while searching. Please try again.', 'mxchat'),
1423 + 'html' => ''
1253 1424 );
1254 - return;
1255 1425 }
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' ),
1426 +
1427 + $results = json_decode(wp_remote_retrieve_body($response), true);
1428 +
1429 + if (json_last_error() !== JSON_ERROR_NONE) {
1430 + return array(
1431 + 'text' => esc_html__('I received an invalid response from the search service.', 'mxchat'),
1432 + 'html' => ''
1262 1433 );
1263 - return;
1264 1434 }
1265 -
1435 +
1266 1436 // Cache results for one hour
1267 - set_transient( $transient_key, $results, HOUR_IN_SECONDS );
1437 + set_transient($transient_key, $results, HOUR_IN_SECONDS);
1268 1438 }
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,
1439 +
1440 + // Process results
1441 + if (!empty($results['web']['results']) && is_array($results['web']['results'])) {
1442 + // Create a more straightforward summary with HTML links
1443 + $search_results_text = '';
1444 +
1445 + // Add a simple intro
1446 + $search_results_text .= sprintf(
1447 + esc_html__("Here's what I found about '%s':", 'mxchat'),
1448 + esc_html($refined_search_query)
1277 1449 );
1278 -
1450 +
1451 + // Add the top results with HTML links
1452 + foreach (array_slice($results['web']['results'], 0, 5) as $result) {
1453 + $title = isset($result['title']) ? wp_strip_all_tags($result['title']) : '';
1454 + $url = isset($result['url']) ? esc_url($result['url']) : '';
1455 + $description = isset($result['description']) ? wp_strip_all_tags($result['description']) : '';
1456 +
1457 + // Add a line break after the intro
1458 + $search_results_text .= '<br><br>';
1459 +
1460 + // Add title as a link
1461 + $search_results_text .= sprintf(
1462 + '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a><br>',
1463 + $url,
1464 + $title
1465 + );
1466 +
1467 + // Add a condensed description
1468 + $search_results_text .= sprintf("%s", $description);
1469 + }
1470 +
1279 1471 // Save to chat history
1280 - $this->mxchat_save_chat_message( $session_id, 'bot', $html );
1472 + $this->mxchat_save_chat_message($session_id, 'bot', $search_results_text);
1473 +
1474 + // Return the formatted text with embedded HTML links
1475 + return array(
1476 + 'text' => $search_results_text,
1477 + 'html' => ''
1478 + );
1281 1479 } else {
1282 - $this->fallbackResponse = array(
1480 + return array(
1283 1481 '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 )
1482 + esc_html__('I searched for "%s" but couldn\'t find any relevant results. Would you like to try different search terms?', 'mxchat'),
1483 + esc_html($refined_search_query)
1286 1484 ),
1485 + 'html' => ''
1287 1486 );
1288 1487 }
1289 1488 }
1290 -
1291 -
1292 1489 /**
1293 1490 * Format search results into a natural text summary.
1294 1491 *
1295 1492 * @since 1.0.0
@@ -1386,21 +1583,26 @@
1386 1583 }
1387 1584
1388 1585
1389 1586 //very good
1587 +/**
1588 + * Handle image search requests from the chatbot
1589 + *
1590 + * @param string $message The user's search query
1591 + * @param int $user_id The user's ID
1592 + * @param string $session_id The chat session ID
1593 + * @return array Response array with text and HTML content
1594 + */
1390 1595 public function mxchat_handle_image_search_request($message, $user_id, $session_id) {
1391 -
1392 - // Step 1: Interpret the search query for better results
1596 + // Step 1: Interpret the search query using the user's selected AI model
1393 1597 $refined_search_query = $this->mxchat_interpret_search_query($message);
1394 1598
1395 -
1396 1599 // If no query was interpreted, return a fallback message
1397 1600 if (empty($refined_search_query)) {
1398 - $this->fallbackResponse = [
1601 + return array(
1399 1602 'text' => __("I'm sorry, I couldn't interpret your search query. Please specify what you'd like to see images of.", 'mxchat'),
1400 1603 'html' => "",
1401 - ];
1402 - return;
1604 + );
1403 1605 }
1404 1606
1405 1607 // Brave API URL
1406 1608 $api_url = 'https://api.search.brave.com/res/v1/images/search';
@@ -1409,19 +1611,12 @@
1409 1611 $options = get_option('mxchat_options');
1410 1612 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1411 1613
1412 1614 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 = [
1615 + return array(
1420 1616 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'),
1421 1617 'html' => "",
1422 - ];
1423 - return;
1618 + );
1424 1619 }
1425 1620
1426 1621 $image_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1427 1622 $safe_search = isset($options['brave_safe_search']) ? sanitize_text_field($options['brave_safe_search']) : 'strict';
@@ -1432,16 +1627,8 @@
1432 1627 'count' => $image_count,
1433 1628 'safesearch' => $safe_search,
1434 1629 ], $api_url);
1435 1630
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 1631 // Implement caching
1445 1632 $transient_key = 'mxchat_image_search_' . md5($refined_search_query);
1446 1633 $body = get_transient($transient_key);
1447 1634
@@ -1457,19 +1644,12 @@
1457 1644
1458 1645 $response = wp_remote_get($api_url, $args);
1459 1646
1460 1647 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 = [
1648 + return array(
1468 1649 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1469 1650 'html' => "",
1470 - ];
1471 - return;
1651 + );
1472 1652 }
1473 1653
1474 1654 $body = json_decode(wp_remote_retrieve_body($response), true);
1475 1655 set_transient($transient_key, $body, HOUR_IN_SECONDS);
@@ -1477,10 +1657,16 @@
1477 1657
1478 1658 // Process the API response
1479 1659 if (isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) {
1480 1660 $html_output = '<div class="mxchat-image-gallery">';
1481 -
1482 - foreach ($body['results'] as $image) {
1661 +
1662 + // Get the configured image count (1-6)
1663 + $display_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1664 + $display_count = min($display_count, count($body['results'])); // Make sure we don't exceed available images
1665 +
1666 + // Use only the requested number of images
1667 + for ($i = 0; $i < $display_count; $i++) {
1668 + $image = $body['results'][$i];
1483 1669 $image_url = isset($image['url']) ? esc_url($image['url']) : '';
1484 1670 $thumbnail_url = isset($image['thumbnail']['src']) ? esc_url($image['thumbnail']['src']) : '';
1485 1671 $title = isset($image['title']) ? esc_html($image['title']) : esc_html__('Image', 'mxchat');
1486 1672
@@ -1494,47 +1680,95 @@
1494 1680 }
1495 1681
1496 1682 $html_output .= '</div>';
1497 1683
1498 - $this->fallbackResponse = [
1499 - 'text' => "",
1500 - 'html' => $html_output,
1501 - ];
1502 -
1503 - // Save response in chat history
1684 + // Create response text
1685 + $response_text = sprintf(__("Here are some images of %s:", 'mxchat'), $refined_search_query);
1686 +
1687 + // Save both response text and HTML to chat history
1688 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1504 1689 $this->mxchat_save_chat_message($session_id, 'bot', $html_output);
1505 1690
1691 + // Return the combined response
1692 + return array(
1693 + 'text' => $response_text,
1694 + 'html' => $html_output,
1695 + );
1506 1696 } 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'),
1697 + $response_text = __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat');
1698 +
1699 + // Save the error message to chat history
1700 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1701 +
1702 + return array(
1703 + 'text' => $response_text,
1515 1704 'html' => "",
1516 - ];
1705 + );
1517 1706 }
1518 1707 }
1708 +
1709 +/**
1710 + * Interpret the search query using the user's selected AI model
1711 + *
1712 + * @param string $user_query The original query from the user
1713 + * @return string The refined search query
1714 + */
1519 1715 public function mxchat_interpret_search_query($user_query) {
1520 1716 $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"));
1717 +
1718 + // Get options and determine the selected model
1719 + $options = $this->options ?? get_option('mxchat_options');
1720 + $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
1721 +
1722 + // Extract model prefix to determine the provider
1723 + $model_parts = explode('-', $selected_model);
1724 + $provider = strtolower($model_parts[0]);
1725 +
1726 + // Determine which API key to use based on the provider
1727 + switch ($provider) {
1728 + case 'gemini':
1729 + $api_key = isset($options['gemini_api_key']) ? sanitize_text_field($options['gemini_api_key']) : '';
1730 + if (empty($api_key)) {
1731 + return sanitize_text_field($user_query); // Default to original query if API key missing
1732 + }
1733 + return $this->interpret_query_with_gemini($user_query, $system_prompt, $api_key, $selected_model);
1734 +
1735 + case 'claude':
1736 + $api_key = isset($options['claude_api_key']) ? sanitize_text_field($options['claude_api_key']) : '';
1737 + if (empty($api_key)) {
1738 + return sanitize_text_field($user_query);
1739 + }
1740 + return $this->interpret_query_with_claude($user_query, $system_prompt, $api_key, $selected_model);
1741 +
1742 + case 'grok':
1743 + $api_key = isset($options['xai_api_key']) ? sanitize_text_field($options['xai_api_key']) : '';
1744 + if (empty($api_key)) {
1745 + return sanitize_text_field($user_query);
1746 + }
1747 + return $this->interpret_query_with_xai($user_query, $system_prompt, $api_key, $selected_model);
1748 +
1749 + case 'deepseek':
1750 + $api_key = isset($options['deepseek_api_key']) ? sanitize_text_field($options['deepseek_api_key']) : '';
1751 + if (empty($api_key)) {
1752 + return sanitize_text_field($user_query);
1753 + }
1754 + return $this->interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $selected_model);
1755 +
1756 + case 'gpt':
1757 + default:
1758 + // Default to OpenAI for custom models or unrecognized prefixes
1759 + $api_key = isset($options['api_key']) ? sanitize_text_field($options['api_key']) : '';
1760 + if (empty($api_key)) {
1761 + return sanitize_text_field($user_query);
1762 + }
1763 + return $this->interpret_query_with_openai($user_query, $system_prompt, $api_key, $selected_model);
1529 1764 }
1530 - */
1765 +}
1531 1766
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 -
1767 +/**
1768 + * Interpret query using OpenAI models
1769 + */
1770 +private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-4o') {
1537 1771 $url = 'https://api.openai.com/v1/chat/completions';
1538 1772 $args = [
1539 1773 'headers' => [
1540 1774 'Authorization' => 'Bearer ' . $api_key,
@@ -1540,9 +1774,9 @@
1540 1774 'Authorization' => 'Bearer ' . $api_key,
1541 1775 'Content-Type' => 'application/json',
1542 1776 ],
1543 1777 'body' => wp_json_encode([
1544 - 'model' => 'gpt-3.5-turbo',
1778 + 'model' => $model,
1545 1779 'messages' => [
1546 1780 ['role' => 'system', 'content' => $system_prompt],
1547 1781 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1548 1782 ],
@@ -1549,35 +1783,176 @@
1549 1783 'temperature' => 0.2,
1550 1784 'max_tokens' => 20,
1551 1785 ]),
1552 1786 'method' => 'POST',
1787 + 'timeout' => 15,
1553 1788 ];
1554 1789
1555 1790 $response = wp_remote_post($url, $args);
1791 + if (is_wp_error($response)) {
1792 + return sanitize_text_field($user_query);
1793 + }
1556 1794
1795 + $body = json_decode(wp_remote_retrieve_body($response), true);
1796 + return isset($body['choices'][0]['message']['content'])
1797 + ? sanitize_text_field(trim($body['choices'][0]['message']['content']))
1798 + : sanitize_text_field($user_query);
1799 +}
1800 +
1801 +/**
1802 + * Interpret query using Claude models
1803 + */
1804 +private function interpret_query_with_claude($user_query, $system_prompt, $api_key, $model) {
1805 + $url = 'https://api.anthropic.com/v1/messages';
1806 +
1807 + $args = [
1808 + 'headers' => [
1809 + 'Content-Type' => 'application/json',
1810 + 'x-api-key' => $api_key,
1811 + 'anthropic-version' => '2023-06-01',
1812 + ],
1813 + 'body' => wp_json_encode([
1814 + 'model' => $model,
1815 + 'system' => $system_prompt,
1816 + 'messages' => [
1817 + ['role' => 'user', 'content' => sanitize_text_field($user_query)]
1818 + ],
1819 + 'max_tokens' => 20,
1820 + 'temperature' => 0.2,
1821 + ]),
1822 + 'method' => 'POST',
1823 + 'timeout' => 15,
1824 + ];
1825 +
1826 + $response = wp_remote_post($url, $args);
1557 1827 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
1828 + return sanitize_text_field($user_query);
1560 1829 }
1561 1830
1562 1831 $body = json_decode(wp_remote_retrieve_body($response), true);
1832 + if (!empty($body['content'][0]['text'])) {
1833 + return sanitize_text_field(trim($body['content'][0]['text']));
1834 + }
1835 +
1836 + return sanitize_text_field($user_query);
1837 +}
1563 1838
1564 - // Check for a valid response and sanitize output
1839 +/**
1840 + * Interpret query using Gemini models
1841 + */
1842 +private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
1843 + // Strip "gemini-" prefix for the API
1844 + $model_version = str_replace('gemini-', '', $model);
1845 +
1846 + $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
1847 +
1848 + $args = [
1849 + 'headers' => [
1850 + 'Content-Type' => 'application/json',
1851 + ],
1852 + 'body' => wp_json_encode([
1853 + 'contents' => [
1854 + [
1855 + 'role' => 'user',
1856 + 'parts' => [
1857 + ['text' => $system_prompt . "\n\nQuery: " . sanitize_text_field($user_query)]
1858 + ]
1859 + ]
1860 + ],
1861 + 'generationConfig' => [
1862 + 'temperature' => 0.2,
1863 + 'maxOutputTokens' => 20,
1864 + ],
1865 + ]),
1866 + 'method' => 'POST',
1867 + 'timeout' => 15,
1868 + ];
1869 +
1870 + $response = wp_remote_post($url, $args);
1871 + if (is_wp_error($response)) {
1872 + return sanitize_text_field($user_query);
1873 + }
1874 +
1875 + $body = json_decode(wp_remote_retrieve_body($response), true);
1876 + if (!empty($body['candidates'][0]['content']['parts'][0]['text'])) {
1877 + return sanitize_text_field(trim($body['candidates'][0]['content']['parts'][0]['text']));
1878 + }
1879 +
1880 + return sanitize_text_field($user_query);
1881 +}
1882 +
1883 +/**
1884 + * Interpret query using X.AI (Grok) models
1885 + */
1886 +private function interpret_query_with_xai($user_query, $system_prompt, $api_key, $model) {
1887 + $url = 'https://api.xai.com/v1/chat/completions';
1888 +
1889 + $args = [
1890 + 'headers' => [
1891 + 'Content-Type' => 'application/json',
1892 + 'Authorization' => 'Bearer ' . $api_key,
1893 + ],
1894 + 'body' => wp_json_encode([
1895 + 'model' => $model,
1896 + 'messages' => [
1897 + ['role' => 'system', 'content' => $system_prompt],
1898 + ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1899 + ],
1900 + 'temperature' => 0.2,
1901 + 'max_tokens' => 20,
1902 + ]),
1903 + 'method' => 'POST',
1904 + 'timeout' => 15,
1905 + ];
1906 +
1907 + $response = wp_remote_post($url, $args);
1908 + if (is_wp_error($response)) {
1909 + return sanitize_text_field($user_query);
1910 + }
1911 +
1912 + $body = json_decode(wp_remote_retrieve_body($response), true);
1565 1913 if (isset($body['choices'][0]['message']['content'])) {
1566 - $interpreted_query = sanitize_text_field(trim($body['choices'][0]['message']['content']));
1914 + return sanitize_text_field(trim($body['choices'][0]['message']['content']));
1915 + }
1916 +
1917 + return sanitize_text_field($user_query);
1918 +}
1567 1919
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 - */
1574 -
1575 - return $interpreted_query;
1576 - } else {
1577 - //error_log("Unexpected API response format: " . print_r($body, true));
1920 +/**
1921 + * Interpret query using DeepSeek models
1922 + */
1923 +private function interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $model) {
1924 + $url = 'https://api.deepseek.com/v1/chat/completions';
1925 +
1926 + $args = [
1927 + 'headers' => [
1928 + 'Content-Type' => 'application/json',
1929 + 'Authorization' => 'Bearer ' . $api_key,
1930 + ],
1931 + 'body' => wp_json_encode([
1932 + 'model' => $model,
1933 + 'messages' => [
1934 + ['role' => 'system', 'content' => $system_prompt],
1935 + ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1936 + ],
1937 + 'temperature' => 0.2,
1938 + 'max_tokens' => 20,
1939 + ]),
1940 + 'method' => 'POST',
1941 + 'timeout' => 15,
1942 + ];
1943 +
1944 + $response = wp_remote_post($url, $args);
1945 + if (is_wp_error($response)) {
1578 1946 return sanitize_text_field($user_query);
1579 1947 }
1948 +
1949 + $body = json_decode(wp_remote_retrieve_body($response), true);
1950 + if (isset($body['choices'][0]['message']['content'])) {
1951 + return sanitize_text_field(trim($body['choices'][0]['message']['content']));
1952 + }
1953 +
1954 + return sanitize_text_field($user_query);
1580 1955 }
1581 1956
1582 1957
1583 1958
@@ -1585,12 +1960,33 @@
1585 1960 global $wpdb;
1586 1961
1587 1962 // Get embedding for the search query
1588 1963 $query_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1589 - if (!is_array($query_embedding)) {
1964 +
1965 + // Check if embedding generation returned an error
1966 + if (is_array($query_embedding) && isset($query_embedding['error'])) {
1967 + $error_message = $query_embedding['error'];
1968 + $error_code = $query_embedding['error_code'] ?? 'embedding_error';
1969 +
1970 + //error_log("Product search embedding error: $error_message (Code: $error_code)");
1971 +
1972 + // Set a user-friendly fallback response
1973 + $this->fallbackResponse['text'] = esc_html__("I'm having trouble processing your product search. Please try again later or contact support if this persists.", 'mxchat');
1974 +
1975 + // Also store the technical error for admin users
1976 + $this->fallbackResponse['admin_error'] = $error_message;
1977 + $this->fallbackResponse['error_code'] = $error_code;
1978 +
1590 1979 return null;
1591 1980 }
1592 -
1981 +
1982 + // Check if embedding is valid
1983 + if (!is_array($query_embedding) || empty($query_embedding)) {
1984 + //error_log("Failed to generate embedding for product search");
1985 + $this->fallbackResponse['text'] = esc_html__("I couldn't process your product search. Please try again with different wording.", 'mxchat');
1986 + return null;
1987 + }
1988 +
1593 1989 // Get relevant content as string
1594 1990 $relevant_content = $this->mxchat_find_relevant_products($query_embedding);
1595 1991 if (empty($relevant_content)) {
1596 1992 // Return null to indicate no results and set fallback response
@@ -1597,8 +1993,9 @@
1597 1993 $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 1994 return null;
1599 1995 }
1600 1996
1997 +
1601 1998 // Extract product URLs from the content
1602 1999 preg_match_all('/https?:\/\/[^\s<>"\']+?\/product\/[^\s<>"\']+/', $relevant_content, $matches);
1603 2000
1604 2001 if (!empty($matches[0])) {
@@ -1656,9 +2053,8 @@
1656 2053 // New method to handle intent responses
1657 2054 private function generate_intent_response($context_content, $session_id) {
1658 2055 // Convert the context array to a structured string for the AI
1659 2056 $context_string = $this->format_intent_context($context_content);
1660 -
1661 2057 // Generate AI response using the context
1662 2058 $response = $this->mxchat_generate_response(
1663 2059 $context_string,
1664 2060 $this->options['api_key'],
@@ -1664,14 +2060,15 @@
1664 2060 $this->options['api_key'],
1665 2061 $this->options['xai_api_key'],
1666 2062 $this->options['claude_api_key'],
1667 2063 $this->options['deepseek_api_key'],
2064 + $this->options['gemini_api_key'], // Added Gemini API key
1668 2065 $this->mxchat_fetch_conversation_history_for_ai($session_id)
1669 2066 );
1670 -
1671 2067 $this->fallbackResponse['text'] = $response;
1672 2068 return true;
1673 2069 }
2070 +
1674 2071 // Helper method to format intent context
1675 2072 private function format_intent_context($context) {
1676 2073 $context_string = esc_html__("INTENT CONTEXT:\n", 'mxchat');
1677 2074
@@ -1792,8 +2189,10 @@
1792 2189
1793 2190 // Default to proceeding with conversation if no specific PDF action is needed
1794 2191 $this->fallbackResponse['text'] = '';
1795 2192 }
2193 +
2194 +
1796 2195 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
1797 2196 $upload_dir = wp_upload_dir();
1798 2197 $temp_file = null;
1799 2198
@@ -1869,9 +2268,9 @@
1869 2268
1870 2269 return $embeddings;
1871 2270
1872 2271 } catch (\Exception $e) {
1873 - // error_log(esc_html__("Error parsing or processing PDF: ", 'mxchat') . $e->getMessage());
2272 + // //error_log(esc_html__("Error parsing or processing PDF: ", 'mxchat') . $e->getMessage());
1874 2273
1875 2274 // Cleanup in case of exception
1876 2275 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
1877 2276 unlink($temp_file);
@@ -2038,10 +2437,8 @@
2038 2437 'new_messages' => array_values($new_messages)
2039 2438 ]);
2040 2439 wp_die();
2041 2440 }
2042 -
2043 -
2044 2441 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
2045 2442 // First check if live agents are available
2046 2443 $live_agent_available = $this->options['live_agent_status'] ?? 'off';
2047 2444 if ($live_agent_available !== 'on') {
@@ -2060,18 +2457,101 @@
2060 2457 ]);
2061 2458 wp_die();
2062 2459 }
2063 2460
2064 - $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2065 - if (empty($slack_webhook_url)) {
2461 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2462 +
2463 + if (empty($slack_bot_token)) {
2066 2464 return false;
2067 2465 }
2068 2466
2069 - // Get recent chat history (last 5 messages)
2467 + // Check if channel already exists for this session
2468 + $channel_id = get_option("mxchat_channel_{$session_id}", '');
2469 +
2470 + if (empty($channel_id)) {
2471 + // Create new channel with session ID as name
2472 + $channel_name = 'chat-' . strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $session_id));
2473 +
2474 + //error_log("Attempting to create channel: $channel_name");
2475 +
2476 + $response = wp_remote_post('https://slack.com/api/conversations.create', [
2477 + 'headers' => [
2478 + 'Content-Type' => 'application/json',
2479 + 'Authorization' => 'Bearer ' . $slack_bot_token
2480 + ],
2481 + 'body' => json_encode([
2482 + 'name' => $channel_name,
2483 + 'is_private' => false // Public channel - anyone in workspace can join
2484 + ])
2485 + ]);
2486 +
2487 + if (!is_wp_error($response)) {
2488 + $response_body = wp_remote_retrieve_body($response);
2489 + $response_data = json_decode($response_body, true);
2490 +
2491 + //error_log("Channel creation response: " . $response_body);
2492 +
2493 + if (isset($response_data['ok']) && $response_data['ok']) {
2494 + $channel_id = $response_data['channel']['id'];
2495 + $actual_channel_name = $response_data['channel']['name'] ?? 'unknown';
2496 + //error_log("Channel created successfully: ID=$channel_id, Name=$actual_channel_name");
2497 + update_option("mxchat_channel_{$session_id}", $channel_id);
2498 +
2499 + // Auto-invite agents to the channel
2500 + $agent_user_ids = $this->options['live_agent_user_ids'] ?? '';
2501 +
2502 + if (!empty($agent_user_ids)) {
2503 + // Parse user IDs (one per line)
2504 + $user_ids = array_filter(array_map('trim', explode("\n", $agent_user_ids)));
2505 +
2506 + foreach ($user_ids as $user_id_to_invite) {
2507 + //error_log("Inviting user to channel: $user_id_to_invite");
2508 +
2509 + $invite_response = wp_remote_post('https://slack.com/api/conversations.invite', [
2510 + 'headers' => [
2511 + 'Content-Type' => 'application/json',
2512 + 'Authorization' => 'Bearer ' . $slack_bot_token
2513 + ],
2514 + 'body' => json_encode([
2515 + 'channel' => $channel_id,
2516 + 'users' => $user_id_to_invite
2517 + ])
2518 + ]);
2519 +
2520 + if (!is_wp_error($invite_response)) {
2521 + $invite_body = wp_remote_retrieve_body($invite_response);
2522 + $invite_data = json_decode($invite_body, true);
2523 + //error_log("Invite response for $user_id_to_invite: " . $invite_body);
2524 +
2525 + if (isset($invite_data['ok']) && $invite_data['ok']) {
2526 + //error_log("Successfully invited user $user_id_to_invite to channel");
2527 + } else {
2528 + //error_log("Failed to invite user $user_id_to_invite: " . ($invite_data['error'] ?? 'Unknown error'));
2529 + }
2530 + } else {
2531 + //error_log("WP Error inviting user $user_id_to_invite: " . $invite_response->get_error_message());
2532 + }
2533 + }
2534 + } else {
2535 + //error_log("No agent user IDs configured for auto-invite");
2536 + }
2537 + } else {
2538 + //error_log("Channel creation failed: " . ($response_data['error'] ?? 'Unknown error'));
2539 + }
2540 + } else {
2541 + //error_log("WP Error creating channel: " . $response->get_error_message());
2542 + }
2543 +
2544 + if (empty($channel_id)) {
2545 + return false; // Failed to create channel
2546 + }
2547 + }
2548 +
2549 + // Get recent chat history
2070 2550 $history = get_option("mxchat_history_{$session_id}", []);
2071 - $recent_history = array_slice($history, -5); // Get last 5 messages
2551 + $recent_history = array_slice($history, -5);
2072 2552
2073 - // Format conversation history
2553 + // Format conversation context
2074 2554 $conversation_context = "";
2075 2555 if (!empty($recent_history)) {
2076 2556 $conversation_context = "*Recent Conversation:*\n";
2077 2557 foreach ($recent_history as $hist_message) {
@@ -2082,83 +2562,32 @@
2082 2562 }
2083 2563
2084 2564 update_option("mxchat_mode_{$session_id}", 'agent');
2085 2565
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
2566 + // Send message to channel
2567 + $channel_message = "🔔 *New Live Agent Request*\n\n";
2568 + $channel_message .= "*Session ID:* `{$session_id}`\n";
2569 + $channel_message .= "*User ID:* `{$user_id}`\n\n";
2570 +
2113 2571 if (!empty($conversation_context)) {
2114 - $webhook_data['blocks'][] = [
2115 - 'type' => 'section',
2116 - 'text' => [
2117 - 'type' => 'mrkdwn',
2118 - 'text' => $conversation_context
2119 - ]
2120 - ];
2572 + $channel_message .= $conversation_context;
2121 2573 }
2574 +
2575 + $channel_message .= "*Current Message:*\n{$message}\n\n";
2576 + $channel_message .= "_Reply directly in this channel - all messages will go to the user_";
2122 2577
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),
2578 + wp_remote_post('https://slack.com/api/chat.postMessage', [
2152 2579 'headers' => [
2153 2580 'Content-Type' => 'application/json',
2581 + 'Authorization' => 'Bearer ' . $slack_bot_token
2154 2582 ],
2583 + 'body' => json_encode([
2584 + 'channel' => $channel_id,
2585 + 'text' => $channel_message,
2586 + 'mrkdwn' => true
2587 + ])
2155 2588 ]);
2156 2589
2157 - if (is_wp_error($response)) {
2158 - return false;
2159 - }
2160 -
2161 2590 $success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
2162 2591 $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
2163 2592
2164 2593 $this->fallbackResponse = [
@@ -2178,78 +2607,30 @@
2178 2607 ]);
2179 2608 wp_die();
2180 2609 }
2181 2610 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
2182 - $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2611 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2612 + $channel_id = get_option("mxchat_channel_{$session_id}", '');
2183 2613
2184 - if (empty($slack_webhook_url)) {
2185 - //error_log(esc_html__('Slack Webhook URL is not configured.', 'mxchat'));
2614 + if (empty($slack_bot_token) || empty($channel_id)) {
2186 2615 return false;
2187 2616 }
2188 2617
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 - ];
2618 + $user_message = "💬 *User:* {$message}";
2237 2619
2238 - $response = wp_remote_post($slack_webhook_url, [
2239 - 'body' => json_encode($webhook_data),
2620 + $response = wp_remote_post('https://slack.com/api/chat.postMessage', [
2240 2621 'headers' => [
2241 2622 'Content-Type' => 'application/json',
2623 + 'Authorization' => 'Bearer ' . $slack_bot_token
2242 2624 ],
2625 + 'body' => json_encode([
2626 + 'channel' => $channel_id,
2627 + 'text' => $user_message,
2628 + 'mrkdwn' => true
2629 + ])
2243 2630 ]);
2244 2631
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;
2632 + return !is_wp_error($response);
2252 2633 }
2253 2634 public function handle_slack_interaction(WP_REST_Request $request) {
2254 2635 //error_log('Received Slack interaction');
2255 2636
@@ -2337,17 +2718,16 @@
2337 2718
2338 2719 // Default acknowledgment
2339 2720 return new WP_REST_Response(['ok' => true]);
2340 2721 }
2341 -
2342 2722 public function mxchat_handle_agent_response(WP_REST_Request $request) {
2343 2723 //error_log('Received agent response request');
2344 2724 //error_log('Request data: ' . print_r($request->get_params(), true));
2345 - // error_log('Raw body: ' . file_get_contents('php://input'));
2725 + // //error_log('Raw body: ' . file_get_contents('php://input'));
2346 2726
2347 2727 // Get the data from Slack's slash command format
2348 2728 $command_text = $request->get_param('text');
2349 - // error_log('Command text: ' . $command_text);
2729 + // //error_log('Command text: ' . $command_text);
2350 2730
2351 2731 if (empty($command_text)) {
2352 2732 //error_log(esc_html__('Agent response error: No command text received', 'mxchat'));
2353 2733 return new WP_REST_Response([
@@ -2372,9 +2752,9 @@
2372 2752 // Save the message
2373 2753 $message_id = $this->mxchat_save_chat_message($session_id, 'agent', $message);
2374 2754
2375 2755 if (!$message_id) {
2376 - // error_log('Failed to save agent message');
2756 + // //error_log('Failed to save agent message');
2377 2757 return new WP_REST_Response([
2378 2758 'error' => esc_html__('Failed to save message', 'mxchat')
2379 2759 ], 500);
2380 2760 }
@@ -2384,10 +2764,8 @@
2384 2764 'response_type' => 'in_channel',
2385 2765 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
2386 2766 ], 200);
2387 2767 }
2388 -
2389 -
2390 2768 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
2391 2769 //error_log(esc_html__("Switching back to chatbot mode via intent.", 'mxchat'));
2392 2770
2393 2771 // Just update mode to AI
@@ -2401,12 +2779,73 @@
2401 2779 $this->fallbackResponse['text'] = esc_html__('You are now chatting with the AI chatbot.', 'mxchat');
2402 2780
2403 2781 return true; // Intent was handled
2404 2782 }
2783 +public function handle_slack_messages(WP_REST_Request $request) {
2784 + // Log the incoming request for debugging
2785 + //error_log('Slack events request received: ' . $request->get_body());
2786 +
2787 + $body = $request->get_body();
2788 + $data = json_decode($body, true);
2789 +
2790 + // Handle Slack URL verification
2791 + if (isset($data['type']) && $data['type'] === 'url_verification') {
2792 + //error_log('Slack URL verification challenge: ' . $data['challenge']);
2793 + // Return just the challenge string, not wrapped in an array
2794 + return new WP_REST_Response($data['challenge'], 200, ['Content-Type' => 'text/plain']);
2795 + }
2796 +
2797 + // Handle message events
2798 + if (isset($data['event']) && $data['event']['type'] === 'message') {
2799 + $event = $data['event'];
2800 +
2801 + // Skip bot messages and messages with subtypes
2802 + if (isset($event['bot_id']) || isset($event['subtype'])) {
2803 + return new WP_REST_Response(['ok' => true]);
2804 + }
2805 +
2806 + $channel_id = $event['channel'];
2807 + $message_text = $event['text'] ?? '';
2808 +
2809 + // Find session ID by looking for matching channel
2810 + global $wpdb;
2811 + $session_option = $wpdb->get_var(
2812 + $wpdb->prepare(
2813 + "SELECT option_name FROM {$wpdb->options}
2814 + WHERE option_name LIKE 'mxchat_channel_%'
2815 + AND option_value = %s",
2816 + $channel_id
2817 + )
2818 + );
2819 +
2820 + if ($session_option) {
2821 + $session_id = str_replace('mxchat_channel_', '', $session_option);
2822 +
2823 + // Save the agent message
2824 + $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
2825 +
2826 + // Send confirmation back to Slack
2827 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2828 + if (!empty($slack_bot_token)) {
2829 + wp_remote_post('https://slack.com/api/chat.postMessage', [
2830 + 'headers' => [
2831 + 'Content-Type' => 'application/json',
2832 + 'Authorization' => 'Bearer ' . $slack_bot_token
2833 + ],
2834 + 'body' => json_encode([
2835 + 'channel' => $channel_id,
2836 + 'text' => "✅ _Message sent to user_",
2837 + 'thread_ts' => $event['ts'] // Reply in thread
2838 + ])
2839 + ]);
2840 + }
2841 + }
2842 + }
2843 +
2844 + return new WP_REST_Response(['ok' => true]);
2845 +}
2405 2846
2406 2847
2407 -
2408 -
2409 2848 // For the word upload handler
2410 2849 public function mxchat_handle_word_upload() {
2411 2850 // Delegate to word handler
2412 2851 $this->word_handler->mxchat_handle_word_upload();
@@ -2429,21 +2868,102 @@
2429 2868 return MxChat_User::mxchat_get_user_identifier();
2430 2869 }
2431 2870
2432 2871 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 -
2872 + try {
2873 + // Get options and selected model
2874 + $options = get_option('mxchat_options');
2875 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
2876 +
2877 + // Determine endpoint and API key based on model
2878 + if (strpos($selected_model, 'voyage') === 0) {
2879 + $endpoint = 'https://api.voyageai.com/v1/embeddings';
2880 + $api_key = $options['voyage_api_key'] ?? '';
2881 +
2882 + // Check if Voyage API key is missing
2883 + if (empty($api_key)) {
2884 + //error_log('Voyage API key is missing');
2885 + return [
2886 + 'error' => esc_html__('Voyage AI API key is not configured', 'mxchat'),
2887 + 'error_code' => 'missing_voyage_api_key'
2888 + ];
2889 + }
2890 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2891 + $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
2892 + $api_key = $options['gemini_api_key'] ?? '';
2893 +
2894 + // Check if Gemini API key is missing
2895 + if (empty($api_key)) {
2896 + //error_log('Gemini API key is missing');
2897 + return [
2898 + 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
2899 + 'error_code' => 'missing_gemini_api_key'
2900 + ];
2901 + }
2902 + } else {
2903 + $endpoint = 'https://api.openai.com/v1/embeddings';
2904 + // Use the passed API key for OpenAI
2905 +
2906 + // Check if OpenAI API key is missing
2907 + if (empty($api_key)) {
2908 + //error_log('OpenAI API key is missing');
2909 + return [
2910 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
2911 + 'error_code' => 'missing_openai_api_key'
2912 + ];
2913 + }
2914 + }
2915 +
2916 + // Check if text is empty
2917 + if (empty($text)) {
2918 + //error_log('Empty text provided for embedding generation');
2919 + return [
2920 + 'error' => esc_html__('No text provided for embedding generation', 'mxchat'),
2921 + 'error_code' => 'empty_embedding_text'
2922 + ];
2923 + }
2924 +
2925 + // Prepare request body based on provider
2926 + if (strpos($selected_model, 'gemini-embedding') === 0) {
2927 + // Gemini API format
2928 + $request_body = [
2929 + 'model' => 'models/' . $selected_model,
2930 + 'content' => [
2931 + 'parts' => [
2932 + ['text' => $text]
2933 + ]
2934 + ],
2935 + 'outputDimensionality' => 1536
2936 + ];
2937 +
2938 + // Prepare headers for Gemini (API key as query parameter)
2939 + $endpoint .= '?key=' . $api_key;
2940 + $headers = [
2941 + 'Content-Type' => 'application/json'
2942 + ];
2943 + } else {
2944 + // OpenAI/Voyage API format
2945 + $request_body = [
2946 + 'input' => $text,
2947 + 'model' => $selected_model
2948 + ];
2949 +
2950 + // Add output_dimension for voyage-3-large
2951 + if ($selected_model === 'voyage-3-large') {
2952 + $request_body['output_dimension'] = 2048;
2953 + }
2954 +
2955 + // Prepare headers for OpenAI/Voyage
2956 + $headers = [
2957 + 'Content-Type' => 'application/json',
2958 + 'Authorization' => 'Bearer ' . $api_key
2959 + ];
2960 + }
2961 +
2962 + // Prepare request arguments
2440 2963 $args = [
2441 - 'body' => $body,
2442 - 'headers' => [
2443 - 'Content-Type' => 'application/json',
2444 - 'Authorization' => 'Bearer ' . $api_key,
2445 - ],
2964 + 'body' => wp_json_encode($request_body),
2965 + 'headers' => $headers,
2446 2966 'timeout' => 60,
2447 2967 'redirection' => 5,
2448 2968 'blocking' => true,
2449 2969 'httpversion' => '1.0',
@@ -2448,25 +2968,109 @@
2448 2968 'blocking' => true,
2449 2969 'httpversion' => '1.0',
2450 2970 'sslverify' => true,
2451 2971 ];
2452 -
2972 +
2973 + // Make the request
2453 2974 $response = wp_remote_post($endpoint, $args);
2454 -
2975 +
2976 + // Handle WordPress errors
2455 2977 if (is_wp_error($response)) {
2456 - return null;
2978 + $error_message = $response->get_error_message();
2979 + //error_log('Embedding Generation Error: ' . $error_message);
2980 + return [
2981 + 'error' => esc_html__('Connection error when generating embeddings: ', 'mxchat') . esc_html($error_message),
2982 + 'error_code' => 'embedding_connection_error'
2983 + ];
2457 2984 }
2458 -
2985 +
2986 + // Check HTTP status code
2987 + $status_code = wp_remote_retrieve_response_code($response);
2988 + if ($status_code !== 200) {
2989 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
2990 +
2991 + $error_message = isset($response_body['error']['message'])
2992 + ? $response_body['error']['message']
2993 + : 'HTTP Error ' . $status_code;
2994 +
2995 + $error_type = isset($response_body['error']['type'])
2996 + ? $response_body['error']['type']
2997 + : 'unknown';
2998 +
2999 + //error_log('Embedding API HTTP Error: ' . $status_code . ' - ' . $error_message);
3000 +
3001 + // Handle specific error types
3002 + switch ($error_type) {
3003 + case 'invalid_request_error':
3004 + if (strpos($error_message, 'API key') !== false) {
3005 + return [
3006 + 'error' => esc_html__('Invalid API key for embedding generation. Please check your API key configuration.', 'mxchat'),
3007 + 'error_code' => 'embedding_invalid_api_key'
3008 + ];
3009 + }
3010 + break;
3011 +
3012 + case 'authentication_error':
3013 + return [
3014 + 'error' => esc_html__('Authentication failed for embedding generation. Please check your API key.', 'mxchat'),
3015 + 'error_code' => 'embedding_auth_error'
3016 + ];
3017 +
3018 + case 'rate_limit_exceeded':
3019 + return [
3020 + 'error' => esc_html__('Rate limit exceeded for embedding generation. Please try again later.', 'mxchat'),
3021 + 'error_code' => 'embedding_rate_limit'
3022 + ];
3023 +
3024 + case 'quota_exceeded':
3025 + return [
3026 + 'error' => esc_html__('API quota exceeded for embedding generation. Please check your billing details.', 'mxchat'),
3027 + 'error_code' => 'embedding_quota_exceeded'
3028 + ];
3029 + }
3030 +
3031 + // Generic error fallback
3032 + return [
3033 + 'error' => esc_html__('Embedding API error - check embedding API key.: ', 'mxchat') . esc_html($error_message),
3034 + 'error_code' => 'embedding_api_error',
3035 + 'status_code' => $status_code
3036 + ];
3037 + }
3038 +
2459 3039 $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'];
3040 +
3041 + // Handle different response formats based on provider
3042 + if (strpos($selected_model, 'gemini-embedding') === 0) {
3043 + // Gemini API response format
3044 + if (isset($response_body['embedding']['values']) && is_array($response_body['embedding']['values'])) {
3045 + return $response_body['embedding']['values'];
3046 + } else {
3047 + //error_log('Invalid Gemini embedding response: ' . wp_json_encode($response_body));
3048 + return [
3049 + 'error' => esc_html__('Received invalid embedding data from the Gemini API.', 'mxchat'),
3050 + 'error_code' => 'invalid_gemini_embedding_response'
3051 + ];
3052 + }
2463 3053 } else {
2464 - return null;
3054 + // OpenAI/Voyage API response format
3055 + if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
3056 + return $response_body['data'][0]['embedding'];
3057 + } else {
3058 + //error_log('Invalid embedding response: ' . wp_json_encode($response_body));
3059 + return [
3060 + 'error' => esc_html__('Received invalid embedding data from the API.', 'mxchat'),
3061 + 'error_code' => 'invalid_embedding_response'
3062 + ];
3063 + }
2465 3064 }
3065 + } catch (Exception $e) {
3066 + //error_log('Embedding Exception: ' . $e->getMessage());
3067 + return [
3068 + 'error' => esc_html__('System error when generating embeddings: ', 'mxchat') . esc_html($e->getMessage()),
3069 + 'error_code' => 'embedding_exception'
3070 + ];
2466 3071 }
2467 -
2468 -
3072 +}
2469 3073 private function mxchat_find_relevant_content($user_embedding) {
2470 3074 //error_log('MXChat Vector Search: Starting content search...');
2471 3075
2472 3076 // Retrieve the add-on settings from the database.
@@ -2485,9 +3089,8 @@
2485 3089 //error_log('MXChat Vector Search: Using WordPress database');
2486 3090 return $this->find_relevant_content_wordpress($user_embedding);
2487 3091 }
2488 3092 }
2489 -
2490 3093 private function find_relevant_content_wordpress($user_embedding) {
2491 3094 global $wpdb;
2492 3095 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2493 3096 $cache_key = 'mxchat_system_prompt_embeddings';
@@ -2495,8 +3098,9 @@
2495 3098
2496 3099 // Retrieve embeddings from cache or database
2497 3100 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
2498 3101 if ($embeddings === false) {
3102 + // Cache miss - load embeddings from database
2499 3103 $embeddings = [];
2500 3104 $offset = 0;
2501 3105
2502 3106 // Load in batches and build cache
@@ -2515,62 +3119,82 @@
2515 3119 }
2516 3120
2517 3121 $embeddings = array_merge($embeddings, $batch);
2518 3122 $offset += $batch_size;
2519 -
2520 - // Free memory
2521 - unset($batch);
2522 -
3123 + unset($batch); // Free memory
2523 3124 } while (true);
2524 3125
2525 3126 if (empty($embeddings)) {
2526 - return ''; // Return an empty string if no embeddings found
3127 + return ''; // Return empty string if no embeddings found
2527 3128 }
3129 +
3130 + // Cache embeddings for future use
2528 3131 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
2529 3132 }
2530 3133
2531 - // Initialize array to store relevant results with similarity scores
3134 + // Get configuration options
3135 + $main_options = get_option('mxchat_options', []);
3136 +
3137 + // Get base similarity threshold (default 75%)
3138 + $similarity_threshold = isset($main_options['similarity_threshold'])
3139 + ? ((int) $main_options['similarity_threshold']) / 100
3140 + : 0.75;
3141 +
3142 + // Calculate similarities and build results array
2532 3143 $relevant_results = [];
2533 - // Iterate through embeddings to calculate similarity
2534 3144 foreach ($embeddings as $embedding) {
2535 3145 $database_embedding = $embedding->embedding_vector
2536 3146 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
2537 3147 : null;
3148 +
2538 3149 if (is_array($database_embedding) && is_array($user_embedding)) {
2539 3150 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
2540 - $relevant_results[] = [
2541 - 'id' => $embedding->id,
2542 - 'similarity' => $similarity
2543 - ];
3151 +
3152 + // Only consider results above threshold
3153 + if ($similarity >= $similarity_threshold) {
3154 + $relevant_results[] = [
3155 + 'id' => $embedding->id,
3156 + 'similarity' => $similarity
3157 + ];
3158 + }
2544 3159 }
2545 - // Free memory
2546 - unset($database_embedding);
3160 +
3161 + unset($database_embedding); // Free memory
2547 3162 }
2548 3163
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;
2555 - });
3164 + // Sort by similarity (highest first)
2556 3165 usort($relevant_results, function ($a, $b) {
2557 3166 return $b['similarity'] <=> $a['similarity'];
2558 3167 });
2559 -
2560 - // Limit to the top 5 results
3168 +
3169 + // Get top 5 results (standard approach)
2561 3170 $top_results = array_slice($relevant_results, 0, 5);
2562 -
2563 - // Initialize the final content
3171 +
3172 + // Initialize final content
2564 3173 $content = '';
2565 -
2566 - // Fetch and combine content for the top results
2567 - foreach ($top_results as $result) {
3174 +
3175 + // Track document IDs to avoid duplicates
3176 + $added_document_ids = [];
3177 +
3178 + // Fetch and format content for each selected result
3179 + foreach ($top_results as $index => $result) {
3180 + // Skip if already added (for PDF surrounding pages)
3181 + if (in_array($result['id'], $added_document_ids)) {
3182 + continue;
3183 + }
3184 +
2568 3185 $chunk_content = $this->fetch_content_with_product_links($result['id']);
2569 - // Check if the content is PDF-related and add surrounding pages
3186 + $added_document_ids[] = $result['id'];
3187 +
3188 + // Add content with minimal markup
3189 + $content .= "## Reference " . ($index + 1) . " ##\n";
3190 + $content .= $chunk_content . "\n\n";
3191 +
3192 + // Check if content is PDF-related
2570 3193 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
3194 + // Get surrounding pages
2571 3195 $surrounding_content = $wpdb->get_results($wpdb->prepare(
2572 - "SELECT article_content FROM {$system_prompt_table}
3196 + "SELECT id, article_content FROM {$system_prompt_table}
2573 3197 WHERE id IN (
2574 3198 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
2575 3199 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
2576 3200 )",
@@ -2576,52 +3200,66 @@
2576 3200 )",
2577 3201 $result['id'],
2578 3202 $result['id']
2579 3203 ));
2580 - // Add previous content if it exists
3204 +
3205 + // Add previous page content if it exists
2581 3206 if (!empty($surrounding_content[0])) {
3207 + $content .= "## Related Content ##\n";
2582 3208 $content .= $surrounding_content[0]->article_content . "\n\n";
3209 + $added_document_ids[] = $surrounding_content[0]->id;
2583 3210 }
2584 - // Add the main chunk content
2585 - $content .= $chunk_content . "\n\n";
2586 - // Add next content if it exists
3211 +
3212 + // Add next page content if it exists
2587 3213 if (!empty($surrounding_content[1])) {
3214 + $content .= "## Related Content ##\n";
2588 3215 $content .= $surrounding_content[1]->article_content . "\n\n";
3216 + $added_document_ids[] = $surrounding_content[1]->id;
2589 3217 }
2590 - } else {
2591 - // For non-PDF content, add directly
2592 - $content .= $chunk_content . "\n\n";
2593 3218 }
2594 3219 }
2595 -
3220 +
3221 + // Add response guidelines that encourage natural conversation but strict adherence to facts
3222 + if (empty($top_results)) {
3223 + $content = "No reference information was found for this query.\n\n" .
3224 + "Respond naturally but inform the user that you don't have specific information " .
3225 + "about their query and suggest they check the documentation, speak to a live agent, " .
3226 + "or create a support ticket.";
3227 + } else {
3228 + $content .= "\n## Response Guidelines ##\n" .
3229 + "Answer naturally as a helpful product representative would, without phrases like " .
3230 + "'based on my knowledge base' or 'according to the information.' Only use facts " .
3231 + "contained in the references above. If essential information is missing, politely " .
3232 + "guide the user to documentation or support.";
3233 + }
3234 +
2596 3235 return trim($content);
2597 3236 }
2598 -/**
2599 - * Find relevant content in Pinecone vector database
2600 - */
2601 3237 private function find_relevant_content_pinecone($user_embedding) {
2602 3238 $options = get_option('mxchat_pinecone_addon_options', array());
2603 3239 $api_key = $options['mxchat_pinecone_api_key'] ?? '';
2604 3240 $host = $options['mxchat_pinecone_host'] ?? '';
2605 -
3241 +
2606 3242 if (empty($host) || empty($api_key)) {
2607 - //error_log('Pinecone credentials not properly configured');
2608 3243 return '';
2609 3244 }
2610 -
2611 - // Get similarity threshold from WordPress settings
2612 - $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
2613 -
3245 +
3246 + // Get the similarity threshold from the main options
3247 + $main_options = get_option('mxchat_options', []);
3248 + $similarity_threshold = isset($main_options['similarity_threshold'])
3249 + ? ((int) $main_options['similarity_threshold']) / 100
3250 + : 0.75; // Default to 75%
3251 +
2614 3252 // Prepare the query request for Pinecone
2615 3253 $api_endpoint = "https://{$host}/query";
2616 -
3254 +
2617 3255 $request_body = array(
2618 3256 'vector' => $user_embedding,
2619 - 'topK' => 5,
3257 + 'topK' => 8, // Request more to ensure we have enough after filtering
2620 3258 'includeMetadata' => true,
2621 3259 'includeValues' => true
2622 3260 );
2623 -
3261 +
2624 3262 $response = wp_remote_post($api_endpoint, array(
2625 3263 'headers' => array(
2626 3264 'Api-Key' => $api_key,
2627 3265 'accept' => 'application/json',
@@ -2629,46 +3267,70 @@
2629 3267 ),
2630 3268 'body' => wp_json_encode($request_body),
2631 3269 'timeout' => 30
2632 3270 ));
2633 -
3271 +
2634 3272 if (is_wp_error($response)) {
2635 - //error_log('Pinecone query error: ' . $response->get_error_message());
2636 3273 return '';
2637 3274 }
2638 -
3275 +
2639 3276 $response_code = wp_remote_retrieve_response_code($response);
2640 3277 if ($response_code !== 200) {
2641 - //error_log('Pinecone API error: ' . wp_remote_retrieve_body($response));
2642 3278 return '';
2643 3279 }
2644 -
3280 +
2645 3281 $results = json_decode(wp_remote_retrieve_body($response), true);
2646 3282 if (empty($results['matches'])) {
2647 3283 return '';
2648 3284 }
2649 -
3285 +
2650 3286 // Initialize the final content
2651 3287 $content = '';
2652 -
3288 + $matches_used = 0;
3289 +
2653 3290 // Process each match
2654 - foreach ($results['matches'] as $match) {
3291 + foreach ($results['matches'] as $index => $match) {
2655 3292 // Skip if similarity is below threshold
2656 3293 if ($match['score'] < $similarity_threshold) {
2657 3294 continue;
2658 3295 }
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";
3296 +
3297 + // Limit to top 5 matches above threshold
3298 + if ($matches_used >= 5) {
3299 + break;
2664 3300 }
3301 +
3302 + if (!empty($match['metadata']['text'])) {
3303 + // Add content with minimal markup
3304 + $content .= "## Reference " . ($matches_used + 1) . " ##\n";
3305 + $content .= $match['metadata']['text'] . "\n\n";
3306 +
3307 + // Add source URL if available
3308 + if (!empty($match['metadata']['source_url'])) {
3309 + $content .= "URL: " . $match['metadata']['source_url'] . "\n\n";
3310 + }
3311 +
3312 + $matches_used++;
3313 + }
2665 3314 }
2666 -
3315 +
3316 + // Add response guidelines that encourage natural conversation but strict adherence to facts
3317 + if ($matches_used === 0) {
3318 + $content = "No reference information was found for this query.\n\n" .
3319 + "Respond naturally but inform the user that you don't have specific information " .
3320 + "about their query and suggest they check the documentation, speak to a live agent, " .
3321 + "or create a support ticket.";
3322 + } else {
3323 + $content .= "\n## Response Guidelines ##\n" .
3324 + "Answer naturally as a helpful product representative would, without phrases like " .
3325 + "'based on my knowledge base' or 'according to the information.' Only use facts " .
3326 + "contained in the references above. If essential information is missing, politely " .
3327 + "guide the user to documentation or support.";
3328 + }
3329 +
2667 3330 return trim($content);
2668 3331 }
2669 3332
2670 -
2671 3333 private function mxchat_find_relevant_products($user_embedding) {
2672 3334 //error_log('MXChat Vector Search: Starting product search...');
2673 3335
2674 3336 // Retrieve the add-on settings from the database
@@ -2686,9 +3348,8 @@
2686 3348 //error_log('MXChat Vector Search: Using WordPress database for products');
2687 3349 return $this->find_relevant_products_wordpress($user_embedding);
2688 3350 }
2689 3351 }
2690 -
2691 3352 private function find_relevant_products_wordpress($user_embedding) {
2692 3353 global $wpdb;
2693 3354 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2694 3355 $cache_key = 'mxchat_system_prompt_embeddings';
@@ -2762,10 +3423,8 @@
2762 3423 }
2763 3424
2764 3425 return trim($content);
2765 3426 }
2766 -
2767 -// Modified search function with correct filter syntax
2768 3427 private function find_relevant_products_pinecone($user_embedding) {
2769 3428 //error_log('Starting Pinecone product search...');
2770 3429
2771 3430 $options = get_option('mxchat_pinecone_addon_options', array());
@@ -2840,10 +3499,8 @@
2840 3499 }
2841 3500
2842 3501 return trim($content);
2843 3502 }
2844 -
2845 -
2846 3503 private function fetch_content_with_product_links($most_relevant_id) {
2847 3504 global $wpdb;
2848 3505 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2849 3506
@@ -2862,251 +3519,460 @@
2862 3519
2863 3520 return null;
2864 3521 }
2865 3522
2866 -// Function definition
2867 -private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $conversation_history) {
3523 +private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $gemini_api_key, $conversation_history) {
2868 3524 try {
2869 3525 if (!$relevant_content) {
2870 - return esc_html__("I'm sorry, I couldn't find relevant information on that topic.", 'mxchat');
3526 + return [
3527 + 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
3528 + 'error_code' => 'no_relevant_content'
3529 + ];
2871 3530 }
2872 -
3531 +
2873 3532 // Ensure conversation_history is an array
2874 3533 if (!is_array($conversation_history)) {
2875 3534 $conversation_history = array();
2876 3535 }
2877 -
3536 +
2878 3537 // Get selected model with default fallback
2879 3538 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
2880 -
3539 +
2881 3540 // Extract model prefix to determine the provider
2882 3541 $model_parts = explode('-', $selected_model);
2883 3542 $provider = strtolower($model_parts[0]);
2884 -
3543 +
2885 3544 // Handle model selection based on provider prefix
2886 3545 switch ($provider) {
3546 + case 'gemini':
3547 + if (empty($gemini_api_key)) {
3548 + return [
3549 + 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
3550 + 'error_code' => 'missing_gemini_api_key'
3551 + ];
3552 + }
3553 + $response = $this->mxchat_generate_response_gemini(
3554 + $selected_model,
3555 + $gemini_api_key,
3556 + $conversation_history,
3557 + $relevant_content
3558 + );
3559 + break;
3560 +
2887 3561 case 'claude':
2888 3562 if (empty($claude_api_key)) {
2889 - throw new Exception(esc_html__('Claude API key is not configured', 'mxchat'));
3563 + return [
3564 + 'error' => esc_html__('Claude API key is not configured', 'mxchat'),
3565 + 'error_code' => 'missing_claude_api_key'
3566 + ];
2890 3567 }
2891 - return $this->mxchat_generate_response_claude(
3568 + $response = $this->mxchat_generate_response_claude(
2892 3569 $selected_model,
2893 3570 $claude_api_key,
2894 3571 $conversation_history,
2895 3572 $relevant_content
2896 3573 );
2897 -
3574 + break;
3575 +
2898 3576 case 'grok':
2899 3577 if (empty($xai_api_key)) {
2900 - throw new Exception(esc_html__('X.AI API key is not configured', 'mxchat'));
3578 + return [
3579 + 'error' => esc_html__('X.AI API key is not configured', 'mxchat'),
3580 + 'error_code' => 'missing_xai_api_key'
3581 + ];
2901 3582 }
2902 - return $this->mxchat_generate_response_xai(
3583 + $response = $this->mxchat_generate_response_xai(
2903 3584 $selected_model,
2904 3585 $xai_api_key,
2905 3586 $conversation_history,
2906 3587 $relevant_content
2907 3588 );
2908 -
3589 + break;
3590 +
2909 3591 case 'deepseek':
2910 3592 if (empty($deepseek_api_key)) {
2911 - throw new Exception(esc_html__('DeepSeek API key is not configured', 'mxchat'));
3593 + return [
3594 + 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
3595 + 'error_code' => 'missing_deepseek_api_key'
3596 + ];
2912 3597 }
2913 - return $this->mxchat_generate_response_deepseek(
3598 + $response = $this->mxchat_generate_response_deepseek(
2914 3599 $selected_model,
2915 3600 $deepseek_api_key,
2916 3601 $conversation_history,
2917 3602 $relevant_content
2918 3603 );
2919 -
3604 + break;
3605 +
2920 3606 case 'gpt':
2921 3607 if (empty($api_key)) {
2922 - throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
3608 + return [
3609 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
3610 + 'error_code' => 'missing_openai_api_key'
3611 + ];
2923 3612 }
2924 - return $this->mxchat_generate_response_openai(
3613 + $response = $this->mxchat_generate_response_openai(
2925 3614 $selected_model,
2926 3615 $api_key,
2927 3616 $conversation_history,
2928 3617 $relevant_content
2929 3618 );
2930 -
3619 + break;
3620 +
2931 3621 default:
2932 3622 // Default to OpenAI for custom models or unrecognized prefixes
2933 3623 if (empty($api_key)) {
2934 - throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
3624 + return [
3625 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
3626 + 'error_code' => 'missing_openai_api_key'
3627 + ];
2935 3628 }
2936 - return $this->mxchat_generate_response_openai(
3629 + $response = $this->mxchat_generate_response_openai(
2937 3630 $selected_model,
2938 3631 $api_key,
2939 3632 $conversation_history,
2940 3633 $relevant_content
2941 3634 );
3635 + break;
2942 3636 }
3637 +
3638 + // Check if the response is an error array from the provider-specific function
3639 + if (is_array($response) && isset($response['error'])) {
3640 + return $response; // Pass through the error
3641 + }
3642 +
3643 + return $response;
3644 +
2943 3645 } catch (Exception $e) {
2944 3646 //error_log('MXChat Error: ' . $e->getMessage());
2945 - return sprintf(
2946 - esc_html__('An error occurred: %s', 'mxchat'),
2947 - esc_html($e->getMessage())
2948 - );
3647 + return [
3648 + 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
3649 + 'error_code' => 'system_exception',
3650 + 'exception_details' => $e->getMessage()
3651 + ];
2949 3652 }
2950 3653 }
2951 3654
3655 +private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
3656 + try {
3657 + // Ensure conversation_history is an array
3658 + if (!is_array($conversation_history)) {
3659 + $conversation_history = array();
3660 + }
2952 3661
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 - }
3662 + // Get system prompt instructions from options
3663 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
2958 3664
2959 - // Get system prompt instructions from options
2960 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3665 + // Create a new array for the formatted conversation
3666 + $formatted_conversation = array();
2961 3667
2962 - // Create a new array for the formatted conversation
2963 - $formatted_conversation = array();
3668 + // Add system message first
3669 + $formatted_conversation[] = array(
3670 + 'role' => 'system',
3671 + 'content' => $system_prompt_instructions . " " . $relevant_content
3672 + );
2964 3673
2965 - // Add system message first
2966 - $formatted_conversation[] = array(
2967 - 'role' => 'system',
2968 - 'content' => $system_prompt_instructions . " " . $relevant_content
2969 - );
3674 + // Add the rest of the conversation history
3675 + foreach ($conversation_history as $message) {
3676 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3677 + $role = $message['role'];
2970 3678
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'];
3679 + // Convert roles to supported format
3680 + if ($role === 'bot' || $role === 'agent') {
3681 + $role = 'assistant';
3682 + }
3683 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3684 + $role = 'user';
3685 + }
2975 3686
2976 - // Convert roles to supported format
2977 - if ($role === 'bot' || $role === 'agent') {
2978 - $role = 'assistant';
3687 + $formatted_conversation[] = array(
3688 + 'role' => $role,
3689 + 'content' => $message['content']
3690 + );
2979 3691 }
2980 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
2981 - $role = 'user';
2982 - }
3692 + }
2983 3693
2984 - $formatted_conversation[] = array(
2985 - 'role' => $role,
2986 - 'content' => $message['content']
2987 - );
3694 + $body = json_encode([
3695 + 'model' => $selected_model,
3696 + 'messages' => $formatted_conversation,
3697 + 'temperature' => 0.8,
3698 + 'stream' => false
3699 + ]);
3700 +
3701 + $args = [
3702 + 'body' => $body,
3703 + 'headers' => [
3704 + 'Content-Type' => 'application/json',
3705 + 'Authorization' => 'Bearer ' . $deepseek_api_key,
3706 + ],
3707 + 'timeout' => 60,
3708 + 'redirection' => 5,
3709 + 'blocking' => true,
3710 + 'httpversion' => '1.0',
3711 + 'sslverify' => true,
3712 + ];
3713 +
3714 + $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
3715 +
3716 + if (is_wp_error($response)) {
3717 + $error_message = $response->get_error_message();
3718 + //error_log('DeepSeek API Error: ' . $error_message);
3719 + return [
3720 + 'error' => esc_html__('Connection error when contacting DeepSeek: ', 'mxchat') . esc_html($error_message),
3721 + 'error_code' => 'deepseek_connection_error',
3722 + 'provider' => 'deepseek'
3723 + ];
2988 3724 }
2989 - }
2990 3725
2991 - $body = json_encode([
2992 - 'model' => $selected_model,
2993 - 'messages' => $formatted_conversation,
2994 - 'temperature' => 0.8,
2995 - 'stream' => false
2996 - ]);
3726 + $status_code = wp_remote_retrieve_response_code($response);
3727 + if ($status_code !== 200) {
3728 + $response_body = wp_remote_retrieve_body($response);
3729 + $decoded_response = json_decode($response_body, true);
2997 3730
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 - ];
3731 + $error_message = isset($decoded_response['error']['message'])
3732 + ? $decoded_response['error']['message']
3733 + : 'HTTP Error ' . $status_code;
3734 +
3735 + $error_type = isset($decoded_response['error']['type'])
3736 + ? $decoded_response['error']['type']
3737 + : 'unknown';
3738 +
3739 + //error_log('DeepSeek API HTTP Error: ' . $status_code . ' - ' . $error_message);
3010 3740
3011 - $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
3741 + // Handle specific error types
3742 + switch ($status_code) {
3743 + case 401:
3744 + return [
3745 + 'error' => esc_html__('Authentication failed with DeepSeek. Please check your API key.', 'mxchat'),
3746 + 'error_code' => 'deepseek_auth_error',
3747 + 'provider' => 'deepseek'
3748 + ];
3749 +
3750 + case 400:
3751 + if (strpos($error_message, 'API key') !== false) {
3752 + return [
3753 + 'error' => esc_html__('Invalid DeepSeek API key. Please check your API key configuration.', 'mxchat'),
3754 + 'error_code' => 'deepseek_invalid_api_key',
3755 + 'provider' => 'deepseek'
3756 + ];
3757 + }
3758 + break;
3759 +
3760 + case 429:
3761 + if (strpos($error_message, 'quota') !== false) {
3762 + return [
3763 + 'error' => esc_html__('DeepSeek API quota exceeded. Please check your billing details.', 'mxchat'),
3764 + 'error_code' => 'deepseek_quota_exceeded',
3765 + 'provider' => 'deepseek'
3766 + ];
3767 + } else {
3768 + return [
3769 + 'error' => esc_html__('DeepSeek rate limit exceeded. Please try again later.', 'mxchat'),
3770 + 'error_code' => 'deepseek_rate_limit',
3771 + 'provider' => 'deepseek'
3772 + ];
3773 + }
3774 +
3775 + case 500:
3776 + case 502:
3777 + case 503:
3778 + case 504:
3779 + return [
3780 + 'error' => esc_html__('DeepSeek service is currently unavailable. Please try again later.', 'mxchat'),
3781 + 'error_code' => 'deepseek_service_unavailable',
3782 + 'provider' => 'deepseek'
3783 + ];
3784 + }
3012 3785
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 - }
3786 + // Generic error fallback
3787 + return [
3788 + 'error' => esc_html__('DeepSeek API error: ', 'mxchat') . esc_html($error_message),
3789 + 'error_code' => 'deepseek_api_error',
3790 + 'provider' => 'deepseek',
3791 + 'status_code' => $status_code
3792 + ];
3793 + }
3017 3794
3018 - $response_body = wp_remote_retrieve_body($response);
3019 - $decoded_response = json_decode($response_body, true);
3795 + $response_body = wp_remote_retrieve_body($response);
3796 + $decoded_response = json_decode($response_body, true);
3020 3797
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.";
3798 + if (isset($decoded_response['choices'][0]['message']['content'])) {
3799 + return trim($decoded_response['choices'][0]['message']['content']);
3800 + } else {
3801 + //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
3802 + return [
3803 + 'error' => esc_html__('Unexpected response format from DeepSeek.', 'mxchat'),
3804 + 'error_code' => 'deepseek_response_format_error',
3805 + 'provider' => 'deepseek'
3806 + ];
3807 + }
3808 + } catch (Exception $e) {
3809 + //error_log('DeepSeek Exception: ' . $e->getMessage());
3810 + return [
3811 + 'error' => esc_html__('System error when processing DeepSeek request: ', 'mxchat') . esc_html($e->getMessage()),
3812 + 'error_code' => 'deepseek_exception',
3813 + 'provider' => 'deepseek'
3814 + ];
3026 3815 }
3027 3816 }
3028 3817
3029 3818 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 - }
3819 + try {
3820 + // Ensure conversation_history is an array
3821 + if (!is_array($conversation_history)) {
3822 + $conversation_history = array();
3823 + }
3034 3824
3035 - // Get system prompt instructions from options
3036 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3825 + // Get system prompt instructions from options
3826 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3037 3827
3038 - // Create a new array for the formatted conversation
3039 - $formatted_conversation = array();
3828 + // Create a new array for the formatted conversation
3829 + $formatted_conversation = array();
3040 3830
3041 - // Add system message first
3042 - $formatted_conversation[] = array(
3043 - 'role' => 'system',
3044 - 'content' => $system_prompt_instructions . " " . $relevant_content
3045 - );
3831 + // Add system message first
3832 + $formatted_conversation[] = array(
3833 + 'role' => 'system',
3834 + 'content' => $system_prompt_instructions . " " . $relevant_content
3835 + );
3046 3836
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'];
3837 + // Add the rest of the conversation history
3838 + foreach ($conversation_history as $message) {
3839 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3840 + $role = $message['role'];
3051 3841
3052 - // Convert roles to supported format
3053 - if ($role === 'bot' || $role === 'agent') {
3054 - $role = 'assistant';
3842 + // Convert roles to supported format
3843 + if ($role === 'bot' || $role === 'agent') {
3844 + $role = 'assistant';
3845 + }
3846 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3847 + $role = 'user';
3848 + }
3849 +
3850 + $formatted_conversation[] = array(
3851 + 'role' => $role,
3852 + 'content' => $message['content']
3853 + );
3055 3854 }
3056 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3057 - $role = 'user';
3058 - }
3855 + }
3059 3856
3060 - $formatted_conversation[] = array(
3061 - 'role' => $role,
3062 - 'content' => $message['content']
3063 - );
3064 - }
3065 - }
3857 + $body = json_encode([
3858 + 'model' => $selected_model,
3859 + 'messages' => $formatted_conversation,
3860 + 'temperature' => 0.8,
3861 + 'stream' => false
3862 + ]);
3066 3863
3067 - $body = json_encode([
3068 - 'model' => $selected_model,
3069 - 'messages' => $formatted_conversation,
3070 - 'temperature' => 0.8,
3071 - 'stream' => false
3072 - ]);
3864 + $args = [
3865 + 'body' => $body,
3866 + 'headers' => [
3867 + 'Content-Type' => 'application/json',
3868 + 'Authorization' => 'Bearer ' . $api_key,
3869 + ],
3870 + 'timeout' => 60,
3871 + 'redirection' => 5,
3872 + 'blocking' => true,
3873 + 'httpversion' => '1.0',
3874 + 'sslverify' => true,
3875 + ];
3073 3876
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 - ];
3877 + $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
3086 3878
3087 - $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
3879 + if (is_wp_error($response)) {
3880 + $error_message = $response->get_error_message();
3881 + //error_log('OpenAI API Error: ' . $error_message);
3882 + return [
3883 + 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
3884 + 'error_code' => 'openai_connection_error',
3885 + 'provider' => 'openai'
3886 + ];
3887 + }
3088 3888
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 - }
3889 + $status_code = wp_remote_retrieve_response_code($response);
3890 + if ($status_code !== 200) {
3891 + $response_body = wp_remote_retrieve_body($response);
3892 + $decoded_response = json_decode($response_body, true);
3893 +
3894 + $error_message = isset($decoded_response['error']['message'])
3895 + ? $decoded_response['error']['message']
3896 + : 'HTTP Error ' . $status_code;
3897 +
3898 + $error_type = isset($decoded_response['error']['type'])
3899 + ? $decoded_response['error']['type']
3900 + : 'unknown';
3901 +
3902 + //error_log('OpenAI API HTTP Error: ' . $status_code . ' - ' . $error_message);
3903 +
3904 + // Handle specific error types
3905 + switch ($error_type) {
3906 + case 'invalid_request_error':
3907 + if (strpos($error_message, 'API key') !== false) {
3908 + return [
3909 + 'error' => esc_html__('Invalid OpenAI API key. Please check your API key configuration.', 'mxchat'),
3910 + 'error_code' => 'openai_invalid_api_key',
3911 + 'provider' => 'openai'
3912 + ];
3913 + }
3914 + break;
3915 +
3916 + case 'authentication_error':
3917 + return [
3918 + 'error' => esc_html__('Authentication failed with OpenAI. Please check your API key.', 'mxchat'),
3919 + 'error_code' => 'openai_auth_error',
3920 + 'provider' => 'openai'
3921 + ];
3922 +
3923 + case 'rate_limit_exceeded':
3924 + return [
3925 + 'error' => esc_html__('OpenAI rate limit exceeded. Please try again later.', 'mxchat'),
3926 + 'error_code' => 'openai_rate_limit',
3927 + 'provider' => 'openai'
3928 + ];
3929 +
3930 + case 'quota_exceeded':
3931 + return [
3932 + 'error' => esc_html__('OpenAI API quota exceeded. Please check your billing details.', 'mxchat'),
3933 + 'error_code' => 'openai_quota_exceeded',
3934 + 'provider' => 'openai'
3935 + ];
3936 + }
3937 +
3938 + // Generic error fallback
3939 + return [
3940 + 'error' => esc_html__('OpenAI API error: ', 'mxchat') . esc_html($error_message),
3941 + 'error_code' => 'openai_api_error',
3942 + 'provider' => 'openai',
3943 + 'status_code' => $status_code
3944 + ];
3945 + }
3093 3946
3094 - $response_body = wp_remote_retrieve_body($response);
3095 - $decoded_response = json_decode($response_body, true);
3947 + $response_body = wp_remote_retrieve_body($response);
3948 + $decoded_response = json_decode($response_body, true);
3096 3949
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.";
3950 + if (isset($decoded_response['choices'][0]['message']['content'])) {
3951 + return trim($decoded_response['choices'][0]['message']['content']);
3952 + } else {
3953 + //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
3954 + return [
3955 + 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
3956 + 'error_code' => 'openai_response_format_error',
3957 + 'provider' => 'openai'
3958 + ];
3959 + }
3960 + } catch (Exception $e) {
3961 + //error_log('OpenAI Exception: ' . $e->getMessage());
3962 + return [
3963 + 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
3964 + 'error_code' => 'openai_exception',
3965 + 'provider' => 'openai'
3966 + ];
3102 3967 }
3103 3968 }
3104 3969 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'] : '';
3970 + try {
3971 + // Get system prompt instructions from options
3972 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3107 3973
3108 - // Add system prompt to relevant content
3974 + // Add system prompt to relevant content
3109 3975 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
3110 3976
3111 3977 // Prepend system instructions to the conversation history
3112 3978 array_unshift($conversation_history, [
@@ -3131,9 +3997,8 @@
3131 3997 $message['role'] = 'user'; // Default to 'user'
3132 3998 }
3133 3999 }
3134 4000
3135 -
3136 4001 // Build the request body
3137 4002 $body = json_encode([
3138 4003 'model' => $selected_model,
3139 4004 'messages' => $conversation_history,
@@ -3159,20 +4024,138 @@
3159 4024 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
3160 4025
3161 4026 // Process the response
3162 4027 if (is_wp_error($response)) {
3163 - return "Sorry, there was an error processing your request.";
4028 + $error_message = $response->get_error_message();
4029 + //error_log('X.AI API Error: ' . $error_message);
4030 + return [
4031 + 'error' => esc_html__('Connection error when contacting X.AI: ', 'mxchat') . esc_html($error_message),
4032 + 'error_code' => 'xai_connection_error',
4033 + 'provider' => 'xai'
4034 + ];
3164 4035 }
3165 4036
3166 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
4037 + $status_code = wp_remote_retrieve_response_code($response);
4038 + if ($status_code !== 200) {
4039 + $response_body = wp_remote_retrieve_body($response);
4040 + $decoded_response = json_decode($response_body, true);
3167 4041
3168 - if (isset($response_body['choices'][0]['message']['content'])) {
3169 - return trim($response_body['choices'][0]['message']['content']);
4042 + // Log the full response for debugging
4043 + //error_log('X.AI Error Response: ' . print_r($decoded_response, true));
4044 +
4045 + // Extract error message from X.AI's specific format
4046 + $error_message = '';
4047 +
4048 + // Check for direct error string (as seen in your logs)
4049 + if (isset($decoded_response['error']) && is_string($decoded_response['error'])) {
4050 + $error_message = $decoded_response['error'];
4051 + }
4052 + // Check for nested error object (OpenAI style)
4053 + elseif (isset($decoded_response['error']['message'])) {
4054 + $error_message = $decoded_response['error']['message'];
4055 + }
4056 + // Check for top-level message
4057 + elseif (isset($decoded_response['message'])) {
4058 + $error_message = $decoded_response['message'];
4059 + }
4060 + // Fallback
4061 + else {
4062 + $error_message = 'HTTP Error ' . $status_code;
4063 + }
4064 +
4065 + //error_log('X.AI API HTTP Error: ' . $status_code . ' - ' . $error_message);
4066 +
4067 + // Check for API key errors using string matching
4068 + if (stripos($error_message, 'api key') !== false ||
4069 + stripos($error_message, 'incorrect api key') !== false ||
4070 + stripos($error_message, 'invalid api key') !== false) {
4071 + return [
4072 + 'error' => esc_html__('Invalid X.AI API key. Please check your API key configuration.', 'mxchat'),
4073 + 'error_code' => 'xai_invalid_api_key',
4074 + 'provider' => 'xai'
4075 + ];
4076 + }
4077 +
4078 + // Authentication errors
4079 + if ($status_code === 401 || $status_code === 403 ||
4080 + stripos($error_message, 'auth') !== false) {
4081 + return [
4082 + 'error' => esc_html__('Authentication failed with X.AI. Please check your API key.', 'mxchat'),
4083 + 'error_code' => 'xai_auth_error',
4084 + 'provider' => 'xai'
4085 + ];
4086 + }
4087 +
4088 + // Model errors
4089 + if (stripos($error_message, 'model') !== false) {
4090 + return [
4091 + 'error' => esc_html__('Invalid model specified for X.AI. Please check your model configuration.', 'mxchat'),
4092 + 'error_code' => 'xai_invalid_model',
4093 + 'provider' => 'xai'
4094 + ];
4095 + }
4096 +
4097 + // Rate limit errors
4098 + if ($status_code === 429 ||
4099 + stripos($error_message, 'rate') !== false ||
4100 + stripos($error_message, 'limit') !== false) {
4101 + return [
4102 + 'error' => esc_html__('X.AI rate limit exceeded. Please try again later.', 'mxchat'),
4103 + 'error_code' => 'xai_rate_limit',
4104 + 'provider' => 'xai'
4105 + ];
4106 + }
4107 +
4108 + // Quota errors
4109 + if (stripos($error_message, 'quota') !== false ||
4110 + stripos($error_message, 'billing') !== false) {
4111 + return [
4112 + 'error' => esc_html__('X.AI API quota exceeded. Please check your billing details.', 'mxchat'),
4113 + 'error_code' => 'xai_quota_exceeded',
4114 + 'provider' => 'xai'
4115 + ];
4116 + }
4117 +
4118 + // Server errors
4119 + if ($status_code >= 500) {
4120 + return [
4121 + 'error' => esc_html__('X.AI service is currently unavailable. Please try again later.', 'mxchat'),
4122 + 'error_code' => 'xai_service_unavailable',
4123 + 'provider' => 'xai'
4124 + ];
4125 + }
4126 +
4127 + // Generic error fallback with the actual error message
4128 + return [
4129 + 'error' => esc_html__('X.AI API error: ', 'mxchat') . esc_html($error_message),
4130 + 'error_code' => 'xai_api_error',
4131 + 'provider' => 'xai',
4132 + 'status_code' => $status_code
4133 + ];
4134 + }
4135 +
4136 + $response_body = wp_remote_retrieve_body($response);
4137 + $decoded_response = json_decode($response_body, true);
4138 +
4139 + if (isset($decoded_response['choices'][0]['message']['content'])) {
4140 + return trim($decoded_response['choices'][0]['message']['content']);
3170 4141 } else {
3171 - return "Sorry, I couldn't process that request.";
4142 + //error_log('X.AI API Response Format Error: ' . print_r($decoded_response, true));
4143 + return [
4144 + 'error' => esc_html__('Unexpected response format from X.AI.', 'mxchat'),
4145 + 'error_code' => 'xai_response_format_error',
4146 + 'provider' => 'xai'
4147 + ];
3172 4148 }
4149 +} catch (Exception $e) {
4150 + //error_log('X.AI Exception: ' . $e->getMessage());
4151 + return [
4152 + 'error' => esc_html__('System error when processing X.AI request: ', 'mxchat') . esc_html($e->getMessage()),
4153 + 'error_code' => 'xai_exception',
4154 + 'provider' => 'xai'
4155 + ];
3173 4156 }
3174 -
4157 +}
3175 4158 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
3176 4159 // Get system prompt instructions from options
3177 4160 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3178 4161
@@ -3271,8 +4254,151 @@
3271 4254 // Log unexpected response format
3272 4255 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
3273 4256 return "Sorry, I received an unexpected response format from the API.";
3274 4257 }
4258 +private function mxchat_generate_response_gemini($selected_model, $gemini_api_key, $conversation_history, $relevant_content) {
4259 + // Get system prompt instructions from options
4260 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4261 +
4262 + // Add system prompt to relevant content
4263 + $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
4264 +
4265 + // Format messages for Gemini API
4266 + $formatted_messages = [];
4267 +
4268 + // Add system message as the first user message with role prefix
4269 + // Note: Gemini doesn't have a dedicated system role, so we use a prefixed user message
4270 + $formatted_messages[] = [
4271 + 'role' => 'user',
4272 + 'parts' => [
4273 + ['text' => "[System Instructions] " . $content_with_instructions]
4274 + ]
4275 + ];
4276 +
4277 + // Add model response to acknowledge system instructions
4278 + $formatted_messages[] = [
4279 + 'role' => 'model',
4280 + 'parts' => [
4281 + ['text' => "I understand and will follow these instructions."]
4282 + ]
4283 + ];
4284 +
4285 + // Process the rest of the conversation history
4286 + $current_role = null;
4287 + $current_parts = [];
4288 +
4289 + foreach ($conversation_history as $message) {
4290 + // Skip the first system message as we already handled it
4291 + if ($message['role'] === 'system') {
4292 + continue;
4293 + }
4294 +
4295 + // Map roles to Gemini format
4296 + $gemini_role = '';
4297 + if ($message['role'] === 'user') {
4298 + $gemini_role = 'user';
4299 + } else if (in_array($message['role'], ['assistant', 'bot', 'agent'])) {
4300 + $gemini_role = 'model';
4301 + } else {
4302 + // Skip unsupported roles
4303 + continue;
4304 + }
4305 +
4306 + // If we have a new role, add the previous message
4307 + if ($current_role !== null && $current_role !== $gemini_role && !empty($current_parts)) {
4308 + $formatted_messages[] = [
4309 + 'role' => $current_role,
4310 + 'parts' => $current_parts
4311 + ];
4312 + $current_parts = [];
4313 + }
4314 +
4315 + // Set current role and add text to parts
4316 + $current_role = $gemini_role;
4317 + $current_parts[] = ['text' => $message['content']];
4318 + }
4319 +
4320 + // Add the last message if there's content
4321 + if ($current_role !== null && !empty($current_parts)) {
4322 + $formatted_messages[] = [
4323 + 'role' => $current_role,
4324 + 'parts' => $current_parts
4325 + ];
4326 + }
4327 +
4328 + // Build the request body
4329 + $body = json_encode([
4330 + 'contents' => $formatted_messages,
4331 + 'generationConfig' => [
4332 + 'temperature' => 0.7,
4333 + 'topP' => 0.95,
4334 + 'topK' => 40,
4335 + 'maxOutputTokens' => 8192,
4336 + ],
4337 + 'safetySettings' => [
4338 + [
4339 + 'category' => 'HARM_CATEGORY_HARASSMENT',
4340 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
4341 + ],
4342 + [
4343 + 'category' => 'HARM_CATEGORY_HATE_SPEECH',
4344 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
4345 + ],
4346 + [
4347 + 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
4348 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
4349 + ],
4350 + [
4351 + 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT',
4352 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
4353 + ]
4354 + ]
4355 + ]);
4356 +
4357 + // Prepare the API endpoint
4358 + $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
4359 +
4360 + // Set up the API request
4361 + $args = [
4362 + 'body' => $body,
4363 + 'headers' => [
4364 + 'Content-Type' => 'application/json',
4365 + ],
4366 + 'timeout' => 60,
4367 + 'redirection' => 5,
4368 + 'blocking' => true,
4369 + 'httpversion' => '1.0',
4370 + 'sslverify' => true,
4371 + ];
4372 +
4373 + // Make the API request
4374 + $response = wp_remote_post($api_endpoint, $args);
4375 +
4376 + // Process the response
4377 + if (is_wp_error($response)) {
4378 + return "Sorry, there was an error processing your request: " . $response->get_error_message();
4379 + }
4380 +
4381 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
4382 +
4383 + // Handle potential errors in the response
4384 + if (isset($response_body['error'])) {
4385 + //error_log('Gemini API Error: ' . json_encode($response_body['error']));
4386 + return "Sorry, there was an error with the Gemini API: " .
4387 + (isset($response_body['error']['message']) ? $response_body['error']['message'] : 'Unknown error');
4388 + }
4389 +
4390 + // Extract the response text
4391 + if (isset($response_body['candidates'][0]['content']['parts'][0]['text'])) {
4392 + return trim($response_body['candidates'][0]['content']['parts'][0]['text']);
4393 + } else {
4394 + //error_log('Unexpected Gemini API response format: ' . json_encode($response_body));
4395 + return "Sorry, I couldn't process that request. The response format was unexpected.";
4396 + }
4397 +}
4398 +
4399 +
4400 +
3275 4401 public function mxchat_dismiss_pre_chat_message() {
3276 4402 // Get and sanitize the user identifier
3277 4403 $user_id = $this->mxchat_get_user_identifier();
3278 4404 $user_id = sanitize_key($user_id);
@@ -3328,10 +4454,10 @@
3328 4454 }
3329 4455
3330 4456 public function mxchat_enqueue_scripts_styles() {
3331 4457 // Define version numbers for the styles and scripts
3332 - $chat_style_version = '2.0.4'; // Replace with your actual version
3333 - $chat_script_version = '2.0.4'; // Replace with your actual version
4458 + $chat_style_version = '2.2.3';
4459 + $chat_script_version = '2.2.3';
3334 4460
3335 4461 // Enqueue the script
3336 4462 wp_enqueue_script(
3337 4463 'mxchat-chat-js',
@@ -3389,30 +4515,91 @@
3389 4515 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
3390 4516 }
3391 4517
3392 4518
4519 +// Modify the mxchat_reset_rate_limits function to handle different timeframes
4520 +// Modify the mxchat_reset_rate_limits function to handle different timeframes
3393 4521 public function mxchat_reset_rate_limits() {
4522 + try {
3394 4523 global $wpdb;
3395 -
3396 - // Define a cache key pattern for rate limits
3397 - $cache_key_pattern = 'mxchat_chat_limit_%';
3398 -
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_%'");
3402 -
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_%'");
3406 -
3407 - // Clear the relevant cache entries
4524 + $all_options = get_option('mxchat_options', []);
4525 + $current_time = time();
4526 +
4527 + // Get rate limit options with a limit to avoid memory issues
4528 + $option_names = $wpdb->get_col(
4529 + "SELECT option_name FROM {$wpdb->options}
4530 + WHERE option_name LIKE 'mxchat_chat_limit_%'
4531 + LIMIT 500"
4532 + );
4533 +
4534 + if (empty($option_names)) {
4535 + return;
4536 + }
4537 +
3408 4538 foreach ($option_names as $option_name) {
3409 - wp_cache_delete($option_name, 'options');
4539 + // Parse the option name to extract role and user ID
4540 + $parts = explode('_', $option_name);
4541 +
4542 + // Skip if the option name doesn't match our expected format
4543 + if (count($parts) < 4) {
4544 + continue;
4545 + }
4546 +
4547 + // Extract role (may be multiple parts like 'shop_manager')
4548 + $role_parts = array_slice($parts, 3, -1);
4549 + $role = implode('_', $role_parts);
4550 +
4551 + // Skip if role doesn't exist in our settings
4552 + if (!isset($all_options['rate_limits'][$role])) {
4553 + continue;
4554 + }
4555 +
4556 + $timeframe = $all_options['rate_limits'][$role]['timeframe'] ?? 'daily';
4557 + $limit_data = get_option($option_name);
4558 +
4559 + if (!$limit_data || !is_array($limit_data) || !isset($limit_data['timestamp'])) {
4560 + continue;
4561 + }
4562 +
4563 + $timestamp = $limit_data['timestamp'];
4564 + $should_reset = false;
4565 +
4566 + // Determine if we should reset based on the timeframe
4567 + switch ($timeframe) {
4568 + case 'hourly':
4569 + $should_reset = ($current_time - $timestamp) >= 3600;
4570 + break;
4571 + case 'daily':
4572 + $should_reset = ($current_time - $timestamp) >= 86400;
4573 + break;
4574 + case 'weekly':
4575 + $should_reset = ($current_time - $timestamp) >= 604800;
4576 + break;
4577 + case 'monthly':
4578 + $should_reset = ($current_time - $timestamp) >= 2592000;
4579 + break;
4580 + }
4581 +
4582 + // Reset the counter if the timeframe has passed
4583 + if ($should_reset) {
4584 + delete_option($option_name);
4585 + wp_cache_delete($option_name, 'options');
4586 + }
3410 4587 }
3411 -
3412 - // Optionally, clear a general cache if you have one
4588 +
4589 + // Clean up any orphaned entries
3413 4590 wp_cache_delete('mxchat_all_chat_limits', 'options');
4591 +
4592 + } catch (Exception $e) {
4593 + //error_log('MxChat: Rate limit reset error: ' . $e->getMessage());
3414 4594 }
4595 +}
4596 +/**
4597 + * Clean up cron events on deactivation
4598 + */
4599 +public function cleanup_cron_events() {
4600 + wp_clear_scheduled_hook('mxchat_reset_rate_limits');
4601 +}
3415 4602
3416 4603 private function mxchat_fetch_woocommerce_products() {
3417 4604 // Ensure WooCommerce is active
3418 4605 if (!class_exists('WooCommerce')) {
@@ -3447,8 +4634,177 @@
3447 4634 );
3448 4635 }
3449 4636
3450 4637 return $product_data;
4638 +}
4639 +
4640 +/**
4641 + * Check if the current user has exceeded their rate limit based on role
4642 + *
4643 + * @return true|array True if limit not exceeded, or array with error message if exceeded
4644 + */
4645 +public function check_rate_limit() {
4646 + $all_options = get_option('mxchat_options', []);
4647 + //error_log('MXChat Rate Limit: Starting check');
4648 + //error_log('MXChat Rate Limit: Options: ' . print_r($all_options, true));
4649 +
4650 + // Determine user role or if logged out
4651 + if (is_user_logged_in()) {
4652 + $user = wp_get_current_user();
4653 + $user_id = $user->ID;
4654 +
4655 + // Get the user's primary role using reset() to safely get the first element
4656 + $user_roles = $user->roles;
4657 +
4658 + // Safely get the first role regardless of array key structure
4659 + if (!empty($user_roles) && is_array($user_roles)) {
4660 + $role = reset($user_roles); // This safely gets the first element regardless of key
4661 + } else {
4662 + $role = 'subscriber'; // Default to subscriber if no role found
4663 + }
4664 +
4665 + //error_log('MXChat Rate Limit: User ID: ' . $user_id . ', Role: ' . $role);
4666 + } else {
4667 + $role = 'logged_out';
4668 + // Use IP address for non-logged-in users
4669 + $user_id = $this->get_client_ip();
4670 + //error_log('MXChat Rate Limit: Logged out user IP: ' . $user_id);
4671 + }
4672 +
4673 + // Check if rate limits are configured for this role
4674 + if (!isset($all_options['rate_limits'][$role])) {
4675 + //error_log('MXChat Rate Limit: No rate limit configured for role: ' . $role);
4676 + return true; // No limit set for this role
4677 + }
4678 +
4679 + $limit = $all_options['rate_limits'][$role]['limit'];
4680 + //error_log('MXChat Rate Limit: Limit for role ' . $role . ': ' . $limit);
4681 +
4682 + // If unlimited, return true immediately
4683 + if ($limit === 'unlimited') {
4684 + //error_log('MXChat Rate Limit: Unlimited setting, no limit applied');
4685 + return true;
4686 + }
4687 +
4688 + // Get the option name for this user/role
4689 + $option_name = 'mxchat_chat_limit_' . $role . '_' . $user_id;
4690 + //error_log('MXChat Rate Limit: Option name: ' . $option_name);
4691 +
4692 + // Get the counter data
4693 + $limit_data = get_option($option_name, ['count' => 0, 'timestamp' => time()]);
4694 + //error_log('MXChat Rate Limit: Current limit data: ' . print_r($limit_data, true));
4695 +
4696 + // If first request or counter reset needed, set the initial timestamp
4697 + if ($limit_data['count'] === 0) {
4698 + $limit_data['timestamp'] = time();
4699 + update_option($option_name, $limit_data);
4700 + //error_log('MXChat Rate Limit: First request, initialized timestamp');
4701 + }
4702 +
4703 + // Get the timeframe
4704 + $timeframe = isset($all_options['rate_limits'][$role]['timeframe']) ?
4705 + $all_options['rate_limits'][$role]['timeframe'] : 'daily';
4706 + //error_log('MXChat Rate Limit: Timeframe: ' . $timeframe);
4707 +
4708 + // Check if the counter needs to be reset based on timeframe
4709 + $current_time = time();
4710 + $timestamp = $limit_data['timestamp'];
4711 + $should_reset = false;
4712 +
4713 + switch ($timeframe) {
4714 + case 'hourly':
4715 + $should_reset = ($current_time - $timestamp) >= 3600; // 1 hour
4716 + break;
4717 + case 'daily':
4718 + $should_reset = ($current_time - $timestamp) >= 86400; // 24 hours
4719 + break;
4720 + case 'weekly':
4721 + $should_reset = ($current_time - $timestamp) >= 604800; // 7 days
4722 + break;
4723 + case 'monthly':
4724 + $should_reset = ($current_time - $timestamp) >= 2592000; // 30 days
4725 + break;
4726 + }
4727 +
4728 + //error_log('MXChat Rate Limit: Current time: ' . $current_time . ', Last timestamp: ' . $timestamp);
4729 + //error_log('MXChat Rate Limit: Time elapsed: ' . ($current_time - $timestamp) . ' seconds');
4730 + //error_log('MXChat Rate Limit: Should reset: ' . ($should_reset ? 'Yes' : 'No'));
4731 +
4732 + // Reset the counter if the timeframe has passed
4733 + if ($should_reset) {
4734 + $limit_data = ['count' => 0, 'timestamp' => $current_time];
4735 + update_option($option_name, $limit_data);
4736 + //error_log('MXChat Rate Limit: Reset counter to 0');
4737 + }
4738 +
4739 + // Check if user has exceeded their limit
4740 + if ($limit_data['count'] >= intval($limit)) {
4741 + // Get the custom message for this role
4742 + $message = !empty($all_options['rate_limits'][$role]['message'])
4743 + ? $all_options['rate_limits'][$role]['message']
4744 + : __('Rate limit exceeded. Please try again later.', 'mxchat');
4745 +
4746 + // Add timeframe information to the message if placeholders exist
4747 + $timeframe_label = '';
4748 + switch ($timeframe) {
4749 + case 'hourly':
4750 + $timeframe_label = __('hour', 'mxchat');
4751 + break;
4752 + case 'daily':
4753 + $timeframe_label = __('day', 'mxchat');
4754 + break;
4755 + case 'weekly':
4756 + $timeframe_label = __('week', 'mxchat');
4757 + break;
4758 + case 'monthly':
4759 + $timeframe_label = __('month', 'mxchat');
4760 + break;
4761 + }
4762 +
4763 + // Replace placeholders in the message
4764 + $message = str_replace(
4765 + ['{limit}', '{count}', '{remaining}', '{timeframe}'],
4766 + [intval($limit), $limit_data['count'], max(0, intval($limit) - $limit_data['count']), $timeframe_label],
4767 + $message
4768 + );
4769 +
4770 + //error_log('MXChat Rate Limit: Limit exceeded. Message: ' . $message);
4771 +
4772 + // Return error with the custom message
4773 + return [
4774 + 'error' => true,
4775 + 'message' => $message
4776 + ];
4777 + }
4778 +
4779 + // Increment the counter
4780 + $limit_data['count']++;
4781 + update_option($option_name, $limit_data);
4782 + //error_log('MXChat Rate Limit: Incremented counter to ' . $limit_data['count']);
4783 +
4784 + return true;
4785 +}
4786 +
4787 +// Helper function to get client IP address
4788 +private function get_client_ip() {
4789 + // Check for shared internet/ISP IP
4790 + if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
4791 + return sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
4792 + }
4793 +
4794 + // Check for IPs passing through proxies
4795 + if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
4796 + // Use the first value in the comma-separated list
4797 + $forwarded_for = explode(',', sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']));
4798 + return trim($forwarded_for[0]);
4799 + }
4800 +
4801 + if (!empty($_SERVER['REMOTE_ADDR'])) {
4802 + return sanitize_text_field($_SERVER['REMOTE_ADDR']);
4803 + }
4804 +
4805 + // Fallback
4806 + return 'unknown';
3451 4807 }
3452 4808
3453 4809 }
3454 4810 ?>