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