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