PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.2.8
MxChat – AI Chatbot & Content Generation for WordPress v2.2.8
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 +3453 -1900 2.0.32.2.8 View file →
@@ -9,50 +9,75 @@
9 9 private $chat_count;
10 10 private $fallbackResponse;
11 11 private $productCardHtml;
12 12 private $word_handler;
13 + private $last_similarity_analysis = null;
13 14
15 +
16 +/**
17 + * Setup the cron jobs for rate limits with error handling
18 + */
19 +public function setup_rate_limit_cron_jobs() {
20 + // Clear previous schedules
21 + wp_clear_scheduled_hook('mxchat_reset_rate_limits');
22 + wp_clear_scheduled_hook('mxchat_reset_hourly_rate_limits');
23 + wp_clear_scheduled_hook('mxchat_reset_daily_rate_limits');
24 + wp_clear_scheduled_hook('mxchat_reset_weekly_rate_limits');
25 + wp_clear_scheduled_hook('mxchat_reset_monthly_rate_limits');
26 +
27 + // Schedule the main rate limit reset check (runs hourly) with error handling
28 + if (!wp_next_scheduled('mxchat_reset_rate_limits')) {
29 + $result = wp_schedule_event(time(), 'hourly', 'mxchat_reset_rate_limits');
30 +
31 + // Log error but don't break the plugin
32 + if ($result === false) {
33 + //error_log('MxChat: Failed to schedule rate limit reset cron. Rate limits will still work but may not auto-reset.');
34 + }
35 + }
36 +}
37 +
38 +/**
39 + * Class constructor
40 + */
14 41 public function __construct() {
15 42 $this->options = get_option('mxchat_options');
16 43 $this->prompts_options = get_option('mxchat_prompts_options', array());
17 -
18 44 $this->chat_count = get_option('mxchat_chat_count', 0);
19 45 $this->word_handler = new MXChat_Word_Handler($this->options);
20 -
46 +
47 + // Setup the cron jobs for rate limits
48 + $this->setup_rate_limit_cron_jobs();
49 +
50 + // Add all action hooks
21 51 add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
22 52 add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
23 53 add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
24 -
25 54 add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
26 55 add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
56 +
27 57 // Add the AJAX actions for checking if the pre-chat message was dismissed
28 58 add_action('wp_ajax_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
29 59 add_action('wp_ajax_nopriv_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
30 -
31 60 add_action('wp_ajax_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
32 61 add_action('wp_ajax_nopriv_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
33 -
34 62 add_action('wp_ajax_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
35 63 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 -
64 +
41 65 // Add REST API routes registration
42 66 add_action('rest_api_init', array($this, 'register_routes'));
43 -
44 67 add_action('wp_ajax_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
45 68 add_action('wp_ajax_nopriv_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
46 -
69 +
70 + // Rate limit action - notice we removed the old schedule setup
47 71 add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
48 -
72 +
73 + // File upload and handling actions
49 74 add_action('wp_ajax_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
50 75 add_action('wp_ajax_nopriv_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
51 76 add_action('wp_ajax_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
52 77 add_action('wp_ajax_nopriv_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
53 -
54 - // Add these with your other add_action hooks
78 +
79 + // Word document handling actions
55 80 add_action('wp_ajax_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
56 81 add_action('wp_ajax_nopriv_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
57 82 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
58 83 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
@@ -57,13 +82,24 @@
57 82 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
58 83 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
59 84 add_action('wp_ajax_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
60 85 add_action('wp_ajax_nopriv_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
61 -
86 +
87 + // Email handling actions
62 88 add_action('wp_ajax_nopriv_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
63 89 add_action('wp_ajax_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
64 90 add_action('wp_ajax_nopriv_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
65 91 add_action('wp_ajax_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
92 +
93 + add_action('wp_ajax_mxchat_stream_chat', array($this, 'mxchat_handle_chat_request'));
94 + add_action('wp_ajax_nopriv_mxchat_stream_chat', array($this, 'mxchat_handle_chat_request'));
95 +
96 + // Testing panel AJAX actions
97 + add_action('wp_ajax_mxchat_get_system_info', array($this, 'mxchat_get_system_info'));
98 + add_action('wp_ajax_mxchat_get_similarity_threshold', array($this, 'mxchat_get_similarity_threshold'));
99 + add_action('wp_ajax_mxchat_get_kb_status', array($this, 'mxchat_get_kb_status'));
100 + add_action('wp_ajax_mxchat_start_fresh_session', array($this, 'mxchat_start_fresh_session'));
101 +
66 102 }
67 103
68 104
69 105 private function mxchat_increment_chat_count() {
@@ -96,24 +132,9 @@
96 132 'chat_mode' => $chat_mode
97 133 ]);
98 134 wp_die();
99 135 }
100 -private function mxchat_fetch_conversation_history_for_ajax($session_id) {
101 - $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history based on session ID
102 - $formatted_history = [];
103 136
104 - // Format the history to align with the expected structure for OpenAI
105 - foreach ($history as $entry) {
106 - $formatted_history[] = [
107 - 'role' => $entry['role'], // Ensure this matches 'user' or 'assistant'
108 - 'content' => $entry['content']
109 - ];
110 - }
111 -
112 - return $formatted_history;
113 -}
114 -
115 -
116 137 private function mxchat_fetch_conversation_history_for_ai($session_id) {
117 138 $history = get_option("mxchat_history_{$session_id}", []);
118 139 $formatted_history = [];
119 140
@@ -206,8 +227,14 @@
206 227 'methods' => 'POST',
207 228 'callback' => [$this, 'handle_slack_interaction'],
208 229 'permission_callback' => [$this, 'verify_slack_request'],
209 230 ]);
231 +
232 + register_rest_route('mxchat/v1', '/slack-messages', [
233 + 'methods' => 'POST',
234 + 'callback' => [$this, 'handle_slack_messages'],
235 + 'permission_callback' => [$this, 'verify_slack_request'],
236 + ]);
210 237
211 238 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
212 239 }
213 240
@@ -260,8 +287,9 @@
260 287
261 288 // Compare signatures
262 289 return hash_equals($my_signature, $slack_signature);
263 290 }
291 +
264 292 public function mxchat_stream_events(WP_REST_Request $request) {
265 293 header('Content-Type: text/event-stream');
266 294 header('Cache-Control: no-cache');
267 295 header('Connection: keep-alive');
@@ -297,18 +325,26 @@
297 325
298 326
299 327 private function mxchat_save_chat_message($session_id, $role, $message) {
300 328 global $wpdb;
301 -
302 329 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
303 330 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
304 -
331 +
332 + // Check if this is the first message in a new session (before any other database operations)
333 + $is_new_session = false;
334 + if ($role === 'user') { // Only check for user messages, not bot responses
335 + $existing_messages = $wpdb->get_var($wpdb->prepare(
336 + "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
337 + $session_id
338 + ));
339 + $is_new_session = ($existing_messages == 0);
340 + }
341 +
305 342 // 1) Extract agent name if present
306 343 $agent_name = '';
307 344 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
308 345 $agent_name = $matches[1];
309 346 $message = str_replace("Agent: $agent_name - ", '', $message);
310 -
311 347 $session_meta_key = "mxchat_agent_name_{$session_id}";
312 348 if (empty(get_option($session_meta_key))) {
313 349 update_option($session_meta_key, $agent_name);
314 350 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
@@ -313,30 +349,24 @@
313 349 update_option($session_meta_key, $agent_name);
314 350 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
315 351 }
316 352 }
317 -
318 353 // 2) Generate unique message_id
319 354 $message_id = uniqid();
320 355 //error_log("[DEBUG] mxchat_save_chat_message -> Generated message_id: {$message_id}");
321 -
322 356 // 3) Determine user_id
323 357 $user_id = is_user_logged_in() ? get_current_user_id() : 0;
324 -
325 358 // 4) Determine user_identifier
326 359 $user_identifier = $agent_name
327 360 ? $agent_name
328 361 : MxChat_User::mxchat_get_user_identifier();
329 -
330 362 // 5) Determine displayed_name
331 363 $user_email = MxChat_User::mxchat_get_user_email();
332 364 $displayed_name = $agent_name ? $agent_name : ($user_email ?: $user_identifier);
333 -
334 365 // 6) Check for a saved email in wp_options
335 366 $email_option_key = "mxchat_email_{$session_id}";
336 367 $saved_email = get_option($email_option_key);
337 368 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for email_option_key: {$email_option_key}, found: {$saved_email}");
338 -
339 369 // If found, update DB user_email
340 370 if ($saved_email) {
341 371 $update_res = $wpdb->update(
342 372 $table_name,
@@ -346,9 +376,8 @@
346 376 ['%s']
347 377 );
348 378 //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email update for session_id {$session_id}. update_res: {$update_res}");
349 379 }
350 -
351 380 // 7) Save to session history in wp_options
352 381 $history_key = "mxchat_history_{$session_id}";
353 382 $history = get_option($history_key, []);
354 383 $history[] = [
@@ -357,11 +386,10 @@
357 386 'content' => $message,
358 387 'timestamp' => round(microtime(true) * 1000),
359 388 'agent_name' => $displayed_name,
360 389 ];
361 - update_option($history_key, $history);
390 + update_option($history_key, $history, 'no');
362 391 //error_log("[DEBUG] mxchat_save_chat_message -> Updated session history in option: {$history_key}");
363 -
364 392 // 8) Save the message to DB (INSERT)
365 393 $insert_data = [
366 394 'user_id' => $user_id,
367 395 'user_identifier'=> $user_identifier,
@@ -372,12 +400,64 @@
372 400 'timestamp' => current_time('mysql', 1),
373 401 ];
374 402 $wpdb->insert($table_name, $insert_data);
375 403 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
376 -
404 +
405 + // 9) Send notification email if this is the first user message in a new session
406 + if ($wpdb->insert_id && $is_new_session && $role === 'user') {
407 + $this->send_new_chat_notification($session_id, array(
408 + 'identifier' => $user_identifier,
409 + 'email' => $saved_email ?: $user_email,
410 + 'ip' => $_SERVER['REMOTE_ADDR']
411 + ));
412 + }
413 +
377 414 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
378 415 return $message_id;
379 416 }
417 +private function send_new_chat_notification($session_id, $user_info = array()) {
418 + $options = get_option('mxchat_transcripts_options');
419 +
420 + // Check if notifications are enabled
421 + if (empty($options['mxchat_enable_notifications'])) {
422 + return false;
423 + }
424 +
425 + // Get notification email
426 + $to = !empty($options['mxchat_notification_email']) ?
427 + $options['mxchat_notification_email'] :
428 + get_option('admin_email');
429 +
430 + if (!is_email($to)) {
431 + return false;
432 + }
433 +
434 + // Prepare email content
435 + $subject = sprintf('[%s] New Chat Session Started', get_bloginfo('name'));
436 +
437 + $user_identifier = isset($user_info['identifier']) ? $user_info['identifier'] : 'Guest';
438 + $user_email = isset($user_info['email']) ? $user_info['email'] : 'Not provided';
439 + $user_ip = isset($user_info['ip']) ? $user_info['ip'] : $_SERVER['REMOTE_ADDR'];
440 +
441 + $message = sprintf(
442 + "A new chat session has started on your website.\n\n" .
443 + "Session ID: %s\n" .
444 + "User: %s\n" .
445 + "Email: %s\n" .
446 + "IP Address: %s\n" .
447 + "Time: %s\n\n" .
448 + "View transcripts: %s",
449 + $session_id,
450 + $user_identifier,
451 + $user_email,
452 + $user_ip,
453 + current_time('mysql'),
454 + admin_url('admin.php?page=mxchat-transcripts')
455 + );
456 +
457 + // Send email
458 + return wp_mail($to, $subject, $message);
459 +}
380 460
381 461 public function mxchat_handle_save_email_and_response() {
382 462 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
383 463
@@ -382,9 +462,9 @@
382 462 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
383 463
384 464 // Validate nonce
385 465 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'));
466 + //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
387 467 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
388 468 wp_die();
389 469 }
390 470
@@ -468,96 +548,29 @@
468 548 wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
469 549 }
470 550 }
471 551
552 +public function mxchat_handle_chat_request() {
553 + global $wpdb;
472 554
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);
555 + // NEW: Check if this is a streaming request
556 + $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat';
557 +
558 + // NEW: Set streaming headers if needed
559 + if ($is_streaming) {
560 + // Disable output buffering
561 + while (ob_get_level()) {
562 + ob_end_clean();
507 563 }
564 +
565 + // Set headers for SSE
566 + header('Content-Type: text/event-stream');
567 + header('Cache-Control: no-cache');
568 + header('Connection: keep-alive');
569 + header('X-Accel-Buffering: no');
508 570 }
509 571
510 - $final_limit = $max_limit > 0 ? (string)$max_limit : '100';
511 - //error_log(esc_html__("Final rate limit: ", 'mxchat') . $final_limit);
512 - return $final_limit;
513 -}
514 -
515 -
516 -// Add this to your plugin's main PHP file
517 -public function mxchat_check_new_messages() {
518 - if (!isset($_POST['session_id']) || !isset($_POST['last_seen_id'])) {
519 - wp_send_json_error(['message' => 'Missing required parameters']);
520 - wp_die();
521 - }
522 -
523 - $session_id = sanitize_text_field($_POST['session_id']);
524 - $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
525 -
526 - // Get chat history
527 - $history = get_option("mxchat_history_{$session_id}", []);
528 -
529 - if (empty($history)) {
530 - wp_send_json_success([
531 - 'hasNewMessages' => false,
532 - 'new_messages' => []
533 - ]);
534 - wp_die();
535 - }
536 -
537 - // Filter new messages
538 - $new_messages = array_filter($history, function($message) use ($last_seen_id) {
539 - return isset($message['id']) && $message['id'] > $last_seen_id;
540 - });
541 -
542 - // Sort by ID to ensure proper order
543 - usort($new_messages, function($a, $b) {
544 - return $a['id'] <=> $b['id'];
545 - });
546 -
547 - wp_send_json_success([
548 - 'hasNewMessages' => !empty($new_messages),
549 - 'new_messages' => array_values($new_messages),
550 - 'latestMessageId' => end($new_messages)['id'] ?? $last_seen_id
551 - ]);
552 - wp_die();
553 -}
554 -
555 -public function mxchat_handle_chat_request() {
556 - global $wpdb;
557 -
558 -
559 - // Check if MX Chat Moderation is active
572 + // Check if MX Chat Moderation is active
560 573 if (class_exists('MX_Chat_Moderation')) {
561 574 // Get user email and IP
562 575 $user_email = '';
563 576 $user_ip = $_SERVER['REMOTE_ADDR'];
@@ -591,14 +604,12 @@
591 604 wp_die();
592 605 }
593 606 }
594 607
595 -
596 - // Reset fallback response at the start of each request
597 608 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
598 609 $this->productCardHtml = '';
599 610
600 - // Get the actual WordPress user ID if logged in
611 + // Get the actual WordPress user ID if logged in
601 612 $is_logged_in = is_user_logged_in();
602 613 if ($is_logged_in) {
603 614 $user_id = get_current_user_id(); // This will get the actual WordPress user ID
604 615 } else {
@@ -608,65 +619,24 @@
608 619
609 620 // Get and sanitize the user identifier
610 621 $user_id = sanitize_key($user_id);
611 622
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'));
623 + // Check rate limit using new settings structure
624 + $rate_limit_result = $this->check_rate_limit();
615 625
616 - // Get rate limit based on user status
617 - // Get rate limit based on user status
618 - $rate_limit = $is_logged_in
619 - ? $this->get_user_role_rate_limit($user_id)
620 - : ($this->options['rate_limit_logged_out'] ?? '10');
621 -
622 - //error_log("Selected rate limit: " . $rate_limit);
623 -
624 - // Rest of your code remains the same
625 - // If rate limit is 'unlimited', skip rate limiting checks
626 - if ($rate_limit !== 'unlimited') {
627 - // Convert rate limit to integer
628 - $rate_limit = intval($rate_limit);
629 - // Setup rate limiting
630 - $rate_limit_transient_key = 'mxchat_chat_limit_' . $user_id;
631 - $chat_count = get_transient($rate_limit_transient_key);
632 - if ($chat_count === false) {
633 - // Initialize new counter if none exists
634 - $chat_count = 0;
635 - }
636 - // Check if user has exceeded their rate limit
637 - if ($chat_count >= $rate_limit) {
638 - // Get custom rate limit message or use default
639 - $rate_limit_message = isset($this->options['rate_limit_message'])
640 - ? $this->options['rate_limit_message']
641 - : esc_html__('Rate limit exceeded. Please try again later.', 'mxchat');
642 - // Replace placeholder if it exists in the message
643 - $rate_limit_message = str_replace(
644 - array('{limit}', '{count}', '{remaining}'),
645 - array($rate_limit, $chat_count, max(0, $rate_limit - $chat_count)),
646 - $rate_limit_message
647 - );
648 - wp_send_json([
649 - 'success' => false,
650 - 'message' => $rate_limit_message,
651 - 'status' => 'rate_limit_exceeded',
652 - 'limit' => $rate_limit,
653 - 'count' => $chat_count
654 - ]);
655 - wp_die();
656 - }
657 - // Increment the counter
658 - $chat_count++;
659 - // Store the updated count with 24-hour expiration
660 - set_transient($rate_limit_transient_key, $chat_count, DAY_IN_SECONDS);
626 + if ($rate_limit_result !== true) {
627 + wp_send_json([
628 + 'success' => false,
629 + 'message' => $rate_limit_result['message'],
630 + 'status' => 'rate_limit_exceeded'
631 + ]);
632 + wp_die();
661 633 }
662 634
663 635 // Rest of your existing code...
664 636 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
665 - //error_log("Session ID: $session_id");
666 637
667 638 if (empty($session_id)) {
668 - //error_log("Error: Session ID is missing.");
669 639 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
670 640 wp_die();
671 641 }
672 642
@@ -671,40 +641,106 @@
671 641 }
672 642
673 643 // Validate and sanitize the incoming message
674 644 if (empty($_POST['message'])) {
675 - //error_log("Error: No message received.");
676 645 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
677 646 wp_die();
678 647 }
679 648
649 + // NEW: Get page context if provided
650 + $page_context = null;
651 + if (isset($_POST['page_context']) && !empty($_POST['page_context'])) {
652 + $page_context_raw = stripslashes($_POST['page_context']);
653 + $page_context = json_decode($page_context_raw, true);
654 +
655 + // Validate page context structure
656 + if (is_array($page_context) &&
657 + isset($page_context['url']) &&
658 + isset($page_context['title']) &&
659 + isset($page_context['content'])) {
660 +
661 + // Sanitize page context
662 + $page_context['url'] = esc_url_raw($page_context['url']);
663 + $page_context['title'] = sanitize_text_field($page_context['title']);
664 + $page_context['content'] = wp_kses_post($page_context['content']);
665 + } else {
666 + $page_context = null;
667 + }
668 + }
680 669
681 -// Modify the message sanitization to preserve PHP tags in code blocks
682 -$allowed_tags = [
683 - 'pre' => [],
684 - 'code' => ['class' => true],
685 - 'span' => ['class' => true],
686 - 'div' => ['class' => true],
687 -];
670 + // Modify the message sanitization to preserve PHP tags in code blocks
671 + $allowed_tags = [
672 + 'pre' => [],
673 + 'code' => ['class' => true],
674 + 'span' => ['class' => true],
675 + 'div' => ['class' => true],
676 + ];
688 677
689 -// First preserve code blocks
690 -$message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
691 - return htmlspecialchars_decode($matches[0]);
692 -}, $_POST['message']);
678 + // First preserve code blocks
679 + $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
680 + return htmlspecialchars_decode($matches[0]);
681 + }, $_POST['message']);
693 682
694 -// Then apply sanitization
695 -$message = wp_kses($message, $allowed_tags);
683 + // Then apply sanitization
684 + $message = wp_kses($message, $allowed_tags);
696 685
697 -// Decode code blocks
698 -$message = preg_replace_callback('/(&lt;pre&gt;&lt;code.*?&gt;.*?&lt;\/code&gt;&lt;\/pre&gt;)/s', function($matches) {
699 - return htmlspecialchars_decode($matches[1]);
700 -}, $message);
686 + // Preserve code blocks from markdown conversion
687 + $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
701 688
702 -$message = trim($message);
689 + // ===== SIMPLIFIED TESTING PANEL INITIALIZATION =====
690 + // Always initialize testing data for admins (no toggle needed)
691 + $testing_data = null;
692 + if (current_user_can('administrator')) {
693 + $testing_data = [
694 + 'query' => $message,
695 + 'timestamp' => time(),
696 + 'top_matches' => [],
697 + 'action_matches' => [], // NEW: Initialize action matches array
698 + 'page_context' => $page_context // NEW: Include page context in testing data
699 + ];
700 +
701 + // Get similarity threshold
702 + $similarity_threshold = isset($this->options['similarity_threshold'])
703 + ? ((int) $this->options['similarity_threshold']) / 100
704 + : 0.75;
705 +
706 + $testing_data['similarity_threshold'] = $similarity_threshold;
707 +
708 + // Determine knowledge base type
709 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
710 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
711 + $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
712 + }
713 + // ===== END SIMPLIFIED TESTING INITIALIZATION =====
703 714
704 -// Preserve code blocks from markdown conversion
705 -$message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
715 + // Check if any add-ons want to pre-process this message (for web search etc.)
716 + $pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
706 717
718 + // If the pre-processing returned a result (not the original message), use it directly
719 + if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
720 + // Save the AI response
721 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
722 +
723 + // Save HTML content if provided
724 + if (!empty($pre_processed_result['html'])) {
725 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
726 + }
727 +
728 + // Add testing data if admin
729 + $response_data = [
730 + 'text' => $pre_processed_result['text'],
731 + 'html' => $pre_processed_result['html'] ?? '',
732 + 'session_id' => $session_id
733 + ];
734 +
735 + if ($testing_data !== null) {
736 + $response_data['testing_data'] = $testing_data;
737 + }
738 +
739 + wp_send_json($response_data);
740 + wp_die();
741 + }
742 +
707 743 // Save the user's message
708 744 $this->mxchat_save_chat_message($session_id, 'user', $message);
709 745
710 746 // Check if the message is an email address
@@ -710,18 +746,33 @@
710 746 // Check if the message is an email address
711 747 if (is_email($message)) {
712 748 // Add the email to Loops
713 749 $this->add_email_to_loops($message);
714 -
750 +
715 751 // Send success response
716 752 $response_message = $this->options['email_capture_response'] ??
717 753 esc_html__('Thank you! Your coupon is on the way!', 'mxchat');
718 -
719 - wp_send_json([
754 +
755 + // Clear streaming headers if they were set
756 + if ($is_streaming) {
757 + header_remove('Content-Type');
758 + header_remove('Cache-Control');
759 + header_remove('Connection');
760 + header_remove('X-Accel-Buffering');
761 + header('Content-Type: application/json');
762 + }
763 +
764 + $email_response = [
720 765 'success' => true,
721 766 'status' => 'email_captured',
722 767 'message' => $response_message
723 - ]);
768 + ];
769 +
770 + if ($testing_data !== null) {
771 + $email_response['testing_data'] = $testing_data;
772 + }
773 +
774 + wp_send_json($email_response);
724 775 wp_die();
725 776 }
726 777
727 778 $intent_info = '';
@@ -727,19 +778,22 @@
727 778 $intent_info = '';
728 779
729 780 // Check chat mode
730 781 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
731 - //error_log("Chat Mode: $chat_mode");
732 782
733 783 // Handle agent mode
784 +// Handle agent mode
734 785 if ($chat_mode === 'agent') {
735 786 // First, check for switch intent before doing anything else
736 787 $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
737 788
789 + // NEW: Capture action analysis for testing panel after intent check
790 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
791 + $testing_data['action_matches'] = $this->last_action_analysis;
792 + }
793 +
738 794 // If we matched an intent and it's the switch intent, handle it
739 795 if ($intent_matched && !empty($this->fallbackResponse['text'])) {
740 - //error_log("Switch to chatbot intent detected");
741 -
742 796 // Update chat mode first
743 797 update_option("mxchat_mode_{$session_id}", 'ai');
744 798
745 799 // Clear any existing PDF context to start fresh
@@ -752,8 +806,12 @@
752 806 'session_id' => $session_id,
753 807 'chat_mode' => 'ai'
754 808 ];
755 809
810 + if ($testing_data !== null) {
811 + $response_data['testing_data'] = $testing_data;
812 + }
813 +
756 814 // Save the mode switch message
757 815 $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
758 816 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
759 817
@@ -763,16 +821,20 @@
763 821 } elseif (!$intent_matched) {
764 822 // No intent matched, handle live agent message
765 823 try {
766 824 $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
767 - //error_log("Message sent to agent.");
768 825
769 - wp_send_json_success([
826 + $agent_response = [
770 827 'status' => 'waiting_for_agent',
771 828 'message' => esc_html__('Message sent to live agent.', 'mxchat')
772 - ]);
829 + ];
830 +
831 + if ($testing_data !== null) {
832 + $agent_response['testing_data'] = $testing_data;
833 + }
834 +
835 + wp_send_json_success($agent_response);
773 836 } catch (\Exception $e) {
774 - //error_log("Error sending message to agent: " . $e->getMessage());
775 837 wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
776 838 }
777 839 wp_die();
778 840 }
@@ -777,160 +839,283 @@
777 839 wp_die();
778 840 }
779 841 }
780 842
781 - // Step 1: Check for new PDF URL in the message
782 - if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
783 - $new_pdf_url = $matches[0];
843 + // Step 1: Check for new PDF URL in the message
844 + if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
845 + $new_pdf_url = $matches[0];
784 846
785 - // Check if this is likely a PDF-related request
786 - $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
787 - $is_pdf_request = false;
847 + // Check if this is likely a PDF-related request
848 + $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
849 + $is_pdf_request = false;
788 850
789 - foreach ($pdf_keywords as $keyword) {
790 - if (stripos($message, $keyword) !== false) {
791 - $is_pdf_request = true;
792 - break;
793 - }
851 + foreach ($pdf_keywords as $keyword) {
852 + if (stripos($message, $keyword) !== false) {
853 + $is_pdf_request = true;
854 + break;
794 855 }
856 + }
795 857
796 - // If it looks like a PDF request or we're waiting for a PDF URL
797 - if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
798 - // Validate HTTPS
799 - if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
800 - // Extract filename from URL
801 - $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
858 + // If it looks like a PDF request or we're waiting for a PDF URL
859 + if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
860 + // Validate HTTPS
861 + if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
862 + // Extract filename from URL
863 + $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
802 864
803 - // Clear previous PDF transients
804 - $this->clear_pdf_transients($session_id);
865 + // Clear previous PDF transients
866 + $this->clear_pdf_transients($session_id);
805 867
806 - // Process new PDF
807 - $max_pages = $this->options['pdf_max_pages'] ?? 69;
808 - $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
868 + // Process new PDF
869 + $max_pages = $this->options['pdf_max_pages'] ?? 69;
870 + $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
809 871
810 - if ($embeddings === 'too_many_pages') {
811 - $error_text = sprintf(
812 - $this->options['pdf_intent_error_text'] ??
813 - esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
814 - $max_pages
815 - );
816 - $this->fallbackResponse['text'] = $error_text;
817 - } elseif ($embeddings) {
818 - // Store new PDF information
819 - // Create a more meaningful filename from URL
820 - $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
872 + if ($embeddings === 'too_many_pages') {
873 + $error_text = sprintf(
874 + $this->options['pdf_intent_error_text'] ??
875 + esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
876 + $max_pages
877 + );
878 + $this->fallbackResponse['text'] = $error_text;
879 + } elseif ($embeddings) {
880 + // Store new PDF information
881 + $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
821 882
822 - // If the filename is generic (like results_download.php), create a more descriptive one
823 - if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
824 - strpos($pdf_filename, '.php') !== false) {
825 - // Create a timestamp-based name
826 - $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
827 - }
883 + // If the filename is generic, create a more descriptive one
884 + if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
885 + strpos($pdf_filename, '.php') !== false) {
886 + $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
887 + }
828 888
829 - set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
830 - set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
831 - set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
832 - set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
889 + set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
890 + set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
891 + set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
892 + set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
833 893
834 - $success_text = $this->options['pdf_intent_success_text'] ??
835 - esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
894 + $success_text = $this->options['pdf_intent_success_text'] ??
895 + esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
836 896
837 - // Return success with filename for UI update
838 - wp_send_json([
839 - 'success' => true,
840 - 'message' => $success_text,
841 - 'data' => [
842 - 'filename' => $pdf_filename
843 - ]
844 - ]);
845 - wp_die();
846 - } else {
847 - $error_text = $this->options['pdf_intent_error_text'] ??
848 - esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
849 - $this->fallbackResponse['text'] = $error_text;
897 + $pdf_response = [
898 + 'success' => true,
899 + 'message' => $success_text,
900 + 'data' => [
901 + 'filename' => $pdf_filename
902 + ]
903 + ];
904 +
905 + if ($testing_data !== null) {
906 + $pdf_response['testing_data'] = $testing_data;
850 907 }
851 908
852 - wp_send_json([
853 - 'success' => false,
854 - 'message' => $this->fallbackResponse['text']
855 - ]);
909 + wp_send_json($pdf_response);
856 910 wp_die();
911 + } else {
912 + $error_text = $this->options['pdf_intent_error_text'] ??
913 + esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
914 + $this->fallbackResponse['text'] = $error_text;
857 915 }
916 +
917 + $pdf_error_response = [
918 + 'success' => false,
919 + 'message' => $this->fallbackResponse['text']
920 + ];
921 +
922 + if ($testing_data !== null) {
923 + $pdf_error_response['testing_data'] = $testing_data;
924 + }
925 +
926 + wp_send_json($pdf_error_response);
927 + wp_die();
858 928 }
859 929 }
930 + }
860 931
932 + // Check if there's an active recommendation flow session
933 + $flow_state = get_option("mxchat_sr_flow_state_{$session_id}", array());
934 + if (!empty($flow_state) && isset($flow_state['flow_id'])) {
935 + // Create a dummy intent object that matches the original intent
936 + $dummy_intent = new stdClass();
937 + $dummy_intent->intent_label = 'Recommendation Flow ' . $flow_state['flow_id'];
938 + $dummy_intent->phrases = ''; // Empty phrases to avoid matching the original trigger
939 +
940 + // Call the recommendation flow handler directly
941 + $response_data = apply_filters('mxchat_sr_recommendation_flow', false, $message, $user_id, $session_id, $dummy_intent);
942 +
943 + // If the handler returned a response, send it
944 + if (is_array($response_data) && (isset($response_data['text']) || isset($response_data['html']))) {
945 + // Save the bot's response to the chat history
946 + if (!empty($response_data['text'])) {
947 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['text']);
948 + }
949 + if (!empty($response_data['html'])) {
950 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['html']);
951 + }
952 +
953 + if ($testing_data !== null) {
954 + $response_data['testing_data'] = $testing_data;
955 + }
956 +
957 + // Send the response
958 + wp_send_json($response_data);
959 + wp_die();
960 + }
961 + }
962 +
861 963 // 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"));
964 + $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
864 965
865 - // Step 3: If intent is matched and handled, respond immediately
866 - if ($intent_matched && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
867 - //error_log("Intent response triggered.");
868 - $response_data = [
869 - 'text' => $this->fallbackResponse['text'],
870 - 'html' => $this->fallbackResponse['html'],
871 - 'session_id' => $session_id
872 - ];
873 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text'] . $this->fallbackResponse['html']);
874 - wp_send_json($response_data);
875 - wp_die();
966 + // NEW: Capture action analysis for testing panel after intent check
967 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
968 + $testing_data['action_matches'] = $this->last_action_analysis;
876 969 }
877 970
878 - // If no intent matched or product not found, proceed with AI response
879 - //error_log("No matching intent or fallback. Generating AI response.");
971 + // Step 3: Handle the intent result appropriately
972 + if ($intent_result !== false) {
973 + // Intent was matched - ALWAYS send as JSON response, never streaming
974 +
975 + if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
976 + // Intent returned a direct response array
977 + $response_data = [
978 + 'text' => $intent_result['text'] ?? '',
979 + 'html' => $intent_result['html'] ?? '',
980 + 'session_id' => $session_id
981 + ];
982 +
983 + if ($testing_data !== null) {
984 + $response_data['testing_data'] = $testing_data;
985 + }
986 +
987 + // Clear streaming headers if they were set
988 + if ($is_streaming) {
989 + header_remove('Content-Type');
990 + header_remove('Cache-Control');
991 + header_remove('Connection');
992 + header_remove('X-Accel-Buffering');
993 + header('Content-Type: application/json');
994 + }
995 +
996 + wp_send_json($response_data);
997 + wp_die();
998 + }
999 + else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1000 + // Intent returned true and set fallbackResponse
1001 + $response_data = [
1002 + 'text' => $this->fallbackResponse['text'] ?? '',
1003 + 'html' => $this->fallbackResponse['html'] ?? '',
1004 + 'session_id' => $session_id
1005 + ];
1006 +
1007 + if ($testing_data !== null) {
1008 + $response_data['testing_data'] = $testing_data;
1009 + }
1010 +
1011 + // Clear streaming headers if they were set
1012 + if ($is_streaming) {
1013 + header_remove('Content-Type');
1014 + header_remove('Cache-Control');
1015 + header_remove('Connection');
1016 + header_remove('X-Accel-Buffering');
1017 + header('Content-Type: application/json');
1018 + }
1019 +
1020 + wp_send_json($response_data);
1021 + wp_die();
1022 + }
1023 + }
880 1024
1025 + // If we get here, no intent matched OR the intent didn't provide a usable response
1026 +
881 1027 // Step 4: Generate AI response
882 1028 $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
883 1029 $this->mxchat_increment_chat_count();
884 -
1030 +
885 1031 // Generate embedding for the user's query
886 1032 $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'));
1033 +
1034 + // Check if the embedding generation returned an error
1035 + if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1036 + $error_message = $user_message_embedding['error'];
1037 + $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
1038 +
1039 + wp_send_json_error([
1040 + 'error_message' => $error_message,
1041 + 'error_code' => $error_code
1042 + ]);
890 1043 wp_die();
891 1044 }
1045 +
1046 + // Check if the embedding is valid
1047 + if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1048 + wp_send_json_error([
1049 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1050 + 'error_code' => 'invalid_embedding'
1051 + ]);
1052 + wp_die();
1053 + }
892 1054
893 1055 // Build context with both knowledge base and PDF content if available
894 1056 $context_content = "User asked: '{$message}'\n\n";
895 1057
896 - // Get relevant content from knowledge base
1058 + // NEW: Add page context if available and contextual awareness is enabled
1059 + if ($page_context && isset($this->options['contextual_awareness_toggle']) && $this->options['contextual_awareness_toggle'] === 'on') {
1060 + $context_content .= "===== CURRENT PAGE CONTEXT =====\n";
1061 + $context_content .= "Page URL: " . $page_context['url'] . "\n";
1062 + $context_content .= "Page Title: " . $page_context['title'] . "\n";
1063 + $context_content .= "Page Content: " . $page_context['content'] . "\n";
1064 + $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1065 + }
1066 +
1067 + // Get relevant content from knowledge base - THIS IS WHERE THE SIMILARITY ANALYSIS HAPPENS
897 1068 $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
1069 +
1070 + // ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1071 + if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1072 + // Update testing data with the REAL similarity analysis
1073 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1074 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1075 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1076 + }
1077 + // ===== END SIMILARITY DATA CAPTURE =====
1078 +
898 1079 if (!empty($relevant_content)) {
899 - $context_content .= "Relevant content from knowledge database:\n" . $relevant_content . "\n\n";
1080 + $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1081 + } else {
1082 + $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
900 1083 }
901 1084
902 -
903 - // Check for and include PDF content
904 - $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
905 - $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
906 - $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
907 - if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
908 - $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
909 - if (!empty($relevant_pdf_pages)) {
910 - $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
911 - foreach ($relevant_pdf_pages as $page_data) {
912 - $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
913 - }
914 - $context_content .= "\n";
1085 + // Check for and include PDF content
1086 + $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
1087 + $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
1088 + $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
1089 + if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
1090 + $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
1091 + if (!empty($relevant_pdf_pages)) {
1092 + $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
1093 + foreach ($relevant_pdf_pages as $page_data) {
1094 + $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
915 1095 }
1096 + $context_content .= "\n";
916 1097 }
1098 + }
917 1099
918 - // Check for and include Word content
919 - $word_url = get_transient('mxchat_word_url_' . $session_id);
920 - $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
921 - $word_filename = get_transient('mxchat_word_filename_' . $session_id);
922 - if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
923 - $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
924 - if (!empty($relevant_word_chunks)) {
925 - $context_content .= "Relevant content from Word document '{$word_filename}':\n";
926 - foreach ($relevant_word_chunks as $chunk_data) {
927 - $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
928 - }
929 - $context_content .= "\n";
1100 + // Check for and include Word content
1101 + $word_url = get_transient('mxchat_word_url_' . $session_id);
1102 + $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
1103 + $word_filename = get_transient('mxchat_word_filename_' . $session_id);
1104 + if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
1105 + $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
1106 + if (!empty($relevant_word_chunks)) {
1107 + $context_content .= "Relevant content from Word document '{$word_filename}':\n";
1108 + foreach ($relevant_word_chunks as $chunk_data) {
1109 + $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
930 1110 }
1111 + $context_content .= "\n";
931 1112 }
932 - // Generate the response using the full context
1113 + }
1114 +
1115 + $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
1116 +
1117 + // Generate response
933 1118 $response = $this->mxchat_generate_response(
934 1119 $context_content,
935 1120 $this->options['api_key'],
936 1121 $this->options['xai_api_key'],
@@ -935,11 +1120,34 @@
935 1120 $this->options['api_key'],
936 1121 $this->options['xai_api_key'],
937 1122 $this->options['claude_api_key'],
938 1123 $this->options['deepseek_api_key'],
939 - $conversation_history
1124 + $this->options['gemini_api_key'],
1125 + $conversation_history,
1126 + $is_streaming,
1127 + $session_id,
1128 + $testing_data
940 1129 );
941 -
1130 +
1131 + // Handle streaming vs non-streaming responses
1132 + if ($is_streaming) {
1133 + // Check if streaming actually happened or if it fell back to regular response
1134 + if ($response === true) {
1135 + wp_die();
1136 + }
1137 + // If we get here, streaming fell back to regular response, continue
1138 + }
1139 +
1140 + // Check if the response is an error array
1141 + if (is_array($response) && isset($response['error'])) {
1142 + wp_send_json_error([
1143 + 'error_message' => $response['error'],
1144 + 'error_code' => $response['error_code'] ?? 'api_error'
1145 + ]);
1146 + wp_die();
1147 + }
1148 +
1149 + // If we get here, the response is valid text
942 1150 $this->mxchat_save_chat_message($session_id, 'bot', $response);
943 1151
944 1152 // Step 5: Save additional content if available
945 1153 if (!empty($this->productCardHtml)) {
@@ -956,59 +1164,60 @@
956 1164 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
957 1165 'session_id' => $session_id
958 1166 ];
959 1167
1168 + // Always add testing data for admins (no toggle needed)
1169 + if ($testing_data !== null) {
1170 + $response_data['testing_data'] = $testing_data;
1171 + }
1172 +
960 1173 wp_send_json($response_data);
961 1174 wp_die();
962 1175 }
963 1176
964 1177
965 -// Helper function to clear PDF and Word document related transients
966 -private function clear_pdf_transients($session_id) {
967 - // PDF transients
968 - delete_transient('mxchat_pdf_url_' . $session_id);
969 - delete_transient('mxchat_pdf_embeddings_' . $session_id);
970 - delete_transient('mxchat_include_pdf_in_context_' . $session_id);
971 - delete_transient('mxchat_waiting_for_pdf_url_' . $session_id);
972 -
973 - // Word document transients
974 - delete_transient('mxchat_word_url_' . $session_id);
975 - delete_transient('mxchat_word_filename_' . $session_id);
976 - delete_transient('mxchat_word_embeddings_' . $session_id);
977 - delete_transient('mxchat_include_word_in_context_' . $session_id);
978 - delete_transient('mxchat_waiting_for_word_' . $session_id);
979 -}
980 -
981 -// New function to check intents and invoke the callback function
1178 +// Updated function to check intents and invoke the callback function
982 1179 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
983 1180 global $wpdb;
984 1181 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
985 1182
986 - //error_log("[MxChat] Checking intents for message: " . $message);
987 -
988 1183 // Generate the user embedding
989 1184 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
990 - if (!is_array($user_embedding)) {
991 - //error_log("[MxChat] Failed to generate user embedding");
992 - return false;
1185 +
1186 + // Check if embedding generation returned an error
1187 + if (is_array($user_embedding) && isset($user_embedding['error'])) {
1188 + $error_message = $user_embedding['error'];
1189 + $error_code = $user_embedding['error_code'] ?? 'embedding_error';
1190 +
1191 + wp_send_json_error([
1192 + 'error_message' => $error_message,
1193 + 'error_code' => $error_code
1194 + ]);
1195 + wp_die();
993 1196 }
994 1197
1198 + // Check if embedding is valid
1199 + if (!is_array($user_embedding) || empty($user_embedding)) {
1200 + wp_send_json_error([
1201 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1202 + 'error_code' => 'invalid_embedding'
1203 + ]);
1204 + wp_die();
1205 + }
1206 +
995 1207 // Fetch intents from the database
996 1208 $table_name = $wpdb->prefix . 'mxchat_intents';
997 1209 if ($chat_mode === 'agent') {
998 1210 $query = $wpdb->prepare(
999 - "SELECT * FROM $table_name WHERE callback_function = %s",
1211 + "SELECT * FROM $table_name WHERE callback_function = %s AND (enabled = 1 OR enabled IS NULL)",
1000 1212 'mxchat_handle_switch_to_chatbot_intent'
1001 1213 );
1002 1214 $intents = $wpdb->get_results($query);
1003 1215 } else {
1004 - $intents = $wpdb->get_results("SELECT * FROM $table_name");
1216 + $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
1005 1217 }
1006 1218
1007 - //error_log("[MxChat] Found " . count($intents) . " intents to check");
1008 -
1009 1219 if (empty($intents)) {
1010 - //error_log("[MxChat] No intents found in database");
1011 1220 return false;
1012 1221 }
1013 1222
1014 1223 $highest_similarity = -INF;
@@ -1013,10 +1222,17 @@
1013 1222
1014 1223 $highest_similarity = -INF;
1015 1224 $matched_intent = null;
1016 1225
1226 + // NEW: Array to store action analysis for testing panel
1227 + $action_analysis = [];
1228 +
1017 1229 foreach ($intents as $intent) {
1018 - //error_log("[MxChat] Checking intent: " . $intent->intent_label . " (callback: " . $intent->callback_function . ")");
1230 + // Additional check for enabled state
1231 + $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
1232 + if (!$is_enabled) {
1233 + continue;
1234 + }
1019 1235
1020 1236 $intent_embedding_serialized = $intent->embedding_vector;
1021 1237 $intent_embedding = $intent_embedding_serialized
1022 1238 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
@@ -1022,9 +1238,8 @@
1022 1238 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
1023 1239 : null;
1024 1240
1025 1241 if (!is_array($intent_embedding)) {
1026 - //error_log("[MxChat] Invalid embedding for intent: " . $intent->intent_label);
1027 1242 continue;
1028 1243 }
1029 1244
1030 1245 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
@@ -1029,20 +1244,45 @@
1029 1244
1030 1245 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
1031 1246 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
1032 1247
1033 - //error_log("[MxChat] Similarity for {$intent->intent_label}: {$similarity} (threshold: {$intent_threshold})");
1248 + // NEW: Store action analysis data for testing panel
1249 + $action_analysis[] = [
1250 + 'intent_label' => $intent->intent_label,
1251 + 'callback_function' => $intent->callback_function,
1252 + 'similarity' => round($similarity, 4),
1253 + 'similarity_percentage' => round($similarity * 100, 2),
1254 + 'threshold' => $intent_threshold,
1255 + 'threshold_percentage' => round($intent_threshold * 100, 2),
1256 + 'above_threshold' => $similarity >= $intent_threshold,
1257 + 'triggered' => false // Will be updated below if this intent is triggered
1258 + ];
1034 1259
1035 1260 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
1036 1261 $highest_similarity = $similarity;
1037 1262 $matched_intent = $intent;
1038 - //error_log("[MxChat] New best match: {$intent->intent_label} with similarity {$similarity}");
1039 1263 }
1040 1264 }
1041 1265
1266 + // NEW: Mark the triggered action if any
1042 1267 if ($matched_intent) {
1043 - //error_log("[MxChat] Invoking callback: " . $matched_intent->callback_function);
1044 -
1268 + foreach ($action_analysis as &$action) {
1269 + if ($action['intent_label'] === $matched_intent->intent_label) {
1270 + $action['triggered'] = true;
1271 + break;
1272 + }
1273 + }
1274 + }
1275 +
1276 + // NEW: Sort actions by similarity (highest first) and store for testing panel
1277 + usort($action_analysis, function($a, $b) {
1278 + return $b['similarity'] <=> $a['similarity'];
1279 + });
1280 +
1281 + // Store action analysis for testing panel capture
1282 + $this->last_action_analysis = $action_analysis;
1283 +
1284 + if ($matched_intent) {
1045 1285 // If the callback is a method on this instance (core callback), call it directly
1046 1286 if (method_exists($this, $matched_intent->callback_function)) {
1047 1287 $callback_result = call_user_func(
1048 1288 [$this, $matched_intent->callback_function],
@@ -1048,9 +1288,10 @@
1048 1288 [$this, $matched_intent->callback_function],
1049 1289 $message,
1050 1290 $user_id,
1051 1291 $session_id,
1052 - $matched_intent
1292 + $matched_intent,
1293 + $user_context ?? null
1053 1294 );
1054 1295 } else {
1055 1296 // Otherwise, use apply_filters for add-on callbacks
1056 1297 $callback_result = apply_filters(
@@ -1062,9 +1303,8 @@
1062 1303 $matched_intent
1063 1304 );
1064 1305 }
1065 1306
1066 - //error_log("[MxChat] Callback result: " . print_r($callback_result, true));
1067 1307 if ($callback_result !== false) {
1068 1308 $this->fallbackResponse = $callback_result;
1069 1309 return true;
1070 1310 }
@@ -1069,152 +1309,29 @@
1069 1309 return true;
1070 1310 }
1071 1311 }
1072 1312
1073 - //error_log("[MxChat] No matching intent found");
1074 1313 return false;
1075 1314 }
1076 1315
1316 +// Helper function to clear PDF and Word document related transients
1317 +private function clear_pdf_transients($session_id) {
1318 + // PDF transients
1319 + delete_transient('mxchat_pdf_url_' . $session_id);
1320 + delete_transient('mxchat_pdf_embeddings_' . $session_id);
1321 + delete_transient('mxchat_include_pdf_in_context_' . $session_id);
1322 + delete_transient('mxchat_waiting_for_pdf_url_' . $session_id);
1077 1323
1078 -//verified good
1079 -public function mxchat_handle_order_history($message, $user_id, $session_id) {
1080 - if (!class_exists('WooCommerce')) {
1081 - $this->fallbackResponse['text'] = esc_html__("I can't access order information right now. The order system seems to be unavailable.", 'mxchat');
1082 - return true;
1083 - }
1084 -
1085 - $orderDetails = MxChat_WooCommerce::mxchat_fetch_user_orders_details('all');
1086 -
1087 - if (empty($orderDetails)) {
1088 - $this->fallbackResponse['text'] = esc_html__("I don't see any orders associated with your account. Are you logged in?", 'mxchat');
1089 - return true;
1090 - }
1091 -
1092 - // Generate AI prompt with context
1093 - $prompt = __("User asked about their orders:", 'mxchat') . " '{$message}'\n\n";
1094 - $prompt .= __("Order information:", 'mxchat') . "\n";
1095 - foreach ($orderDetails as $order) {
1096 - $items_list = array_map(function($item) {
1097 - return "{$item['name']} ({$item['quantity']})";
1098 - }, $order['items']);
1099 -
1100 - $prompt .= __("Order #", 'mxchat') . "{$order['order_id']}: {$order['formatted_total']} " . __("on", 'mxchat') . " {$order['date']}\n";
1101 - $prompt .= __("Items:", 'mxchat') . " " . implode(', ', $items_list) . "\n";
1102 - }
1103 -
1104 - $prompt .= __("\nProvide a natural, conversational response focusing on the specific information the user asked about. ", 'mxchat');
1105 - $prompt .= __("If they ask about a specific order or detail, provide just that information. ", 'mxchat');
1106 - $prompt .= __("If they ask about license keys or sensitive information, inform them to check their email or contact support.", 'mxchat');
1107 -
1108 - // Get AI response
1109 - $ai_response = $this->mxchat_call_ai_api($prompt);
1110 - $this->fallbackResponse['text'] = $ai_response['text'];
1111 -
1112 - return true;
1324 + // Word document transients
1325 + delete_transient('mxchat_word_url_' . $session_id);
1326 + delete_transient('mxchat_word_filename_' . $session_id);
1327 + delete_transient('mxchat_word_embeddings_' . $session_id);
1328 + delete_transient('mxchat_include_word_in_context_' . $session_id);
1329 + delete_transient('mxchat_waiting_for_word_' . $session_id);
1113 1330 }
1114 1331
1115 1332
1116 -public function mxchat_handle_product_inquiry($message, $user_id, $session_id) {
1117 - //error_log("PRODUCT INQUIRY START - Message: $message, User ID: $user_id");
1118 1333
1119 - // Sanitize user ID
1120 - $sanitized_user_id = sanitize_key($user_id);
1121 - //error_log("Sanitized User ID: $sanitized_user_id");
1122 -
1123 - // Find product
1124 - $product_id = $this->find_product_in_message($message);
1125 - //error_log("Initial product ID from message: " . ($product_id ?? 'null'));
1126 -
1127 - // Check transient if no product found
1128 - if (!$product_id) {
1129 - $product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1130 - //error_log("Product ID from transient: " . ($product_id ?? 'null'));
1131 - }
1132 -
1133 - // Handle product inquiries
1134 - if ($product_id && class_exists('WooCommerce')) {
1135 - $product = wc_get_product($product_id);
1136 - if ($product) {
1137 - //error_log("Found product: " . $product->get_name() . " (ID: $product_id)");
1138 -
1139 - // Prepare product details
1140 - $product_name = esc_html($product->get_name());
1141 - $product_price = $product->get_price_html();
1142 - $product_image_url = esc_url(wp_get_attachment_url($product->get_image_id()));
1143 - $product_url = esc_url(get_permalink($product_id));
1144 - $product_id_attr = esc_attr($product_id);
1145 -
1146 - // Get product description
1147 - $product_description = $product->get_description() ?: $product->get_short_description();
1148 -
1149 - // Log embeddings process
1150 - //error_log("Generating embedding for message: $message");
1151 - $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1152 - //error_log("Finding relevant content for product inquiry");
1153 - $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
1154 -
1155 - // Build AI prompt
1156 - $ai_prompt = esc_html__("You are a knowledgeable product assistant. ", 'mxchat');
1157 - $ai_prompt .= esc_html__("Respond to this user query: '{$message}'\n\n", 'mxchat');
1158 -
1159 - if (!empty($relevant_content)) {
1160 - $ai_prompt .= esc_html__("Relevant information from our knowledge base:\n{$relevant_content}\n\n", 'mxchat');
1161 - }
1162 -
1163 - $ai_prompt .= esc_html__("Product details:\n", 'mxchat');
1164 - $ai_prompt .= esc_html__("Name: {$product_name}\n", 'mxchat');
1165 - $ai_prompt .= esc_html__("Price: ", 'mxchat') . strip_tags($product_price) . "\n";
1166 - if ($product_description) {
1167 - $ai_prompt .= esc_html__("Description: {$product_description}\n", 'mxchat');
1168 - }
1169 -
1170 - $ai_prompt .= esc_html__("\nInstructions:\n", 'mxchat');
1171 - $ai_prompt .= esc_html__("1. Address the user's specific question or concern about the product\n", 'mxchat');
1172 - $ai_prompt .= esc_html__("2. Incorporate relevant information from our knowledge base if provided\n", 'mxchat');
1173 - $ai_prompt .= esc_html__("3. Highlight key product features that relate to their query\n", 'mxchat');
1174 - $ai_prompt .= esc_html__("4. Include a natural suggestion to check out the product\n", 'mxchat');
1175 - $ai_prompt .= esc_html__("5. Keep the response conversational and helpful\n", 'mxchat');
1176 -
1177 - //error_log("Sending AI prompt: " . $ai_prompt);
1178 -
1179 - // Build and log AI response
1180 - $ai_response = $this->mxchat_call_ai_api($ai_prompt);
1181 - //error_log("AI Response received for product inquiry: " . print_r($ai_response, true));
1182 -
1183 - // Generate and log product card
1184 - $product_card_html = <<<HTML
1185 -<div class="mxchat-product-card">
1186 - <a href="{$product_url}" target="_blank">
1187 - <img src="{$product_image_url}" alt="{$product_name}" class="mxchat-product-image" />
1188 - <h3 class="mxchat-product-name">{$product_name}</h3>
1189 - </a>
1190 - <div class="mxchat-product-price">{$product_price}</div>
1191 - <button class="mxchat-add-to-cart-button" data-product-id="{$product_id_attr}">Add to Cart</button>
1192 -</div>
1193 -HTML;
1194 -
1195 - // Save response and set transient
1196 - $this->productCardHtml = $product_card_html;
1197 - $this->fallbackResponse = [
1198 - 'text' => $ai_response['text'],
1199 - 'html' => $this->productCardHtml,
1200 - ];
1201 -
1202 - //error_log("Setting transient for product: $product_id with key: mxchat_last_discussed_product_$sanitized_user_id");
1203 - set_transient('mxchat_last_discussed_product_' . $sanitized_user_id, $product_id, HOUR_IN_SECONDS);
1204 -
1205 - // Verify transient was set
1206 - $verify_transient = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1207 - //error_log("Verification - Retrieved transient value: " . ($verify_transient ?? 'null'));
1208 -
1209 - return true;
1210 - }
1211 - }
1212 -
1213 - //error_log("PRODUCT INQUIRY END - No product found or WooCommerce not available");
1214 - return false;
1215 -}
1216 -
1217 1334 //verified good
1218 1335 public function mxchat_handle_email_capture($message, $user_id, $session_id) {
1219 1336 // Log the message safely
1220 1337 //error_log("Triggered email capture intent for message: " . sanitize_text_field($message));
@@ -1220,56 +1337,75 @@
1220 1337 //error_log("Triggered email capture intent for message: " . sanitize_text_field($message));
1221 1338
1222 1339 // Initiate email capture flow
1223 1340 $response = esc_html($this->options['triggered_phrase_response'] ?? esc_html__("Would you like to join our mailing list? Please provide your email below.", 'mxchat'));
1224 -
1225 1341 set_transient('mxchat_email_capture_' . $user_id, true, 5 * MINUTE_IN_SECONDS);
1226 1342 $this->mxchat_save_chat_message($session_id, 'bot', $response);
1227 -
1228 - // Respond to the user
1229 - wp_send_json(['message' => $response]);
1230 - wp_die();
1343 +
1344 + // FIXED: Return response data instead of sending JSON directly
1345 + // This allows the main chat handler to add testing data before sending
1346 + return [
1347 + 'text' => $response,
1348 + 'html' => '',
1349 + 'session_id' => $session_id
1350 + ];
1231 1351 }
1232 1352
1233 -//very good
1234 1353 public function mxchat_generate_image($message, $user_id, $session_id) {
1354 + //error_log("Starting image generation for message: " . $message);
1355 +
1235 1356 // Prepare a prompt for DALL-E
1236 1357 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
1237 -
1358 +
1238 1359 // Use the existing OpenAI API key
1239 1360 $openai_api_key = sanitize_text_field($this->options['api_key']);
1240 -
1361 +
1241 1362 // Call DALL-E to generate an image
1242 1363 $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1243 -
1364 +
1244 1365 // Check if the response contains an image URL
1245 1366 if (isset($image_response['imageUrl'])) {
1246 1367 $image_url = esc_url_raw($image_response['imageUrl']);
1247 -
1368 +
1248 1369 // Construct the HTML with a CSS class instead of inline styles
1249 1370 $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
1371 + $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1372 +
1373 + // Save the bot message with both text and HTML
1374 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1375 + $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
1376 +
1377 + // Set the fallback response for the chat handler
1378 + $this->fallbackResponse = [
1379 + 'text' => $response_text,
1380 + 'html' => $response_html,
1381 + 'images' => [$image_url]
1382 + ];
1383 +
1384 + // For debugging/verification - Use json_encode to verify what's being set
1385 + //error_log("Image generation successful - fallbackResponse set: " . json_encode($this->fallbackResponse));
1250 1386
1251 - $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1387 + // Return the response directly instead of relying on the property
1388 + return $this->fallbackResponse;
1252 1389 } else {
1253 1390 $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
1254 - $response_html = '';
1391 +
1392 + // Save the error message
1393 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1394 +
1395 + // Set the fallback response for the chat handler
1396 + $this->fallbackResponse = [
1397 + 'text' => $response_text,
1398 + 'html' => '',
1399 + 'images' => []
1400 + ];
1401 +
1255 1402 //error_log("DALL-E image generation error: " . esc_html($image_response['error'] ?? 'Unknown error.'));
1403 + //error_log("Error fallbackResponse set: " . json_encode($this->fallbackResponse));
1404 +
1405 + // Return the response directly instead of relying on the property
1406 + return $this->fallbackResponse;
1256 1407 }
1257 -
1258 - // Save both text and HTML responses
1259 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text . "\n" . $response_html);
1260 -
1261 - // Prepare the response data
1262 - $response_data = [
1263 - 'message' => $response_text,
1264 - 'html' => $response_html,
1265 - 'image_url' => $image_url ?? '',
1266 - ];
1267 -
1268 - // Send the JSON response
1269 - header('Content-Type: application/json; charset=' . get_option('blog_charset'));
1270 - echo json_encode($response_data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
1271 - wp_die();
1272 1408 }
1273 1409 private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
1274 1410 $api_url = 'https://api.openai.com/v1/images/generations';
1275 1411 $body = json_encode([
@@ -1308,44 +1444,43 @@
1308 1444
1309 1445 /**
1310 1446 * Handle web search requests.
1311 1447 *
1312 - * Sends the refined search query to the Brave Search API and displays neatly formatted,
1313 - * styled search results. Results are cached for performance.
1448 + * Sends the refined search query to the Brave Search API and uses the
1449 + * results to generate a conversational response with the AI model.
1314 1450 *
1315 1451 * @since 1.0.0
1316 1452 * @param string $message The user's search query.
1317 1453 * @param string $user_id The user identifier.
1318 1454 * @param string $session_id The current session ID.
1319 - * @return void
1455 + * @return array Response array containing text with embedded HTML links
1320 1456 */
1321 -public function mxchat_handle_search_request( $message, $user_id, $session_id ) {
1457 +public function mxchat_handle_search_request($message, $user_id, $session_id) {
1322 1458 // Step 1: Interpret and refine the search query
1323 - $refined_search_query = $this->mxchat_interpret_search_query( $message );
1324 -
1325 - if ( empty( $refined_search_query ) ) {
1326 - $this->fallbackResponse = array(
1327 - 'text' => esc_html__( 'I apologize, but could you please rephrase your search request?', 'mxchat' ),
1459 + $refined_search_query = $this->mxchat_interpret_search_query($message);
1460 + if (empty($refined_search_query)) {
1461 + return array(
1462 + 'text' => esc_html__('I apologize, but could you please rephrase your search request?', 'mxchat'),
1463 + 'html' => ''
1328 1464 );
1329 - return;
1330 1465 }
1331 -
1466 +
1332 1467 // Retrieve and validate API settings
1333 - $options = get_option( 'mxchat_options' );
1334 - $api_key = isset( $options['brave_api_key'] ) ? sanitize_text_field( $options['brave_api_key'] ) : '';
1335 - $results_count = isset( $options['brave_results_count'] ) ? absint( $options['brave_results_count'] ) : 5;
1336 -
1337 - if ( empty( $api_key ) ) {
1338 - $this->fallbackResponse = array(
1339 - 'text' => esc_html__( 'Search functionality is temporarily unavailable. Please try again later.', 'mxchat' ),
1468 + $options = get_option('mxchat_options');
1469 + $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1470 + $results_count = isset($options['brave_results_count']) ? absint($options['brave_results_count']) : 5;
1471 +
1472 + if (empty($api_key)) {
1473 + return array(
1474 + 'text' => esc_html__('Search functionality is temporarily unavailable. Please try again later.', 'mxchat'),
1475 + 'html' => ''
1340 1476 );
1341 - return;
1342 1477 }
1343 -
1478 +
1344 1479 // Build the API request URL
1345 1480 $api_url = add_query_arg(
1346 1481 array(
1347 - 'q' => rawurlencode( $refined_search_query ),
1482 + 'q' => rawurlencode($refined_search_query),
1348 1483 'count' => $results_count,
1349 1484 'text_decorations' => 'true',
1350 1485 'rich_data' => 'true',
1351 1486 ),
@@ -1350,14 +1485,14 @@
1350 1485 'rich_data' => 'true',
1351 1486 ),
1352 1487 'https://api.search.brave.com/res/v1/web/search'
1353 1488 );
1354 -
1489 +
1355 1490 // Attempt to retrieve cached results first
1356 - $transient_key = 'mxchat_search_' . md5( $refined_search_query );
1357 - $results = get_transient( $transient_key );
1358 -
1359 - if ( false === $results ) {
1491 + $transient_key = 'mxchat_search_' . md5($refined_search_query);
1492 + $results = get_transient($transient_key);
1493 +
1494 + if (false === $results) {
1360 1495 // Fetch new results from the Brave Search API
1361 1496 $response = wp_remote_get(
1362 1497 $api_url,
1363 1498 array(
@@ -1368,162 +1503,98 @@
1368 1503 ),
1369 1504 'timeout' => 10,
1370 1505 )
1371 1506 );
1372 -
1373 - if ( is_wp_error( $response ) ) {
1374 - $this->fallbackResponse = array(
1375 - 'text' => esc_html__( 'I encountered an error while searching. Please try again.', 'mxchat' ),
1507 +
1508 + if (is_wp_error($response)) {
1509 + return array(
1510 + 'text' => esc_html__('I encountered an error while searching. Please try again.', 'mxchat'),
1511 + 'html' => ''
1376 1512 );
1377 - return;
1378 1513 }
1379 -
1380 - $results = json_decode( wp_remote_retrieve_body( $response ), true );
1381 -
1382 - if ( json_last_error() !== JSON_ERROR_NONE ) {
1383 - $this->fallbackResponse = array(
1384 - 'text' => esc_html__( 'I received an invalid response from the search service.', 'mxchat' ),
1514 +
1515 + $results = json_decode(wp_remote_retrieve_body($response), true);
1516 +
1517 + if (json_last_error() !== JSON_ERROR_NONE) {
1518 + return array(
1519 + 'text' => esc_html__('I received an invalid response from the search service.', 'mxchat'),
1520 + 'html' => ''
1385 1521 );
1386 - return;
1387 1522 }
1388 -
1523 +
1389 1524 // Cache results for one hour
1390 - set_transient( $transient_key, $results, HOUR_IN_SECONDS );
1525 + set_transient($transient_key, $results, HOUR_IN_SECONDS);
1391 1526 }
1392 -
1393 - // Process and display results
1394 - if ( ! empty( $results['web']['results'] ) && is_array( $results['web']['results'] ) ) {
1395 - $html = $this->generate_search_results_html( $results['web']['results'], $refined_search_query );
1396 -
1397 - // Only return HTML (no large text summary)
1398 - $this->fallbackResponse = array(
1399 - 'html' => $html,
1527 +
1528 + // Process results
1529 + if (!empty($results['web']['results']) && is_array($results['web']['results'])) {
1530 + // Create a more straightforward summary with HTML links
1531 + $search_results_text = '';
1532 +
1533 + // Add a simple intro
1534 + $search_results_text .= sprintf(
1535 + esc_html__("Here's what I found about '%s':", 'mxchat'),
1536 + esc_html($refined_search_query)
1400 1537 );
1401 -
1538 +
1539 + // Add the top results with HTML links
1540 + foreach (array_slice($results['web']['results'], 0, 5) as $result) {
1541 + $title = isset($result['title']) ? wp_strip_all_tags($result['title']) : '';
1542 + $url = isset($result['url']) ? esc_url($result['url']) : '';
1543 + $description = isset($result['description']) ? wp_strip_all_tags($result['description']) : '';
1544 +
1545 + // Add a line break after the intro
1546 + $search_results_text .= '<br><br>';
1547 +
1548 + // Add title as a link
1549 + $search_results_text .= sprintf(
1550 + '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a><br>',
1551 + $url,
1552 + $title
1553 + );
1554 +
1555 + // Add a condensed description
1556 + $search_results_text .= sprintf("%s", $description);
1557 + }
1558 +
1402 1559 // Save to chat history
1403 - $this->mxchat_save_chat_message( $session_id, 'bot', $html );
1560 + $this->mxchat_save_chat_message($session_id, 'bot', $search_results_text);
1561 +
1562 + // Return the formatted text with embedded HTML links
1563 + return array(
1564 + 'text' => $search_results_text,
1565 + 'html' => ''
1566 + );
1404 1567 } else {
1405 - $this->fallbackResponse = array(
1568 + return array(
1406 1569 'text' => sprintf(
1407 - esc_html__( 'I couldn\'t find any relevant results for "%s". Would you like to try different search terms?', 'mxchat' ),
1408 - esc_html( $refined_search_query )
1570 + esc_html__('I searched for "%s" but couldn\'t find any relevant results. Would you like to try different search terms?', 'mxchat'),
1571 + esc_html($refined_search_query)
1409 1572 ),
1573 + 'html' => ''
1410 1574 );
1411 1575 }
1412 1576 }
1413 1577
1414 -
1578 +//very good
1415 1579 /**
1416 - * Format search results into a natural text summary.
1580 + * Handle image search requests from the chatbot
1417 1581 *
1418 - * @since 1.0.0
1419 - * @param array $results The search results from the API.
1420 - * @param string $query The original search query.
1421 - * @return string The text summary of the top results.
1582 + * @param string $message The user's search query
1583 + * @param int $user_id The user's ID
1584 + * @param string $session_id The chat session ID
1585 + * @return array Response array with text and HTML content
1422 1586 */
1423 -private function format_search_results( $results, $query ) {
1424 - $summary = sprintf(
1425 - esc_html__( 'Here are the most relevant results for "%s":', 'mxchat' ),
1426 - esc_html( $query )
1427 - ) . "\n\n";
1428 -
1429 - $max_results = min( count( $results ), 3 );
1430 - for ( $i = 0; $i < $max_results; $i++ ) {
1431 - $result = $results[ $i ];
1432 - $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1433 - $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1434 -
1435 - // Append title and description to the summary
1436 - $summary .= sprintf(
1437 - "%s\n%s\n\n",
1438 - esc_html( $title ),
1439 - esc_html( $description )
1440 - );
1441 - }
1442 -
1443 - return $summary;
1444 -}
1445 -
1446 -/**
1447 - * Generate HTML markup for search results.
1448 - *
1449 - * @since 1.0.0
1450 - * @param array $results The search results from the API.
1451 - * @param string $query The user-refined query.
1452 - * @return string The HTML markup for displaying the results.
1453 - */
1454 -private function generate_search_results_html( $results, $query ) {
1455 - ob_start();
1456 - ?>
1457 - <div class="mxchat-search-results">
1458 - <?php foreach ( $results as $result ) :
1459 - $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1460 - $url = isset( $result['url'] ) ? esc_url( $result['url'] ) : '#';
1461 - $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1462 - $favicon = isset( $result['meta_url']['favicon'] ) ? esc_url( $result['meta_url']['favicon'] ) : '';
1463 - $thumbnail = isset( $result['thumbnail']['src'] ) ? esc_url( $result['thumbnail']['src'] ) : '';
1464 - $domain = parse_url( $url, PHP_URL_HOST );
1465 - ?>
1466 - <div class="mxchat-search-item">
1467 - <div class="mxchat-search-header">
1468 - <?php if ( $favicon ) : ?>
1469 - <img
1470 - src="<?php echo esc_url( $favicon ); ?>"
1471 - class="mxchat-site-icon"
1472 - alt="<?php echo esc_attr__( 'Site icon', 'mxchat' ); ?>"
1473 - width="16"
1474 - height="16"
1475 - />
1476 - <?php endif; ?>
1477 - <div class="mxchat-site-url"><?php echo esc_html( $domain ); ?></div>
1478 - </div>
1479 -
1480 - <div class="mxchat-search-content">
1481 - <h3 class="mxchat-search-title">
1482 - <a href="<?php echo esc_url( $url ); ?>"
1483 - target="_blank"
1484 - rel="noopener noreferrer"
1485 - >
1486 - <?php echo esc_html( $title ); ?>
1487 - </a>
1488 - </h3>
1489 -
1490 - <?php if ( $thumbnail ) : ?>
1491 - <div class="mxchat-search-thumbnail">
1492 - <img
1493 - src="<?php echo esc_url( $thumbnail ); ?>"
1494 - alt="<?php echo esc_attr__( 'Thumbnail image', 'mxchat' ); ?>"
1495 - loading="lazy"
1496 - />
1497 - </div>
1498 - <?php endif; ?>
1499 -
1500 - <div class="mxchat-search-description">
1501 - <?php echo esc_html( $description ); ?>
1502 - </div>
1503 - </div>
1504 - </div>
1505 - <?php endforeach; ?>
1506 - </div>
1507 - <?php
1508 - return ob_get_clean();
1509 -}
1510 -
1511 -
1512 -//very good
1513 1587 public function mxchat_handle_image_search_request($message, $user_id, $session_id) {
1514 -
1515 - // Step 1: Interpret the search query for better results
1588 + // Step 1: Interpret the search query using the user's selected AI model
1516 1589 $refined_search_query = $this->mxchat_interpret_search_query($message);
1517 1590
1518 -
1519 1591 // If no query was interpreted, return a fallback message
1520 1592 if (empty($refined_search_query)) {
1521 - $this->fallbackResponse = [
1593 + return array(
1522 1594 'text' => __("I'm sorry, I couldn't interpret your search query. Please specify what you'd like to see images of.", 'mxchat'),
1523 1595 'html' => "",
1524 - ];
1525 - return;
1596 + );
1526 1597 }
1527 1598
1528 1599 // Brave API URL
1529 1600 $api_url = 'https://api.search.brave.com/res/v1/images/search';
@@ -1532,19 +1603,12 @@
1532 1603 $options = get_option('mxchat_options');
1533 1604 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1534 1605
1535 1606 if (empty($api_key)) {
1536 -/*
1537 - if (defined('WP_DEBUG') && WP_DEBUG) {
1538 - error_log("Brave API key is missing.");
1539 - }
1540 -*/
1541 -
1542 - $this->fallbackResponse = [
1607 + return array(
1543 1608 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'),
1544 1609 'html' => "",
1545 - ];
1546 - return;
1610 + );
1547 1611 }
1548 1612
1549 1613 $image_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1550 1614 $safe_search = isset($options['brave_safe_search']) ? sanitize_text_field($options['brave_safe_search']) : 'strict';
@@ -1555,16 +1619,8 @@
1555 1619 'count' => $image_count,
1556 1620 'safesearch' => $safe_search,
1557 1621 ], $api_url);
1558 1622
1559 -/*
1560 - // Log the final API URL for the search
1561 - if (defined('WP_DEBUG') && WP_DEBUG) {
1562 - error_log("Final API URL for Brave Image Search: " . esc_url_raw($api_url));
1563 - }
1564 -*/
1565 -
1566 -
1567 1623 // Implement caching
1568 1624 $transient_key = 'mxchat_image_search_' . md5($refined_search_query);
1569 1625 $body = get_transient($transient_key);
1570 1626
@@ -1580,19 +1636,12 @@
1580 1636
1581 1637 $response = wp_remote_get($api_url, $args);
1582 1638
1583 1639 if (is_wp_error($response)) {
1584 -/*
1585 - if (defined('WP_DEBUG') && WP_DEBUG) {
1586 - error_log("Brave Image API request failed: " . $response->get_error_message());
1587 - }
1588 -*/
1589 -
1590 - $this->fallbackResponse = [
1640 + return array(
1591 1641 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1592 1642 'html' => "",
1593 - ];
1594 - return;
1643 + );
1595 1644 }
1596 1645
1597 1646 $body = json_decode(wp_remote_retrieve_body($response), true);
1598 1647 set_transient($transient_key, $body, HOUR_IN_SECONDS);
@@ -1600,10 +1649,16 @@
1600 1649
1601 1650 // Process the API response
1602 1651 if (isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) {
1603 1652 $html_output = '<div class="mxchat-image-gallery">';
1604 -
1605 - foreach ($body['results'] as $image) {
1653 +
1654 + // Get the configured image count (1-6)
1655 + $display_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1656 + $display_count = min($display_count, count($body['results'])); // Make sure we don't exceed available images
1657 +
1658 + // Use only the requested number of images
1659 + for ($i = 0; $i < $display_count; $i++) {
1660 + $image = $body['results'][$i];
1606 1661 $image_url = isset($image['url']) ? esc_url($image['url']) : '';
1607 1662 $thumbnail_url = isset($image['thumbnail']['src']) ? esc_url($image['thumbnail']['src']) : '';
1608 1663 $title = isset($image['title']) ? esc_html($image['title']) : esc_html__('Image', 'mxchat');
1609 1664
@@ -1617,47 +1672,95 @@
1617 1672 }
1618 1673
1619 1674 $html_output .= '</div>';
1620 1675
1621 - $this->fallbackResponse = [
1622 - 'text' => "",
1623 - 'html' => $html_output,
1624 - ];
1625 -
1626 - // Save response in chat history
1676 + // Create response text
1677 + $response_text = sprintf(__("Here are some images of %s:", 'mxchat'), $refined_search_query);
1678 +
1679 + // Save both response text and HTML to chat history
1680 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1627 1681 $this->mxchat_save_chat_message($session_id, 'bot', $html_output);
1628 1682
1683 + // Return the combined response
1684 + return array(
1685 + 'text' => $response_text,
1686 + 'html' => $html_output,
1687 + );
1629 1688 } else {
1630 -/*
1631 - if (defined('WP_DEBUG') && WP_DEBUG) {
1632 - error_log("Brave Image API response did not contain expected data structure or was empty: " . print_r($body, true));
1633 - }
1634 -*/
1635 -
1636 - $this->fallbackResponse = [
1637 - 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1689 + $response_text = __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat');
1690 +
1691 + // Save the error message to chat history
1692 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1693 +
1694 + return array(
1695 + 'text' => $response_text,
1638 1696 'html' => "",
1639 - ];
1697 + );
1640 1698 }
1641 1699 }
1700 +
1701 +/**
1702 + * Interpret the search query using the user's selected AI model
1703 + *
1704 + * @param string $user_query The original query from the user
1705 + * @return string The refined search query
1706 + */
1642 1707 public function mxchat_interpret_search_query($user_query) {
1643 1708 $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');
1644 -
1645 - // Retrieve OpenAI API key using 'api_key' as the option key
1646 - $api_key = isset($this->options['api_key']) ? sanitize_text_field($this->options['api_key']) : sanitize_text_field(get_option('mxchat_options')['api_key']);
1647 -
1648 - /*
1649 - // Log the API key check, without exposing the key
1650 - if (defined('WP_DEBUG') && WP_DEBUG) {
1651 - error_log("Retrieved OpenAI API Key: " . ($api_key ? "Present" : "Missing"));
1709 +
1710 + // Get options and determine the selected model
1711 + $options = $this->options ?? get_option('mxchat_options');
1712 + $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
1713 +
1714 + // Extract model prefix to determine the provider
1715 + $model_parts = explode('-', $selected_model);
1716 + $provider = strtolower($model_parts[0]);
1717 +
1718 + // Determine which API key to use based on the provider
1719 + switch ($provider) {
1720 + case 'gemini':
1721 + $api_key = isset($options['gemini_api_key']) ? sanitize_text_field($options['gemini_api_key']) : '';
1722 + if (empty($api_key)) {
1723 + return sanitize_text_field($user_query); // Default to original query if API key missing
1724 + }
1725 + return $this->interpret_query_with_gemini($user_query, $system_prompt, $api_key, $selected_model);
1726 +
1727 + case 'claude':
1728 + $api_key = isset($options['claude_api_key']) ? sanitize_text_field($options['claude_api_key']) : '';
1729 + if (empty($api_key)) {
1730 + return sanitize_text_field($user_query);
1731 + }
1732 + return $this->interpret_query_with_claude($user_query, $system_prompt, $api_key, $selected_model);
1733 +
1734 + case 'grok':
1735 + $api_key = isset($options['xai_api_key']) ? sanitize_text_field($options['xai_api_key']) : '';
1736 + if (empty($api_key)) {
1737 + return sanitize_text_field($user_query);
1738 + }
1739 + return $this->interpret_query_with_xai($user_query, $system_prompt, $api_key, $selected_model);
1740 +
1741 + case 'deepseek':
1742 + $api_key = isset($options['deepseek_api_key']) ? sanitize_text_field($options['deepseek_api_key']) : '';
1743 + if (empty($api_key)) {
1744 + return sanitize_text_field($user_query);
1745 + }
1746 + return $this->interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $selected_model);
1747 +
1748 + case 'gpt':
1749 + default:
1750 + // Default to OpenAI for custom models or unrecognized prefixes
1751 + $api_key = isset($options['api_key']) ? sanitize_text_field($options['api_key']) : '';
1752 + if (empty($api_key)) {
1753 + return sanitize_text_field($user_query);
1754 + }
1755 + return $this->interpret_query_with_openai($user_query, $system_prompt, $api_key, $selected_model);
1652 1756 }
1653 - */
1757 +}
1654 1758
1655 - if (empty($api_key)) {
1656 - //error_log("OpenAI API key is missing.");
1657 - return sanitize_text_field($user_query); // Default to the original query if API key is missing
1658 - }
1659 -
1759 +/**
1760 + * Interpret query using OpenAI models
1761 + */
1762 +private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-4o') {
1660 1763 $url = 'https://api.openai.com/v1/chat/completions';
1661 1764 $args = [
1662 1765 'headers' => [
1663 1766 'Authorization' => 'Bearer ' . $api_key,
@@ -1663,9 +1766,9 @@
1663 1766 'Authorization' => 'Bearer ' . $api_key,
1664 1767 'Content-Type' => 'application/json',
1665 1768 ],
1666 1769 'body' => wp_json_encode([
1667 - 'model' => 'gpt-3.5-turbo',
1770 + 'model' => $model,
1668 1771 'messages' => [
1669 1772 ['role' => 'system', 'content' => $system_prompt],
1670 1773 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1671 1774 ],
@@ -1672,292 +1775,178 @@
1672 1775 'temperature' => 0.2,
1673 1776 'max_tokens' => 20,
1674 1777 ]),
1675 1778 'method' => 'POST',
1779 + 'timeout' => 15,
1676 1780 ];
1677 1781
1678 1782 $response = wp_remote_post($url, $args);
1679 -
1680 1783 if (is_wp_error($response)) {
1681 - //error_log("OpenAI request failed: " . $response->get_error_message());
1682 - return sanitize_text_field($user_query); // Fallback to the original query if there's an error
1784 + return sanitize_text_field($user_query);
1683 1785 }
1684 1786
1685 1787 $body = json_decode(wp_remote_retrieve_body($response), true);
1788 + return isset($body['choices'][0]['message']['content'])
1789 + ? sanitize_text_field(trim($body['choices'][0]['message']['content']))
1790 + : sanitize_text_field($user_query);
1791 +}
1686 1792
1687 - // Check for a valid response and sanitize output
1688 - if (isset($body['choices'][0]['message']['content'])) {
1689 - $interpreted_query = sanitize_text_field(trim($body['choices'][0]['message']['content']));
1793 +/**
1794 + * Interpret query using Claude models
1795 + */
1796 +private function interpret_query_with_claude($user_query, $system_prompt, $api_key, $model) {
1797 + $url = 'https://api.anthropic.com/v1/messages';
1798 +
1799 + $args = [
1800 + 'headers' => [
1801 + 'Content-Type' => 'application/json',
1802 + 'x-api-key' => $api_key,
1803 + 'anthropic-version' => '2023-06-01',
1804 + ],
1805 + 'body' => wp_json_encode([
1806 + 'model' => $model,
1807 + 'system' => $system_prompt,
1808 + 'messages' => [
1809 + ['role' => 'user', 'content' => sanitize_text_field($user_query)]
1810 + ],
1811 + 'max_tokens' => 20,
1812 + 'temperature' => 0.2,
1813 + ]),
1814 + 'method' => 'POST',
1815 + 'timeout' => 15,
1816 + ];
1690 1817
1691 - /*
1692 - // Log the interpreted query for debugging
1693 - if (defined('WP_DEBUG') && WP_DEBUG) {
1694 - error_log("Interpreted search query: " . $interpreted_query);
1695 - }
1696 - */
1697 -
1698 - return $interpreted_query;
1699 - } else {
1700 - //error_log("Unexpected API response format: " . print_r($body, true));
1818 + $response = wp_remote_post($url, $args);
1819 + if (is_wp_error($response)) {
1701 1820 return sanitize_text_field($user_query);
1702 1821 }
1703 -}
1704 1822
1705 -public function mxchat_handle_add_to_cart_intent($message, $user_id, $session_id) {
1706 - //error_log("ADD TO CART START - Message: $message, User ID: $user_id");
1707 -
1708 - if (!class_exists('WooCommerce')) {
1709 - //error_log("WooCommerce not available");
1710 - return $this->generate_intent_response([
1711 - 'intent' => 'add_to_cart',
1712 - 'status' => 'error',
1713 - 'reason' => esc_html__('woocommerce_not_available', 'mxchat')
1714 - ], $session_id);
1715 - }
1716 -
1717 - $sanitized_user_id = sanitize_key($user_id);
1718 - // error_log("Sanitized User ID: $sanitized_user_id");
1719 - $product_id = null;
1720 -
1721 - // If button click, use transient
1722 - if ($message === '!addtocart') {
1723 - //error_log("Button click detected - using transient");
1724 - $product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1725 - //error_log("Product ID from transient: " . ($product_id ?? 'null'));
1726 - } else {
1727 - // For text commands, search message first
1728 - //error_log("Text command detected - searching message first");
1729 - $product_id = $this->find_product_in_message($message);
1730 - //error_log("Product ID from message search: " . ($product_id ?? 'null'));
1731 -
1732 - // Only use transient as fallback if no product found in message
1733 - if (!$product_id) {
1734 - //error_log("No product found in message, checking transient as fallback");
1735 - $product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1736 - //error_log("Product ID from transient fallback: " . ($product_id ?? 'null'));
1737 - }
1738 - }
1739 -
1740 - // Handle no product found
1741 - if (!$product_id) {
1742 - //error_log("No product ID found through any method");
1743 - return $this->generate_intent_response([
1744 - 'intent' => 'add_to_cart',
1745 - 'status' => 'error',
1746 - 'reason' => esc_html__('no_product_context', 'mxchat'),
1747 - 'action_needed' => esc_html__('request_product_name', 'mxchat'),
1748 - 'searched_message' => $message
1749 - ], $session_id);
1750 - }
1751 -
1752 - // Get and verify product
1753 - $product = wc_get_product($product_id);
1754 - if (!$product) {
1755 - //error_log("Product not found with ID: $product_id");
1756 - return $this->generate_intent_response([
1757 - 'intent' => 'add_to_cart',
1758 - 'status' => 'error',
1759 - 'reason' => esc_html__('product_not_found', 'mxchat'),
1760 - 'product_id' => $product_id
1761 - ], $session_id);
1762 - }
1763 -
1764 - //error_log("Found product: " . $product->get_name() . " (ID: $product_id)");
1765 -
1766 - // Add to cart
1767 - $added = WC()->cart->add_to_cart($product_id);
1768 - if ($added) {
1769 - //error_log("Successfully added to cart: " . $product->get_name());
1770 - return $this->generate_intent_response([
1771 - 'intent' => 'add_to_cart',
1772 - 'status' => 'success',
1773 - 'product' => [
1774 - 'name' => $product->get_name(),
1775 - 'id' => $product_id
1776 - ],
1777 - 'cart_url' => wc_get_cart_url(),
1778 - 'available_actions' => [
1779 - esc_html__('view_cart', 'mxchat'),
1780 - esc_html__('checkout', 'mxchat'),
1781 - esc_html__('continue_shopping', 'mxchat')
1782 - ]
1783 - ], $session_id);
1784 - } else {
1785 - //error_log("Failed to add to cart: " . $product->get_name());
1786 - return $this->generate_intent_response([
1787 - 'intent' => 'add_to_cart',
1788 - 'status' => 'error',
1789 - 'reason' => esc_html__('add_to_cart_failed', 'mxchat'),
1790 - 'product' => [
1791 - 'name' => $product->get_name(),
1792 - 'id' => $product_id
1793 - ]
1794 - ], $session_id);
1795 - }
1823 + $body = json_decode(wp_remote_retrieve_body($response), true);
1824 + if (!empty($body['content'][0]['text'])) {
1825 + return sanitize_text_field(trim($body['content'][0]['text']));
1826 + }
1827 +
1828 + return sanitize_text_field($user_query);
1796 1829 }
1797 -private function find_product_in_message($message) {
1798 - global $wpdb;
1799 1830
1800 - // Get embedding for the search query
1801 - $query_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1802 - if (!is_array($query_embedding)) {
1803 - return null;
1831 +/**
1832 + * Interpret query using Gemini models
1833 + */
1834 +private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
1835 + // Strip "gemini-" prefix for the API
1836 + $model_version = str_replace('gemini-', '', $model);
1837 +
1838 + $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
1839 +
1840 + $args = [
1841 + 'headers' => [
1842 + 'Content-Type' => 'application/json',
1843 + ],
1844 + 'body' => wp_json_encode([
1845 + 'contents' => [
1846 + [
1847 + 'role' => 'user',
1848 + 'parts' => [
1849 + ['text' => $system_prompt . "\n\nQuery: " . sanitize_text_field($user_query)]
1850 + ]
1851 + ]
1852 + ],
1853 + 'generationConfig' => [
1854 + 'temperature' => 0.2,
1855 + 'maxOutputTokens' => 20,
1856 + ],
1857 + ]),
1858 + 'method' => 'POST',
1859 + 'timeout' => 15,
1860 + ];
1861 +
1862 + $response = wp_remote_post($url, $args);
1863 + if (is_wp_error($response)) {
1864 + return sanitize_text_field($user_query);
1804 1865 }
1805 -
1806 - // Get relevant content as string
1807 - $relevant_content = $this->mxchat_find_relevant_products($query_embedding);
1808 - if (empty($relevant_content)) {
1809 - // Return null to indicate no results and set fallback response
1810 - $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');
1811 - return null;
1866 +
1867 + $body = json_decode(wp_remote_retrieve_body($response), true);
1868 + if (!empty($body['candidates'][0]['content']['parts'][0]['text'])) {
1869 + return sanitize_text_field(trim($body['candidates'][0]['content']['parts'][0]['text']));
1812 1870 }
1871 +
1872 + return sanitize_text_field($user_query);
1873 +}
1813 1874
1814 - // Extract product URLs from the content
1815 - preg_match_all('/https?:\/\/[^\s<>"\']+?\/product\/[^\s<>"\']+/', $relevant_content, $matches);
1816 -
1817 - if (!empty($matches[0])) {
1818 - // Try each URL found
1819 - foreach ($matches[0] as $url) {
1820 - // Clean the URL
1821 - $url = rtrim($url, '/."\']');
1822 -
1823 - // Get the product slug
1824 - $path = parse_url($url, PHP_URL_PATH);
1825 - $slug = basename(rtrim($path, '/'));
1826 -
1827 - // Find product by slug
1828 - $args = array(
1829 - 'post_type' => 'product',
1830 - 'post_status' => 'publish',
1831 - 'name' => $slug,
1832 - 'posts_per_page' => 1
1833 - );
1834 -
1835 - $products = get_posts($args);
1836 -
1837 - if (!empty($products)) {
1838 - $product_id = $products[0]->ID;
1839 - $product = wc_get_product($product_id);
1840 -
1841 - if ($product && $product->is_purchasable()) {
1842 - return $product_id;
1843 - }
1844 - }
1845 - }
1875 +/**
1876 + * Interpret query using X.AI (Grok) models
1877 + */
1878 +private function interpret_query_with_xai($user_query, $system_prompt, $api_key, $model) {
1879 + $url = 'https://api.xai.com/v1/chat/completions';
1880 +
1881 + $args = [
1882 + 'headers' => [
1883 + 'Content-Type' => 'application/json',
1884 + 'Authorization' => 'Bearer ' . $api_key,
1885 + ],
1886 + 'body' => wp_json_encode([
1887 + 'model' => $model,
1888 + 'messages' => [
1889 + ['role' => 'system', 'content' => $system_prompt],
1890 + ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1891 + ],
1892 + 'temperature' => 0.2,
1893 + 'max_tokens' => 20,
1894 + ]),
1895 + 'method' => 'POST',
1896 + 'timeout' => 15,
1897 + ];
1898 +
1899 + $response = wp_remote_post($url, $args);
1900 + if (is_wp_error($response)) {
1901 + return sanitize_text_field($user_query);
1846 1902 }
1847 -
1848 - // Fallback: Look for product names in the content
1849 - $products = wc_get_products([
1850 - 'status' => 'publish',
1851 - 'limit' => -1,
1852 - 'return' => 'all'
1853 - ]);
1854 -
1855 - foreach ($products as $product) {
1856 - $name = $product->get_name();
1857 - if (stripos($relevant_content, $name) !== false) {
1858 - if ($product->is_purchasable()) {
1859 - return $product->get_id();
1860 - }
1861 - }
1903 +
1904 + $body = json_decode(wp_remote_retrieve_body($response), true);
1905 + if (isset($body['choices'][0]['message']['content'])) {
1906 + return sanitize_text_field(trim($body['choices'][0]['message']['content']));
1862 1907 }
1863 -
1864 - // If no product is found after all checks, set the fallback response
1865 - $this->fallbackResponse['text'] = esc_html__("I couldn't find any relevant products based on your query. Try to be more specific", 'mxchat');
1866 - return null;
1908 +
1909 + return sanitize_text_field($user_query);
1867 1910 }
1868 1911
1869 -// New method to handle intent responses
1870 -private function generate_intent_response($context_content, $session_id) {
1871 - // Convert the context array to a structured string for the AI
1872 - $context_string = $this->format_intent_context($context_content);
1873 -
1874 - // Generate AI response using the context
1875 - $response = $this->mxchat_generate_response(
1876 - $context_string,
1877 - $this->options['api_key'],
1878 - $this->options['xai_api_key'],
1879 - $this->options['claude_api_key'],
1880 - $this->options['deepseek_api_key'],
1881 - $this->mxchat_fetch_conversation_history_for_ai($session_id)
1882 - );
1883 -
1884 - $this->fallbackResponse['text'] = $response;
1885 - return true;
1886 -}
1887 -// Helper method to format intent context
1888 -private function format_intent_context($context) {
1889 - $context_string = esc_html__("INTENT CONTEXT:\n", 'mxchat');
1890 -
1891 - switch ($context['intent']) {
1892 - case 'add_to_cart':
1893 - if ($context['status'] === 'success') {
1894 - $context_string .= esc_html__("Action: Successfully added product to cart\n", 'mxchat');
1895 - $context_string .= sprintf(esc_html__("Product: %s\n", 'mxchat'), $context['product']['name']);
1896 - $context_string .= esc_html__("Available actions: ", 'mxchat') . implode(', ', $context['available_actions']) . "\n";
1897 - $context_string .= sprintf(esc_html__("Cart URL: %s\n", 'mxchat'), $context['cart_url']);
1898 - $context_string .= esc_html__("\nPlease inform the user of the successful addition and their available options.", 'mxchat');
1899 - } else {
1900 - $context_string .= esc_html__("Action: Failed to add product to cart\n", 'mxchat');
1901 - $context_string .= sprintf(esc_html__("Reason: %s\n", 'mxchat'), $context['reason']);
1902 - switch ($context['reason']) {
1903 - case 'woocommerce_not_available':
1904 - $context_string .= esc_html__("\nPlease inform the user that shopping features are not available.", 'mxchat');
1905 - break;
1906 - case 'no_product_context':
1907 - $context_string .= esc_html__("\nPlease ask the user to specify which product they want to add.", 'mxchat');
1908 - break;
1909 - case 'product_not_found':
1910 - $context_string .= esc_html__("\nPlease inform the user that the product couldn't be found and ask them to try again.", 'mxchat');
1911 - break;
1912 - case 'add_to_cart_failed':
1913 - $context_string .= esc_html__("\nPlease apologize to the user and suggest they try again or ask for assistance.", 'mxchat');
1914 - break;
1915 - }
1916 - }
1917 - break;
1912 +/**
1913 + * Interpret query using DeepSeek models
1914 + */
1915 +private function interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $model) {
1916 + $url = 'https://api.deepseek.com/v1/chat/completions';
1917 +
1918 + $args = [
1919 + 'headers' => [
1920 + 'Content-Type' => 'application/json',
1921 + 'Authorization' => 'Bearer ' . $api_key,
1922 + ],
1923 + 'body' => wp_json_encode([
1924 + 'model' => $model,
1925 + 'messages' => [
1926 + ['role' => 'system', 'content' => $system_prompt],
1927 + ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1928 + ],
1929 + 'temperature' => 0.2,
1930 + 'max_tokens' => 20,
1931 + ]),
1932 + 'method' => 'POST',
1933 + 'timeout' => 15,
1934 + ];
1935 +
1936 + $response = wp_remote_post($url, $args);
1937 + if (is_wp_error($response)) {
1938 + return sanitize_text_field($user_query);
1918 1939 }
1919 -
1920 - return $context_string;
1921 -}
1922 -
1923 -
1924 -public function mxchat_handle_checkout_intent($message, $user_id, $session_id) {
1925 - if (!class_exists('WooCommerce')) {
1926 - $this->fallbackResponse['text'] = esc_html__("I apologize, but the checkout feature isn't available at the moment.", 'mxchat');
1927 - return true;
1940 +
1941 + $body = json_decode(wp_remote_retrieve_body($response), true);
1942 + if (isset($body['choices'][0]['message']['content'])) {
1943 + return sanitize_text_field(trim($body['choices'][0]['message']['content']));
1928 1944 }
1929 -
1930 - // Check if cart has items
1931 - if (WC()->cart->is_empty()) {
1932 - $this->fallbackResponse['text'] = esc_html__("Your cart is empty at the moment. Would you like to see our products?", 'mxchat');
1933 - return true;
1934 - }
1935 -
1936 - // Get cart summary
1937 - $cart_count = WC()->cart->get_cart_contents_count();
1938 - $cart_total = WC()->cart->get_total();
1939 -
1940 - // Get and validate checkout URL
1941 - $checkout_url = wc_get_checkout_url();
1942 - if (!$checkout_url) {
1943 - $this->fallbackResponse['text'] = esc_html__("I'm having trouble accessing the checkout page. Please try again in a moment.", 'mxchat');
1944 - return true;
1945 - }
1946 -
1947 - wp_send_json([
1948 - 'text' => sprintf(
1949 - esc_html__("You have %d item%s in your cart totaling %s. I'll redirect you to checkout now.", 'mxchat'),
1950 - $cart_count,
1951 - $cart_count > 1 ? esc_html__('s', 'mxchat') : '',
1952 - strip_tags($cart_total)
1953 - ),
1954 - 'redirect_url' => esc_url_raw($checkout_url)
1955 - ]);
1956 - wp_die();
1945 +
1946 + return sanitize_text_field($user_query);
1957 1947 }
1958 1948
1959 -
1960 1949 //very good
1961 1950 private function add_email_to_loops($email) {
1962 1951 // Sanitize the email
1963 1952 $email = sanitize_email($email);
@@ -2041,8 +2030,10 @@
2041 2030
2042 2031 // Default to proceeding with conversation if no specific PDF action is needed
2043 2032 $this->fallbackResponse['text'] = '';
2044 2033 }
2034 +
2035 +
2045 2036 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
2046 2037 $upload_dir = wp_upload_dir();
2047 2038 $temp_file = null;
2048 2039
@@ -2118,9 +2109,9 @@
2118 2109
2119 2110 return $embeddings;
2120 2111
2121 2112 } catch (\Exception $e) {
2122 - // error_log(esc_html__("Error parsing or processing PDF: ", 'mxchat') . $e->getMessage());
2113 + // //error_log(esc_html__("Error parsing or processing PDF: ", 'mxchat') . $e->getMessage());
2123 2114
2124 2115 // Cleanup in case of exception
2125 2116 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
2126 2117 unlink($temp_file);
@@ -2250,408 +2241,10 @@
2250 2241 wp_die();
2251 2242 }
2252 2243
2253 2244
2254 -/**
2255 - * Calls the AI API with the provided prompt.
2256 - *
2257 - * @param string $prompt The prompt to send to the AI.
2258 - * @return array An array containing the AI's response text.
2259 - */
2260 -private function mxchat_call_ai_api( $prompt ) {
2261 - //error_log( esc_html__( 'Calling AI API with the provided prompt.', 'mxchat' ) );
2262 2245
2263 - $api_key = $this->options['api_key'];
2264 - if ( empty( $api_key ) ) {
2265 - //error_log( esc_html__( 'API key is not set.', 'mxchat' ) );
2266 - return [ 'text' => esc_html__( 'API key is not set.', 'mxchat' ) ];
2267 - }
2268 2246
2269 - $url = 'https://api.openai.com/v1/chat/completions';
2270 - $messages = [
2271 - [
2272 - 'role' => 'system',
2273 - 'content' => __( 'You are a helpful assistant that provides concise and personalized product recommendations. Someone has asked you for some recommendations please respond very concisely appropriate for an AI chatbot.', 'mxchat' ),
2274 - ],
2275 - [
2276 - 'role' => 'user',
2277 - 'content' => $prompt,
2278 - ],
2279 - ];
2280 -
2281 - $args = [
2282 - 'headers' => [
2283 - 'Authorization' => 'Bearer ' . $api_key,
2284 - 'Content-Type' => 'application/json',
2285 - ],
2286 - 'body' => wp_json_encode(
2287 - [
2288 - 'model' => 'gpt-4o',
2289 - 'messages' => $messages,
2290 - 'temperature' => 0.7,
2291 - ]
2292 - ),
2293 - 'timeout' => 10,
2294 - 'method' => 'POST',
2295 - ];
2296 -
2297 - $response = wp_remote_post( $url, $args );
2298 -
2299 - if ( is_wp_error( $response ) ) {
2300 - //error_log( esc_html__( 'Error communicating with AI API: ', 'mxchat' ) . $response->get_error_message() );
2301 - return [ 'text' => esc_html__( 'Error communicating with AI API.', 'mxchat' ) ];
2302 - }
2303 -
2304 - $body = wp_remote_retrieve_body( $response );
2305 - //error_log( esc_html__( 'API response body: ', 'mxchat' ) . $body );
2306 -
2307 - $decoded_body = json_decode( $body, true );
2308 - if ( isset( $decoded_body['choices'][0]['message']['content'] ) ) {
2309 - return [ 'text' => $decoded_body['choices'][0]['message']['content'] ];
2310 - } else {
2311 - //error_log( esc_html__( 'Unexpected API response format: ', 'mxchat' ) . wp_json_encode( $decoded_body ) );
2312 - return [ 'text' => esc_html__( 'No response received from AI.', 'mxchat' ) ];
2313 - }
2314 -}
2315 -
2316 -/**
2317 - * Fetches the AI response for the given prompt.
2318 - *
2319 - * @param string $prompt The prompt to send to the AI.
2320 - * @return string|null The AI's response text or null if not available.
2321 - */
2322 -private function mxchat_fetch_ai_response( $prompt ) {
2323 - $response = $this->mxchat_call_ai_api( $prompt );
2324 - return isset( $response['text'] ) ? $response['text'] : null;
2325 -}
2326 -/**
2327 - * Generates the AI prompt for product recommendations.
2328 - *
2329 - * @param array $recommendations An array of product recommendations.
2330 - * @return string The generated AI prompt.
2331 - */
2332 -private function mxchat_generate_ai_recommendation_prompt($recommendations) {
2333 - $recommendation_list = '';
2334 - foreach ($recommendations as $index => $rec) {
2335 - $number = $index + 1;
2336 - $name = $rec['name'];
2337 - $price = $rec['price'];
2338 - $url = $rec['url'];
2339 - $image = $rec['image'];
2340 - $recommendation_list .= "{$number}. " . esc_html__('Product:', 'mxchat') . " {$name} (" . esc_html__('Price:', 'mxchat') . " \\${$price})\n";
2341 - $recommendation_list .= " [Link]({$url})\n";
2342 - $recommendation_list .= " ![Image]({$image})\n\n";
2343 - }
2344 -
2345 - $prompt = esc_html__('Based on the following list of products, generate a unique, friendly, and personalized response to a user. ', 'mxchat');
2346 - $prompt .= esc_html__('If some products aren\'t exactly what the user asked for but share similar styles or patterns, acknowledge this and explain why you\'re suggesting them. ', 'mxchat');
2347 - $prompt .= esc_html__('For each product, provide a brief justification that clearly explains why it\'s relevant, especially if it\'s a different type of product than requested. ', 'mxchat');
2348 - $prompt .= esc_html__('Please number your responses to match the product numbers.', 'mxchat') . "\n\n";
2349 - $prompt .= esc_html__('Products:', 'mxchat') . "\n\n{$recommendation_list}";
2350 - $prompt .= esc_html__('Please ensure that the number of each product matches the order in which the products are listed.', 'mxchat');
2351 -
2352 - return $prompt;
2353 -}
2354 -private function mxchat_generate_recommendations($user_id, $message) {
2355 - // 1. Gather user context
2356 - $user_context = $this->mxchat_get_user_context($user_id);
2357 -
2358 - // Get cart item IDs for filtering
2359 - $cart_item_ids = [];
2360 - if (!empty($user_context['cart_items'])) {
2361 - $cart_item_ids = array_map(function($item) {
2362 - return $item['product_id'];
2363 - }, $user_context['cart_items']);
2364 - }
2365 -
2366 - // 2. Get AI recommendation based on context
2367 - $ai_suggestion = $this->mxchat_get_ai_shopping_suggestion($message, $user_context);
2368 - //error_log("AI Shopping Suggestion: " . $ai_suggestion);
2369 -
2370 - // 3. Generate embedding for the AI suggestion
2371 - $suggestion_embedding = $this->mxchat_generate_embedding($ai_suggestion, $this->options['api_key']);
2372 - if (!$suggestion_embedding) {
2373 - //error_log("Could not generate embedding for AI suggestion");
2374 - return ['recommendations' => [], 'sources' => []];
2375 - }
2376 -
2377 - // 4. Use existing relevant content function to find matches
2378 - $relevant_content = $this->mxchat_find_relevant_content($suggestion_embedding);
2379 -
2380 - // 5. Extract product URLs from the relevant content
2381 - preg_match_all('/https?:\/\/[^\s<>"\']+?\/product\/[^\s<>"\']+/', $relevant_content, $matches);
2382 -
2383 - $found_products = [];
2384 - if (!empty($matches[0])) {
2385 - foreach ($matches[0] as $url) {
2386 - // Clean the URL
2387 - $url = rtrim($url, '/."\']');
2388 -
2389 - // Get the product slug
2390 - $path = parse_url($url, PHP_URL_PATH);
2391 - $slug = basename(rtrim($path, '/'));
2392 -
2393 - // Find product by slug
2394 - $args = array(
2395 - 'post_type' => 'product',
2396 - 'post_status' => 'publish',
2397 - 'name' => $slug,
2398 - 'posts_per_page' => 1
2399 - );
2400 -
2401 - $products = get_posts($args);
2402 -
2403 - if (!empty($products)) {
2404 - $product_id = $products[0]->ID;
2405 -
2406 - // Skip if product is in cart
2407 - if (in_array($product_id, $cart_item_ids)) {
2408 - //error_log("Skipping product ID $product_id - already in cart");
2409 - continue;
2410 - }
2411 -
2412 - $product = wc_get_product($product_id);
2413 -
2414 - if ($product && $product->is_purchasable() && !in_array($product_id, array_map(function($p) {
2415 - return $p->get_id();
2416 - }, $found_products))) {
2417 - $found_products[] = $product;
2418 - }
2419 - }
2420 - }
2421 - }
2422 -
2423 - // If no products found by URLs, look for product names in the content
2424 - if (empty($found_products)) {
2425 - $products = wc_get_products([
2426 - 'status' => 'publish',
2427 - 'limit' => -1,
2428 - 'return' => 'objects'
2429 - ]);
2430 -
2431 - foreach ($products as $product) {
2432 - // Skip if product is in cart
2433 - if (in_array($product->get_id(), $cart_item_ids)) {
2434 - continue;
2435 - }
2436 -
2437 - $name = $product->get_name();
2438 - if (stripos($relevant_content, $name) !== false) {
2439 - if ($product->is_purchasable() && !in_array($product->get_id(), array_map(function($p) {
2440 - return $p->get_id();
2441 - }, $found_products))) {
2442 - $found_products[] = $product;
2443 - }
2444 - }
2445 - }
2446 - }
2447 -
2448 - // Keep searching until we have 4 products or run out of options
2449 - $formatted_recommendations = [];
2450 - foreach ($found_products as $product) {
2451 - if (count($formatted_recommendations) >= 4) break;
2452 -
2453 - $formatted_recommendations[] = [
2454 - 'name' => $product->get_name(),
2455 - 'price' => $product->get_price(),
2456 - 'url' => get_permalink($product->get_id()),
2457 - 'image' => wp_get_attachment_url($product->get_image_id())
2458 - ];
2459 - }
2460 -
2461 - return [
2462 - 'recommendations' => $formatted_recommendations,
2463 - 'sources' => [__('AI-powered personalized recommendations', 'mxchat')]
2464 - ];
2465 -}
2466 -private function mxchat_get_ai_shopping_suggestion($message, $user_context) {
2467 - $prompt = esc_html__("As a shopping assistant, analyze this user's context and generate a specific product search suggestion. ", 'mxchat');
2468 - $prompt .= esc_html__("User's Question: \"{$message}\"\n\n", 'mxchat');
2469 -
2470 - if (!empty($user_context['order_history'])) {
2471 - $prompt .= esc_html__("Their recent orders include:\n", 'mxchat');
2472 - foreach ($user_context['order_history'] as $order) {
2473 - $prompt .= esc_html__("- {$order['name']} ({$order['date']})\n", 'mxchat');
2474 - }
2475 - }
2476 -
2477 - if (!empty($user_context['cart_items'])) {
2478 - $prompt .= esc_html__("\nThey currently have in their cart:\n", 'mxchat');
2479 - foreach ($user_context['cart_items'] as $item) {
2480 - $prompt .= esc_html__("- {$item['name']}\n", 'mxchat');
2481 - }
2482 - }
2483 -
2484 - $prompt .= esc_html__("\nBased on their question and history, suggest a specific search query that would help find the most relevant products. ", 'mxchat');
2485 - $prompt .= esc_html__("Respond with ONLY the search query, nothing else.", 'mxchat');
2486 -
2487 - $response = $this->mxchat_call_ai_api($prompt);
2488 - return isset($response['text']) ? trim($response['text']) : $message;
2489 -}
2490 -public function mxchat_handle_product_recommendations($message, $user_id, $session_id) {
2491 - try {
2492 - //error_log("Starting product recommendations for user: $user_id, session: $session_id");
2493 -
2494 - // Pass the message to get context-aware recommendations
2495 - $recommendation_data = $this->mxchat_generate_recommendations($user_id, $message);
2496 - //error_log('Generated recommendation data: ' . wp_json_encode($recommendation_data));
2497 -
2498 - if (empty($recommendation_data['recommendations'])) {
2499 - //error_log("No recommendations found for user: $user_id");
2500 - $this->fallbackResponse = [
2501 - 'text' => __("I couldn't find any product recommendations for you right now. Please try again later!", 'mxchat'),
2502 - ];
2503 - return;
2504 - }
2505 -
2506 - // Remove duplicates and limit to top 4 recommendations
2507 - $unique_recommendations = [];
2508 - foreach ($recommendation_data['recommendations'] as $rec) {
2509 - $unique_recommendations[$rec['url']] = $rec;
2510 - }
2511 -
2512 - $unique_recommendations = array_slice($unique_recommendations, 0, 4);
2513 - //error_log('Top 4 recommendations: ' . wp_json_encode($unique_recommendations));
2514 -
2515 - $recommendations_summary = [];
2516 - foreach ($unique_recommendations as $rec) {
2517 - $recommendations_summary[] = [
2518 - 'name' => $rec['name'],
2519 - 'price' => strip_tags($rec['price']),
2520 - 'url' => $rec['url'],
2521 - 'image' => $rec['image'],
2522 - ];
2523 - }
2524 -
2525 - // Generate AI prompt and fetch response
2526 - $ai_prompt = $this->mxchat_generate_ai_recommendation_prompt($recommendations_summary);
2527 - $ai_response = $this->mxchat_fetch_ai_response($ai_prompt);
2528 -
2529 - if (empty($ai_response)) {
2530 - $this->fallbackResponse = [
2531 - 'text' => __('An unexpected error occurred while generating recommendations. Please try again later.', 'mxchat'),
2532 - ];
2533 - return;
2534 - }
2535 -
2536 - // Split the AI's response into lines
2537 - $ai_lines = preg_split('/\r\n|\r|\n/', $ai_response);
2538 -
2539 - // Initialize variables
2540 - $formatted_response = '';
2541 - $justifications = [];
2542 - $current_number = 0;
2543 - $in_introduction = true;
2544 - $introduction = '';
2545 -
2546 - // Parse the AI response to separate the introduction and the justifications
2547 - foreach ($ai_lines as $line) {
2548 - if (preg_match('/^\s*(\d+)\.\s*(.*)$/', $line, $matches)) {
2549 - // This line is a numbered justification
2550 - $current_number = intval($matches[1]) - 1;
2551 - $justifications[$current_number] = $matches[2];
2552 - $in_introduction = false;
2553 - } elseif ($in_introduction) {
2554 - // This line is part of the introduction
2555 - $introduction .= $line . ' ';
2556 - } else {
2557 - // This line is a continuation of the current justification
2558 - if (isset($justifications[$current_number])) {
2559 - $justifications[$current_number] .= ' ' . $line;
2560 - }
2561 - }
2562 - }
2563 -
2564 - // Build the formatted response
2565 - if (!empty($introduction)) {
2566 - $formatted_response .= esc_html(trim($introduction)) . "<br><br>";
2567 - }
2568 -
2569 - foreach ($recommendations_summary as $index => $rec) {
2570 - $name = esc_html($rec['name']);
2571 - $price = number_format((float)$rec['price'], 2, '.', '');
2572 - $url = esc_url($rec['url']);
2573 - $image = esc_url($rec['image']);
2574 -
2575 - $formatted_response .= ($index + 1) . ". <strong>" . $name . "</strong> - <strong>$" . $price . "</strong><br>";
2576 - $formatted_response .= "<img src=\"{$image}\" alt=\"{$name}\" style=\"max-width: 200px; height: auto; display: block; margin: 10px 0;\" /><br>";
2577 -
2578 - if (isset($justifications[$index])) {
2579 - $formatted_response .= "<em>" . esc_html(trim($justifications[$index])) . "</em><br><br>";
2580 - }
2581 - }
2582 -
2583 - $this->fallbackResponse = [
2584 - 'text' => $formatted_response,
2585 - ];
2586 - //error_log('Final formatted response set.');
2587 - } catch (Exception $e) {
2588 - //error_log('Error in mxchat_handle_product_recommendations: ' . $e->getMessage());
2589 - $this->fallbackResponse = [
2590 - 'text' => __('An unexpected error occurred while generating recommendations. Please try again later.', 'mxchat'),
2591 - ];
2592 - }
2593 -}
2594 -private function mxchat_get_user_context($user_id) {
2595 - $context = [
2596 - 'order_history' => [],
2597 - 'cart_items' => [],
2598 - 'recently_viewed' => [],
2599 - ];
2600 -
2601 - // Get order history
2602 - if (is_user_logged_in() && $user_id) {
2603 - $orders = wc_get_orders([
2604 - 'customer_id' => $user_id,
2605 - 'limit' => 5, // Last 5 orders
2606 - 'orderby' => 'date',
2607 - 'order' => 'DESC',
2608 - ]);
2609 -
2610 - foreach ($orders as $order) {
2611 - foreach ($order->get_items() as $item) {
2612 - $context['order_history'][] = [
2613 - 'name' => $item->get_name(),
2614 - 'product_id' => $item->get_product_id(),
2615 - 'date' => $order->get_date_created()->format('Y-m-d')
2616 - ];
2617 - }
2618 - }
2619 - }
2620 -
2621 - // Get cart items
2622 - if (WC()->cart && WC()->cart->get_cart_contents_count() > 0) {
2623 - foreach (WC()->cart->get_cart() as $cart_item) {
2624 - $product = wc_get_product($cart_item['product_id']);
2625 - if ($product) {
2626 - $context['cart_items'][] = [
2627 - 'name' => $product->get_name(),
2628 - 'product_id' => $product->get_id()
2629 - ];
2630 - }
2631 - }
2632 - }
2633 -
2634 - // Get recently viewed items from session
2635 - $viewed_products = WC()->session->get('recently_viewed_products', []);
2636 - foreach ($viewed_products as $product_id) {
2637 - $product = wc_get_product($product_id);
2638 - if ($product) {
2639 - $context['recently_viewed'][] = [
2640 - 'name' => $product->get_name(),
2641 - 'product_id' => $product->get_id()
2642 - ];
2643 - }
2644 - }
2645 -
2646 - return $context;
2647 -}
2648 -
2649 -
2650 -
2651 -
2652 -
2653 -
2654 2247 function mxchat_fetch_new_messages() {
2655 2248 $session_id = sanitize_text_field($_POST['session_id']);
2656 2249 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
2657 2250 $persistence_enabled = $_POST['persistence_enabled'] === 'true';
@@ -2685,10 +2278,8 @@
2685 2278 'new_messages' => array_values($new_messages)
2686 2279 ]);
2687 2280 wp_die();
2688 2281 }
2689 -
2690 -
2691 2282 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
2692 2283 // First check if live agents are available
2693 2284 $live_agent_available = $this->options['live_agent_status'] ?? 'off';
2694 2285 if ($live_agent_available !== 'on') {
@@ -2707,18 +2298,101 @@
2707 2298 ]);
2708 2299 wp_die();
2709 2300 }
2710 2301
2711 - $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2712 - if (empty($slack_webhook_url)) {
2302 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2303 +
2304 + if (empty($slack_bot_token)) {
2713 2305 return false;
2714 2306 }
2715 2307
2716 - // Get recent chat history (last 5 messages)
2308 + // Check if channel already exists for this session
2309 + $channel_id = get_option("mxchat_channel_{$session_id}", '');
2310 +
2311 + if (empty($channel_id)) {
2312 + // Create new channel with session ID as name
2313 + $channel_name = 'chat-' . strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $session_id));
2314 +
2315 + //error_log("Attempting to create channel: $channel_name");
2316 +
2317 + $response = wp_remote_post('https://slack.com/api/conversations.create', [
2318 + 'headers' => [
2319 + 'Content-Type' => 'application/json',
2320 + 'Authorization' => 'Bearer ' . $slack_bot_token
2321 + ],
2322 + 'body' => json_encode([
2323 + 'name' => $channel_name,
2324 + 'is_private' => false // Public channel - anyone in workspace can join
2325 + ])
2326 + ]);
2327 +
2328 + if (!is_wp_error($response)) {
2329 + $response_body = wp_remote_retrieve_body($response);
2330 + $response_data = json_decode($response_body, true);
2331 +
2332 + //error_log("Channel creation response: " . $response_body);
2333 +
2334 + if (isset($response_data['ok']) && $response_data['ok']) {
2335 + $channel_id = $response_data['channel']['id'];
2336 + $actual_channel_name = $response_data['channel']['name'] ?? 'unknown';
2337 + //error_log("Channel created successfully: ID=$channel_id, Name=$actual_channel_name");
2338 + update_option("mxchat_channel_{$session_id}", $channel_id);
2339 +
2340 + // Auto-invite agents to the channel
2341 + $agent_user_ids = $this->options['live_agent_user_ids'] ?? '';
2342 +
2343 + if (!empty($agent_user_ids)) {
2344 + // Parse user IDs (one per line)
2345 + $user_ids = array_filter(array_map('trim', explode("\n", $agent_user_ids)));
2346 +
2347 + foreach ($user_ids as $user_id_to_invite) {
2348 + //error_log("Inviting user to channel: $user_id_to_invite");
2349 +
2350 + $invite_response = wp_remote_post('https://slack.com/api/conversations.invite', [
2351 + 'headers' => [
2352 + 'Content-Type' => 'application/json',
2353 + 'Authorization' => 'Bearer ' . $slack_bot_token
2354 + ],
2355 + 'body' => json_encode([
2356 + 'channel' => $channel_id,
2357 + 'users' => $user_id_to_invite
2358 + ])
2359 + ]);
2360 +
2361 + if (!is_wp_error($invite_response)) {
2362 + $invite_body = wp_remote_retrieve_body($invite_response);
2363 + $invite_data = json_decode($invite_body, true);
2364 + //error_log("Invite response for $user_id_to_invite: " . $invite_body);
2365 +
2366 + if (isset($invite_data['ok']) && $invite_data['ok']) {
2367 + //error_log("Successfully invited user $user_id_to_invite to channel");
2368 + } else {
2369 + //error_log("Failed to invite user $user_id_to_invite: " . ($invite_data['error'] ?? 'Unknown error'));
2370 + }
2371 + } else {
2372 + //error_log("WP Error inviting user $user_id_to_invite: " . $invite_response->get_error_message());
2373 + }
2374 + }
2375 + } else {
2376 + //error_log("No agent user IDs configured for auto-invite");
2377 + }
2378 + } else {
2379 + //error_log("Channel creation failed: " . ($response_data['error'] ?? 'Unknown error'));
2380 + }
2381 + } else {
2382 + //error_log("WP Error creating channel: " . $response->get_error_message());
2383 + }
2384 +
2385 + if (empty($channel_id)) {
2386 + return false; // Failed to create channel
2387 + }
2388 + }
2389 +
2390 + // Get recent chat history
2717 2391 $history = get_option("mxchat_history_{$session_id}", []);
2718 - $recent_history = array_slice($history, -5); // Get last 5 messages
2392 + $recent_history = array_slice($history, -5);
2719 2393
2720 - // Format conversation history
2394 + // Format conversation context
2721 2395 $conversation_context = "";
2722 2396 if (!empty($recent_history)) {
2723 2397 $conversation_context = "*Recent Conversation:*\n";
2724 2398 foreach ($recent_history as $hist_message) {
@@ -2729,83 +2403,32 @@
2729 2403 }
2730 2404
2731 2405 update_option("mxchat_mode_{$session_id}", 'agent');
2732 2406
2733 - $webhook_data = [
2734 - 'blocks' => [
2735 - [
2736 - 'type' => 'header',
2737 - 'text' => [
2738 - 'type' => 'plain_text',
2739 - 'text' => '🔔 New Live Agent Request',
2740 - 'emoji' => true
2741 - ]
2742 - ],
2743 - [
2744 - 'type' => 'section',
2745 - 'fields' => [
2746 - [
2747 - 'type' => 'mrkdwn',
2748 - 'text' => sprintf('*User ID:*\n`%s`', $user_id)
2749 - ],
2750 - [
2751 - 'type' => 'mrkdwn',
2752 - 'text' => sprintf('*Session ID:*\n`%s`', $session_id)
2753 - ]
2754 - ]
2755 - ]
2756 - ]
2757 - ];
2758 -
2759 - // Add conversation history if exists
2407 + // Send message to channel
2408 + $channel_message = "🔔 *New Live Agent Request*\n\n";
2409 + $channel_message .= "*Session ID:* `{$session_id}`\n";
2410 + $channel_message .= "*User ID:* `{$user_id}`\n\n";
2411 +
2760 2412 if (!empty($conversation_context)) {
2761 - $webhook_data['blocks'][] = [
2762 - 'type' => 'section',
2763 - 'text' => [
2764 - 'type' => 'mrkdwn',
2765 - 'text' => $conversation_context
2766 - ]
2767 - ];
2413 + $channel_message .= $conversation_context;
2768 2414 }
2415 +
2416 + $channel_message .= "*Current Message:*\n{$message}\n\n";
2417 + $channel_message .= "_Reply directly in this channel - all messages will go to the user_";
2769 2418
2770 - // Add the current message
2771 - $webhook_data['blocks'][] = [
2772 - 'type' => 'section',
2773 - 'text' => [
2774 - 'type' => 'mrkdwn',
2775 - 'text' => sprintf('*Current Message:*\n%s', $message)
2776 - ]
2777 - ];
2778 -
2779 - // Add the reply button
2780 - $webhook_data['blocks'][] = [
2781 - 'type' => 'actions',
2782 - 'elements' => [
2783 - [
2784 - 'type' => 'button',
2785 - 'text' => [
2786 - 'type' => 'plain_text',
2787 - 'text' => '✍️ Reply',
2788 - 'emoji' => true
2789 - ],
2790 - 'value' => $session_id,
2791 - 'action_id' => 'reply_to_user',
2792 - 'style' => 'primary'
2793 - ]
2794 - ]
2795 - ];
2796 -
2797 - $response = wp_remote_post($slack_webhook_url, [
2798 - 'body' => json_encode($webhook_data),
2419 + wp_remote_post('https://slack.com/api/chat.postMessage', [
2799 2420 'headers' => [
2800 2421 'Content-Type' => 'application/json',
2422 + 'Authorization' => 'Bearer ' . $slack_bot_token
2801 2423 ],
2424 + 'body' => json_encode([
2425 + 'channel' => $channel_id,
2426 + 'text' => $channel_message,
2427 + 'mrkdwn' => true
2428 + ])
2802 2429 ]);
2803 2430
2804 - if (is_wp_error($response)) {
2805 - return false;
2806 - }
2807 -
2808 2431 $success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
2809 2432 $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
2810 2433
2811 2434 $this->fallbackResponse = [
@@ -2825,78 +2448,30 @@
2825 2448 ]);
2826 2449 wp_die();
2827 2450 }
2828 2451 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
2829 - $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2452 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2453 + $channel_id = get_option("mxchat_channel_{$session_id}", '');
2830 2454
2831 - if (empty($slack_webhook_url)) {
2832 - //error_log(esc_html__('Slack Webhook URL is not configured.', 'mxchat'));
2455 + if (empty($slack_bot_token) || empty($channel_id)) {
2833 2456 return false;
2834 2457 }
2835 2458
2836 - $webhook_data = [
2837 - 'blocks' => [
2838 - [
2839 - 'type' => 'header',
2840 - 'text' => [
2841 - 'type' => 'plain_text',
2842 - 'text' => esc_html__('📩 New Chat Message', 'mxchat'),
2843 - 'emoji' => true
2844 - ]
2845 - ],
2846 - [
2847 - 'type' => 'section',
2848 - 'fields' => [
2849 - [
2850 - 'type' => 'mrkdwn',
2851 - 'text' => sprintf(esc_html__('*User ID:*\n`%s`', 'mxchat'), $user_id)
2852 - ],
2853 - [
2854 - 'type' => 'mrkdwn',
2855 - 'text' => sprintf(esc_html__('*Session ID:*\n`%s`', 'mxchat'), $session_id)
2856 - ]
2857 - ]
2858 - ],
2859 - [
2860 - 'type' => 'section',
2861 - 'text' => [
2862 - 'type' => 'mrkdwn',
2863 - 'text' => sprintf(esc_html__('*Message:*\n%s', 'mxchat'), $message)
2864 - ]
2865 - ],
2866 - [
2867 - 'type' => 'actions',
2868 - 'elements' => [
2869 - [
2870 - 'type' => 'button',
2871 - 'text' => [
2872 - 'type' => 'plain_text',
2873 - 'text' => esc_html__('✍️ Reply', 'mxchat'),
2874 - 'emoji' => true
2875 - ],
2876 - 'value' => $session_id,
2877 - 'action_id' => 'reply_to_user',
2878 - 'style' => 'primary'
2879 - ]
2880 - ]
2881 - ]
2882 - ]
2883 - ];
2459 + $user_message = "💬 *User:* {$message}";
2884 2460
2885 - $response = wp_remote_post($slack_webhook_url, [
2886 - 'body' => json_encode($webhook_data),
2461 + $response = wp_remote_post('https://slack.com/api/chat.postMessage', [
2887 2462 'headers' => [
2888 2463 'Content-Type' => 'application/json',
2464 + 'Authorization' => 'Bearer ' . $slack_bot_token
2889 2465 ],
2466 + 'body' => json_encode([
2467 + 'channel' => $channel_id,
2468 + 'text' => $user_message,
2469 + 'mrkdwn' => true
2470 + ])
2890 2471 ]);
2891 2472
2892 - if (is_wp_error($response)) {
2893 - //error_log(esc_html__('Error sending message to Slack: ', 'mxchat') . $response->get_error_message());
2894 - return false;
2895 - }
2896 -
2897 - //error_log(esc_html__('Message sent to Slack successfully.', 'mxchat'));
2898 - return true;
2473 + return !is_wp_error($response);
2899 2474 }
2900 2475 public function handle_slack_interaction(WP_REST_Request $request) {
2901 2476 //error_log('Received Slack interaction');
2902 2477
@@ -2984,17 +2559,16 @@
2984 2559
2985 2560 // Default acknowledgment
2986 2561 return new WP_REST_Response(['ok' => true]);
2987 2562 }
2988 -
2989 2563 public function mxchat_handle_agent_response(WP_REST_Request $request) {
2990 2564 //error_log('Received agent response request');
2991 2565 //error_log('Request data: ' . print_r($request->get_params(), true));
2992 - // error_log('Raw body: ' . file_get_contents('php://input'));
2566 + // //error_log('Raw body: ' . file_get_contents('php://input'));
2993 2567
2994 2568 // Get the data from Slack's slash command format
2995 2569 $command_text = $request->get_param('text');
2996 - // error_log('Command text: ' . $command_text);
2570 + // //error_log('Command text: ' . $command_text);
2997 2571
2998 2572 if (empty($command_text)) {
2999 2573 //error_log(esc_html__('Agent response error: No command text received', 'mxchat'));
3000 2574 return new WP_REST_Response([
@@ -3019,9 +2593,9 @@
3019 2593 // Save the message
3020 2594 $message_id = $this->mxchat_save_chat_message($session_id, 'agent', $message);
3021 2595
3022 2596 if (!$message_id) {
3023 - // error_log('Failed to save agent message');
2597 + // //error_log('Failed to save agent message');
3024 2598 return new WP_REST_Response([
3025 2599 'error' => esc_html__('Failed to save message', 'mxchat')
3026 2600 ], 500);
3027 2601 }
@@ -3031,10 +2605,8 @@
3031 2605 'response_type' => 'in_channel',
3032 2606 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
3033 2607 ], 200);
3034 2608 }
3035 -
3036 -
3037 2609 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
3038 2610 //error_log(esc_html__("Switching back to chatbot mode via intent.", 'mxchat'));
3039 2611
3040 2612 // Just update mode to AI
@@ -3048,12 +2620,122 @@
3048 2620 $this->fallbackResponse['text'] = esc_html__('You are now chatting with the AI chatbot.', 'mxchat');
3049 2621
3050 2622 return true; // Intent was handled
3051 2623 }
2624 +public function handle_slack_messages(WP_REST_Request $request) {
2625 + // Log the incoming request for debugging
2626 + //error_log('Slack events request received: ' . $request->get_body());
2627 +
2628 + $body = $request->get_body();
2629 + $data = json_decode($body, true);
2630 +
2631 + // Handle Slack URL verification
2632 + if (isset($data['type']) && $data['type'] === 'url_verification') {
2633 + //error_log('Slack URL verification challenge: ' . $data['challenge']);
2634 + return new WP_REST_Response($data['challenge'], 200, ['Content-Type' => 'text/plain']);
2635 + }
2636 +
2637 + // IMPORTANT: Handle Slack's event deduplication
2638 + if (isset($data['event_id'])) {
2639 + $event_id = $data['event_id'];
2640 + $processed_events = get_transient('mxchat_slack_events') ?: [];
2641 +
2642 + // Check if we've already processed this event
2643 + if (in_array($event_id, $processed_events)) {
2644 + //error_log("Duplicate event detected: $event_id");
2645 + return new WP_REST_Response(['ok' => true]);
2646 + }
2647 +
2648 + // Add this event to processed list
2649 + $processed_events[] = $event_id;
2650 + // Keep only last 100 events to prevent memory issues
2651 + if (count($processed_events) > 100) {
2652 + $processed_events = array_slice($processed_events, -100);
2653 + }
2654 + // Store for 1 hour
2655 + set_transient('mxchat_slack_events', $processed_events, HOUR_IN_SECONDS);
2656 + }
2657 +
2658 + // Handle message events
2659 + if (isset($data['event']) && $data['event']['type'] === 'message') {
2660 + $event = $data['event'];
2661 +
2662 + // Skip bot messages and messages with subtypes (like bot_message)
2663 + if (isset($event['bot_id']) || isset($event['subtype'])) {
2664 + return new WP_REST_Response(['ok' => true]);
2665 + }
2666 +
2667 + // Additional check: Skip if this is a threaded reply to our confirmation
2668 + if (isset($event['thread_ts']) && $event['thread_ts'] !== $event['ts']) {
2669 + return new WP_REST_Response(['ok' => true]);
2670 + }
2671 +
2672 + $channel_id = $event['channel'];
2673 + $message_text = $event['text'] ?? '';
2674 + $message_ts = $event['ts'] ?? '';
2675 +
2676 + // Find session ID by looking for matching channel
2677 + global $wpdb;
2678 + $session_option = $wpdb->get_var(
2679 + $wpdb->prepare(
2680 + "SELECT option_name FROM {$wpdb->options}
2681 + WHERE option_name LIKE 'mxchat_channel_%'
2682 + AND option_value = %s",
2683 + $channel_id
2684 + )
2685 + );
2686 +
2687 + if ($session_option) {
2688 + $session_id = str_replace('mxchat_channel_', '', $session_option);
2689 +
2690 + // Create a unique key for this specific message
2691 + $message_key = md5($session_id . $message_ts . $message_text);
2692 + $processed_messages = get_transient('mxchat_processed_messages_' . $session_id) ?: [];
2693 +
2694 + // Check if we've already processed this exact message
2695 + if (in_array($message_key, $processed_messages)) {
2696 + //error_log("Duplicate message detected for session $session_id");
2697 + return new WP_REST_Response(['ok' => true]);
2698 + }
2699 +
2700 + // Add to processed messages
2701 + $processed_messages[] = $message_key;
2702 + // Keep only last 50 messages per session
2703 + if (count($processed_messages) > 50) {
2704 + $processed_messages = array_slice($processed_messages, -50);
2705 + }
2706 + set_transient('mxchat_processed_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
2707 +
2708 + // Save the agent message
2709 + $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
2710 +
2711 + // Send confirmation back to Slack (only once)
2712 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2713 + if (!empty($slack_bot_token)) {
2714 + // Use a transient to prevent duplicate confirmations
2715 + $confirm_key = 'mxchat_confirm_' . $message_key;
2716 + if (!get_transient($confirm_key)) {
2717 + wp_remote_post('https://slack.com/api/chat.postMessage', [
2718 + 'headers' => [
2719 + 'Content-Type' => 'application/json',
2720 + 'Authorization' => 'Bearer ' . $slack_bot_token
2721 + ],
2722 + 'body' => json_encode([
2723 + 'channel' => $channel_id,
2724 + 'text' => "✅ _Message sent to user_",
2725 + 'thread_ts' => $event['ts'] // Reply in thread
2726 + ])
2727 + ]);
2728 + // Set transient to prevent duplicate confirmations
2729 + set_transient($confirm_key, true, 300); // 5 minutes
2730 + }
2731 + }
2732 + }
2733 + }
2734 +
2735 + return new WP_REST_Response(['ok' => true]);
2736 +}
3052 2737
3053 -
3054 -
3055 -
3056 2738 // For the word upload handler
3057 2739 public function mxchat_handle_word_upload() {
3058 2740 // Delegate to word handler
3059 2741 $this->word_handler->mxchat_handle_word_upload();
@@ -3076,21 +2758,102 @@
3076 2758 return MxChat_User::mxchat_get_user_identifier();
3077 2759 }
3078 2760
3079 2761 private function mxchat_generate_embedding($text, $api_key) {
3080 - $endpoint = 'https://api.openai.com/v1/embeddings';
3081 -
3082 - $body = wp_json_encode([
3083 - 'input' => $text,
3084 - 'model' => 'text-embedding-ada-002'
3085 - ]);
3086 -
2762 + try {
2763 + // Get options and selected model
2764 + $options = get_option('mxchat_options');
2765 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
2766 +
2767 + // Determine endpoint and API key based on model
2768 + if (strpos($selected_model, 'voyage') === 0) {
2769 + $endpoint = 'https://api.voyageai.com/v1/embeddings';
2770 + $api_key = $options['voyage_api_key'] ?? '';
2771 +
2772 + // Check if Voyage API key is missing
2773 + if (empty($api_key)) {
2774 + //error_log('Voyage API key is missing');
2775 + return [
2776 + 'error' => esc_html__('Voyage AI API key is not configured', 'mxchat'),
2777 + 'error_code' => 'missing_voyage_api_key'
2778 + ];
2779 + }
2780 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2781 + $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
2782 + $api_key = $options['gemini_api_key'] ?? '';
2783 +
2784 + // Check if Gemini API key is missing
2785 + if (empty($api_key)) {
2786 + //error_log('Gemini API key is missing');
2787 + return [
2788 + 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
2789 + 'error_code' => 'missing_gemini_api_key'
2790 + ];
2791 + }
2792 + } else {
2793 + $endpoint = 'https://api.openai.com/v1/embeddings';
2794 + // Use the passed API key for OpenAI
2795 +
2796 + // Check if OpenAI API key is missing
2797 + if (empty($api_key)) {
2798 + //error_log('OpenAI API key is missing');
2799 + return [
2800 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
2801 + 'error_code' => 'missing_openai_api_key'
2802 + ];
2803 + }
2804 + }
2805 +
2806 + // Check if text is empty
2807 + if (empty($text)) {
2808 + //error_log('Empty text provided for embedding generation');
2809 + return [
2810 + 'error' => esc_html__('No text provided for embedding generation', 'mxchat'),
2811 + 'error_code' => 'empty_embedding_text'
2812 + ];
2813 + }
2814 +
2815 + // Prepare request body based on provider
2816 + if (strpos($selected_model, 'gemini-embedding') === 0) {
2817 + // Gemini API format
2818 + $request_body = [
2819 + 'model' => 'models/' . $selected_model,
2820 + 'content' => [
2821 + 'parts' => [
2822 + ['text' => $text]
2823 + ]
2824 + ],
2825 + 'outputDimensionality' => 1536
2826 + ];
2827 +
2828 + // Prepare headers for Gemini (API key as query parameter)
2829 + $endpoint .= '?key=' . $api_key;
2830 + $headers = [
2831 + 'Content-Type' => 'application/json'
2832 + ];
2833 + } else {
2834 + // OpenAI/Voyage API format
2835 + $request_body = [
2836 + 'input' => $text,
2837 + 'model' => $selected_model
2838 + ];
2839 +
2840 + // Add output_dimension for voyage-3-large
2841 + if ($selected_model === 'voyage-3-large') {
2842 + $request_body['output_dimension'] = 2048;
2843 + }
2844 +
2845 + // Prepare headers for OpenAI/Voyage
2846 + $headers = [
2847 + 'Content-Type' => 'application/json',
2848 + 'Authorization' => 'Bearer ' . $api_key
2849 + ];
2850 + }
2851 +
2852 + // Prepare request arguments
3087 2853 $args = [
3088 - 'body' => $body,
3089 - 'headers' => [
3090 - 'Content-Type' => 'application/json',
3091 - 'Authorization' => 'Bearer ' . $api_key,
3092 - ],
2854 + 'body' => wp_json_encode($request_body),
2855 + 'headers' => $headers,
3093 2856 'timeout' => 60,
3094 2857 'redirection' => 5,
3095 2858 'blocking' => true,
3096 2859 'httpversion' => '1.0',
@@ -3095,25 +2858,109 @@
3095 2858 'blocking' => true,
3096 2859 'httpversion' => '1.0',
3097 2860 'sslverify' => true,
3098 2861 ];
3099 -
2862 +
2863 + // Make the request
3100 2864 $response = wp_remote_post($endpoint, $args);
3101 -
2865 +
2866 + // Handle WordPress errors
3102 2867 if (is_wp_error($response)) {
3103 - return null;
2868 + $error_message = $response->get_error_message();
2869 + //error_log('Embedding Generation Error: ' . $error_message);
2870 + return [
2871 + 'error' => esc_html__('Connection error when generating embeddings: ', 'mxchat') . esc_html($error_message),
2872 + 'error_code' => 'embedding_connection_error'
2873 + ];
3104 2874 }
3105 -
2875 +
2876 + // Check HTTP status code
2877 + $status_code = wp_remote_retrieve_response_code($response);
2878 + if ($status_code !== 200) {
2879 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
2880 +
2881 + $error_message = isset($response_body['error']['message'])
2882 + ? $response_body['error']['message']
2883 + : 'HTTP Error ' . $status_code;
2884 +
2885 + $error_type = isset($response_body['error']['type'])
2886 + ? $response_body['error']['type']
2887 + : 'unknown';
2888 +
2889 + //error_log('Embedding API HTTP Error: ' . $status_code . ' - ' . $error_message);
2890 +
2891 + // Handle specific error types
2892 + switch ($error_type) {
2893 + case 'invalid_request_error':
2894 + if (strpos($error_message, 'API key') !== false) {
2895 + return [
2896 + 'error' => esc_html__('Invalid API key for embedding generation. Please check your API key configuration.', 'mxchat'),
2897 + 'error_code' => 'embedding_invalid_api_key'
2898 + ];
2899 + }
2900 + break;
2901 +
2902 + case 'authentication_error':
2903 + return [
2904 + 'error' => esc_html__('Authentication failed for embedding generation. Please check your API key.', 'mxchat'),
2905 + 'error_code' => 'embedding_auth_error'
2906 + ];
2907 +
2908 + case 'rate_limit_exceeded':
2909 + return [
2910 + 'error' => esc_html__('Rate limit exceeded for embedding generation. Please try again later.', 'mxchat'),
2911 + 'error_code' => 'embedding_rate_limit'
2912 + ];
2913 +
2914 + case 'quota_exceeded':
2915 + return [
2916 + 'error' => esc_html__('API quota exceeded for embedding generation. Please check your billing details.', 'mxchat'),
2917 + 'error_code' => 'embedding_quota_exceeded'
2918 + ];
2919 + }
2920 +
2921 + // Generic error fallback
2922 + return [
2923 + 'error' => esc_html__('Embedding API error - check embedding API key.: ', 'mxchat') . esc_html($error_message),
2924 + 'error_code' => 'embedding_api_error',
2925 + 'status_code' => $status_code
2926 + ];
2927 + }
2928 +
3106 2929 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3107 -
3108 - if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
3109 - return $response_body['data'][0]['embedding'];
2930 +
2931 + // Handle different response formats based on provider
2932 + if (strpos($selected_model, 'gemini-embedding') === 0) {
2933 + // Gemini API response format
2934 + if (isset($response_body['embedding']['values']) && is_array($response_body['embedding']['values'])) {
2935 + return $response_body['embedding']['values'];
2936 + } else {
2937 + //error_log('Invalid Gemini embedding response: ' . wp_json_encode($response_body));
2938 + return [
2939 + 'error' => esc_html__('Received invalid embedding data from the Gemini API.', 'mxchat'),
2940 + 'error_code' => 'invalid_gemini_embedding_response'
2941 + ];
2942 + }
3110 2943 } else {
3111 - return null;
2944 + // OpenAI/Voyage API response format
2945 + if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
2946 + return $response_body['data'][0]['embedding'];
2947 + } else {
2948 + //error_log('Invalid embedding response: ' . wp_json_encode($response_body));
2949 + return [
2950 + 'error' => esc_html__('Received invalid embedding data from the API.', 'mxchat'),
2951 + 'error_code' => 'invalid_embedding_response'
2952 + ];
2953 + }
3112 2954 }
2955 + } catch (Exception $e) {
2956 + //error_log('Embedding Exception: ' . $e->getMessage());
2957 + return [
2958 + 'error' => esc_html__('System error when generating embeddings: ', 'mxchat') . esc_html($e->getMessage()),
2959 + 'error_code' => 'embedding_exception'
2960 + ];
3113 2961 }
3114 -
3115 -
2962 +}
3116 2963 private function mxchat_find_relevant_content($user_embedding) {
3117 2964 //error_log('MXChat Vector Search: Starting content search...');
3118 2965
3119 2966 // Retrieve the add-on settings from the database.
@@ -3119,9 +2966,8 @@
3119 2966 // Retrieve the add-on settings from the database.
3120 2967 $addon_options = get_option('mxchat_pinecone_addon_options', array());
3121 2968
3122 2969 // Determine whether Pinecone is enabled.
3123 - // We expect the sanitized setting to be a string '1' if enabled, otherwise '0'.
3124 2970 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
3125 2971
3126 2972 //error_log('Pinecone enabled flag: ' . $use_pinecone);
3127 2973
@@ -3139,18 +2985,26 @@
3139 2985 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3140 2986 $cache_key = 'mxchat_system_prompt_embeddings';
3141 2987 $batch_size = 500;
3142 2988
2989 + // Initialize similarity analysis storage
2990 + $this->last_similarity_analysis = [
2991 + 'knowledge_base_type' => 'WordPress Database',
2992 + 'top_matches' => [],
2993 + 'threshold_used' => 0,
2994 + 'total_checked' => 0
2995 + ];
2996 +
3143 2997 // Retrieve embeddings from cache or database
3144 2998 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3145 2999 if ($embeddings === false) {
3000 + // Cache miss - load embeddings from database WITH CONTENT for testing
3146 3001 $embeddings = [];
3147 3002 $offset = 0;
3148 3003
3149 - // Load in batches and build cache
3150 3004 do {
3151 3005 $query = $wpdb->prepare(
3152 - "SELECT id, embedding_vector
3006 + "SELECT id, embedding_vector, article_content, source_url
3153 3007 FROM {$system_prompt_table}
3154 3008 LIMIT %d OFFSET %d",
3155 3009 $batch_size,
3156 3010 $offset
@@ -3162,62 +3016,125 @@
3162 3016 }
3163 3017
3164 3018 $embeddings = array_merge($embeddings, $batch);
3165 3019 $offset += $batch_size;
3166 -
3167 - // Free memory
3168 3020 unset($batch);
3169 -
3170 3021 } while (true);
3171 3022
3172 3023 if (empty($embeddings)) {
3173 - return ''; // Return an empty string if no embeddings found
3024 + return '';
3174 3025 }
3026 +
3027 + // Cache embeddings for future use (but note: this now includes content)
3175 3028 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3176 3029 }
3177 3030
3178 - // Initialize array to store relevant results with similarity scores
3031 + // Get configuration options
3032 + $main_options = get_option('mxchat_options', []);
3033 +
3034 + // Get base similarity threshold (default 75%)
3035 + $similarity_threshold = isset($main_options['similarity_threshold'])
3036 + ? ((int) $main_options['similarity_threshold']) / 100
3037 + : 0.75;
3038 +
3039 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3040 +
3041 + // Calculate similarities and build results array
3042 + $all_similarities = [];
3179 3043 $relevant_results = [];
3180 - // Iterate through embeddings to calculate similarity
3044 +
3181 3045 foreach ($embeddings as $embedding) {
3182 3046 $database_embedding = $embedding->embedding_vector
3183 3047 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3184 3048 : null;
3049 +
3185 3050 if (is_array($database_embedding) && is_array($user_embedding)) {
3186 3051 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
3187 - $relevant_results[] = [
3188 - 'id' => $embedding->id,
3189 - 'similarity' => $similarity
3052 +
3053 + // Store ALL similarities for testing (top 10)
3054 + $source_display = '';
3055 + if (!empty($embedding->source_url) && $embedding->source_url !== '#') {
3056 + $source_display = $embedding->source_url;
3057 + } else {
3058 + $content_preview = strip_tags($embedding->article_content ?? '');
3059 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3060 + $source_display = substr(trim($content_preview), 0, 50) . '...';
3061 + }
3062 +
3063 + $all_similarities[] = [
3064 + 'document_id' => $embedding->id,
3065 + 'similarity' => $similarity,
3066 + 'similarity_percentage' => round($similarity * 100, 2),
3067 + 'above_threshold' => $similarity >= $similarity_threshold,
3068 + 'source_display' => $source_display,
3069 + 'content_preview' => substr(strip_tags($embedding->article_content ?? ''), 0, 100) . '...',
3070 + 'used_for_context' => false // Initialize as false, we'll update this later
3190 3071 ];
3072 +
3073 + // Only consider results above threshold for actual content retrieval
3074 + if ($similarity >= $similarity_threshold) {
3075 + $relevant_results[] = [
3076 + 'id' => $embedding->id,
3077 + 'similarity' => $similarity
3078 + ];
3079 + }
3191 3080 }
3192 - // Free memory
3081 +
3193 3082 unset($database_embedding);
3194 3083 }
3195 3084
3196 - // Retrieve the similarity threshold
3197 - $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
3198 -
3199 - // Filter and sort relevant results by similarity
3200 - $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
3201 - return $result['similarity'] >= $similarity_threshold;
3085 + // Sort ALL similarities for testing display (highest first)
3086 + usort($all_similarities, function ($a, $b) {
3087 + return $b['similarity'] <=> $a['similarity'];
3202 3088 });
3089 +
3090 + // Sort relevant results by similarity (highest first)
3203 3091 usort($relevant_results, function ($a, $b) {
3204 3092 return $b['similarity'] <=> $a['similarity'];
3205 3093 });
3206 -
3207 - // Limit to the top 5 results
3094 +
3095 + // Get top 5 results for actual content (standard approach)
3208 3096 $top_results = array_slice($relevant_results, 0, 5);
3209 -
3210 - // Initialize the final content
3097 +
3098 + // NOW mark which documents are actually used for context
3099 + $used_document_ids = [];
3100 + foreach ($top_results as $result) {
3101 + $used_document_ids[] = $result['id'];
3102 + }
3103 +
3104 + // Update the all_similarities array to mark which were actually used
3105 + foreach ($all_similarities as &$similarity_item) {
3106 + $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
3107 + }
3108 +
3109 + // Store top 10 for testing panel (now with correct used_for_context flags)
3110 + $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
3111 + $this->last_similarity_analysis['total_checked'] = count($embeddings);
3112 +
3113 + //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " top matches for testing");
3114 +
3115 + // Initialize final content
3211 3116 $content = '';
3212 -
3213 - // Fetch and combine content for the top results
3214 - foreach ($top_results as $result) {
3117 +
3118 + // Track document IDs to avoid duplicates
3119 + $added_document_ids = [];
3120 +
3121 + // Fetch and format content for each selected result
3122 + foreach ($top_results as $index => $result) {
3123 + if (in_array($result['id'], $added_document_ids)) {
3124 + continue;
3125 + }
3126 +
3215 3127 $chunk_content = $this->fetch_content_with_product_links($result['id']);
3216 - // Check if the content is PDF-related and add surrounding pages
3128 + $added_document_ids[] = $result['id'];
3129 +
3130 + $content .= "## Reference " . ($index + 1) . " ##\n";
3131 + $content .= $chunk_content . "\n\n";
3132 +
3133 + // PDF surrounding pages logic (unchanged)
3217 3134 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
3218 3135 $surrounding_content = $wpdb->get_results($wpdb->prepare(
3219 - "SELECT article_content FROM {$system_prompt_table}
3136 + "SELECT id, article_content FROM {$system_prompt_table}
3220 3137 WHERE id IN (
3221 3138 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
3222 3139 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
3223 3140 )",
@@ -3223,52 +3140,73 @@
3223 3140 )",
3224 3141 $result['id'],
3225 3142 $result['id']
3226 3143 ));
3227 - // Add previous content if it exists
3144 +
3228 3145 if (!empty($surrounding_content[0])) {
3146 + $content .= "## Related Content ##\n";
3229 3147 $content .= $surrounding_content[0]->article_content . "\n\n";
3148 + $added_document_ids[] = $surrounding_content[0]->id;
3230 3149 }
3231 - // Add the main chunk content
3232 - $content .= $chunk_content . "\n\n";
3233 - // Add next content if it exists
3150 +
3234 3151 if (!empty($surrounding_content[1])) {
3152 + $content .= "## Related Content ##\n";
3235 3153 $content .= $surrounding_content[1]->article_content . "\n\n";
3154 + $added_document_ids[] = $surrounding_content[1]->id;
3236 3155 }
3156 + }
3157 + }
3158 +
3159 + // Add response guidelines
3160 + if (empty($top_results)) {
3161 + $content = "No reference information was found for this query.\n\n";
3237 3162 } else {
3238 - // For non-PDF content, add directly
3239 - $content .= $chunk_content . "\n\n";
3163 + $content .= "\n## Response Guidelines ##\n" .
3164 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3165 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
3166 + "If you don't have specific information or are uncertain about any details, it's always " .
3167 + "better to honestly say you don't know rather than making up or guessing at answers. " .
3168 + "When information is incomplete, let them know you are unsure.";
3240 3169 }
3241 - }
3242 -
3170 +
3243 3171 return trim($content);
3244 3172 }
3245 -/**
3246 - * Find relevant content in Pinecone vector database
3247 - */
3173 +
3248 3174 private function find_relevant_content_pinecone($user_embedding) {
3249 3175 $options = get_option('mxchat_pinecone_addon_options', array());
3250 3176 $api_key = $options['mxchat_pinecone_api_key'] ?? '';
3251 3177 $host = $options['mxchat_pinecone_host'] ?? '';
3252 -
3178 +
3179 + // Initialize similarity analysis storage
3180 + $this->last_similarity_analysis = [
3181 + 'knowledge_base_type' => 'Pinecone',
3182 + 'top_matches' => [],
3183 + 'threshold_used' => 0,
3184 + 'total_checked' => 0
3185 + ];
3186 +
3253 3187 if (empty($host) || empty($api_key)) {
3254 - //error_log('Pinecone credentials not properly configured');
3255 3188 return '';
3256 3189 }
3257 -
3258 - // Get similarity threshold from WordPress settings
3259 - $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
3260 -
3261 - // Prepare the query request for Pinecone
3190 +
3191 + // Get the similarity threshold from the main options
3192 + $main_options = get_option('mxchat_options', []);
3193 + $similarity_threshold = isset($main_options['similarity_threshold'])
3194 + ? ((int) $main_options['similarity_threshold']) / 100
3195 + : 0.75;
3196 +
3197 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3198 +
3199 + // Prepare the query request for Pinecone (request more for testing)
3262 3200 $api_endpoint = "https://{$host}/query";
3263 -
3201 +
3264 3202 $request_body = array(
3265 3203 'vector' => $user_embedding,
3266 - 'topK' => 5,
3204 + 'topK' => 20, // Request more to get good testing data
3267 3205 'includeMetadata' => true,
3268 3206 'includeValues' => true
3269 3207 );
3270 -
3208 +
3271 3209 $response = wp_remote_post($api_endpoint, array(
3272 3210 'headers' => array(
3273 3211 'Api-Key' => $api_key,
3274 3212 'accept' => 'application/json',
@@ -3276,46 +3214,120 @@
3276 3214 ),
3277 3215 'body' => wp_json_encode($request_body),
3278 3216 'timeout' => 30
3279 3217 ));
3280 -
3218 +
3281 3219 if (is_wp_error($response)) {
3282 - //error_log('Pinecone query error: ' . $response->get_error_message());
3283 3220 return '';
3284 3221 }
3285 -
3222 +
3286 3223 $response_code = wp_remote_retrieve_response_code($response);
3287 3224 if ($response_code !== 200) {
3288 - //error_log('Pinecone API error: ' . wp_remote_retrieve_body($response));
3289 3225 return '';
3290 3226 }
3291 -
3227 +
3292 3228 $results = json_decode(wp_remote_retrieve_body($response), true);
3293 3229 if (empty($results['matches'])) {
3294 3230 return '';
3295 3231 }
3296 -
3232 +
3233 + // First, determine which matches will actually be used for content
3234 + $matches_used_for_context = [];
3235 + $matches_used = 0;
3236 +
3237 + foreach ($results['matches'] as $index => $match) {
3238 + // Skip if similarity is below threshold
3239 + if ($match['score'] < $similarity_threshold) {
3240 + continue;
3241 + }
3242 +
3243 + // Limit to top 5 matches above threshold
3244 + if ($matches_used >= 5) {
3245 + break;
3246 + }
3247 +
3248 + if (!empty($match['metadata']['text'])) {
3249 + $matches_used_for_context[] = $match['id'] ?? $index;
3250 + $matches_used++;
3251 + }
3252 + }
3253 +
3254 + // Process ALL matches for testing data (top 10)
3255 + $all_matches = [];
3256 + foreach ($results['matches'] as $index => $match) {
3257 + if ($index >= 10) break; // Limit to top 10 for testing
3258 +
3259 + $source_display = '';
3260 + if (!empty($match['metadata']['source_url'])) {
3261 + $source_display = $match['metadata']['source_url'];
3262 + } else {
3263 + $content_preview = strip_tags($match['metadata']['text'] ?? '');
3264 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3265 + $source_display = substr(trim($content_preview), 0, 50) . '...';
3266 + }
3267 +
3268 + $match_id = $match['id'] ?? $index;
3269 +
3270 + $all_matches[] = [
3271 + 'document_id' => $match_id,
3272 + 'similarity' => $match['score'],
3273 + 'similarity_percentage' => round($match['score'] * 100, 2),
3274 + 'above_threshold' => $match['score'] >= $similarity_threshold,
3275 + 'source_display' => $source_display,
3276 + 'content_preview' => substr(strip_tags($match['metadata']['text'] ?? ''), 0, 100) . '...',
3277 + 'used_for_context' => in_array($match_id, $matches_used_for_context) // Correct usage flag
3278 + ];
3279 + }
3280 +
3281 + // Store for testing panel
3282 + $this->last_similarity_analysis['top_matches'] = $all_matches;
3283 + $this->last_similarity_analysis['total_checked'] = count($results['matches']);
3284 +
3285 + //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " Pinecone matches for testing");
3286 +
3297 3287 // Initialize the final content
3298 3288 $content = '';
3299 -
3300 - // Process each match
3301 - foreach ($results['matches'] as $match) {
3289 + $matches_used = 0;
3290 +
3291 + // Process each match for actual content (this is the real content generation)
3292 + foreach ($results['matches'] as $index => $match) {
3302 3293 // Skip if similarity is below threshold
3303 3294 if ($match['score'] < $similarity_threshold) {
3304 3295 continue;
3305 3296 }
3306 -
3307 - if (!empty($match['metadata']['text']) && !empty($match['metadata']['source_url'])) {
3308 - // Add content with citation
3309 - $content .= $match['metadata']['text'] . "\n";
3310 - $content .= "Source: " . $match['metadata']['source_url'] . "\n\n";
3297 +
3298 + // Limit to top 5 matches above threshold
3299 + if ($matches_used >= 5) {
3300 + break;
3311 3301 }
3302 +
3303 + if (!empty($match['metadata']['text'])) {
3304 + $content .= "## Reference " . ($matches_used + 1) . " ##\n";
3305 + $content .= $match['metadata']['text'] . "\n\n";
3306 +
3307 + if (!empty($match['metadata']['source_url'])) {
3308 + $content .= "URL: " . $match['metadata']['source_url'] . "\n\n";
3309 + }
3310 +
3311 + $matches_used++;
3312 + }
3312 3313 }
3313 -
3314 +
3315 + // Add response guidelines
3316 + if ($matches_used === 0) {
3317 + $content = "No reference information was found for this query.\n\n";
3318 + } else {
3319 + $content .= "\n## Response Guidelines ##\n" .
3320 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3321 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
3322 + "If you don't have specific information or are uncertain about any details, it's always " .
3323 + "better to honestly say you don't know rather than making up or guessing at answers. " .
3324 + "When information is incomplete, let them know you are unsure.";
3325 + }
3326 +
3314 3327 return trim($content);
3315 3328 }
3316 3329
3317 -
3318 3330 private function mxchat_find_relevant_products($user_embedding) {
3319 3331 //error_log('MXChat Vector Search: Starting product search...');
3320 3332
3321 3333 // Retrieve the add-on settings from the database
@@ -3333,9 +3345,8 @@
3333 3345 //error_log('MXChat Vector Search: Using WordPress database for products');
3334 3346 return $this->find_relevant_products_wordpress($user_embedding);
3335 3347 }
3336 3348 }
3337 -
3338 3349 private function find_relevant_products_wordpress($user_embedding) {
3339 3350 global $wpdb;
3340 3351 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3341 3352 $cache_key = 'mxchat_system_prompt_embeddings';
@@ -3409,10 +3420,8 @@
3409 3420 }
3410 3421
3411 3422 return trim($content);
3412 3423 }
3413 -
3414 -// Modified search function with correct filter syntax
3415 3424 private function find_relevant_products_pinecone($user_embedding) {
3416 3425 //error_log('Starting Pinecone product search...');
3417 3426
3418 3427 $options = get_option('mxchat_pinecone_addon_options', array());
@@ -3487,10 +3496,8 @@
3487 3496 }
3488 3497
3489 3498 return trim($content);
3490 3499 }
3491 -
3492 -
3493 3500 private function fetch_content_with_product_links($most_relevant_id) {
3494 3501 global $wpdb;
3495 3502 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3496 3503
@@ -3509,315 +3516,645 @@
3509 3516
3510 3517 return null;
3511 3518 }
3512 3519
3513 -// Function definition
3514 -private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $conversation_history) {
3520 +/**
3521 + * Modified streaming functions to include testing data
3522 + */
3523 +
3524 +// 1. Update the main handler to pass testing data to streaming functions
3525 +private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $gemini_api_key, $conversation_history, $streaming = false, $session_id = '', $testing_data = null) {
3515 3526 try {
3516 3527 if (!$relevant_content) {
3517 - return esc_html__("I'm sorry, I couldn't find relevant information on that topic.", 'mxchat');
3528 + $error_response = [
3529 + 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
3530 + 'error_code' => 'no_relevant_content'
3531 + ];
3532 +
3533 + // Add testing data to error response if available
3534 + if ($testing_data !== null) {
3535 + $error_response['testing_data'] = $testing_data;
3536 + //error_log("MxChat Testing: Added testing data to no_relevant_content error");
3537 + }
3538 +
3539 + return $error_response;
3518 3540 }
3519 -
3541 +
3520 3542 // Ensure conversation_history is an array
3521 3543 if (!is_array($conversation_history)) {
3522 3544 $conversation_history = array();
3523 3545 }
3524 -
3546 +
3525 3547 // Get selected model with default fallback
3526 3548 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
3527 -
3549 +
3528 3550 // Extract model prefix to determine the provider
3529 3551 $model_parts = explode('-', $selected_model);
3530 3552 $provider = strtolower($model_parts[0]);
3531 -
3553 +
3532 3554 // Handle model selection based on provider prefix
3533 3555 switch ($provider) {
3534 - case 'claude':
3535 - if (empty($claude_api_key)) {
3536 - throw new Exception(esc_html__('Claude API key is not configured', 'mxchat'));
3556 + case 'gemini':
3557 + if (empty($gemini_api_key)) {
3558 + $error_response = [
3559 + 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
3560 + 'error_code' => 'missing_gemini_api_key'
3561 + ];
3562 + if ($testing_data !== null) {
3563 + $error_response['testing_data'] = $testing_data;
3564 + }
3565 + return $error_response;
3537 3566 }
3538 - return $this->mxchat_generate_response_claude(
3567 + $response = $this->mxchat_generate_response_gemini(
3539 3568 $selected_model,
3540 - $claude_api_key,
3569 + $gemini_api_key,
3541 3570 $conversation_history,
3542 3571 $relevant_content
3543 3572 );
3544 -
3573 + break;
3574 +
3575 + case 'claude':
3576 + if (empty($claude_api_key)) {
3577 + $error_response = [
3578 + 'error' => esc_html__('Claude API key is not configured', 'mxchat'),
3579 + 'error_code' => 'missing_claude_api_key'
3580 + ];
3581 + if ($testing_data !== null) {
3582 + $error_response['testing_data'] = $testing_data;
3583 + }
3584 + return $error_response;
3585 + }
3586 + if ($streaming) {
3587 + return $this->mxchat_generate_response_claude_stream(
3588 + $selected_model,
3589 + $claude_api_key,
3590 + $conversation_history,
3591 + $relevant_content,
3592 + $session_id,
3593 + $testing_data // Pass testing data
3594 + );
3595 + } else {
3596 + $response = $this->mxchat_generate_response_claude(
3597 + $selected_model,
3598 + $claude_api_key,
3599 + $conversation_history,
3600 + $relevant_content
3601 + );
3602 + }
3603 + break;
3604 +
3545 3605 case 'grok':
3546 3606 if (empty($xai_api_key)) {
3547 - throw new Exception(esc_html__('X.AI API key is not configured', 'mxchat'));
3607 + $error_response = [
3608 + 'error' => esc_html__('X.AI API key is not configured', 'mxchat'),
3609 + 'error_code' => 'missing_xai_api_key'
3610 + ];
3611 + if ($testing_data !== null) {
3612 + $error_response['testing_data'] = $testing_data;
3613 + }
3614 + return $error_response;
3548 3615 }
3549 - return $this->mxchat_generate_response_xai(
3616 + $response = $this->mxchat_generate_response_xai(
3550 3617 $selected_model,
3551 3618 $xai_api_key,
3552 3619 $conversation_history,
3553 3620 $relevant_content
3554 3621 );
3555 -
3622 + break;
3623 +
3556 3624 case 'deepseek':
3557 3625 if (empty($deepseek_api_key)) {
3558 - throw new Exception(esc_html__('DeepSeek API key is not configured', 'mxchat'));
3626 + $error_response = [
3627 + 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
3628 + 'error_code' => 'missing_deepseek_api_key'
3629 + ];
3630 + if ($testing_data !== null) {
3631 + $error_response['testing_data'] = $testing_data;
3632 + }
3633 + return $error_response;
3559 3634 }
3560 - return $this->mxchat_generate_response_deepseek(
3635 + $response = $this->mxchat_generate_response_deepseek(
3561 3636 $selected_model,
3562 3637 $deepseek_api_key,
3563 3638 $conversation_history,
3564 3639 $relevant_content
3565 3640 );
3566 -
3641 + break;
3642 +
3567 3643 case 'gpt':
3644 + case 'o1':
3568 3645 if (empty($api_key)) {
3569 - throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
3646 + $error_response = [
3647 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
3648 + 'error_code' => 'missing_openai_api_key'
3649 + ];
3650 + if ($testing_data !== null) {
3651 + $error_response['testing_data'] = $testing_data;
3652 + }
3653 + return $error_response;
3570 3654 }
3571 - return $this->mxchat_generate_response_openai(
3572 - $selected_model,
3573 - $api_key,
3574 - $conversation_history,
3575 - $relevant_content
3576 - );
3577 -
3655 + if ($streaming) {
3656 + return $this->mxchat_generate_response_openai_stream(
3657 + $selected_model,
3658 + $api_key,
3659 + $conversation_history,
3660 + $relevant_content,
3661 + $session_id,
3662 + $testing_data // Pass testing data
3663 + );
3664 + } else {
3665 + $response = $this->mxchat_generate_response_openai(
3666 + $selected_model,
3667 + $api_key,
3668 + $conversation_history,
3669 + $relevant_content
3670 + );
3671 + }
3672 + break;
3673 +
3578 3674 default:
3579 3675 // Default to OpenAI for custom models or unrecognized prefixes
3580 3676 if (empty($api_key)) {
3581 - throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
3677 + $error_response = [
3678 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
3679 + 'error_code' => 'missing_openai_api_key'
3680 + ];
3681 + if ($testing_data !== null) {
3682 + $error_response['testing_data'] = $testing_data;
3683 + }
3684 + return $error_response;
3582 3685 }
3583 - return $this->mxchat_generate_response_openai(
3584 - $selected_model,
3585 - $api_key,
3586 - $conversation_history,
3587 - $relevant_content
3588 - );
3686 + if ($streaming) {
3687 + return $this->mxchat_generate_response_openai_stream(
3688 + $selected_model,
3689 + $api_key,
3690 + $conversation_history,
3691 + $relevant_content,
3692 + $session_id,
3693 + $testing_data // Pass testing data
3694 + );
3695 + } else {
3696 + $response = $this->mxchat_generate_response_openai(
3697 + $selected_model,
3698 + $api_key,
3699 + $conversation_history,
3700 + $relevant_content
3701 + );
3702 + }
3703 + break;
3589 3704 }
3705 +
3706 + // Check if the response is an error array from the provider-specific function
3707 + if (is_array($response) && isset($response['error'])) {
3708 + // Add testing data to error response if available
3709 + if ($testing_data !== null) {
3710 + $response['testing_data'] = $testing_data;
3711 + //error_log("MxChat Testing: Added testing data to provider error response");
3712 + }
3713 + return $response; // Pass through the error with testing data
3714 + }
3715 +
3716 + // For successful non-streaming responses, we don't add testing data here
3717 + // because it will be added in the main handler
3718 + return $response;
3719 +
3590 3720 } catch (Exception $e) {
3591 3721 //error_log('MXChat Error: ' . $e->getMessage());
3592 - return sprintf(
3593 - esc_html__('An error occurred: %s', 'mxchat'),
3594 - esc_html($e->getMessage())
3595 - );
3722 + $error_response = [
3723 + 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
3724 + 'error_code' => 'system_exception',
3725 + 'exception_details' => $e->getMessage()
3726 + ];
3727 +
3728 + // Add testing data to exception response if available
3729 + if ($testing_data !== null) {
3730 + $error_response['testing_data'] = $testing_data;
3731 + //error_log("MxChat Testing: Added testing data to exception response");
3732 + }
3733 +
3734 + return $error_response;
3596 3735 }
3597 3736 }
3737 +// 2. Update Claude streaming function
3738 +private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
3739 + try {
3740 + // Get system prompt instructions from options
3741 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3598 3742
3743 + // Ensure conversation_history is an array
3744 + if (!is_array($conversation_history)) {
3745 + $conversation_history = array();
3746 + }
3599 3747
3600 -private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
3601 - // Ensure conversation_history is an array
3602 - if (!is_array($conversation_history)) {
3603 - $conversation_history = array();
3604 - }
3748 + // Clean and validate conversation history
3749 + foreach ($conversation_history as &$message) {
3750 + // Convert bot and agent roles to assistant
3751 + if ($message['role'] === 'bot' || $message['role'] === 'agent') {
3752 + $message['role'] = 'assistant';
3753 + }
3754 +
3755 + // Remove unsupported roles - Claude only supports 'assistant' and 'user'
3756 + if (!in_array($message['role'], ['assistant', 'user'])) {
3757 + $message['role'] = 'user';
3758 + }
3605 3759
3606 - // Get system prompt instructions from options
3607 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3760 + // Ensure content field exists
3761 + if (!isset($message['content']) || empty($message['content'])) {
3762 + $message['content'] = '';
3763 + }
3608 3764
3609 - // Create a new array for the formatted conversation
3610 - $formatted_conversation = array();
3765 + // Remove any unsupported fields
3766 + $message = array_intersect_key($message, array_flip(['role', 'content']));
3767 + }
3611 3768
3612 - // Add system message first
3613 - $formatted_conversation[] = array(
3614 - 'role' => 'system',
3615 - 'content' => $system_prompt_instructions . " " . $relevant_content
3616 - );
3769 + // Add relevant content as the latest user message
3770 + $conversation_history[] = [
3771 + 'role' => 'user',
3772 + 'content' => $relevant_content
3773 + ];
3617 3774
3618 - // Add the rest of the conversation history
3619 - foreach ($conversation_history as $message) {
3620 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3621 - $role = $message['role'];
3775 + // Prepare the request body with stream: true
3776 + $body = json_encode([
3777 + 'model' => $selected_model,
3778 + 'messages' => $conversation_history,
3779 + 'max_tokens' => 1000,
3780 + 'temperature' => 0.8,
3781 + 'system' => $system_prompt_instructions,
3782 + 'stream' => true
3783 + ]);
3622 3784
3623 - // Convert roles to supported format
3624 - if ($role === 'bot' || $role === 'agent') {
3625 - $role = 'assistant';
3785 + // Check if we can actually stream (headers not sent, etc.)
3786 + if (headers_sent() || !function_exists('curl_init')) {
3787 + // Fallback to regular response with testing data
3788 + //error_log("MxChat: Streaming not possible, falling back to regular response");
3789 + $regular_response = $this->mxchat_generate_response_claude(
3790 + $selected_model,
3791 + $claude_api_key,
3792 + array_slice($conversation_history, 0, -1), // Remove the added content
3793 + $relevant_content
3794 + );
3795 +
3796 + // Return as JSON with testing data
3797 + $response_data = [
3798 + 'text' => $regular_response,
3799 + 'html' => '',
3800 + 'session_id' => $session_id
3801 + ];
3802 +
3803 + if ($testing_data !== null) {
3804 + $response_data['testing_data'] = $testing_data;
3805 + //error_log("MxChat Testing: Added testing data to Claude fallback response");
3626 3806 }
3627 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3628 - $role = 'user';
3807 +
3808 + // Clear any streaming headers and send JSON
3809 + if (headers_sent() === false) {
3810 + header('Content-Type: application/json');
3629 3811 }
3630 -
3631 - $formatted_conversation[] = array(
3632 - 'role' => $role,
3633 - 'content' => $message['content']
3634 - );
3812 + echo json_encode($response_data);
3813 + return true; // Indicate we handled the response
3635 3814 }
3636 - }
3637 3815
3638 - $body = json_encode([
3639 - 'model' => $selected_model,
3640 - 'messages' => $formatted_conversation,
3641 - 'temperature' => 0.8,
3642 - 'stream' => false
3643 - ]);
3816 + // Use cURL for streaming support
3817 + $ch = curl_init();
3818 + curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
3819 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
3820 + curl_setopt($ch, CURLOPT_POST, true);
3821 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
3822 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
3823 + 'Content-Type: application/json',
3824 + 'x-api-key: ' . $claude_api_key,
3825 + 'anthropic-version: 2023-06-01'
3826 + ));
3827 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
3828 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
3644 3829
3645 - $args = [
3646 - 'body' => $body,
3647 - 'headers' => [
3648 - 'Content-Type' => 'application/json',
3649 - 'Authorization' => 'Bearer ' . $deepseek_api_key,
3650 - ],
3651 - 'timeout' => 60,
3652 - 'redirection' => 5,
3653 - 'blocking' => true,
3654 - 'httpversion' => '1.0',
3655 - 'sslverify' => true,
3656 - ];
3830 + $full_response = ''; // Accumulate full response for saving
3831 + $stream_started = false;
3657 3832
3658 - $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
3833 + // Buffer control for real-time streaming
3834 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
3835 + // Send testing data as the first event if available
3836 + if (!$stream_started && $testing_data !== null) {
3837 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
3838 + flush();
3839 + $stream_started = true;
3840 + //error_log("MxChat Testing: Sent testing data in Claude stream");
3841 + }
3842 +
3843 + // Process each chunk of data
3844 + $lines = explode("\n", $data);
3659 3845
3660 - if (is_wp_error($response)) {
3661 - //error_log('DeepSeek API Error: ' . $response->get_error_message());
3662 - return "Sorry, there was an error processing your request.";
3663 - }
3846 + foreach ($lines as $line) {
3847 + if (trim($line) === '') {
3848 + continue;
3849 + }
3664 3850
3665 - $response_body = wp_remote_retrieve_body($response);
3666 - $decoded_response = json_decode($response_body, true);
3851 + // Claude uses event: and data: format
3852 + if (strpos($line, 'event: ') === 0) {
3853 + // Store the event type for the next data line
3854 + continue;
3855 + }
3667 3856
3668 - if (isset($decoded_response['choices'][0]['message']['content'])) {
3669 - return trim($decoded_response['choices'][0]['message']['content']);
3670 - } else {
3671 - //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
3672 - return "Sorry, I couldn't process that request.";
3673 - }
3674 -}
3857 + if (strpos($line, 'data: ') === 0) {
3858 + $json_str = substr($line, 6); // Remove 'data: ' prefix
3675 3859
3676 -private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
3677 - // Ensure conversation_history is an array
3678 - if (!is_array($conversation_history)) {
3679 - $conversation_history = array();
3680 - }
3860 + $json = json_decode($json_str, true);
3861 + if (json_last_error() !== JSON_ERROR_NONE) {
3862 + continue;
3863 + }
3681 3864
3682 - // Get system prompt instructions from options
3683 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3865 + // Handle different event types
3866 + if (isset($json['type'])) {
3867 + switch ($json['type']) {
3868 + case 'content_block_delta':
3869 + if (isset($json['delta']['text'])) {
3870 + $content = $json['delta']['text'];
3871 + $full_response .= $content; // Accumulate
3872 + // Send as SSE format compatible with your frontend
3873 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
3874 + flush();
3875 + }
3876 + break;
3684 3877
3685 - // Create a new array for the formatted conversation
3686 - $formatted_conversation = array();
3878 + case 'message_stop':
3879 + echo "data: [DONE]\n\n";
3880 + flush();
3881 + break;
3687 3882
3688 - // Add system message first
3689 - $formatted_conversation[] = array(
3690 - 'role' => 'system',
3691 - 'content' => $system_prompt_instructions . " " . $relevant_content
3692 - );
3883 + case 'error':
3884 + echo "data: " . json_encode(['error' => $json['error']['message'] ?? 'Unknown error']) . "\n\n";
3885 + flush();
3886 + break;
3887 + }
3888 + }
3889 + }
3890 + }
3693 3891
3694 - // Add the rest of the conversation history
3695 - foreach ($conversation_history as $message) {
3696 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3697 - $role = $message['role'];
3892 + return strlen($data);
3893 + });
3698 3894
3699 - // Convert roles to supported format
3700 - if ($role === 'bot' || $role === 'agent') {
3701 - $role = 'assistant';
3702 - }
3703 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3704 - $role = 'user';
3705 - }
3895 + $response = curl_exec($ch);
3896 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
3706 3897
3707 - $formatted_conversation[] = array(
3708 - 'role' => $role,
3709 - 'content' => $message['content']
3710 - );
3898 + if (curl_errno($ch)) {
3899 + curl_close($ch);
3900 + throw new Exception('cURL Error: ' . curl_error($ch));
3711 3901 }
3712 - }
3713 3902
3714 - $body = json_encode([
3715 - 'model' => $selected_model,
3716 - 'messages' => $formatted_conversation,
3717 - 'temperature' => 0.8,
3718 - 'stream' => false
3719 - ]);
3903 + curl_close($ch);
3720 3904
3721 - $args = [
3722 - 'body' => $body,
3723 - 'headers' => [
3724 - 'Content-Type' => 'application/json',
3725 - 'Authorization' => 'Bearer ' . $api_key,
3726 - ],
3727 - 'timeout' => 60,
3728 - 'redirection' => 5,
3729 - 'blocking' => true,
3730 - 'httpversion' => '1.0',
3731 - 'sslverify' => true,
3732 - ];
3905 + if ($http_code !== 200) {
3906 + // Fallback to regular response
3907 + //error_log("MxChat: Claude streaming failed with HTTP $http_code, falling back");
3908 + $regular_response = $this->mxchat_generate_response_claude(
3909 + $selected_model,
3910 + $claude_api_key,
3911 + array_slice($conversation_history, 0, -1), // Remove the added content
3912 + $relevant_content
3913 + );
3914 +
3915 + $response_data = [
3916 + 'text' => $regular_response,
3917 + 'html' => '',
3918 + 'session_id' => $session_id
3919 + ];
3920 +
3921 + if ($testing_data !== null) {
3922 + $response_data['testing_data'] = $testing_data;
3923 + //error_log("MxChat Testing: Added testing data to Claude error fallback");
3924 + }
3925 +
3926 + header('Content-Type: application/json');
3927 + echo json_encode($response_data);
3928 + return true;
3929 + }
3733 3930
3734 - $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
3931 + // Save the complete response to maintain chat persistence
3932 + if (!empty($full_response) && !empty($session_id)) {
3933 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
3934 + }
3735 3935
3736 - if (is_wp_error($response)) {
3737 - //error_log('OpenAI API Error: ' . $response->get_error_message());
3738 - return "Sorry, there was an error processing your request.";
3739 - }
3936 + return true; // Indicate streaming completed successfully
3740 3937
3741 - $response_body = wp_remote_retrieve_body($response);
3742 - $decoded_response = json_decode($response_body, true);
3743 -
3744 - if (isset($decoded_response['choices'][0]['message']['content'])) {
3745 - return trim($decoded_response['choices'][0]['message']['content']);
3746 - } else {
3747 - //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
3748 - return "Sorry, I couldn't process that request.";
3938 + } catch (Exception $e) {
3939 + //error_log("MxChat Claude streaming exception: " . $e->getMessage());
3940 +
3941 + // Fallback to regular response on exception
3942 + $regular_response = $this->mxchat_generate_response_claude(
3943 + $selected_model,
3944 + $claude_api_key,
3945 + $conversation_history,
3946 + $relevant_content
3947 + );
3948 +
3949 + $response_data = [
3950 + 'text' => $regular_response,
3951 + 'html' => '',
3952 + 'session_id' => $session_id
3953 + ];
3954 +
3955 + if ($testing_data !== null) {
3956 + $response_data['testing_data'] = $testing_data;
3957 + //error_log("MxChat Testing: Added testing data to Claude exception fallback");
3958 + }
3959 +
3960 + header('Content-Type: application/json');
3961 + echo json_encode($response_data);
3962 + return true;
3749 3963 }
3750 3964 }
3751 -private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
3752 - // Get system prompt instructions from options
3753 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3754 3965
3755 - // Add system prompt to relevant content
3756 - $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
3966 +// 3. Update OpenAI streaming function similarly
3967 +private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
3968 + try {
3969 + // Get system prompt instructions from options
3970 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3971 +
3972 + // Ensure conversation_history is an array
3973 + if (!is_array($conversation_history)) {
3974 + $conversation_history = array();
3975 + }
3757 3976
3758 - // Prepend system instructions to the conversation history
3759 - array_unshift($conversation_history, [
3760 - 'role' => 'system',
3761 - 'content' => "Here are your instructions: " . $content_with_instructions
3762 - ]);
3977 + // Format conversation history for OpenAI
3978 + $formatted_conversation = array();
3763 3979
3764 - // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3765 - foreach ($conversation_history as &$message) {
3766 - if ($message['role'] === 'bot') {
3767 - $message['role'] = 'assistant';
3768 - } elseif ($message['role'] === 'agent') {
3769 - // Tag the message as coming from a live agent
3770 - $message['role'] = 'assistant';
3771 - if (!isset($message['metadata'])) {
3772 - $message['metadata'] = ['source' => 'live_agent'];
3980 + $formatted_conversation[] = array(
3981 + 'role' => 'system',
3982 + 'content' => $system_prompt_instructions . " " . $relevant_content
3983 + );
3984 +
3985 + foreach ($conversation_history as $message) {
3986 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3987 + $role = $message['role'];
3988 + if ($role === 'bot' || $role === 'agent') {
3989 + $role = 'assistant';
3990 + }
3991 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3992 + $role = 'user';
3993 + }
3994 + $formatted_conversation[] = array(
3995 + 'role' => $role,
3996 + 'content' => $message['content']
3997 + );
3773 3998 }
3774 3999 }
3775 4000
3776 - // Ensure all roles are valid
3777 - if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3778 - $message['role'] = 'user'; // Default to 'user'
4001 + // Check if we can actually stream
4002 + if (headers_sent() || !function_exists('curl_init')) {
4003 + // Fallback to regular response with testing data
4004 + //error_log("MxChat: OpenAI streaming not possible, falling back to regular response");
4005 + $regular_response = $this->mxchat_generate_response_openai(
4006 + $selected_model,
4007 + $api_key,
4008 + $conversation_history,
4009 + $relevant_content
4010 + );
4011 +
4012 + $response_data = [
4013 + 'text' => $regular_response,
4014 + 'html' => '',
4015 + 'session_id' => $session_id
4016 + ];
4017 +
4018 + if ($testing_data !== null) {
4019 + $response_data['testing_data'] = $testing_data;
4020 + //error_log("MxChat Testing: Added testing data to OpenAI fallback response");
4021 + }
4022 +
4023 + header('Content-Type: application/json');
4024 + echo json_encode($response_data);
4025 + return true;
3779 4026 }
3780 - }
3781 4027
4028 + // Prepare the request body with stream: true
4029 + $body = json_encode([
4030 + 'model' => $selected_model,
4031 + 'messages' => $formatted_conversation,
4032 + 'temperature' => 0.8,
4033 + 'stream' => true
4034 + ]);
3782 4035
3783 - // Build the request body
3784 - $body = json_encode([
3785 - 'model' => $selected_model,
3786 - 'messages' => $conversation_history,
3787 - 'temperature' => 0.8,
3788 - 'stream' => false
3789 - ]);
3790 -
3791 - // Set up the API request
3792 - $args = [
3793 - 'body' => $body,
3794 - 'headers' => [
3795 - 'Content-Type' => 'application/json',
3796 - 'Authorization' => 'Bearer ' . $xai_api_key,
3797 - ],
3798 - 'timeout' => 60,
3799 - 'redirection' => 5,
3800 - 'blocking' => true,
3801 - 'httpversion' => '1.0',
3802 - 'sslverify' => true,
3803 - ];
3804 -
3805 - // Make the API request
3806 - $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
3807 -
3808 - // Process the response
3809 - if (is_wp_error($response)) {
3810 - return "Sorry, there was an error processing your request.";
4036 + // Use cURL for streaming support
4037 + $ch = curl_init();
4038 + curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
4039 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4040 + curl_setopt($ch, CURLOPT_POST, true);
4041 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4042 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4043 + 'Content-Type: application/json',
4044 + 'Authorization: Bearer ' . $api_key
4045 + ));
4046 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4047 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
4048 +
4049 + $full_response = ''; // Accumulate full response for saving
4050 + $stream_started = false;
4051 +
4052 + // Buffer control for real-time streaming
4053 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
4054 + // Send testing data as the first event if available
4055 + if (!$stream_started && $testing_data !== null) {
4056 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4057 + flush();
4058 + $stream_started = true;
4059 + //error_log("MxChat Testing: Sent testing data in OpenAI stream");
4060 + }
4061 +
4062 + // Process each chunk of data
4063 + $lines = explode("\n", $data);
4064 +
4065 + foreach ($lines as $line) {
4066 + if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
4067 + continue;
4068 + }
4069 +
4070 + $json_str = substr($line, 6); // Remove 'data: ' prefix
4071 +
4072 + if ($json_str === '[DONE]') {
4073 + echo "data: [DONE]\n\n";
4074 + flush();
4075 + continue;
4076 + }
4077 +
4078 + $json = json_decode($json_str, true);
4079 + if (isset($json['choices'][0]['delta']['content'])) {
4080 + $content = $json['choices'][0]['delta']['content'];
4081 + $full_response .= $content; // Accumulate
4082 + // Send as SSE format
4083 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
4084 + flush();
4085 + }
4086 + }
4087 +
4088 + return strlen($data);
4089 + });
4090 +
4091 + $response = curl_exec($ch);
4092 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4093 +
4094 + if (curl_errno($ch) || $http_code !== 200) {
4095 + curl_close($ch);
4096 +
4097 + // Fallback to regular response
4098 + //error_log("MxChat: OpenAI streaming failed, falling back");
4099 + $regular_response = $this->mxchat_generate_response_openai(
4100 + $selected_model,
4101 + $api_key,
4102 + $conversation_history,
4103 + $relevant_content
4104 + );
4105 +
4106 + $response_data = [
4107 + 'text' => $regular_response,
4108 + 'html' => '',
4109 + 'session_id' => $session_id
4110 + ];
4111 +
4112 + if ($testing_data !== null) {
4113 + $response_data['testing_data'] = $testing_data;
4114 + //error_log("MxChat Testing: Added testing data to OpenAI error fallback");
4115 + }
4116 +
4117 + header('Content-Type: application/json');
4118 + echo json_encode($response_data);
4119 + return true;
4120 + }
4121 +
4122 + curl_close($ch);
4123 +
4124 + // Save the complete response to maintain chat persistence
4125 + if (!empty($full_response) && !empty($session_id)) {
4126 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
4127 + }
4128 +
4129 + return true; // Indicate streaming completed successfully
4130 +
4131 + } catch (Exception $e) {
4132 + //error_log("MxChat OpenAI streaming exception: " . $e->getMessage());
4133 +
4134 + // Fallback to regular response
4135 + $regular_response = $this->mxchat_generate_response_openai(
4136 + $selected_model,
4137 + $api_key,
4138 + $conversation_history,
4139 + $relevant_content
4140 + );
4141 +
4142 + $response_data = [
4143 + 'text' => $regular_response,
4144 + 'html' => '',
4145 + 'session_id' => $session_id
4146 + ];
4147 +
4148 + if ($testing_data !== null) {
4149 + $response_data['testing_data'] = $testing_data;
4150 + //error_log("MxChat Testing: Added testing data to OpenAI exception fallback");
4151 + }
4152 +
4153 + header('Content-Type: application/json');
4154 + echo json_encode($response_data);
4155 + return true;
3811 4156 }
3812 -
3813 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
3814 -
3815 - if (isset($response_body['choices'][0]['message']['content'])) {
3816 - return trim($response_body['choices'][0]['message']['content']);
3817 - } else {
3818 - return "Sorry, I couldn't process that request.";
3819 - }
3820 4157 }
3821 4158
3822 4159 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
3823 4160 // Get system prompt instructions from options
@@ -3918,8 +4255,654 @@
3918 4255 // Log unexpected response format
3919 4256 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
3920 4257 return "Sorry, I received an unexpected response format from the API.";
3921 4258 }
4259 +private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
4260 + try {
4261 + // Ensure conversation_history is an array
4262 + if (!is_array($conversation_history)) {
4263 + $conversation_history = array();
4264 + }
4265 +
4266 + // Get system prompt instructions from options
4267 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4268 +
4269 + // Create a new array for the formatted conversation
4270 + $formatted_conversation = array();
4271 +
4272 + // Add system message first
4273 + $formatted_conversation[] = array(
4274 + 'role' => 'system',
4275 + 'content' => $system_prompt_instructions . " " . $relevant_content
4276 + );
4277 +
4278 + // Add the rest of the conversation history
4279 + foreach ($conversation_history as $message) {
4280 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4281 + $role = $message['role'];
4282 +
4283 + // Convert roles to supported format
4284 + if ($role === 'bot' || $role === 'agent') {
4285 + $role = 'assistant';
4286 + }
4287 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4288 + $role = 'user';
4289 + }
4290 +
4291 + $formatted_conversation[] = array(
4292 + 'role' => $role,
4293 + 'content' => $message['content']
4294 + );
4295 + }
4296 + }
4297 +
4298 + $body = json_encode([
4299 + 'model' => $selected_model,
4300 + 'messages' => $formatted_conversation,
4301 + 'temperature' => 0.8,
4302 + 'stream' => false
4303 + ]);
4304 +
4305 + $args = [
4306 + 'body' => $body,
4307 + 'headers' => [
4308 + 'Content-Type' => 'application/json',
4309 + 'Authorization' => 'Bearer ' . $api_key,
4310 + ],
4311 + 'timeout' => 60,
4312 + 'redirection' => 5,
4313 + 'blocking' => true,
4314 + 'httpversion' => '1.0',
4315 + 'sslverify' => true,
4316 + ];
4317 +
4318 + $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
4319 +
4320 + if (is_wp_error($response)) {
4321 + $error_message = $response->get_error_message();
4322 + //error_log('OpenAI API Error: ' . $error_message);
4323 + return [
4324 + 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
4325 + 'error_code' => 'openai_connection_error',
4326 + 'provider' => 'openai'
4327 + ];
4328 + }
4329 +
4330 + $status_code = wp_remote_retrieve_response_code($response);
4331 + if ($status_code !== 200) {
4332 + $response_body = wp_remote_retrieve_body($response);
4333 + $decoded_response = json_decode($response_body, true);
4334 +
4335 + $error_message = isset($decoded_response['error']['message'])
4336 + ? $decoded_response['error']['message']
4337 + : 'HTTP Error ' . $status_code;
4338 +
4339 + $error_type = isset($decoded_response['error']['type'])
4340 + ? $decoded_response['error']['type']
4341 + : 'unknown';
4342 +
4343 + //error_log('OpenAI API HTTP Error: ' . $status_code . ' - ' . $error_message);
4344 +
4345 + // Handle specific error types
4346 + switch ($error_type) {
4347 + case 'invalid_request_error':
4348 + if (strpos($error_message, 'API key') !== false) {
4349 + return [
4350 + 'error' => esc_html__('Invalid OpenAI API key. Please check your API key configuration.', 'mxchat'),
4351 + 'error_code' => 'openai_invalid_api_key',
4352 + 'provider' => 'openai'
4353 + ];
4354 + }
4355 + break;
4356 +
4357 + case 'authentication_error':
4358 + return [
4359 + 'error' => esc_html__('Authentication failed with OpenAI. Please check your API key.', 'mxchat'),
4360 + 'error_code' => 'openai_auth_error',
4361 + 'provider' => 'openai'
4362 + ];
4363 +
4364 + case 'rate_limit_exceeded':
4365 + return [
4366 + 'error' => esc_html__('OpenAI rate limit exceeded. Please try again later.', 'mxchat'),
4367 + 'error_code' => 'openai_rate_limit',
4368 + 'provider' => 'openai'
4369 + ];
4370 +
4371 + case 'quota_exceeded':
4372 + return [
4373 + 'error' => esc_html__('OpenAI API quota exceeded. Please check your billing details.', 'mxchat'),
4374 + 'error_code' => 'openai_quota_exceeded',
4375 + 'provider' => 'openai'
4376 + ];
4377 + }
4378 +
4379 + // Generic error fallback
4380 + return [
4381 + 'error' => esc_html__('OpenAI API error: ', 'mxchat') . esc_html($error_message),
4382 + 'error_code' => 'openai_api_error',
4383 + 'provider' => 'openai',
4384 + 'status_code' => $status_code
4385 + ];
4386 + }
4387 +
4388 + $response_body = wp_remote_retrieve_body($response);
4389 + $decoded_response = json_decode($response_body, true);
4390 +
4391 + if (isset($decoded_response['choices'][0]['message']['content'])) {
4392 + return trim($decoded_response['choices'][0]['message']['content']);
4393 + } else {
4394 + //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
4395 + return [
4396 + 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
4397 + 'error_code' => 'openai_response_format_error',
4398 + 'provider' => 'openai'
4399 + ];
4400 + }
4401 + } catch (Exception $e) {
4402 + //error_log('OpenAI Exception: ' . $e->getMessage());
4403 + return [
4404 + 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
4405 + 'error_code' => 'openai_exception',
4406 + 'provider' => 'openai'
4407 + ];
4408 + }
4409 +}
4410 +
4411 +private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
4412 + try {
4413 + // Ensure conversation_history is an array
4414 + if (!is_array($conversation_history)) {
4415 + $conversation_history = array();
4416 + }
4417 +
4418 + // Get system prompt instructions from options
4419 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4420 +
4421 + // Create a new array for the formatted conversation
4422 + $formatted_conversation = array();
4423 +
4424 + // Add system message first
4425 + $formatted_conversation[] = array(
4426 + 'role' => 'system',
4427 + 'content' => $system_prompt_instructions . " " . $relevant_content
4428 + );
4429 +
4430 + // Add the rest of the conversation history
4431 + foreach ($conversation_history as $message) {
4432 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4433 + $role = $message['role'];
4434 +
4435 + // Convert roles to supported format
4436 + if ($role === 'bot' || $role === 'agent') {
4437 + $role = 'assistant';
4438 + }
4439 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4440 + $role = 'user';
4441 + }
4442 +
4443 + $formatted_conversation[] = array(
4444 + 'role' => $role,
4445 + 'content' => $message['content']
4446 + );
4447 + }
4448 + }
4449 +
4450 + $body = json_encode([
4451 + 'model' => $selected_model,
4452 + 'messages' => $formatted_conversation,
4453 + 'temperature' => 0.8,
4454 + 'stream' => false
4455 + ]);
4456 +
4457 + $args = [
4458 + 'body' => $body,
4459 + 'headers' => [
4460 + 'Content-Type' => 'application/json',
4461 + 'Authorization' => 'Bearer ' . $deepseek_api_key,
4462 + ],
4463 + 'timeout' => 60,
4464 + 'redirection' => 5,
4465 + 'blocking' => true,
4466 + 'httpversion' => '1.0',
4467 + 'sslverify' => true,
4468 + ];
4469 +
4470 + $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
4471 +
4472 + if (is_wp_error($response)) {
4473 + $error_message = $response->get_error_message();
4474 + //error_log('DeepSeek API Error: ' . $error_message);
4475 + return [
4476 + 'error' => esc_html__('Connection error when contacting DeepSeek: ', 'mxchat') . esc_html($error_message),
4477 + 'error_code' => 'deepseek_connection_error',
4478 + 'provider' => 'deepseek'
4479 + ];
4480 + }
4481 +
4482 + $status_code = wp_remote_retrieve_response_code($response);
4483 + if ($status_code !== 200) {
4484 + $response_body = wp_remote_retrieve_body($response);
4485 + $decoded_response = json_decode($response_body, true);
4486 +
4487 + $error_message = isset($decoded_response['error']['message'])
4488 + ? $decoded_response['error']['message']
4489 + : 'HTTP Error ' . $status_code;
4490 +
4491 + $error_type = isset($decoded_response['error']['type'])
4492 + ? $decoded_response['error']['type']
4493 + : 'unknown';
4494 +
4495 + //error_log('DeepSeek API HTTP Error: ' . $status_code . ' - ' . $error_message);
4496 +
4497 + // Handle specific error types
4498 + switch ($status_code) {
4499 + case 401:
4500 + return [
4501 + 'error' => esc_html__('Authentication failed with DeepSeek. Please check your API key.', 'mxchat'),
4502 + 'error_code' => 'deepseek_auth_error',
4503 + 'provider' => 'deepseek'
4504 + ];
4505 +
4506 + case 400:
4507 + if (strpos($error_message, 'API key') !== false) {
4508 + return [
4509 + 'error' => esc_html__('Invalid DeepSeek API key. Please check your API key configuration.', 'mxchat'),
4510 + 'error_code' => 'deepseek_invalid_api_key',
4511 + 'provider' => 'deepseek'
4512 + ];
4513 + }
4514 + break;
4515 +
4516 + case 429:
4517 + if (strpos($error_message, 'quota') !== false) {
4518 + return [
4519 + 'error' => esc_html__('DeepSeek API quota exceeded. Please check your billing details.', 'mxchat'),
4520 + 'error_code' => 'deepseek_quota_exceeded',
4521 + 'provider' => 'deepseek'
4522 + ];
4523 + } else {
4524 + return [
4525 + 'error' => esc_html__('DeepSeek rate limit exceeded. Please try again later.', 'mxchat'),
4526 + 'error_code' => 'deepseek_rate_limit',
4527 + 'provider' => 'deepseek'
4528 + ];
4529 + }
4530 +
4531 + case 500:
4532 + case 502:
4533 + case 503:
4534 + case 504:
4535 + return [
4536 + 'error' => esc_html__('DeepSeek service is currently unavailable. Please try again later.', 'mxchat'),
4537 + 'error_code' => 'deepseek_service_unavailable',
4538 + 'provider' => 'deepseek'
4539 + ];
4540 + }
4541 +
4542 + // Generic error fallback
4543 + return [
4544 + 'error' => esc_html__('DeepSeek API error: ', 'mxchat') . esc_html($error_message),
4545 + 'error_code' => 'deepseek_api_error',
4546 + 'provider' => 'deepseek',
4547 + 'status_code' => $status_code
4548 + ];
4549 + }
4550 +
4551 + $response_body = wp_remote_retrieve_body($response);
4552 + $decoded_response = json_decode($response_body, true);
4553 +
4554 + if (isset($decoded_response['choices'][0]['message']['content'])) {
4555 + return trim($decoded_response['choices'][0]['message']['content']);
4556 + } else {
4557 + //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
4558 + return [
4559 + 'error' => esc_html__('Unexpected response format from DeepSeek.', 'mxchat'),
4560 + 'error_code' => 'deepseek_response_format_error',
4561 + 'provider' => 'deepseek'
4562 + ];
4563 + }
4564 + } catch (Exception $e) {
4565 + //error_log('DeepSeek Exception: ' . $e->getMessage());
4566 + return [
4567 + 'error' => esc_html__('System error when processing DeepSeek request: ', 'mxchat') . esc_html($e->getMessage()),
4568 + 'error_code' => 'deepseek_exception',
4569 + 'provider' => 'deepseek'
4570 + ];
4571 + }
4572 +}
4573 +private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
4574 + try {
4575 + // Get system prompt instructions from options
4576 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4577 +
4578 + // Add system prompt to relevant content
4579 + $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
4580 +
4581 + // Prepend system instructions to the conversation history
4582 + array_unshift($conversation_history, [
4583 + 'role' => 'system',
4584 + 'content' => "Here are your instructions: " . $content_with_instructions
4585 + ]);
4586 +
4587 + // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
4588 + foreach ($conversation_history as &$message) {
4589 + if ($message['role'] === 'bot') {
4590 + $message['role'] = 'assistant';
4591 + } elseif ($message['role'] === 'agent') {
4592 + // Tag the message as coming from a live agent
4593 + $message['role'] = 'assistant';
4594 + if (!isset($message['metadata'])) {
4595 + $message['metadata'] = ['source' => 'live_agent'];
4596 + }
4597 + }
4598 +
4599 + // Ensure all roles are valid
4600 + if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
4601 + $message['role'] = 'user'; // Default to 'user'
4602 + }
4603 + }
4604 +
4605 + // Build the request body
4606 + $body = json_encode([
4607 + 'model' => $selected_model,
4608 + 'messages' => $conversation_history,
4609 + 'temperature' => 0.8,
4610 + 'stream' => false
4611 + ]);
4612 +
4613 + // Set up the API request
4614 + $args = [
4615 + 'body' => $body,
4616 + 'headers' => [
4617 + 'Content-Type' => 'application/json',
4618 + 'Authorization' => 'Bearer ' . $xai_api_key,
4619 + ],
4620 + 'timeout' => 60,
4621 + 'redirection' => 5,
4622 + 'blocking' => true,
4623 + 'httpversion' => '1.0',
4624 + 'sslverify' => true,
4625 + ];
4626 +
4627 + // Make the API request
4628 + $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
4629 +
4630 + // Process the response
4631 + if (is_wp_error($response)) {
4632 + $error_message = $response->get_error_message();
4633 + //error_log('X.AI API Error: ' . $error_message);
4634 + return [
4635 + 'error' => esc_html__('Connection error when contacting X.AI: ', 'mxchat') . esc_html($error_message),
4636 + 'error_code' => 'xai_connection_error',
4637 + 'provider' => 'xai'
4638 + ];
4639 + }
4640 +
4641 + $status_code = wp_remote_retrieve_response_code($response);
4642 + if ($status_code !== 200) {
4643 + $response_body = wp_remote_retrieve_body($response);
4644 + $decoded_response = json_decode($response_body, true);
4645 +
4646 + // Log the full response for debugging
4647 + //error_log('X.AI Error Response: ' . print_r($decoded_response, true));
4648 +
4649 + // Extract error message from X.AI's specific format
4650 + $error_message = '';
4651 +
4652 + // Check for direct error string (as seen in your logs)
4653 + if (isset($decoded_response['error']) && is_string($decoded_response['error'])) {
4654 + $error_message = $decoded_response['error'];
4655 + }
4656 + // Check for nested error object (OpenAI style)
4657 + elseif (isset($decoded_response['error']['message'])) {
4658 + $error_message = $decoded_response['error']['message'];
4659 + }
4660 + // Check for top-level message
4661 + elseif (isset($decoded_response['message'])) {
4662 + $error_message = $decoded_response['message'];
4663 + }
4664 + // Fallback
4665 + else {
4666 + $error_message = 'HTTP Error ' . $status_code;
4667 + }
4668 +
4669 + //error_log('X.AI API HTTP Error: ' . $status_code . ' - ' . $error_message);
4670 +
4671 + // Check for API key errors using string matching
4672 + if (stripos($error_message, 'api key') !== false ||
4673 + stripos($error_message, 'incorrect api key') !== false ||
4674 + stripos($error_message, 'invalid api key') !== false) {
4675 + return [
4676 + 'error' => esc_html__('Invalid X.AI API key. Please check your API key configuration.', 'mxchat'),
4677 + 'error_code' => 'xai_invalid_api_key',
4678 + 'provider' => 'xai'
4679 + ];
4680 + }
4681 +
4682 + // Authentication errors
4683 + if ($status_code === 401 || $status_code === 403 ||
4684 + stripos($error_message, 'auth') !== false) {
4685 + return [
4686 + 'error' => esc_html__('Authentication failed with X.AI. Please check your API key.', 'mxchat'),
4687 + 'error_code' => 'xai_auth_error',
4688 + 'provider' => 'xai'
4689 + ];
4690 + }
4691 +
4692 + // Model errors
4693 + if (stripos($error_message, 'model') !== false) {
4694 + return [
4695 + 'error' => esc_html__('Invalid model specified for X.AI. Please check your model configuration.', 'mxchat'),
4696 + 'error_code' => 'xai_invalid_model',
4697 + 'provider' => 'xai'
4698 + ];
4699 + }
4700 +
4701 + // Rate limit errors
4702 + if ($status_code === 429 ||
4703 + stripos($error_message, 'rate') !== false ||
4704 + stripos($error_message, 'limit') !== false) {
4705 + return [
4706 + 'error' => esc_html__('X.AI rate limit exceeded. Please try again later.', 'mxchat'),
4707 + 'error_code' => 'xai_rate_limit',
4708 + 'provider' => 'xai'
4709 + ];
4710 + }
4711 +
4712 + // Quota errors
4713 + if (stripos($error_message, 'quota') !== false ||
4714 + stripos($error_message, 'billing') !== false) {
4715 + return [
4716 + 'error' => esc_html__('X.AI API quota exceeded. Please check your billing details.', 'mxchat'),
4717 + 'error_code' => 'xai_quota_exceeded',
4718 + 'provider' => 'xai'
4719 + ];
4720 + }
4721 +
4722 + // Server errors
4723 + if ($status_code >= 500) {
4724 + return [
4725 + 'error' => esc_html__('X.AI service is currently unavailable. Please try again later.', 'mxchat'),
4726 + 'error_code' => 'xai_service_unavailable',
4727 + 'provider' => 'xai'
4728 + ];
4729 + }
4730 +
4731 + // Generic error fallback with the actual error message
4732 + return [
4733 + 'error' => esc_html__('X.AI API error: ', 'mxchat') . esc_html($error_message),
4734 + 'error_code' => 'xai_api_error',
4735 + 'provider' => 'xai',
4736 + 'status_code' => $status_code
4737 + ];
4738 + }
4739 +
4740 + $response_body = wp_remote_retrieve_body($response);
4741 + $decoded_response = json_decode($response_body, true);
4742 +
4743 + if (isset($decoded_response['choices'][0]['message']['content'])) {
4744 + return trim($decoded_response['choices'][0]['message']['content']);
4745 + } else {
4746 + //error_log('X.AI API Response Format Error: ' . print_r($decoded_response, true));
4747 + return [
4748 + 'error' => esc_html__('Unexpected response format from X.AI.', 'mxchat'),
4749 + 'error_code' => 'xai_response_format_error',
4750 + 'provider' => 'xai'
4751 + ];
4752 + }
4753 +} catch (Exception $e) {
4754 + //error_log('X.AI Exception: ' . $e->getMessage());
4755 + return [
4756 + 'error' => esc_html__('System error when processing X.AI request: ', 'mxchat') . esc_html($e->getMessage()),
4757 + 'error_code' => 'xai_exception',
4758 + 'provider' => 'xai'
4759 + ];
4760 +}
4761 +}
4762 +private function mxchat_generate_response_gemini($selected_model, $gemini_api_key, $conversation_history, $relevant_content) {
4763 + // Get system prompt instructions from options
4764 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4765 +
4766 + // Add system prompt to relevant content
4767 + $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
4768 +
4769 + // Format messages for Gemini API
4770 + $formatted_messages = [];
4771 +
4772 + // Add system message as the first user message with role prefix
4773 + // Note: Gemini doesn't have a dedicated system role, so we use a prefixed user message
4774 + $formatted_messages[] = [
4775 + 'role' => 'user',
4776 + 'parts' => [
4777 + ['text' => "[System Instructions] " . $content_with_instructions]
4778 + ]
4779 + ];
4780 +
4781 + // Add model response to acknowledge system instructions
4782 + $formatted_messages[] = [
4783 + 'role' => 'model',
4784 + 'parts' => [
4785 + ['text' => "I understand and will follow these instructions."]
4786 + ]
4787 + ];
4788 +
4789 + // Process the rest of the conversation history
4790 + $current_role = null;
4791 + $current_parts = [];
4792 +
4793 + foreach ($conversation_history as $message) {
4794 + // Skip the first system message as we already handled it
4795 + if ($message['role'] === 'system') {
4796 + continue;
4797 + }
4798 +
4799 + // Map roles to Gemini format
4800 + $gemini_role = '';
4801 + if ($message['role'] === 'user') {
4802 + $gemini_role = 'user';
4803 + } else if (in_array($message['role'], ['assistant', 'bot', 'agent'])) {
4804 + $gemini_role = 'model';
4805 + } else {
4806 + // Skip unsupported roles
4807 + continue;
4808 + }
4809 +
4810 + // If we have a new role, add the previous message
4811 + if ($current_role !== null && $current_role !== $gemini_role && !empty($current_parts)) {
4812 + $formatted_messages[] = [
4813 + 'role' => $current_role,
4814 + 'parts' => $current_parts
4815 + ];
4816 + $current_parts = [];
4817 + }
4818 +
4819 + // Set current role and add text to parts
4820 + $current_role = $gemini_role;
4821 + $current_parts[] = ['text' => $message['content']];
4822 + }
4823 +
4824 + // Add the last message if there's content
4825 + if ($current_role !== null && !empty($current_parts)) {
4826 + $formatted_messages[] = [
4827 + 'role' => $current_role,
4828 + 'parts' => $current_parts
4829 + ];
4830 + }
4831 +
4832 + // Build the request body
4833 + $body = json_encode([
4834 + 'contents' => $formatted_messages,
4835 + 'generationConfig' => [
4836 + 'temperature' => 0.7,
4837 + 'topP' => 0.95,
4838 + 'topK' => 40,
4839 + 'maxOutputTokens' => 8192,
4840 + ],
4841 + 'safetySettings' => [
4842 + [
4843 + 'category' => 'HARM_CATEGORY_HARASSMENT',
4844 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
4845 + ],
4846 + [
4847 + 'category' => 'HARM_CATEGORY_HATE_SPEECH',
4848 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
4849 + ],
4850 + [
4851 + 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
4852 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
4853 + ],
4854 + [
4855 + 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT',
4856 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
4857 + ]
4858 + ]
4859 + ]);
4860 +
4861 + // Prepare the API endpoint
4862 + $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
4863 +
4864 + // Set up the API request
4865 + $args = [
4866 + 'body' => $body,
4867 + 'headers' => [
4868 + 'Content-Type' => 'application/json',
4869 + ],
4870 + 'timeout' => 60,
4871 + 'redirection' => 5,
4872 + 'blocking' => true,
4873 + 'httpversion' => '1.0',
4874 + 'sslverify' => true,
4875 + ];
4876 +
4877 + // Make the API request
4878 + $response = wp_remote_post($api_endpoint, $args);
4879 +
4880 + // Process the response
4881 + if (is_wp_error($response)) {
4882 + return "Sorry, there was an error processing your request: " . $response->get_error_message();
4883 + }
4884 +
4885 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
4886 +
4887 + // Handle potential errors in the response
4888 + if (isset($response_body['error'])) {
4889 + //error_log('Gemini API Error: ' . json_encode($response_body['error']));
4890 + return "Sorry, there was an error with the Gemini API: " .
4891 + (isset($response_body['error']['message']) ? $response_body['error']['message'] : 'Unknown error');
4892 + }
4893 +
4894 + // Extract the response text
4895 + if (isset($response_body['candidates'][0]['content']['parts'][0]['text'])) {
4896 + return trim($response_body['candidates'][0]['content']['parts'][0]['text']);
4897 + } else {
4898 + //error_log('Unexpected Gemini API response format: ' . json_encode($response_body));
4899 + return "Sorry, I couldn't process that request. The response format was unexpected.";
4900 + }
4901 +}
4902 +
4903 +
4904 +
3922 4905 public function mxchat_dismiss_pre_chat_message() {
3923 4906 // Get and sanitize the user identifier
3924 4907 $user_id = $this->mxchat_get_user_identifier();
3925 4908 $user_id = sanitize_key($user_id);
@@ -3975,11 +4958,10 @@
3975 4958 }
3976 4959
3977 4960 public function mxchat_enqueue_scripts_styles() {
3978 4961 // Define version numbers for the styles and scripts
3979 - $chat_style_version = '2.0.3'; // Replace with your actual version
3980 - $chat_script_version = '2.0.3'; // Replace with your actual version
3981 -
4962 + $chat_style_version = '2.2.8';
4963 + $chat_script_version = '2.2.8';
3982 4964 // Enqueue the script
3983 4965 wp_enqueue_script(
3984 4966 'mxchat-chat-js',
3985 4967 plugin_dir_url(__FILE__) . '../js/chat-script.js',
@@ -3986,9 +4968,8 @@
3986 4968 array('jquery'),
3987 4969 $chat_script_version,
3988 4970 true
3989 4971 );
3990 -
3991 4972 // Enqueue the CSS
3992 4973 wp_enqueue_style(
3993 4974 'mxchat-chat-css',
3994 4975 plugin_dir_url(__FILE__) . '../css/chat-style.css',
@@ -3994,17 +4975,19 @@
3994 4975 plugin_dir_url(__FILE__) . '../css/chat-style.css',
3995 4976 array(),
3996 4977 $chat_style_version
3997 4978 );
3998 -
3999 4979 // Fetch options from the database
4000 4980 $this->options = get_option('mxchat_options');
4001 4981 $prompts_options = get_option('mxchat_prompts_options', array());
4002 -
4982 +
4003 4983 // Prepare settings for JavaScript
4004 4984 $style_settings = array(
4005 4985 'ajax_url' => admin_url('admin-ajax.php'),
4006 4986 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
4987 + 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-4o',
4988 + 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
4989 + 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off', // ADD THIS LINE
4007 4990 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
4008 4991 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
4009 4992 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
4010 4993 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
@@ -4018,10 +5001,9 @@
4018 5001 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
4019 5002 'icon_color' => $this->options['icon_color'] ?? '#fff',
4020 5003 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
4021 5004 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
4022 - 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off', // Example for consistency
4023 -
5005 + 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
4024 5006 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
4025 5007 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
4026 5008 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
4027 5009 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
@@ -4026,76 +5008,647 @@
4026 5008 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
4027 5009 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
4028 5010 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
4029 5011 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
4030 -
4031 5012 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
4032 5013 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
4033 5014 );
4034 -
4035 5015 // Pass the settings to the script
4036 5016 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
4037 5017 }
4038 5018
4039 5019
5020 +// Modify the mxchat_reset_rate_limits function to handle different timeframes
4040 5021 public function mxchat_reset_rate_limits() {
5022 + try {
4041 5023 global $wpdb;
5024 + $all_options = get_option('mxchat_options', []);
5025 + $current_time = time();
5026 +
5027 + // Get rate limit options with a limit to avoid memory issues
5028 + $option_names = $wpdb->get_col(
5029 + "SELECT option_name FROM {$wpdb->options}
5030 + WHERE option_name LIKE 'mxchat_chat_limit_%'
5031 + LIMIT 500"
5032 + );
5033 +
5034 + if (empty($option_names)) {
5035 + return;
5036 + }
5037 +
5038 + foreach ($option_names as $option_name) {
5039 + // Parse the option name to extract role and user ID
5040 + $parts = explode('_', $option_name);
5041 +
5042 + // Skip if the option name doesn't match our expected format
5043 + if (count($parts) < 4) {
5044 + continue;
5045 + }
5046 +
5047 + // Extract role (may be multiple parts like 'shop_manager')
5048 + $role_parts = array_slice($parts, 3, -1);
5049 + $role = implode('_', $role_parts);
5050 +
5051 + // Skip if role doesn't exist in our settings
5052 + if (!isset($all_options['rate_limits'][$role])) {
5053 + continue;
5054 + }
5055 +
5056 + $timeframe = $all_options['rate_limits'][$role]['timeframe'] ?? 'daily';
5057 + $limit_data = get_option($option_name);
5058 +
5059 + if (!$limit_data || !is_array($limit_data) || !isset($limit_data['timestamp'])) {
5060 + continue;
5061 + }
5062 +
5063 + $timestamp = $limit_data['timestamp'];
5064 + $should_reset = false;
5065 +
5066 + // Determine if we should reset based on the timeframe
5067 + switch ($timeframe) {
5068 + case 'hourly':
5069 + $should_reset = ($current_time - $timestamp) >= 3600;
5070 + break;
5071 + case 'daily':
5072 + $should_reset = ($current_time - $timestamp) >= 86400;
5073 + break;
5074 + case 'weekly':
5075 + $should_reset = ($current_time - $timestamp) >= 604800;
5076 + break;
5077 + case 'monthly':
5078 + $should_reset = ($current_time - $timestamp) >= 2592000;
5079 + break;
5080 + }
5081 +
5082 + // Reset the counter if the timeframe has passed
5083 + if ($should_reset) {
5084 + delete_option($option_name);
5085 + wp_cache_delete($option_name, 'options');
5086 + }
5087 + }
5088 +
5089 + // Clean up any orphaned entries
5090 + wp_cache_delete('mxchat_all_chat_limits', 'options');
5091 +
5092 + } catch (Exception $e) {
5093 + //error_log('MxChat: Rate limit reset error: ' . $e->getMessage());
5094 + }
5095 +}
5096 +/**
5097 + * Clean up cron events on deactivation
5098 + */
5099 +public function cleanup_cron_events() {
5100 + wp_clear_scheduled_hook('mxchat_reset_rate_limits');
5101 +}
4042 5102
4043 - // Define a cache key pattern for rate limits
4044 - $cache_key_pattern = 'mxchat_chat_limit_%';
5103 +/**
5104 + * Check if the current user has exceeded their rate limit based on role
5105 + *
5106 + * @return true|array True if limit not exceeded, or array with error message if exceeded
5107 + */
5108 +public function check_rate_limit() {
5109 + $all_options = get_option('mxchat_options', []);
5110 + //error_log('MXChat Rate Limit: Starting check');
5111 + //error_log('MXChat Rate Limit: Options: ' . print_r($all_options, true));
5112 +
5113 + // Determine user role or if logged out
5114 + if (is_user_logged_in()) {
5115 + $user = wp_get_current_user();
5116 + $user_id = $user->ID;
5117 +
5118 + // Get the user's primary role using reset() to safely get the first element
5119 + $user_roles = $user->roles;
5120 +
5121 + // Safely get the first role regardless of array key structure
5122 + if (!empty($user_roles) && is_array($user_roles)) {
5123 + $role = reset($user_roles); // This safely gets the first element regardless of key
5124 + } else {
5125 + $role = 'subscriber'; // Default to subscriber if no role found
5126 + }
5127 +
5128 + //error_log('MXChat Rate Limit: User ID: ' . $user_id . ', Role: ' . $role);
5129 + } else {
5130 + $role = 'logged_out';
5131 + // Use IP address for non-logged-in users
5132 + $user_id = $this->get_client_ip();
5133 + //error_log('MXChat Rate Limit: Logged out user IP: ' . $user_id);
5134 + }
5135 +
5136 + // Check if rate limits are configured for this role
5137 + if (!isset($all_options['rate_limits'][$role])) {
5138 + //error_log('MXChat Rate Limit: No rate limit configured for role: ' . $role);
5139 + return true; // No limit set for this role
5140 + }
5141 +
5142 + $limit = $all_options['rate_limits'][$role]['limit'];
5143 + //error_log('MXChat Rate Limit: Limit for role ' . $role . ': ' . $limit);
5144 +
5145 + // If unlimited, return true immediately
5146 + if ($limit === 'unlimited') {
5147 + //error_log('MXChat Rate Limit: Unlimited setting, no limit applied');
5148 + return true;
5149 + }
5150 +
5151 + // Get the option name for this user/role
5152 + $option_name = 'mxchat_chat_limit_' . $role . '_' . $user_id;
5153 + //error_log('MXChat Rate Limit: Option name: ' . $option_name);
5154 +
5155 + // Get the counter data
5156 + $limit_data = get_option($option_name, ['count' => 0, 'timestamp' => time()]);
5157 + //error_log('MXChat Rate Limit: Current limit data: ' . print_r($limit_data, true));
5158 +
5159 + // If first request or counter reset needed, set the initial timestamp
5160 + if ($limit_data['count'] === 0) {
5161 + $limit_data['timestamp'] = time();
5162 + update_option($option_name, $limit_data);
5163 + //error_log('MXChat Rate Limit: First request, initialized timestamp');
5164 + }
5165 +
5166 + // Get the timeframe
5167 + $timeframe = isset($all_options['rate_limits'][$role]['timeframe']) ?
5168 + $all_options['rate_limits'][$role]['timeframe'] : 'daily';
5169 + //error_log('MXChat Rate Limit: Timeframe: ' . $timeframe);
5170 +
5171 + // Check if the counter needs to be reset based on timeframe
5172 + $current_time = time();
5173 + $timestamp = $limit_data['timestamp'];
5174 + $should_reset = false;
5175 +
5176 + switch ($timeframe) {
5177 + case 'hourly':
5178 + $should_reset = ($current_time - $timestamp) >= 3600; // 1 hour
5179 + break;
5180 + case 'daily':
5181 + $should_reset = ($current_time - $timestamp) >= 86400; // 24 hours
5182 + break;
5183 + case 'weekly':
5184 + $should_reset = ($current_time - $timestamp) >= 604800; // 7 days
5185 + break;
5186 + case 'monthly':
5187 + $should_reset = ($current_time - $timestamp) >= 2592000; // 30 days
5188 + break;
5189 + }
5190 +
5191 + //error_log('MXChat Rate Limit: Current time: ' . $current_time . ', Last timestamp: ' . $timestamp);
5192 + //error_log('MXChat Rate Limit: Time elapsed: ' . ($current_time - $timestamp) . ' seconds');
5193 + //error_log('MXChat Rate Limit: Should reset: ' . ($should_reset ? 'Yes' : 'No'));
5194 +
5195 + // Reset the counter if the timeframe has passed
5196 + if ($should_reset) {
5197 + $limit_data = ['count' => 0, 'timestamp' => $current_time];
5198 + update_option($option_name, $limit_data);
5199 + //error_log('MXChat Rate Limit: Reset counter to 0');
5200 + }
5201 +
5202 + // Check if user has exceeded their limit
5203 + if ($limit_data['count'] >= intval($limit)) {
5204 + // Get the custom message for this role
5205 + $message = !empty($all_options['rate_limits'][$role]['message'])
5206 + ? $all_options['rate_limits'][$role]['message']
5207 + : __('Rate limit exceeded. Please try again later.', 'mxchat');
5208 +
5209 + // Add timeframe information to the message if placeholders exist
5210 + $timeframe_label = '';
5211 + switch ($timeframe) {
5212 + case 'hourly':
5213 + $timeframe_label = __('hour', 'mxchat');
5214 + break;
5215 + case 'daily':
5216 + $timeframe_label = __('day', 'mxchat');
5217 + break;
5218 + case 'weekly':
5219 + $timeframe_label = __('week', 'mxchat');
5220 + break;
5221 + case 'monthly':
5222 + $timeframe_label = __('month', 'mxchat');
5223 + break;
5224 + }
5225 +
5226 + // Replace placeholders in the message
5227 + $message = str_replace(
5228 + ['{limit}', '{count}', '{remaining}', '{timeframe}'],
5229 + [intval($limit), $limit_data['count'], max(0, intval($limit) - $limit_data['count']), $timeframe_label],
5230 + $message
5231 + );
5232 +
5233 + // NEW: Process HTML links in the message
5234 + $message = $this->process_rate_limit_message_html($message);
5235 +
5236 + //error_log('MXChat Rate Limit: Limit exceeded. Message: ' . $message);
5237 +
5238 + // Return error with the processed message
5239 + return [
5240 + 'error' => true,
5241 + 'message' => $message
5242 + ];
5243 + }
5244 +
5245 + // Increment the counter
5246 + $limit_data['count']++;
5247 + update_option($option_name, $limit_data);
5248 + //error_log('MXChat Rate Limit: Incremented counter to ' . $limit_data['count']);
5249 +
5250 + return true;
5251 +}
4045 5252
4046 - // Retrieve all option names matching the pattern
4047 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
4048 - $option_names = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
4049 5253
4050 - // db call ok; no-cache ok
4051 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- db call ok
4052 - $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
5254 +/**
5255 + * Process HTML links in rate limit messages
5256 + *
5257 + * @param string $message The rate limit message
5258 + * @return string The processed message with safe HTML links
5259 + */
5260 +private function process_rate_limit_message_html($message) {
5261 + // Return original message if empty
5262 + if (empty($message)) {
5263 + return $message;
5264 + }
5265 +
5266 + // First, convert markdown links to HTML
5267 + $message = $this->convert_markdown_links($message);
5268 +
5269 + // Then, auto-convert any remaining plain URLs to links
5270 + $message = $this->auto_link_urls($message);
5271 +
5272 + // Allow basic HTML tags for links and formatting
5273 + $allowed_tags = [
5274 + 'a' => [
5275 + 'href' => true,
5276 + 'target' => true,
5277 + 'rel' => true,
5278 + 'title' => true,
5279 + 'class' => true
5280 + ],
5281 + 'strong' => [],
5282 + 'em' => [],
5283 + 'br' => [],
5284 + 'b' => [],
5285 + 'i' => [],
5286 + 'span' => ['class' => true]
5287 + ];
5288 +
5289 + // Sanitize but allow the specified HTML tags
5290 + $processed_message = wp_kses($message, $allowed_tags);
5291 +
5292 + // If wp_kses stripped everything, return the original message as plain text
5293 + if (empty($processed_message) && !empty($message)) {
5294 + // Strip all HTML and return plain text as fallback
5295 + return wp_strip_all_tags($message);
5296 + }
5297 +
5298 + return $processed_message;
5299 +}
4053 5300
4054 - // Clear the relevant cache entries
4055 - foreach ($option_names as $option_name) {
4056 - wp_cache_delete($option_name, 'options');
4057 - }
5301 +/**
5302 + * Convert markdown links to HTML
5303 + *
5304 + * @param string $text The text to process
5305 + * @return string The text with markdown links converted to HTML
5306 + */
5307 +private function convert_markdown_links($text) {
5308 + // Return original text if empty
5309 + if (empty($text)) {
5310 + return $text;
5311 + }
5312 +
5313 + // Pattern to match markdown links: [text](url)
5314 + $pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
5315 +
5316 + $processed_text = preg_replace_callback($pattern, function($matches) {
5317 + $link_text = $matches[1];
5318 + $url = $matches[2];
5319 +
5320 + // Clean up any trailing punctuation from the URL
5321 + $url = rtrim($url, '.,;:!?');
5322 +
5323 + // Sanitize the link text and URL
5324 + $safe_text = esc_html($link_text);
5325 + $safe_url = esc_url($url);
5326 +
5327 + // Create the HTML link
5328 + return '<a href="' . $safe_url . '" target="_blank" rel="noopener noreferrer">' . $safe_text . '</a>';
5329 + }, $text);
5330 +
5331 + // If preg_replace_callback failed, return original text
5332 + if ($processed_text === null) {
5333 + return $text;
5334 + }
5335 +
5336 + return $processed_text;
5337 +}
4058 5338
4059 - // Optionally, clear a general cache if you have one
4060 - wp_cache_delete('mxchat_all_chat_limits', 'options');
5339 +/**
5340 + * Auto-convert plain URLs to clickable links
5341 + *
5342 + * @param string $text The text to process
5343 + * @return string The text with URLs converted to links
5344 + */
5345 +private function auto_link_urls($text) {
5346 + // Return original text if empty
5347 + if (empty($text)) {
5348 + return $text;
4061 5349 }
5350 +
5351 + // Simple pattern that avoids complex lookbehinds
5352 + // This will match URLs that are not already inside href attributes or markdown links
5353 + $pattern = '/(?<!href=["\'])(?<!\]\()https?:\/\/[^\s<>"\')\]]+/i';
5354 +
5355 + $processed_text = preg_replace_callback($pattern, function($matches) {
5356 + $url = $matches[0];
5357 + // Clean up any trailing punctuation that might have been captured
5358 + $url = rtrim($url, '.,;:!?');
5359 +
5360 + // Add target="_blank" and rel="noopener noreferrer" for security
5361 + return '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($url) . '</a>';
5362 + }, $text);
5363 +
5364 + // If preg_replace_callback failed, return original text
5365 + if ($processed_text === null) {
5366 + return $text;
5367 + }
5368 +
5369 + return $processed_text;
5370 +}
4062 5371
4063 -private function mxchat_fetch_woocommerce_products() {
4064 - // Ensure WooCommerce is active
4065 - if (!class_exists('WooCommerce')) {
4066 - return [];
5372 +
5373 +// Helper function to get client IP address
5374 +private function get_client_ip() {
5375 + // Check for shared internet/ISP IP
5376 + if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
5377 + return sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
4067 5378 }
5379 +
5380 + // Check for IPs passing through proxies
5381 + if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
5382 + // Use the first value in the comma-separated list
5383 + $forwarded_for = explode(',', sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']));
5384 + return trim($forwarded_for[0]);
5385 + }
5386 +
5387 + if (!empty($_SERVER['REMOTE_ADDR'])) {
5388 + return sanitize_text_field($_SERVER['REMOTE_ADDR']);
5389 + }
5390 +
5391 + // Fallback
5392 + return 'unknown';
5393 +}
4068 5394
4069 - $args = array(
4070 - 'post_type' => 'product',
4071 - 'post_status' => 'publish',
4072 - 'posts_per_page' => -1,
4073 - );
5395 +/**
5396 + * AJAX handler to get system information for testing panel
5397 + */
5398 +public function mxchat_get_system_info() {
5399 + // Verify nonce for security
5400 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
5401 + wp_send_json_error(['message' => 'Invalid nonce']);
5402 + return;
5403 + }
5404 +
5405 + // Only allow admin users
5406 + if (!current_user_can('administrator')) {
5407 + wp_send_json_error(['message' => 'Unauthorized']);
5408 + return;
5409 + }
5410 +
5411 + // Get system prompt from options
5412 + $system_prompt = isset($this->options['system_prompt_instructions'])
5413 + ? $this->options['system_prompt_instructions']
5414 + : 'No system prompt configured';
5415 +
5416 + // Get selected model
5417 + $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
5418 +
5419 + // Get API key status (just check if they exist, don't expose the keys)
5420 + $api_status = [];
5421 + $api_status['openai'] = !empty($this->options['api_key']);
5422 + $api_status['claude'] = !empty($this->options['claude_api_key']);
5423 + $api_status['gemini'] = !empty($this->options['gemini_api_key']);
5424 + $api_status['xai'] = !empty($this->options['xai_api_key']);
5425 + $api_status['deepseek'] = !empty($this->options['deepseek_api_key']);
5426 +
5427 + wp_send_json_success([
5428 + 'system_prompt' => $system_prompt,
5429 + 'selected_model' => $selected_model,
5430 + 'api_status' => $api_status
5431 + ]);
5432 +}
4074 5433
4075 - $products = get_posts($args);
4076 - $product_data = [];
5434 +/**
5435 + * AJAX handler to get similarity threshold
5436 + */
5437 +public function mxchat_get_similarity_threshold() {
5438 + // Verify nonce for security
5439 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
5440 + wp_send_json_error(['message' => 'Invalid nonce']);
5441 + return;
5442 + }
5443 +
5444 + // Only allow admin users
5445 + if (!current_user_can('administrator')) {
5446 + wp_send_json_error(['message' => 'Unauthorized']);
5447 + return;
5448 + }
5449 +
5450 + // Get similarity threshold from main options (default 75%)
5451 + $similarity_threshold = isset($this->options['similarity_threshold'])
5452 + ? ((int) $this->options['similarity_threshold']) / 100
5453 + : 0.75;
5454 +
5455 + wp_send_json_success([
5456 + 'threshold' => $similarity_threshold,
5457 + 'threshold_percentage' => ($similarity_threshold * 100) . '%'
5458 + ]);
5459 +}
4077 5460
4078 - foreach ($products as $product) {
4079 - $product_id = $product->ID;
4080 - $product_obj = wc_get_product($product_id);
5461 +/**
5462 + * AJAX handler to get knowledge base status
5463 + */
5464 +public function mxchat_get_kb_status() {
5465 + // Verify nonce for security
5466 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
5467 + wp_send_json_error(['message' => 'Invalid nonce']);
5468 + return;
5469 + }
5470 +
5471 + // Only allow admin users
5472 + if (!current_user_can('administrator')) {
5473 + wp_send_json_error(['message' => 'Unauthorized']);
5474 + return;
5475 + }
5476 +
5477 + // Check Pinecone vs WordPress
5478 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
5479 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
5480 +
5481 + $kb_info = [
5482 + 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
5483 + 'status' => 'Active'
5484 + ];
5485 +
5486 + // Get document count
5487 + if ($use_pinecone) {
5488 + $kb_info['documents'] = 'Connected to Pinecone';
5489 + $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
5490 + } else {
5491 + // Count documents in WordPress database
5492 + global $wpdb;
5493 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5494 + $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
5495 + $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
5496 + }
5497 +
5498 + wp_send_json_success($kb_info);
5499 +}
4081 5500
4082 - $product_data[] = array(
4083 - 'id' => $product_id,
4084 - 'name' => $product_obj->get_name(),
4085 - 'description' => $product_obj->get_description(),
4086 - 'short_description' => $product_obj->get_short_description(),
4087 - 'url' => get_permalink($product_id),
4088 - 'price' => $product_obj->get_regular_price(),
4089 - 'sale_price' => $product_obj->get_sale_price(),
4090 - 'stock_status' => $product_obj->get_stock_status(),
4091 - 'sku' => $product_obj->get_sku(),
4092 - 'in_stock' => $product_obj->is_in_stock(),
4093 - 'total_sales' => $product_obj->get_total_sales(),
4094 - );
5501 +/**
5502 + * AJAX handler to start a completely fresh session (NEW - replaces old clear session)
5503 + */
5504 +public function mxchat_start_fresh_session() {
5505 + // Verify nonce for security
5506 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
5507 + wp_send_json_error(['message' => 'Invalid nonce']);
5508 + return;
4095 5509 }
5510 +
5511 + // Only allow admin users
5512 + if (!current_user_can('administrator')) {
5513 + wp_send_json_error(['message' => 'Unauthorized']);
5514 + return;
5515 + }
5516 +
5517 + $old_session_id = isset($_POST['old_session_id']) ? sanitize_text_field($_POST['old_session_id']) : '';
5518 + $new_session_id = isset($_POST['new_session_id']) ? sanitize_text_field($_POST['new_session_id']) : '';
5519 +
5520 + if (empty($old_session_id)) {
5521 + wp_send_json_error(['message' => 'Old session ID required']);
5522 + return;
5523 + }
5524 +
5525 + // If no new session ID provided, generate one
5526 + if (empty($new_session_id)) {
5527 + $new_session_id = 'mxchat_chat_' . substr(md5(uniqid()), 0, 9);
5528 + }
5529 +
5530 + // Clear ALL data associated with the old session
5531 + $this->clear_complete_session_data($old_session_id);
5532 +
5533 + // Initialize the new session
5534 + $this->initialize_fresh_session($new_session_id);
5535 +
5536 + wp_send_json_success([
5537 + 'message' => 'Fresh session started successfully',
5538 + 'new_session_id' => $new_session_id,
5539 + 'old_session_id' => $old_session_id
5540 + ]);
5541 +}
4096 5542
4097 - return $product_data;
5543 +/**
5544 + * Clear ALL data associated with a session (ENHANCED)
5545 + */
5546 +private function clear_complete_session_data($session_id) {
5547 + // Clear chat history
5548 + delete_option("mxchat_history_{$session_id}");
5549 +
5550 + // Clear chat mode
5551 + delete_option("mxchat_mode_{$session_id}");
5552 +
5553 + // Clear any PDF/Word transients
5554 + $this->clear_pdf_transients($session_id);
5555 + if (method_exists($this, 'clear_word_transients')) {
5556 + $this->clear_word_transients($session_id);
5557 + }
5558 +
5559 + // Clear agent-related data
5560 + delete_option("mxchat_channel_{$session_id}");
5561 + delete_option("mxchat_agent_name_{$session_id}");
5562 + delete_option("mxchat_email_{$session_id}");
5563 +
5564 + // Clear any recommendation flow state
5565 + delete_option("mxchat_sr_flow_state_{$session_id}");
5566 +
5567 + // Clear any cached embeddings or context
5568 + delete_transient("mxchat_context_{$session_id}");
5569 + delete_transient("mxchat_last_query_{$session_id}");
5570 +
5571 + // Clear any testing data
5572 + delete_transient("mxchat_testing_data_{$session_id}");
5573 +
5574 + // Clear any rate limiting data for this session
5575 + delete_transient("mxchat_rate_limit_{$session_id}");
5576 +
5577 + // Clear any other session-specific transients
5578 + delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
5579 + delete_transient("mxchat_include_pdf_in_context_{$session_id}");
5580 + delete_transient("mxchat_include_word_in_context_{$session_id}");
5581 +
5582 + //error_log("MxChat: Cleared all data for session: {$session_id}");
4098 5583 }
5584 +
5585 +/**
5586 + * Initialize a fresh session with default data
5587 + */
5588 +private function initialize_fresh_session($session_id) {
5589 + // Set default chat mode
5590 + update_option("mxchat_mode_{$session_id}", 'ai');
5591 +
5592 + //error_log("MxChat: Initialized fresh session: {$session_id}");
5593 +}
5594 +
5595 +/**
5596 + * Helper method to clear Word document transients (if you have Word support)
5597 + */
5598 +private function clear_word_transients($session_id) {
5599 + delete_transient('mxchat_word_url_' . $session_id);
5600 + delete_transient('mxchat_word_filename_' . $session_id);
5601 + delete_transient('mxchat_word_embeddings_' . $session_id);
5602 + delete_transient('mxchat_include_word_in_context_' . $session_id);
5603 +}
5604 +
5605 +/**
5606 + * Simplified testing data capture method (CLEANED UP)
5607 + */
5608 +private function capture_testing_data($user_embedding, $message, $session_id) {
5609 + // Only capture for admin users
5610 + if (!current_user_can('administrator')) {
5611 + return null;
5612 + }
5613 +
5614 + $testing_data = [
5615 + 'query' => $message,
5616 + 'timestamp' => time(),
5617 + 'top_matches' => [],
5618 + 'action_matches' => [] // NEW: Add action matches
5619 + ];
5620 +
5621 + // Get similarity threshold
5622 + $similarity_threshold = isset($this->options['similarity_threshold'])
5623 + ? ((int) $this->options['similarity_threshold']) / 100
5624 + : 0.75;
5625 +
5626 + $testing_data['similarity_threshold'] = $similarity_threshold;
5627 +
5628 + // Use the real similarity analysis if available
5629 + if ($this->last_similarity_analysis !== null) {
5630 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
5631 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
5632 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
5633 + } else {
5634 + // Fallback: determine knowledge base type
5635 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
5636 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
5637 +
5638 + $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
5639 + }
5640 +
5641 + // NEW: Include action analysis if available
5642 + if (isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
5643 + $testing_data['action_matches'] = $this->last_action_analysis;
5644 +
5645 + // Clear it after capturing to avoid stale data
5646 + $this->last_action_analysis = null;
5647 + }
5648 +
5649 + return $testing_data;
5650 +}
5651 +
4099 5652
4100 5653 }
4101 5654 ?>