PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.3.9
MxChat – AI Chatbot & Content Generation for WordPress v2.3.9
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 +1096 -5394 3.2.62.3.9 View file →
@@ -10,40 +10,10 @@
10 10 private $fallbackResponse;
11 11 private $productCardHtml;
12 12 private $word_handler;
13 13 private $last_similarity_analysis = null;
14 - private $current_valid_urls = [];
15 - private $last_vectorstore_error = null;
16 - private $is_streaming = false; // ADDED: Track if current request is streaming
17 - private $streaming_headers_sent = false; // Track if streaming headers have been sent
18 14
19 -/**
20 - * Setup streaming headers - call this right before actually streaming
21 - * This delays header setup to allow actions/forms to return JSON responses
22 - */
23 -private function setup_streaming_headers() {
24 - if ($this->streaming_headers_sent || headers_sent()) {
25 - return false;
26 - }
27 15
28 - // Disable output buffering
29 - while (ob_get_level()) {
30 - ob_end_flush();
31 - }
32 -
33 - // Set headers for SSE
34 - header('Content-Type: text/event-stream');
35 - header('Cache-Control: no-cache');
36 - header('Connection: keep-alive');
37 - header('X-Accel-Buffering: no');
38 -
39 - ob_implicit_flush(true);
40 - flush();
41 -
42 - $this->streaming_headers_sent = true;
43 - return true;
44 -}
45 -
46 16 /**
47 17 * Class constructor
48 18 */
49 19 public function __construct() {
@@ -106,33 +76,16 @@
106 76 // Add to your existing constructor, in the section with other AJAX actions:
107 77 add_action('wp_ajax_mxchat_track_url_click', array($this, 'mxchat_track_url_click'));
108 78 add_action('wp_ajax_nopriv_mxchat_track_url_click', array($this, 'mxchat_track_url_click'));
109 79 add_action('wp_ajax_mxchat_track_originating_page', array($this, 'mxchat_track_originating_page'));
110 - add_action('wp_ajax_nopriv_mxchat_track_originating_page', array($this, 'mxchat_track_originating_page'));
111 - // Add chat mode checking actions
112 - add_action('wp_ajax_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
113 - add_action('wp_ajax_nopriv_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
80 +add_action('wp_ajax_nopriv_mxchat_track_originating_page', array($this, 'mxchat_track_originating_page'));
81 +
114 82
115 - // Nonce refresh for page-cache compatibility (WP Rocket, LiteSpeed, etc.)
116 - add_action('wp_ajax_mxchat_refresh_nonce', array($this, 'mxchat_refresh_nonce'));
117 - add_action('wp_ajax_nopriv_mxchat_refresh_nonce', array($this, 'mxchat_refresh_nonce'));
118 -
119 - // Auto-email transcript action
120 - add_action('mxchat_send_delayed_transcript', array($this, 'mxchat_send_delayed_transcript'), 10, 1);
121 -
122 83 add_filter('mxchat_check_actions_only', array($this, 'check_actions_for_addons'), 10, 4);
123 84
124 85
125 86 }
126 87
127 -/**
128 - * Return a fresh nonce so cached pages can replace the stale one.
129 - */
130 -public function mxchat_refresh_nonce() {
131 - nocache_headers();
132 - wp_send_json_success(array('nonce' => wp_create_nonce('mxchat_chat_nonce')));
133 -}
134 -
135 88 // In your core plugin's check_actions_for_addons method:
136 89 public function check_actions_for_addons($default, $message, $user_id, $session_id) {
137 90 //error_log('MxChat Core: check_actions_for_addons called with message: ' . $message);
138 91
@@ -155,22 +108,8 @@
155 108 wp_die();
156 109 }
157 110
158 111 $session_id = sanitize_text_field($_POST['session_id']);
159 -
160 - // SECURITY FIX: Verify session ownership before retrieving data
161 - // If IP/user changed, signal frontend to reset session instead of blocking
162 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
163 -
164 - // Check if this session has an owner recorded
165 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
166 -
167 - // Update session owner if it changed (e.g. IP changed due to network switch)
168 - // The session ID itself is the authentication — if the client has it, they own it
169 - if (!$session_owner || $session_owner !== $current_user_identifier) {
170 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
171 - }
172 -
173 112 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history
174 113 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai'); // Get current chat mode
175 114
176 115 if (empty($history)) {
@@ -187,25 +126,11 @@
187 126 'chat_mode' => $chat_mode
188 127 ]);
189 128 wp_die();
190 129 }
191 -private function mxchat_fetch_conversation_history_for_ai($session_id, $session_start_timestamp = 0) {
130 +
131 +private function mxchat_fetch_conversation_history_for_ai($session_id) {
192 132 $history = get_option("mxchat_history_{$session_id}", []);
193 -
194 - // Check persistence setting - when OFF, only include messages from current page load
195 - $options = get_option('mxchat_options', []);
196 - $persistence_enabled = isset($options['chat_persistence_toggle']) && $options['chat_persistence_toggle'] === 'on';
197 -
198 - // Filter history when persistence is OFF to match what the user sees
199 - if (!$persistence_enabled && $session_start_timestamp > 0) {
200 - $history = array_filter($history, function($entry) use ($session_start_timestamp) {
201 - // Include messages from this page load onwards
202 - return isset($entry['timestamp']) && $entry['timestamp'] >= $session_start_timestamp;
203 - });
204 - // Re-index array after filtering
205 - $history = array_values($history);
206 - }
207 -
208 133 $formatted_history = [];
209 134
210 135 // Adjusted for code-heavy conversations
211 136 $max_tokens = 120000; // Context window size
@@ -303,15 +228,8 @@
303 228 'callback' => [$this, 'handle_slack_messages'],
304 229 'permission_callback' => [$this, 'verify_slack_request'],
305 230 ]);
306 231
307 - // Telegram webhook endpoint
308 - register_rest_route('mxchat/v1', '/telegram-webhook', [
309 - 'methods' => 'POST',
310 - 'callback' => [$this, 'handle_telegram_webhook'],
311 - 'permission_callback' => [$this, 'verify_telegram_request'],
312 - ]);
313 -
314 232 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
315 233 }
316 234
317 235 /**
@@ -351,11 +269,10 @@
351 269 //error_log(esc_html__('Slack request timestamp too old', 'mxchat'));
352 270 return false;
353 271 }
354 272
355 - // Get raw request body from the WP_REST_Request object
356 - // (php://input may already be consumed by WordPress at this point)
357 - $request_body = $request->get_body();
273 + // Get raw request body
274 + $request_body = file_get_contents('php://input');
358 275
359 276 // Create the signature base string
360 277 $sig_basestring = "v0:{$timestamp}:{$request_body}";
361 278
@@ -364,43 +281,8 @@
364 281
365 282 // Compare signatures
366 283 return hash_equals($my_signature, $slack_signature);
367 284 }
368 -
369 -/**
370 - * Verify request is coming from Telegram.
371 - *
372 - * @param WP_REST_Request $request
373 - * @return bool True if valid, false otherwise.
374 - */
375 -public function verify_telegram_request($request) {
376 - $secret_token = $this->options['telegram_webhook_secret'] ?? '';
377 -
378 - //error_log('[MxChat Telegram DEBUG] verify_telegram_request called');
379 - //error_log('[MxChat Telegram DEBUG] Stored secret: ' . (empty($secret_token) ? 'EMPTY' : substr($secret_token, 0, 10) . '...'));
380 -
381 - if (empty($secret_token)) {
382 - // If no secret is configured, allow the request (for initial setup)
383 - //error_log('[MxChat Telegram DEBUG] No secret configured, allowing request');
384 - return true;
385 - }
386 -
387 - // Telegram sends the secret token in the X-Telegram-Bot-Api-Secret-Token header
388 - $request_token = $request->get_header('X-Telegram-Bot-Api-Secret-Token');
389 -
390 - //error_log('[MxChat Telegram DEBUG] Request token: ' . (empty($request_token) ? 'EMPTY' : substr($request_token, 0, 10) . '...'));
391 -
392 - if (empty($request_token)) {
393 - //error_log('[MxChat Telegram DEBUG] Request rejected: No token in header');
394 - return false;
395 - }
396 -
397 - // Timing-safe comparison
398 - $result = hash_equals($secret_token, $request_token);
399 - //error_log('[MxChat Telegram DEBUG] Token comparison result: ' . ($result ? 'MATCH' : 'MISMATCH'));
400 - return $result;
401 -}
402 -
403 285 public function mxchat_stream_events(WP_REST_Request $request) {
404 286 header('Content-Type: text/event-stream');
405 287 header('Cache-Control: no-cache');
406 288 header('Connection: keep-alive');
@@ -434,9 +316,9 @@
434 316
435 317
436 318
437 319
438 -private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null, $rag_context = null) {
320 +private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null) {
439 321 global $wpdb;
440 322 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
441 323 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
442 324
@@ -448,26 +330,14 @@
448 330 $session_id
449 331 ));
450 332 $is_new_session = ($existing_messages == 0);
451 333
452 - // Log for debugging
334 + // NEW: Log for debugging
453 335 if ($is_new_session) {
454 336 //error_log("[DEBUG] This is a NEW session - first message");
455 337 }
456 338 }
457 339
458 - // SECURITY FIX: Set session ownership for new sessions
459 - if ($is_new_session && $role === 'user') {
460 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
461 - $session_owner_key = "mxchat_session_owner_{$session_id}";
462 -
463 - // Only set ownership if not already set
464 - if (!get_option($session_owner_key)) {
465 - update_option($session_owner_key, $current_user_identifier, 'no');
466 - //error_log("[DEBUG] Set session ownership for {$session_id} to {$current_user_identifier}");
467 - }
468 - }
469 -
470 340 // 1) Extract agent name if present
471 341 $agent_name = '';
472 342 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
473 343 $agent_name = $matches[1];
@@ -499,9 +369,9 @@
499 369 $email_option_key = "mxchat_email_{$session_id}";
500 370 $saved_email = get_option($email_option_key);
501 371 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for email_option_key: {$email_option_key}, found: {$saved_email}");
502 372
503 - // Check for a saved name in wp_options
373 + // NEW: Check for a saved name in wp_options
504 374 $name_option_key = "mxchat_name_{$session_id}";
505 375 $saved_name = get_option($name_option_key);
506 376 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for name_option_key: {$name_option_key}, found: {$saved_name}");
507 377
@@ -544,9 +414,9 @@
544 414 $insert_data = [
545 415 'user_id' => $user_id,
546 416 'user_identifier'=> $user_identifier,
547 417 'user_email' => $saved_email ?: $user_email,
548 - 'user_name' => $saved_name ?: '', // Add name to insert data
418 + 'user_name' => $saved_name ?: '', // NEW: Add name to insert data
549 419 'session_id' => $session_id,
550 420 'role' => $role,
551 421 'message' => $message,
552 422 'timestamp' => current_time('mysql', 1),
@@ -610,17 +480,9 @@
610 480 $insert_data['originating_page_title'] = $stored_originating['title'] ?? '';
611 481 }
612 482 }
613 483 }
614 -
615 - // Add RAG context if provided (for bot messages)
616 - if ($rag_context !== null && $role === 'bot') {
617 - $rag_context_column_exists = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'rag_context'");
618 - if ($rag_context_column_exists) {
619 - $insert_data['rag_context'] = is_array($rag_context) ? wp_json_encode($rag_context) : $rag_context;
620 - }
621 - }
622 -
484 +
623 485 $wpdb->insert($table_name, $insert_data);
624 486 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
625 487
626 488 // 9) Send notification email if this is the first user message in a new session
@@ -631,17 +493,11 @@
631 493 'ip' => $_SERVER['REMOTE_ADDR']
632 494 ));
633 495 }
634 496
635 - // 10) Schedule delayed transcript email if enabled and message is from user
636 - if ($wpdb->insert_id && $role === 'user') {
637 - $this->schedule_delayed_transcript_email($session_id);
638 - }
639 -
640 497 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
641 498 return $message_id;
642 499 }
643 -
644 500 private function send_new_chat_notification($session_id, $user_info = array()) {
645 501 $options = get_option('mxchat_transcripts_options');
646 502
647 503 // Check if notifications are enabled
@@ -684,200 +540,12 @@
684 540 // Send email
685 541 return wp_mail($to, $subject, $message);
686 542 }
687 543
688 -/**
689 - * Schedule delayed transcript email for a session
690 - * Reschedules if a new user message is received
691 - */
692 -private function schedule_delayed_transcript_email($session_id) {
693 - $options = get_option('mxchat_transcripts_options');
694 -
695 - // Check if auto-email is enabled
696 - if (empty($options['mxchat_auto_email_transcript_enabled'])) {
697 - return;
698 - }
699 -
700 - // Get notification email
701 - $email = !empty($options['mxchat_notification_email']) ?
702 - $options['mxchat_notification_email'] :
703 - get_option('admin_email');
704 -
705 - if (!is_email($email)) {
706 - return;
707 - }
708 -
709 - // Get delay in minutes (default 30)
710 - $delay_minutes = isset($options['mxchat_auto_email_transcript_delay']) ?
711 - intval($options['mxchat_auto_email_transcript_delay']) : 30;
712 -
713 - // Clear any existing scheduled event for this session
714 - $hook = 'mxchat_send_delayed_transcript';
715 - $args = array($session_id);
716 - $timestamp = wp_next_scheduled($hook, $args);
717 -
718 - if ($timestamp) {
719 - wp_unschedule_event($timestamp, $hook, $args);
720 - }
721 -
722 - // Schedule new event
723 - $schedule_time = time() + ($delay_minutes * 60);
724 - wp_schedule_single_event($schedule_time, $hook, $args);
725 -}
726 -
727 -/**
728 - * Check if chat messages contain contact information (email or phone number)
729 - *
730 - * @param array $messages Array of message objects with 'message' property
731 - * @param object|null $session_data Session data object with user_email property
732 - * @return bool True if contact info found, false otherwise
733 - */
734 -private function chat_contains_contact_info($messages, $session_data = null) {
735 - // Check if session already has a stored email
736 - if ($session_data && !empty($session_data->user_email)) {
737 - return true;
738 - }
739 -
740 - // Email regex pattern
741 - $email_pattern = '/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/';
742 -
743 - // Phone number patterns (covers various formats including international, WhatsApp style)
744 - // Matches: +1234567890, (123) 456-7890, 123-456-7890, 123.456.7890, 1234567890, +1 234 567 8900, etc.
745 - $phone_pattern = '/(?:\+?\d{1,3}[-.\s]?)?\(?\d{2,4}\)?[-.\s]?\d{2,4}[-.\s]?\d{2,4}(?:[-.\s]?\d{1,4})?/';
746 -
747 - // Only check user messages (not assistant responses)
748 - foreach ($messages as $msg) {
749 - if ($msg->role !== 'user') {
750 - continue;
751 - }
752 -
753 - $message_text = $msg->message;
754 -
755 - // Check for email
756 - if (preg_match($email_pattern, $message_text)) {
757 - return true;
758 - }
759 -
760 - // Check for phone number (must be at least 7 digits total to avoid false positives)
761 - if (preg_match($phone_pattern, $message_text, $matches)) {
762 - // Count actual digits to avoid matching short numbers
763 - $digits_only = preg_replace('/\D/', '', $matches[0]);
764 - if (strlen($digits_only) >= 7) {
765 - return true;
766 - }
767 - }
768 - }
769 -
770 - return false;
771 -}
772 -
773 -/**
774 - * Send the delayed transcript email with .txt attachment
775 - */
776 -public function mxchat_send_delayed_transcript($session_id) {
777 - global $wpdb;
778 -
779 - $options = get_option('mxchat_transcripts_options');
780 -
781 - // Get notification email
782 - $to = !empty($options['mxchat_notification_email']) ?
783 - $options['mxchat_notification_email'] :
784 - get_option('admin_email');
785 -
786 - if (!is_email($to)) {
787 - return false;
788 - }
789 -
790 - // Get all messages for this session
791 - $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
792 - $messages = $wpdb->get_results($wpdb->prepare(
793 - "SELECT role, message, timestamp FROM {$table_name}
794 - WHERE session_id = %s
795 - ORDER BY timestamp ASC",
796 - $session_id
797 - ));
798 -
799 - if (empty($messages)) {
800 - return false;
801 - }
802 -
803 - // Get session metadata
804 - $sessions_table = $wpdb->prefix . 'mxchat_sessions';
805 - $session_data = $wpdb->get_row($wpdb->prepare(
806 - "SELECT * FROM {$sessions_table} WHERE session_id = %s",
807 - $session_id
808 - ));
809 -
810 - // Check if contact info is required and if it's present
811 - $require_contact = !empty($options['mxchat_auto_email_transcript_require_contact']);
812 - if ($require_contact && !$this->chat_contains_contact_info($messages, $session_data)) {
813 - // Contact info required but not found - skip sending
814 - return false;
815 - }
816 -
817 - // Build transcript content
818 - $transcript_content = "Chat Transcript\n";
819 - $transcript_content .= "================\n\n";
820 - $transcript_content .= "Session ID: " . $session_id . "\n";
821 -
822 - if ($session_data) {
823 - $transcript_content .= "User: " . ($session_data->user_identifier ?: 'Guest') . "\n";
824 - $transcript_content .= "Email: " . ($session_data->user_email ?: 'Not provided') . "\n";
825 - $transcript_content .= "Started: " . $session_data->created_at . "\n";
826 - }
827 -
828 - $transcript_content .= "\n" . str_repeat("=", 50) . "\n\n";
829 -
830 - // Add messages
831 - foreach ($messages as $msg) {
832 - $role_label = ($msg->role === 'user') ? 'User' : 'Assistant';
833 - $transcript_content .= "[{$msg->timestamp}] {$role_label}:\n";
834 - $transcript_content .= $msg->message . "\n\n";
835 - }
836 -
837 - // Create temporary file for attachment using WP_Filesystem
838 - $upload_dir = wp_upload_dir();
839 - $temp_file = $upload_dir['basedir'] . '/mxchat-transcript-' . $session_id . '.txt';
840 - global $wp_filesystem;
841 - if (empty($wp_filesystem)) {
842 - require_once ABSPATH . 'wp-admin/includes/file.php';
843 - WP_Filesystem();
844 - }
845 - $wp_filesystem->put_contents($temp_file, $transcript_content, FS_CHMOD_FILE);
846 -
847 - // Prepare email
848 - $subject = sprintf('[%s] Chat Transcript - Session %s', get_bloginfo('name'), substr($session_id, 0, 8));
849 -
850 - $message = "Please find attached the full chat transcript.\n\n";
851 - $message .= "Session ID: {$session_id}\n";
852 -
853 - if ($session_data) {
854 - $message .= "User: " . ($session_data->user_identifier ?: 'Guest') . "\n";
855 - $message .= "Email: " . ($session_data->user_email ?: 'Not provided') . "\n";
856 - }
857 -
858 - $message .= "\nView online: " . admin_url('admin.php?page=mxchat-transcripts');
859 -
860 - // Send email with attachment
861 - $attachments = array($temp_file);
862 - $result = wp_mail($to, $subject, $message, '', $attachments);
863 -
864 - // Clean up temporary file
865 - if (file_exists($temp_file)) {
866 - unlink($temp_file);
867 - }
868 -
869 - return $result;
870 -}
871 -
872 -
873 -
874 544 public function mxchat_handle_save_email_and_response() {
875 545 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
876 546 //error_log('DEBUG: POST data: ' . print_r($_POST, true));
877 547
878 - nocache_headers();
879 -
880 548 // Validate nonce
881 549 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
882 550 //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
883 551 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
@@ -889,15 +557,15 @@
889 557 $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
890 558
891 559 //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}, name: {$name}");
892 560
893 - if (empty($session_id) || $session_id === 'null' || empty($email)) {
561 + if (empty($session_id) || empty($email)) {
894 562 //error_log("[ERROR] Missing session_id or email: session_id={$session_id}, email={$email}");
895 563 wp_send_json_error(['message' => esc_html__('Session ID or email is missing.', 'mxchat')]);
896 564 wp_die();
897 565 }
898 566
899 - // Validate name if provided (check if name field is enabled and name is required)
567 + // NEW: Validate name if provided (check if name field is enabled and name is required)
900 568 $options = get_option('mxchat_options', []);
901 569 $name_field_enabled = isset($options['enable_name_field']) &&
902 570 ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
903 571
@@ -908,15 +576,15 @@
908 576 }
909 577
910 578 // 1) Always store email in wp_options
911 579 $email_option_key = "mxchat_email_{$session_id}";
912 - update_option($email_option_key, $email, 'no');
580 + update_option($email_option_key, $email);
913 581 //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$email_option_key} => {$email}");
914 582
915 - // Store name in wp_options if provided
583 + // NEW: Store name in wp_options if provided
916 584 if (!empty($name)) {
917 585 $name_option_key = "mxchat_name_{$session_id}";
918 - update_option($name_option_key, $name, 'no');
586 + update_option($name_option_key, $name);
919 587 //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$name_option_key} => {$name}");
920 588 }
921 589
922 590 // 2) (Optional) Also store in DB if a row already exists
@@ -929,9 +597,9 @@
929 597
930 598 //error_log("[DEBUG] handle_save_email_and_response -> session_count for {$session_id}: {$session_count} (SQL: {$sql})");
931 599
932 600 if ($session_count) {
933 - // Update both user_email and user_name if row(s) exist
601 + // NEW: Update both user_email and user_name if row(s) exist
934 602 if (!empty($name)) {
935 603 $update_sql = $wpdb->prepare(
936 604 "UPDATE {$table_name} SET user_email = %s, user_name = %s WHERE session_id = %s",
937 605 $email,
@@ -960,10 +628,8 @@
960 628
961 629 public function mxchat_check_email_provided() {
962 630 //error_log('[DEBUG] ---------- mxchat_check_email_provided START ----------');
963 631
964 - nocache_headers();
965 -
966 632 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
967 633 //error_log('[ERROR] Invalid nonce in mxchat_check_email_provided');
968 634 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
969 635 }
@@ -968,9 +634,9 @@
968 634 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
969 635 }
970 636
971 637 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
972 - if (empty($session_id) || $session_id === 'null') {
638 + if (empty($session_id)) {
973 639 //error_log('[ERROR] No session ID provided in mxchat_check_email_provided');
974 640 wp_send_json_error(['message' => esc_html__('No session ID provided', 'mxchat')]);
975 641 }
976 642
@@ -978,9 +644,9 @@
978 644 if (is_user_logged_in()) {
979 645 $current_user = wp_get_current_user();
980 646 //error_log("[DEBUG] User is logged in as {$current_user->user_email}");
981 647
982 - // Get user's display name for logged in users
648 + // NEW: Get user's display name for logged in users
983 649 $user_name = !empty($current_user->display_name) ? $current_user->display_name :
984 650 (!empty($current_user->first_name) ? $current_user->first_name : '');
985 651
986 652 $response_data = ['logged_in' => true, 'email' => $current_user->user_email];
@@ -990,9 +656,9 @@
990 656
991 657 wp_send_json_success($response_data);
992 658 }
993 659
994 - // Check if name field is required
660 + // NEW: Check if name field is required
995 661 $options = get_option('mxchat_options', []);
996 662 $name_field_enabled = isset($options['enable_name_field']) &&
997 663 ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
998 664
@@ -998,9 +664,9 @@
998 664
999 665 $email_option_key = "mxchat_email_{$session_id}";
1000 666 $stored_email = get_option($email_option_key, '');
1001 667
1002 - // Check for stored name
668 + // NEW: Check for stored name
1003 669 $name_option_key = "mxchat_name_{$session_id}";
1004 670 $stored_name = get_option($name_option_key, '');
1005 671
1006 672 //error_log("[DEBUG] mxchat_check_email_provided -> Checking email option: {$email_option_key}, found: {$stored_email}");
@@ -1005,9 +671,9 @@
1005 671
1006 672 //error_log("[DEBUG] mxchat_check_email_provided -> Checking email option: {$email_option_key}, found: {$stored_email}");
1007 673 //error_log("[DEBUG] mxchat_check_email_provided -> Checking name option: {$name_option_key}, found: {$stored_name}, required: " . ($name_field_enabled ? 'yes' : 'no'));
1008 674
1009 - // Check if we have email and name (if name is required)
675 + // NEW: Check if we have email and name (if name is required)
1010 676 $has_required_info = !empty($stored_email);
1011 677
1012 678 if ($name_field_enabled) {
1013 679 $has_required_info = $has_required_info && !empty($stored_name);
@@ -1027,59 +693,32 @@
1027 693 wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
1028 694 }
1029 695 }
1030 696
1031 -/**
1032 - * Send error response in appropriate format based on streaming mode
1033 - * ADDED: Helper method to consistently handle errors in both streaming and non-streaming modes
1034 - *
1035 - * @param string $error_message The error message to display
1036 - * @param string $error_code Optional error code for debugging
1037 - */
1038 -private function send_error_response($error_message, $error_code = 'api_error') {
1039 - if ($this->is_streaming) {
1040 - echo "data: " . json_encode([
1041 - 'error' => true,
1042 - 'error_message' => $error_message,
1043 - 'error_code' => $error_code,
1044 - 'text' => $error_message,
1045 - 'message' => $error_message
1046 - ]) . "\n\n";
1047 - echo "data: [DONE]\n\n";
1048 - flush();
1049 - } else {
1050 - wp_send_json_error([
1051 - 'error_message' => $error_message,
1052 - 'error_code' => $error_code
1053 - ]);
1054 - }
1055 - wp_die();
1056 -}
1057 -
1058 697 public function mxchat_handle_chat_request() {
1059 698 global $wpdb;
1060 699
1061 - // Debug: Log incoming bot_id
1062 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
1063 - //error_log("=== MXCHAT DEBUG: Starting chat request ===");
1064 - //error_log("MXCHAT DEBUG: Bot ID received: " . $bot_id);
700 + // NEW: Check if this is a streaming request
701 + $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat';
1065 702
1066 - // Get bot-specific options
1067 - $bot_options = $this->get_bot_options($bot_id);
1068 - $current_options = !empty($bot_options) ? $bot_options : $this->options;
703 + // NEW: Set streaming headers if needed
704 + if ($is_streaming) {
705 + // Disable output buffering
706 + while (ob_get_level()) {
707 + ob_end_flush(); // Changed from ob_end_clean()
708 + }
709 +
710 + // Set headers for SSE
711 + header('Content-Type: text/event-stream');
712 + header('Cache-Control: no-cache');
713 + header('Connection: keep-alive');
714 + header('X-Accel-Buffering: no');
715 +
716 + // Add these new lines:
717 + ob_implicit_flush(true);
718 + flush();
719 + }
1069 720
1070 - // Check if this is a streaming request
1071 - // Allow force_streaming_test parameter to bypass the setting check (for admin compatibility testing)
1072 - $force_streaming_test = isset($_POST['force_streaming_test']) && $_POST['force_streaming_test'] === '1' && current_user_can('administrator');
1073 - $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat' &&
1074 - ($force_streaming_test || (isset($current_options['enable_streaming_toggle']) && $current_options['enable_streaming_toggle'] === 'on'));
1075 -
1076 - // ADDED: Store streaming state in class property for use in private methods
1077 - $this->is_streaming = $is_streaming;
1078 -
1079 - // NOTE: Streaming headers are now set later via setup_streaming_headers()
1080 - // This allows actions/forms to return JSON responses without header conflicts
1081 -
1082 721 // Check if MX Chat Moderation is active
1083 722 if (class_exists('MX_Chat_Moderation')) {
1084 723 // Get user email and IP
1085 724 $user_email = '';
@@ -1144,31 +783,13 @@
1144 783
1145 784 // Rest of your existing code...
1146 785 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
1147 786
1148 - // Treat the literal strings 'null' / 'undefined' as missing too. Browser edge cases
1149 - // (Safari ITP, private mode, cross-origin iframes with partitioned storage) can cause
1150 - // the frontend FormData.append() to stringify a null session_id into the literal
1151 - // "null", which would otherwise pass empty() and pollute the transcripts table with
1152 - // ghost sessions that group every visitor's first message under one row.
1153 - if ($session_id === 'null' || $session_id === 'undefined') {
1154 - $session_id = '';
1155 - }
1156 -
1157 787 if (empty($session_id)) {
1158 788 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
1159 789 wp_die();
1160 790 }
1161 791
1162 - // Update session owner if it changed (e.g. IP changed due to network switch)
1163 - // The session ID itself is the authentication — if the client has it, they own it
1164 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
1165 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
1166 -
1167 - if (!$session_owner || $session_owner !== $current_user_identifier) {
1168 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
1169 - }
1170 -
1171 792 // Validate and sanitize the incoming message
1172 793 if (empty($_POST['message'])) {
1173 794 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
1174 795 wp_die();
@@ -1174,191 +795,209 @@
1174 795 wp_die();
1175 796 }
1176 797
1177 798
1178 - // Track originating page for first message in session
1179 - $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
799 + // NEW: Track originating page for first message in session
800 +$table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1180 801
1181 - // Check if originating page columns exist
1182 - $columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
802 +// Check if originating page columns exist
803 +$columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
1183 804
1184 - if ($columns_exist) {
1185 - // Check if this session already has messages
1186 - $message_count = $wpdb->get_var($wpdb->prepare(
1187 - "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
1188 - $session_id
1189 - ));
805 +if ($columns_exist) {
806 + // Check if this session already has messages
807 + $message_count = $wpdb->get_var($wpdb->prepare(
808 + "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
809 + $session_id
810 + ));
811 +
812 + // If this is the first message in the session
813 + if ($message_count == 0) {
814 + // Get originating page from JavaScript (preferred) or HTTP_REFERER (fallback)
815 + $originating_url = '';
816 + $originating_title = '';
1190 817
1191 - // If this is the first message in the session
1192 - if ($message_count == 0) {
1193 - // Get originating page from JavaScript (preferred) or HTTP_REFERER (fallback)
1194 - $originating_url = '';
1195 - $originating_title = '';
818 + // Try to get from POST data first (sent by JavaScript)
819 + if (isset($_POST['current_page_url'])) {
820 + $originating_url = esc_url_raw($_POST['current_page_url']);
821 + $originating_title = isset($_POST['current_page_title'])
822 + ? sanitize_text_field($_POST['current_page_title'])
823 + : '';
824 + }
825 + // Fallback to HTTP_REFERER if not provided by JavaScript
826 + else if (isset($_SERVER['HTTP_REFERER'])) {
827 + $originating_url = esc_url_raw($_SERVER['HTTP_REFERER']);
828 + }
829 +
830 + // Generate title if we have URL but no title
831 + if ($originating_url && empty($originating_title)) {
832 + $parsed_url = parse_url($originating_url);
833 + $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
1196 834
1197 - // Try to get from POST data first (sent by JavaScript)
1198 - if (isset($_POST['current_page_url'])) {
1199 - $originating_url = esc_url_raw($_POST['current_page_url']);
1200 - $originating_title = isset($_POST['current_page_title'])
1201 - ? sanitize_text_field($_POST['current_page_title'])
1202 - : '';
835 + if (empty($path) || $path === 'index.php' || $path === 'index.html') {
836 + $originating_title = 'Homepage';
837 + } else {
838 + // Clean up the path to make a readable title
839 + $originating_title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
840 + $originating_title = ucwords(trim($originating_title));
1203 841 }
1204 - // Fallback to HTTP_REFERER if not provided by JavaScript
1205 - else if (isset($_SERVER['HTTP_REFERER'])) {
1206 - $originating_url = esc_url_raw($_SERVER['HTTP_REFERER']);
1207 - }
1208 -
1209 - // Generate title if we have URL but no title
1210 - if ($originating_url && empty($originating_title)) {
1211 - $parsed_url = parse_url($originating_url);
1212 - $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
1213 -
1214 - if (empty($path) || $path === 'index.php' || $path === 'index.html') {
1215 - $originating_title = 'Homepage';
1216 - } else {
1217 - // Clean up the path to make a readable title
1218 - $originating_title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
1219 - $originating_title = ucwords(trim($originating_title));
1220 - }
1221 - }
1222 -
1223 - // Store for later use when saving the message
1224 - $this->pending_originating_page = [
1225 - 'url' => $originating_url,
1226 - 'title' => $originating_title
1227 - ];
1228 842 }
843 +
844 + // Store for later use when saving the message
845 + $this->pending_originating_page = [
846 + 'url' => $originating_url,
847 + 'title' => $originating_title
848 + ];
1229 849 }
850 +}
851 +
852 +
853 +
854 + // NEW: Get page context if provided
855 + $page_context = null;
856 + if (isset($_POST['page_context']) && !empty($_POST['page_context'])) {
857 + $page_context_raw = stripslashes($_POST['page_context']);
858 + $page_context = json_decode($page_context_raw, true);
1230 859
1231 -
1232 -
1233 - // Get page context if provided
1234 - $page_context = null;
1235 - if (isset($_POST['page_context']) && !empty($_POST['page_context'])) {
1236 - $page_context_raw = stripslashes($_POST['page_context']);
1237 - $page_context = json_decode($page_context_raw, true);
860 + // Validate page context structure
861 + if (is_array($page_context) &&
862 + isset($page_context['url']) &&
863 + isset($page_context['title']) &&
864 + isset($page_context['content'])) {
1238 865
1239 - // Validate page context structure
1240 - if (is_array($page_context) &&
1241 - isset($page_context['url']) &&
1242 - isset($page_context['title']) &&
1243 - isset($page_context['content'])) {
1244 -
1245 - // Sanitize page context
1246 - $page_context['url'] = esc_url_raw($page_context['url']);
1247 - $page_context['title'] = sanitize_text_field($page_context['title']);
1248 - $page_context['content'] = wp_kses_post($page_context['content']);
1249 - } else {
1250 - $page_context = null;
1251 - }
866 + // Sanitize page context
867 + $page_context['url'] = esc_url_raw($page_context['url']);
868 + $page_context['title'] = sanitize_text_field($page_context['title']);
869 + $page_context['content'] = wp_kses_post($page_context['content']);
870 + } else {
871 + $page_context = null;
1252 872 }
873 + }
1253 874
1254 - // Modify the message sanitization to preserve PHP tags in code blocks
1255 - $allowed_tags = [
1256 - 'pre' => [],
1257 - 'code' => ['class' => true],
1258 - 'span' => ['class' => true],
1259 - 'div' => ['class' => true],
1260 - ];
875 + // Modify the message sanitization to preserve PHP tags in code blocks
876 + $allowed_tags = [
877 + 'pre' => [],
878 + 'code' => ['class' => true],
879 + 'span' => ['class' => true],
880 + 'div' => ['class' => true],
881 + ];
1261 882
1262 - // First preserve code blocks
1263 - $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
1264 - return htmlspecialchars_decode($matches[0]);
1265 - }, $_POST['message']);
883 + // First preserve code blocks
884 + $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
885 + return htmlspecialchars_decode($matches[0]);
886 + }, $_POST['message']);
1266 887
1267 - // Then apply sanitization
1268 - $message = wp_kses($message, $allowed_tags);
888 + // Then apply sanitization
889 + $message = wp_kses($message, $allowed_tags);
1269 890
1270 - // Preserve code blocks from markdown conversion
1271 - $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
1272 - $message = apply_filters('mxchat_filter_message', $message, 'prompt', $session_id);
891 + // Preserve code blocks from markdown conversion
892 + $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
893 + $message = apply_filters('mxchat_filter_message', $message, 'prompt', $session_id);
1273 894
1274 - // ===== SIMPLIFIED TESTING PANEL INITIALIZATION =====
1275 - // Always initialize testing data for admins (no toggle needed)
1276 - $testing_data = null;
1277 - if (current_user_can('administrator')) {
1278 - // For vision messages, use the original user message for the query display
1279 - $query_for_testing = $message;
1280 - if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
1281 - $query_for_testing = sanitize_textarea_field($_POST['original_user_message']);
1282 - }
1283 -
1284 - $testing_data = [
1285 - 'query' => $query_for_testing,
1286 - 'timestamp' => time(),
1287 - 'top_matches' => [],
1288 - 'action_matches' => [], // Initialize action matches array
1289 - 'page_context' => $page_context, // Include page context in testing data
1290 - 'is_vision' => isset($_POST['vision_processed']) && $_POST['vision_processed'],
1291 - 'bot_id' => $bot_id // Include bot ID in testing data
1292 - ];
1293 -
1294 - // Get similarity threshold from bot options or default options
1295 - $similarity_threshold = isset($current_options['similarity_threshold'])
1296 - ? ((int) $current_options['similarity_threshold']) / 100
1297 - : 0.35;
1298 -
1299 - $testing_data['similarity_threshold'] = $similarity_threshold;
1300 -
1301 - // Determine knowledge base type using bot-specific config
1302 - $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
1303 - $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
1304 - $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
895 +// ===== SIMPLIFIED TESTING PANEL INITIALIZATION =====
896 + // Always initialize testing data for admins (no toggle needed)
897 + $testing_data = null;
898 + if (current_user_can('administrator')) {
899 + // For vision messages, use the original user message for the query display
900 + $query_for_testing = $message;
901 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
902 + $query_for_testing = sanitize_textarea_field($_POST['original_user_message']);
1305 903 }
1306 - // ===== END SIMPLIFIED TESTING INITIALIZATION =====
904 +
905 + $testing_data = [
906 + 'query' => $query_for_testing,
907 + 'timestamp' => time(),
908 + 'top_matches' => [],
909 + 'action_matches' => [], // NEW: Initialize action matches array
910 + 'page_context' => $page_context, // NEW: Include page context in testing data
911 + 'is_vision' => isset($_POST['vision_processed']) && $_POST['vision_processed']
912 + ];
913 +
914 + // Get similarity threshold
915 + $similarity_threshold = isset($this->options['similarity_threshold'])
916 + ? ((int) $this->options['similarity_threshold']) / 100
917 + : 0.75;
918 +
919 + $testing_data['similarity_threshold'] = $similarity_threshold;
920 +
921 + // Determine knowledge base type
922 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
923 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
924 + $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
925 + }
926 + // ===== END SIMPLIFIED TESTING INITIALIZATION =====
1307 927
1308 - // Add debug before and after:
1309 - //error_log('MxChat Core: About to call mxchat_pre_process_message filter with message: ' . $message);
1310 - $pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
1311 - //error_log('MxChat Core: Filter returned: ' . (is_array($pre_processed_result) ? 'array' : $pre_processed_result));
928 +// Add debug before and after:
929 +//error_log('MxChat Core: About to call mxchat_pre_process_message filter with message: ' . $message);
930 +$pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
931 +//error_log('MxChat Core: Filter returned: ' . (is_array($pre_processed_result) ? 'array' : $pre_processed_result));
1312 932
1313 933
1314 - // If the pre-processing returned a result (not the original message), use it directly
1315 - if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
1316 - // Save the AI response
1317 - $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
1318 -
1319 - // Save HTML content if provided
1320 - if (!empty($pre_processed_result['html'])) {
1321 - $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
1322 - }
1323 -
1324 - // Add testing data if admin
1325 - $response_data = [
1326 - 'text' => $pre_processed_result['text'],
1327 - 'html' => $pre_processed_result['html'] ?? '',
1328 - 'session_id' => $session_id
1329 - ];
1330 -
1331 - if ($testing_data !== null) {
1332 - $response_data['testing_data'] = $testing_data;
1333 - }
1334 -
1335 - wp_send_json($response_data);
1336 - wp_die();
934 + // If the pre-processing returned a result (not the original message), use it directly
935 + if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
936 + // Save the AI response
937 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
938 +
939 + // Save HTML content if provided
940 + if (!empty($pre_processed_result['html'])) {
941 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
1337 942 }
943 +
944 + // Add testing data if admin
945 + $response_data = [
946 + 'text' => $pre_processed_result['text'],
947 + 'html' => $pre_processed_result['html'] ?? '',
948 + 'session_id' => $session_id
949 + ];
950 +
951 + if ($testing_data !== null) {
952 + $response_data['testing_data'] = $testing_data;
953 + }
954 +
955 + wp_send_json($response_data);
956 + wp_die();
957 + }
1338 958
1339 - // Save the user's message - handle vision processed messages differently
1340 - if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
1341 - // For vision messages, save the original user message with image indicator
1342 - $original_message = sanitize_textarea_field($_POST['original_user_message']);
1343 - if (isset($_POST['vision_images_count']) && $_POST['vision_images_count'] > 0) {
1344 - $image_count = intval($_POST['vision_images_count']);
1345 - $original_message .= " [{$image_count} image(s)]";
1346 - }
1347 - $this->mxchat_save_chat_message($session_id, 'user', $original_message);
1348 - } else {
1349 - // Regular message - save as normal
1350 - $this->mxchat_save_chat_message($session_id, 'user', $message);
959 + // Save the user's message - handle vision processed messages differently
960 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
961 + // For vision messages, save the original user message with image indicator
962 + $original_message = sanitize_textarea_field($_POST['original_user_message']);
963 + if (isset($_POST['vision_images_count']) && $_POST['vision_images_count'] > 0) {
964 + $image_count = intval($_POST['vision_images_count']);
965 + $original_message .= " [{$image_count} image(s)]";
1351 966 }
967 + $this->mxchat_save_chat_message($session_id, 'user', $original_message);
968 + } else {
969 + // Regular message - save as normal
970 + $this->mxchat_save_chat_message($session_id, 'user', $message);
971 + }
1352 972
973 +
974 +if (is_email($message)) {
975 + // Add the email to Loops
976 + $this->add_email_to_loops($message);
1353 977
1354 - if (is_email($message)) {
1355 - // Add the email to Loops
1356 - $this->add_email_to_loops($message);
978 + // Get the user's success message instruction
979 + $user_success_message = $this->options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
980 +
981 + // Set instruction for AI using the user's success message
982 + $this->current_action_instruction = $user_success_message;
983 +
984 + // Clear the email capture transient since we got the email
985 + delete_transient('mxchat_email_capture_' . $user_id);
986 + }
987 +
988 + // NEW: Check if we're in an email capture flow but user hasn't provided email yet
989 + elseif (get_transient('mxchat_email_capture_' . $user_id)) {
990 + // Check if the message contains an email (not the whole message being an email)
991 + if (preg_match('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', $message, $matches)) {
992 + $extracted_email = $matches[0];
1357 993
1358 - // Get the user's success message instruction using current_options
1359 - $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
994 + // Add the extracted email to Loops
995 + $this->add_email_to_loops($extracted_email);
1360 996
997 + // Get the user's success message instruction
998 + $user_success_message = $this->options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
999 +
1361 1000 // Set instruction for AI using the user's success message
1362 1001 $this->current_action_instruction = $user_success_message;
1363 1002
1364 1003 // Clear the email capture transient since we got the email
@@ -1363,692 +1002,464 @@
1363 1002
1364 1003 // Clear the email capture transient since we got the email
1365 1004 delete_transient('mxchat_email_capture_' . $user_id);
1366 1005 }
1367 -
1368 - // Check if we're in an email capture flow but user hasn't provided email yet
1369 - elseif (get_transient('mxchat_email_capture_' . $user_id)) {
1370 - // Check if the message contains an email (not the whole message being an email)
1371 - if (preg_match('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', $message, $matches)) {
1372 - $extracted_email = $matches[0];
1373 -
1374 - // Add the extracted email to Loops
1375 - $this->add_email_to_loops($extracted_email);
1376 -
1377 - // Get the user's success message instruction using current_options
1378 - $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
1379 -
1380 - // Set instruction for AI using the user's success message
1381 - $this->current_action_instruction = $user_success_message;
1382 -
1383 - // Clear the email capture transient since we got the email
1384 - delete_transient('mxchat_email_capture_' . $user_id);
1385 - }
1386 - // If no email found but we're in capture mode, remind them
1387 - else {
1388 - // Get the original instruction to remind them using current_options
1389 - $original_instruction = $current_options['triggered_phrase_response'] ?? __("Please provide your email address.", 'mxchat');
1390 - $this->current_action_instruction = $original_instruction;
1391 - }
1006 + // If no email found but we're in capture mode, remind them
1007 + else {
1008 + // Get the original instruction to remind them
1009 + $original_instruction = $this->options['triggered_phrase_response'] ?? __("Please provide your email address.", 'mxchat');
1010 + $this->current_action_instruction = $original_instruction;
1392 1011 }
1012 + }
1393 1013
1394 - $intent_info = '';
1014 + $intent_info = '';
1395 1015
1396 - // Check chat mode
1397 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
1016 + // Check chat mode
1017 + $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
1398 1018
1399 - // Handle agent mode
1400 1019 // Handle agent mode
1401 - if ($chat_mode === 'agent') {
1402 - // First, check for switch intent before doing anything else
1403 - $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1020 +// Handle agent mode
1021 + if ($chat_mode === 'agent') {
1022 + // First, check for switch intent before doing anything else
1023 + $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1404 1024
1405 - // Capture action analysis for testing panel after intent check
1406 - if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1407 - $testing_data['action_matches'] = $this->last_action_analysis;
1025 + // NEW: Capture action analysis for testing panel after intent check
1026 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1027 + $testing_data['action_matches'] = $this->last_action_analysis;
1028 + }
1029 +
1030 + // If we matched an intent and it's the switch intent, handle it
1031 + if ($intent_matched && !empty($this->fallbackResponse['text'])) {
1032 + // Update chat mode first
1033 + update_option("mxchat_mode_{$session_id}", 'ai');
1034 +
1035 + // Clear any existing PDF context to start fresh
1036 + $this->clear_pdf_transients($session_id);
1037 +
1038 + // Prepare clean switch response
1039 + $response_data = [
1040 + 'text' => $this->fallbackResponse['text'],
1041 + 'html' => '',
1042 + 'session_id' => $session_id,
1043 + 'chat_mode' => 'ai'
1044 + ];
1045 +
1046 + if ($testing_data !== null) {
1047 + $response_data['testing_data'] = $testing_data;
1408 1048 }
1409 -
1410 - // Around line 506, in the agent mode handling section:
1411 - if ($intent_matched && !empty($this->fallbackResponse['text'])) {
1412 - // Update chat mode first
1413 - update_option("mxchat_mode_{$session_id}", 'ai');
1414 -
1415 - // Clear any existing PDF context to start fresh
1416 - $this->clear_pdf_transients($session_id);
1417 -
1418 - // Prepare clean switch response with explicit chat_mode
1419 - $response_data = [
1420 - 'text' => $this->fallbackResponse['text'],
1421 - 'html' => $this->fallbackResponse['html'] ?? '',
1422 - 'session_id' => $session_id,
1423 - 'chat_mode' => 'ai' // EXPLICITLY SET THIS
1049 +
1050 + // Save the mode switch message
1051 + $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
1052 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1053 +
1054 + // Send response and exit
1055 + wp_send_json($response_data);
1056 + wp_die();
1057 + } elseif (!$intent_matched) {
1058 + // No intent matched, handle live agent message
1059 + try {
1060 + $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
1061 +
1062 + $agent_response = [
1063 + 'status' => 'waiting_for_agent',
1064 + 'message' => esc_html__('Message sent to live agent.', 'mxchat')
1424 1065 ];
1425 -
1066 +
1426 1067 if ($testing_data !== null) {
1427 - $response_data['testing_data'] = $testing_data;
1068 + $agent_response['testing_data'] = $testing_data;
1428 1069 }
1429 -
1430 - // Save the mode switch message
1431 - $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
1432 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1433 -
1434 - // Send response and exit
1435 - wp_send_json($response_data);
1436 - wp_die();
1437 - } elseif (!$intent_matched) {
1438 - // No intent matched, handle live agent message
1439 - try {
1440 - $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
1441 1070
1442 - $agent_response = [
1443 - 'status' => 'waiting_for_agent',
1444 - 'message' => esc_html__('Message sent to live agent.', 'mxchat')
1445 - ];
1446 -
1447 - if ($testing_data !== null) {
1448 - $agent_response['testing_data'] = $testing_data;
1449 - }
1450 -
1451 - wp_send_json_success($agent_response);
1452 - } catch (\Exception $e) {
1453 - wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
1454 - }
1455 - wp_die();
1071 + wp_send_json_success($agent_response);
1072 + } catch (\Exception $e) {
1073 + wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
1456 1074 }
1075 + wp_die();
1457 1076 }
1077 + }
1458 1078
1459 - // Step 1: Check for new PDF URL in the message
1460 - if (!isset($_POST['vision_processed']) && preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1461 - $new_pdf_url = $matches[0];
1079 + // Step 1: Check for new PDF URL in the message
1080 + if (!isset($_POST['vision_processed']) && preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1081 + $new_pdf_url = $matches[0];
1462 1082
1463 - // Check if this is likely a PDF-related request
1464 - $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
1465 - $is_pdf_request = false;
1083 + // Check if this is likely a PDF-related request
1084 + $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
1085 + $is_pdf_request = false;
1466 1086
1467 - foreach ($pdf_keywords as $keyword) {
1468 - if (stripos($message, $keyword) !== false) {
1469 - $is_pdf_request = true;
1470 - break;
1471 - }
1087 + foreach ($pdf_keywords as $keyword) {
1088 + if (stripos($message, $keyword) !== false) {
1089 + $is_pdf_request = true;
1090 + break;
1472 1091 }
1092 + }
1473 1093
1474 - // If it looks like a PDF request or we're waiting for a PDF URL
1475 - if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
1476 - // Validate HTTPS
1477 - if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
1478 - // Extract filename from URL
1479 - $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1094 + // If it looks like a PDF request or we're waiting for a PDF URL
1095 + if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
1096 + // Validate HTTPS
1097 + if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
1098 + // Extract filename from URL
1099 + $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1480 1100
1481 - // Clear previous PDF transients
1482 - $this->clear_pdf_transients($session_id);
1101 + // Clear previous PDF transients
1102 + $this->clear_pdf_transients($session_id);
1483 1103
1484 - // Process new PDF using current_options
1485 - $max_pages = $current_options['pdf_max_pages'] ?? 69;
1486 - $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
1104 + // Process new PDF
1105 + $max_pages = $this->options['pdf_max_pages'] ?? 69;
1106 + $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
1487 1107
1488 - if ($embeddings === 'too_many_pages') {
1489 - $error_text = sprintf(
1490 - $current_options['pdf_intent_error_text'] ??
1491 - esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
1492 - $max_pages
1493 - );
1494 - $this->fallbackResponse['text'] = $error_text;
1495 - } elseif ($embeddings) {
1496 - // Store new PDF information
1497 - $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1108 + if ($embeddings === 'too_many_pages') {
1109 + $error_text = sprintf(
1110 + $this->options['pdf_intent_error_text'] ??
1111 + esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
1112 + $max_pages
1113 + );
1114 + $this->fallbackResponse['text'] = $error_text;
1115 + } elseif ($embeddings) {
1116 + // Store new PDF information
1117 + $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1498 1118
1499 - // If the filename is generic, create a more descriptive one
1500 - if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
1501 - strpos($pdf_filename, '.php') !== false) {
1502 - $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
1503 - }
1119 + // If the filename is generic, create a more descriptive one
1120 + if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
1121 + strpos($pdf_filename, '.php') !== false) {
1122 + $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
1123 + }
1504 1124
1505 - set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
1506 - set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
1507 - set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
1508 - set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
1125 + set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
1126 + set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
1127 + set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
1128 + set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
1509 1129
1510 - $success_text = $current_options['pdf_intent_success_text'] ??
1511 - esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
1130 + $success_text = $this->options['pdf_intent_success_text'] ??
1131 + esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
1512 1132
1513 - $pdf_response = [
1514 - 'success' => true,
1515 - 'message' => $success_text,
1516 - 'data' => [
1517 - 'filename' => $pdf_filename
1518 - ]
1519 - ];
1520 -
1521 - if ($testing_data !== null) {
1522 - $pdf_response['testing_data'] = $testing_data;
1523 - }
1524 -
1525 - wp_send_json($pdf_response);
1526 - wp_die();
1527 - } else {
1528 - $error_text = $current_options['pdf_intent_error_text'] ??
1529 - esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
1530 - $this->fallbackResponse['text'] = $error_text;
1531 - }
1532 -
1533 - $pdf_error_response = [
1534 - 'success' => false,
1535 - 'message' => $this->fallbackResponse['text']
1133 + $pdf_response = [
1134 + 'success' => true,
1135 + 'message' => $success_text,
1136 + 'data' => [
1137 + 'filename' => $pdf_filename
1138 + ]
1536 1139 ];
1537 1140
1538 1141 if ($testing_data !== null) {
1539 - $pdf_error_response['testing_data'] = $testing_data;
1142 + $pdf_response['testing_data'] = $testing_data;
1540 1143 }
1541 1144
1542 - wp_send_json($pdf_error_response);
1145 + wp_send_json($pdf_response);
1543 1146 wp_die();
1147 + } else {
1148 + $error_text = $this->options['pdf_intent_error_text'] ??
1149 + esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
1150 + $this->fallbackResponse['text'] = $error_text;
1544 1151 }
1545 - }
1546 - }
1547 1152
1548 -
1549 - // Step 2: Detect intent and handle intent-based responses
1550 - $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1551 -
1552 - // Capture action analysis for testing panel after intent check
1553 - if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1554 - $testing_data['action_matches'] = $this->last_action_analysis;
1555 - }
1556 -
1557 - // Step 3: Handle the intent result appropriately
1558 - if ($intent_result !== false) {
1559 - // Intent was matched - ALWAYS send as JSON response, never streaming
1560 -
1561 - if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
1562 - // Intent returned a direct response array
1563 - $response_data = [
1564 - 'text' => $intent_result['text'] ?? '',
1565 - 'html' => $intent_result['html'] ?? '',
1566 - 'session_id' => $session_id
1153 + $pdf_error_response = [
1154 + 'success' => false,
1155 + 'message' => $this->fallbackResponse['text']
1567 1156 ];
1568 -
1569 - // IMPORTANT: Include chat_mode if present (for WhatsApp, Slack, etc.)
1570 - if (isset($intent_result['chat_mode'])) {
1571 - $response_data['chat_mode'] = $intent_result['chat_mode'];
1572 - }
1573 -
1157 +
1574 1158 if ($testing_data !== null) {
1575 - $response_data['testing_data'] = $testing_data;
1159 + $pdf_error_response['testing_data'] = $testing_data;
1576 1160 }
1577 1161
1578 - wp_send_json($response_data);
1162 + wp_send_json($pdf_error_response);
1579 1163 wp_die();
1580 - } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1581 - // Intent returned true and set fallbackResponse
1582 -
1583 - // SAVE TO TRANSCRIPT
1584 - if (!empty($this->fallbackResponse['text'])) {
1585 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1586 - }
1587 - // Save action HTML (product cards, featured products, etc.) so it renders in transcripts
1588 - if (!empty($this->fallbackResponse['html'])) {
1589 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1590 - }
1591 -
1592 - $response_data = [
1593 - 'text' => $this->fallbackResponse['text'] ?? '',
1594 - 'html' => $this->fallbackResponse['html'] ?? '',
1595 - 'session_id' => $session_id
1596 - ];
1597 -
1598 - if (isset($this->fallbackResponse['chat_mode'])) {
1599 - $response_data['chat_mode'] = $this->fallbackResponse['chat_mode'];
1600 - }
1601 -
1602 - if ($testing_data !== null) {
1603 - $response_data['testing_data'] = $testing_data;
1604 - }
1605 -
1606 - wp_send_json($response_data);
1607 - wp_die();
1608 1164 }
1609 1165 }
1166 + }
1610 1167
1611 - // If we get here, no intent matched OR the intent didn't provide a usable response
1612 -
1613 - // Step 4: Generate AI response
1614 - // Get session start timestamp - when persistence is OFF, only include messages from this page load
1615 - $session_start_timestamp = isset($_POST['session_start_timestamp']) ? intval($_POST['session_start_timestamp']) : 0;
1616 - $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id, $session_start_timestamp);
1617 - $this->mxchat_increment_chat_count();
1168 + // Check if there's an active recommendation flow session
1169 + $flow_state = get_option("mxchat_sr_flow_state_{$session_id}", array());
1170 + if (!empty($flow_state) && isset($flow_state['flow_id'])) {
1171 + // Create a dummy intent object that matches the original intent
1172 + $dummy_intent = new stdClass();
1173 + $dummy_intent->intent_label = 'Recommendation Flow ' . $flow_state['flow_id'];
1174 + $dummy_intent->phrases = ''; // Empty phrases to avoid matching the original trigger
1618 1175
1619 - // Generate embedding for the user's query - USE BOT-SPECIFIC API KEY
1620 - $api_key = $current_options['api_key'] ?? $this->options['api_key'];
1621 - $user_message_embedding = $this->mxchat_generate_embedding($message, $api_key);
1176 + // Call the recommendation flow handler directly
1177 + $response_data = apply_filters('mxchat_sr_recommendation_flow', false, $message, $user_id, $session_id, $dummy_intent);
1622 1178
1623 - // Check if the embedding generation returned an error
1624 - if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1625 - $error_message = $user_message_embedding['error'];
1626 - $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
1627 -
1628 - // FIXED: Send error in appropriate format based on streaming mode
1629 - if ($is_streaming) {
1630 - echo "data: " . json_encode([
1631 - 'error' => true,
1632 - 'error_message' => $error_message,
1633 - 'error_code' => $error_code,
1634 - 'text' => $error_message,
1635 - 'message' => $error_message
1636 - ]) . "\n\n";
1637 - echo "data: [DONE]\n\n";
1638 - flush();
1639 - } else {
1640 - wp_send_json_error([
1641 - 'error_message' => $error_message,
1642 - 'error_code' => $error_code
1643 - ]);
1179 + // If the handler returned a response, send it
1180 + if (is_array($response_data) && (isset($response_data['text']) || isset($response_data['html']))) {
1181 + // Save the bot's response to the chat history
1182 + if (!empty($response_data['text'])) {
1183 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['text']);
1644 1184 }
1185 + if (!empty($response_data['html'])) {
1186 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['html']);
1187 + }
1188 +
1189 + if ($testing_data !== null) {
1190 + $response_data['testing_data'] = $testing_data;
1191 + }
1192 +
1193 + // Send the response
1194 + wp_send_json($response_data);
1645 1195 wp_die();
1646 1196 }
1197 + }
1647 1198
1648 - // Check if the embedding is valid
1649 - if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1650 - $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
1199 + // Step 2: Detect intent and handle intent-based responses
1200 + $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1651 1201
1652 - // FIXED: Send error in appropriate format based on streaming mode
1202 + // NEW: Capture action analysis for testing panel after intent check
1203 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1204 + $testing_data['action_matches'] = $this->last_action_analysis;
1205 + }
1206 +
1207 + // Step 3: Handle the intent result appropriately
1208 + if ($intent_result !== false) {
1209 + // Intent was matched - ALWAYS send as JSON response, never streaming
1210 +
1211 + if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
1212 + // Intent returned a direct response array
1213 + $response_data = [
1214 + 'text' => $intent_result['text'] ?? '',
1215 + 'html' => $intent_result['html'] ?? '',
1216 + 'session_id' => $session_id
1217 + ];
1218 +
1219 + if ($testing_data !== null) {
1220 + $response_data['testing_data'] = $testing_data;
1221 + }
1222 +
1223 + // Clear streaming headers if they were set
1653 1224 if ($is_streaming) {
1654 - echo "data: " . json_encode([
1655 - 'error' => true,
1656 - 'error_message' => $error_message,
1657 - 'error_code' => 'invalid_embedding',
1658 - 'text' => $error_message,
1659 - 'message' => $error_message
1660 - ]) . "\n\n";
1661 - echo "data: [DONE]\n\n";
1662 - flush();
1663 - } else {
1664 - wp_send_json_error([
1665 - 'error_message' => $error_message,
1666 - 'error_code' => 'invalid_embedding'
1667 - ]);
1225 + header_remove('Content-Type');
1226 + header_remove('Cache-Control');
1227 + header_remove('Connection');
1228 + header_remove('X-Accel-Buffering');
1229 + header('Content-Type: application/json');
1668 1230 }
1231 +
1232 + wp_send_json($response_data);
1669 1233 wp_die();
1670 - }
1671 -
1672 - // Build context with both knowledge base and PDF content if available
1673 - $context_content = "User asked: '{$message}'\n\n";
1674 -
1675 - // Add action instruction if present (add this right after the above line)
1676 - if (!empty($this->current_action_instruction)) {
1677 - $context_content .= "===== SPECIAL INSTRUCTION =====\n";
1678 - $context_content .= "IMPORTANT: " . $this->current_action_instruction . "\n";
1679 - $context_content .= "Respond naturally and conversationally while following this instruction.\n";
1680 - $context_content .= "===== END SPECIAL INSTRUCTION =====\n\n";
1234 + } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1235 + // Intent returned true and set fallbackResponse
1681 1236
1682 - // Clear the instruction after using it
1683 - $this->current_action_instruction = null;
1684 - }
1685 -
1686 -
1687 - // Add page context if available and contextual awareness is enabled using current_options
1688 - if ($page_context && isset($current_options['contextual_awareness_toggle']) && $current_options['contextual_awareness_toggle'] === 'on') {
1689 - $context_content .= "===== CURRENT PAGE CONTEXT =====\n";
1690 - $context_content .= "Page URL: " . $page_context['url'] . "\n";
1691 - $context_content .= "Page Title: " . $page_context['title'] . "\n";
1692 - $context_content .= "Page Content: " . $page_context['content'] . "\n";
1693 - $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1694 - }
1695 -
1696 - // Get relevant content from knowledge base - PASS BOT_ID and MESSAGE for Vector Store
1697 - $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding, $bot_id, $message);
1698 -
1699 - // NEW: Also extract URLs from system instructions (only if citation links enabled)
1700 - // Use fresh options to ensure we get the latest setting value
1701 - $fresh_options = get_option('mxchat_options', []);
1702 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
1703 -
1704 - $system_instructions = $this->get_system_instructions($bot_id, $session_id);
1705 - if ($citation_links_enabled && !empty($system_instructions)) {
1706 - preg_match_all(
1707 - '#\bhttps?://[^\s<>"\']+#i',
1708 - $system_instructions,
1709 - $system_instruction_urls
1710 - );
1711 -
1712 - if (!empty($system_instruction_urls[0])) {
1713 - // Merge with existing valid URLs
1714 - $this->current_valid_urls = array_merge(
1715 - $this->current_valid_urls,
1716 - $system_instruction_urls[0]
1717 - );
1718 - // Remove duplicates
1719 - $this->current_valid_urls = array_unique($this->current_valid_urls);
1720 -
1721 - //error_log("Added " . count($system_instruction_urls[0]) . " URLs from system instructions");
1237 + // SAVE TO TRANSCRIPT FIRST
1238 + if (!empty($this->fallbackResponse['text'])) {
1239 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1722 1240 }
1723 - }
1724 -
1725 -// ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1726 -if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1727 - // Update testing data with the REAL similarity analysis
1728 - $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1729 - $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1730 - $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1731 - $testing_data['sources_used'] = $this->last_similarity_analysis['sources_used'] ?? 0;
1732 - $testing_data['total_chunks_used'] = $this->last_similarity_analysis['total_chunks_used'] ?? 0;
1733 -}
1734 -// ===== END SIMILARITY DATA CAPTURE =====
1735 -
1736 -// NEW: Add valid URLs to testing data for admin panel display (AFTER similarity data)
1737 -if ($testing_data !== null && !empty($this->current_valid_urls)) {
1738 - $testing_data['approved_urls'] = array_values($this->current_valid_urls);
1739 - //error_log("Added " . count($this->current_valid_urls) . " approved URLs to testing data");
1740 -}
1741 -
1742 - if (!empty($relevant_content)) {
1743 - $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1744 - } else {
1745 - $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
1746 - }
1747 -
1748 - // NEW: Add approved URLs list to context for AI (only if citation links enabled)
1749 - if ($citation_links_enabled && !empty($this->current_valid_urls)) {
1750 - $context_content .= "===== APPROVED URLS FOR CITATIONS =====\n";
1751 - $context_content .= "You may ONLY use these exact URLs in your response:\n";
1752 - foreach ($this->current_valid_urls as $url) {
1753 - $context_content .= "- " . $url . "\n";
1241 + if (!empty($this->fallbackResponse['html'])) {
1242 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1754 1243 }
1755 - $context_content .= "\nCRITICAL: Do NOT create, modify, extend, or invent any other URLs. ";
1756 - $context_content .= "===== END APPROVED URLS =====\n\n";
1757 - }
1758 -
1759 - // Check for and include PDF content
1760 - $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
1761 - $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
1762 - $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
1763 - if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
1764 - $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
1765 - if (!empty($relevant_pdf_pages)) {
1766 - $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
1767 - foreach ($relevant_pdf_pages as $page_data) {
1768 - $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
1769 - }
1770 - $context_content .= "\n";
1244 +
1245 + $response_data = [
1246 + 'text' => $this->fallbackResponse['text'] ?? '',
1247 + 'html' => $this->fallbackResponse['html'] ?? '',
1248 + 'session_id' => $session_id
1249 + ];
1250 +
1251 + if ($testing_data !== null) {
1252 + $response_data['testing_data'] = $testing_data;
1771 1253 }
1772 - }
1773 -
1774 - // Check for and include Word content
1775 - $word_url = get_transient('mxchat_word_url_' . $session_id);
1776 - $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
1777 - $word_filename = get_transient('mxchat_word_filename_' . $session_id);
1778 - if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
1779 - $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
1780 - if (!empty($relevant_word_chunks)) {
1781 - $context_content .= "Relevant content from Word document '{$word_filename}':\n";
1782 - foreach ($relevant_word_chunks as $chunk_data) {
1783 - $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
1784 - }
1785 - $context_content .= "\n";
1254 +
1255 + // Clear streaming headers if they were set
1256 + if ($is_streaming) {
1257 + header_remove('Content-Type');
1258 + header_remove('Cache-Control');
1259 + header_remove('Connection');
1260 + header_remove('X-Accel-Buffering');
1261 + header('Content-Type: application/json');
1786 1262 }
1263 +
1264 + wp_send_json($response_data);
1265 + wp_die();
1787 1266 }
1788 -
1789 - $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
1267 + }
1790 1268
1791 - // Extract model from current options for bot-specific model support
1792 - $selected_model = isset($current_options['model']) ? $current_options['model'] : 'gpt-5.1-chat-latest';
1269 + // If we get here, no intent matched OR the intent didn't provide a usable response
1270 +
1271 + // Step 4: Generate AI response
1272 + $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
1273 + $this->mxchat_increment_chat_count();
1274 +
1275 + // Generate embedding for the user's query
1276 + $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1277 +
1278 + // Check if the embedding generation returned an error
1279 + if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1280 + $error_message = $user_message_embedding['error'];
1281 + $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
1793 1282
1794 - $response = $this->mxchat_generate_response(
1795 - $context_content,
1796 - $current_options['api_key'] ?? $this->options['api_key'],
1797 - $current_options['xai_api_key'] ?? $this->options['xai_api_key'],
1798 - $current_options['claude_api_key'] ?? $this->options['claude_api_key'],
1799 - $current_options['deepseek_api_key'] ?? $this->options['deepseek_api_key'],
1800 - $current_options['gemini_api_key'] ?? $this->options['gemini_api_key'],
1801 - $current_options['openrouter_api_key'] ?? $this->options['openrouter_api_key'],
1802 - $conversation_history,
1803 - $is_streaming,
1804 - $session_id,
1805 - $testing_data,
1806 - $selected_model
1807 - );
1808 -
1809 - // Handle streaming vs non-streaming responses
1810 - if ($is_streaming) {
1811 - // Check if streaming actually happened or if it fell back to regular response
1812 - if ($response === true) {
1813 - wp_die();
1814 - }
1815 - // If we get here, streaming fell back to regular response, continue
1816 - // But if there's an error, we need to send it as SSE format since headers are already set
1817 - if (is_array($response) && isset($response['error'])) {
1818 - $error_message = $response['error'];
1819 - $error_code = $response['error_code'] ?? 'api_error';
1820 - // Send error in SSE format that the client JS can handle
1821 - echo "data: " . json_encode([
1822 - 'error' => true,
1823 - 'error_message' => $error_message,
1824 - 'error_code' => $error_code,
1825 - 'text' => $error_message, // Also include as text for fallback handling
1826 - 'message' => $error_message
1827 - ]) . "\n\n";
1828 - echo "data: [DONE]\n\n";
1829 - flush();
1830 - wp_die();
1831 - }
1832 - }
1283 + wp_send_json_error([
1284 + 'error_message' => $error_message,
1285 + 'error_code' => $error_code
1286 + ]);
1287 + wp_die();
1288 + }
1289 +
1290 + // Check if the embedding is valid
1291 + if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1292 + wp_send_json_error([
1293 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1294 + 'error_code' => 'invalid_embedding'
1295 + ]);
1296 + wp_die();
1297 + }
1833 1298
1834 - // Check if the response is an error array (non-streaming mode)
1835 - if (is_array($response) && isset($response['error'])) {
1836 - wp_send_json_error([
1837 - 'error_message' => $response['error'],
1838 - 'error_code' => $response['error_code'] ?? 'api_error'
1839 - ]);
1840 - wp_die();
1841 - }
1299 + // Build context with both knowledge base and PDF content if available
1300 + $context_content = "User asked: '{$message}'\n\n";
1301 +
1302 + // NEW: Add action instruction if present (add this right after the above line)
1303 + if (!empty($this->current_action_instruction)) {
1304 + $context_content .= "===== SPECIAL INSTRUCTION =====\n";
1305 + $context_content .= "IMPORTANT: " . $this->current_action_instruction . "\n";
1306 + $context_content .= "Respond naturally and conversationally while following this instruction.\n";
1307 + $context_content .= "===== END SPECIAL INSTRUCTION =====\n\n";
1842 1308
1843 - // DEBUG: Check what we have
1844 - //error_log("=== BEFORE URL VALIDATION ===");
1845 - //error_log("current_valid_urls is empty? " . (empty($this->current_valid_urls) ? 'YES' : 'NO'));
1846 - //error_log("current_valid_urls count: " . count($this->current_valid_urls));
1847 - //error_log("current_valid_urls content: " . print_r($this->current_valid_urls, true));
1848 -
1849 - // If we get here, the response is valid text - now validate URLs
1850 - if (!empty($this->current_valid_urls)) {
1851 - //error_log("CALLING validate_and_clean_urls");
1852 - $response = $this->validate_and_clean_urls($response, $this->current_valid_urls);
1853 - } else {
1854 - //error_log("SKIPPING validation - current_valid_urls is empty");
1855 - }
1856 - // ===== END URL VALIDATION =====
1309 + // Clear the instruction after using it
1310 + $this->current_action_instruction = null;
1311 + }
1857 1312
1858 - // Prepare RAG context data for storage (only include documents used for context)
1859 - $rag_context_for_storage = null;
1860 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
1861 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
1862 1313
1863 - if ($has_rag_data || $has_action_data) {
1864 - $rag_context_for_storage = [];
1314 + // NEW: Add page context if available and contextual awareness is enabled
1315 + if ($page_context && isset($this->options['contextual_awareness_toggle']) && $this->options['contextual_awareness_toggle'] === 'on') {
1316 + $context_content .= "===== CURRENT PAGE CONTEXT =====\n";
1317 + $context_content .= "Page URL: " . $page_context['url'] . "\n";
1318 + $context_content .= "Page Title: " . $page_context['title'] . "\n";
1319 + $context_content .= "Page Content: " . $page_context['content'] . "\n";
1320 + $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1321 + }
1865 1322
1866 - // Add RAG/source data if available
1867 - if ($has_rag_data) {
1868 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
1869 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
1870 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
1871 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
1872 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1873 - $rag_context_for_storage['sources_used'] = $this->last_similarity_analysis['sources_used'] ?? 0;
1874 - $rag_context_for_storage['total_chunks_used'] = $this->last_similarity_analysis['total_chunks_used'] ?? 0;
1875 - }
1323 + // Get relevant content from knowledge base - THIS IS WHERE THE SIMILARITY ANALYSIS HAPPENS
1324 + $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
1325 +
1326 + // ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1327 + if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1328 + // Update testing data with the REAL similarity analysis
1329 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1330 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1331 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1332 + }
1333 + // ===== END SIMILARITY DATA CAPTURE =====
1334 +
1335 + if (!empty($relevant_content)) {
1336 + $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1337 + } else {
1338 + $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
1339 + }
1876 1340
1877 - // Add action analysis data if available
1878 - if ($has_action_data) {
1879 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
1341 + // Check for and include PDF content
1342 + $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
1343 + $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
1344 + $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
1345 + if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
1346 + $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
1347 + if (!empty($relevant_pdf_pages)) {
1348 + $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
1349 + foreach ($relevant_pdf_pages as $page_data) {
1350 + $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
1880 1351 }
1352 + $context_content .= "\n";
1881 1353 }
1354 + }
1882 1355
1883 - // Save the cleaned response with RAG context
1884 - $this->mxchat_save_chat_message($session_id, 'bot', $response, null, $rag_context_for_storage);
1885 -
1886 - // Step 5: Save additional content if available
1887 - if (!empty($this->productCardHtml)) {
1888 - $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
1356 + // Check for and include Word content
1357 + $word_url = get_transient('mxchat_word_url_' . $session_id);
1358 + $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
1359 + $word_filename = get_transient('mxchat_word_filename_' . $session_id);
1360 + if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
1361 + $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
1362 + if (!empty($relevant_word_chunks)) {
1363 + $context_content .= "Relevant content from Word document '{$word_filename}':\n";
1364 + foreach ($relevant_word_chunks as $chunk_data) {
1365 + $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
1366 + }
1367 + $context_content .= "\n";
1889 1368 }
1369 + }
1370 +
1371 + $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
1890 1372
1891 - if (!empty($this->fallbackResponse['html'])) {
1892 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1373 + // Generate response
1374 + $response = $this->mxchat_generate_response(
1375 + $context_content,
1376 + $this->options['api_key'],
1377 + $this->options['xai_api_key'],
1378 + $this->options['claude_api_key'],
1379 + $this->options['deepseek_api_key'],
1380 + $this->options['gemini_api_key'],
1381 + $conversation_history,
1382 + $is_streaming,
1383 + $session_id,
1384 + $testing_data
1385 + );
1386 +
1387 + // Handle streaming vs non-streaming responses
1388 + if ($is_streaming) {
1389 + // Check if streaming actually happened or if it fell back to regular response
1390 + if ($response === true) {
1391 + wp_die();
1893 1392 }
1894 -
1895 - // Step 6: Return the response
1896 - // DEBUG: Check if newlines exist in the response
1897 - //error_log("=== MXCHAT NON-STREAMING RESPONSE DEBUG ===");
1898 - //error_log("Response has newlines: " . (strpos($response, "\n") !== false ? 'YES' : 'NO'));
1899 - //error_log("Response first 500 chars: " . substr($response, 0, 500));
1900 -
1901 - $response_data = [
1902 - 'text' => $response,
1903 - 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1904 - 'session_id' => $session_id
1905 - ];
1906 -
1907 - // Include vectorstore error info for admin debugging (only visible to admins via testing_data)
1908 - if (!empty($this->last_vectorstore_error) && $testing_data !== null) {
1909 - $testing_data['vectorstore_error'] = $this->last_vectorstore_error;
1910 - }
1911 -
1912 - // Also pass it as a top-level field so JS can show a better error message to admins
1913 - if (!empty($this->last_vectorstore_error) && current_user_can('manage_options')) {
1914 - $response_data['vectorstore_error'] = $this->last_vectorstore_error;
1915 - }
1916 -
1917 - // Always add testing data for admins (no toggle needed)
1918 - if ($testing_data !== null) {
1919 - $response_data['testing_data'] = $testing_data;
1920 - }
1921 -
1922 - wp_send_json($response_data);
1393 + // If we get here, streaming fell back to regular response, continue
1394 + }
1395 +
1396 + // Check if the response is an error array
1397 + if (is_array($response) && isset($response['error'])) {
1398 + wp_send_json_error([
1399 + 'error_message' => $response['error'],
1400 + 'error_code' => $response['error_code'] ?? 'api_error'
1401 + ]);
1923 1402 wp_die();
1924 -}
1925 -
1926 -/**
1927 - * Get bot-specific options for multi-bot functionality
1928 - * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
1929 - */
1930 -// Also debug the bot options retrieval
1931 -private function get_bot_options($bot_id = 'default') {
1932 - //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
1933 -
1934 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1935 - //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
1936 - return array();
1937 1403 }
1938 1404
1939 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
1940 -
1941 - if (!empty($bot_options)) {
1942 - //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
1943 - if (isset($bot_options['similarity_threshold'])) {
1944 - //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
1945 - }
1405 + // If we get here, the response is valid text
1406 + $this->mxchat_save_chat_message($session_id, 'bot', $response);
1407 +
1408 + // Step 5: Save additional content if available
1409 + if (!empty($this->productCardHtml)) {
1410 + $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
1946 1411 }
1947 -
1948 - return is_array($bot_options) ? $bot_options : array();
1949 -}
1950 1412
1951 -/**
1952 - * Get bot-specific Pinecone configuration
1953 - * Used in the knowledge retrieval functions
1954 - */
1955 -// Also add debugging to your get_bot_pinecone_config function
1956 -private function get_bot_pinecone_config($bot_id = 'default') {
1957 - //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
1958 -
1959 - // If default bot or multi-bot add-on not active, use default Pinecone config
1960 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1961 - //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
1962 - $addon_options = get_option('mxchat_pinecone_addon_options', array());
1963 - $config = array(
1964 - 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
1965 - 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
1966 - 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
1967 - 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
1968 - );
1969 - //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
1970 - return $config;
1413 + if (!empty($this->fallbackResponse['html'])) {
1414 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1971 1415 }
1972 -
1973 - //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
1974 -
1975 - // Hook for multi-bot add-on to provide bot-specific Pinecone config
1976 - $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
1977 -
1978 - if (!empty($bot_pinecone_config)) {
1979 - //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
1980 - //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
1981 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
1982 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
1983 - } else {
1984 - //error_log("MXCHAT DEBUG: Filter returned empty config!");
1416 +
1417 + // Step 6: Return the response
1418 + $response_data = [
1419 + 'text' => $response,
1420 + 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1421 + 'session_id' => $session_id
1422 + ];
1423 +
1424 + // Always add testing data for admins (no toggle needed)
1425 + if ($testing_data !== null) {
1426 + $response_data['testing_data'] = $testing_data;
1985 1427 }
1986 -
1987 - return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
1428 +
1429 + wp_send_json($response_data);
1430 + wp_die();
1988 1431 }
1989 1432
1990 -
1991 1433 // Updated function to check intents and invoke the callback function
1992 1434 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
1993 1435 global $wpdb;
1994 1436 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
1995 1437
1996 - // Get the current bot_id
1997 - $current_bot_id = $this->get_current_bot_id($session_id);
1998 -
1999 1438 // Generate the user embedding
2000 1439 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
2001 -
1440 +
2002 1441 // Check if embedding generation returned an error
2003 1442 if (is_array($user_embedding) && isset($user_embedding['error'])) {
2004 1443 $error_message = $user_embedding['error'];
2005 1444 $error_code = $user_embedding['error_code'] ?? 'embedding_error';
2006 -
2007 - // FIXED: Send error in appropriate format based on streaming mode
2008 - if ($this->is_streaming) {
2009 - echo "data: " . json_encode([
2010 - 'error' => true,
2011 - 'error_message' => $error_message,
2012 - 'error_code' => $error_code,
2013 - 'text' => $error_message,
2014 - 'message' => $error_message
2015 - ]) . "\n\n";
2016 - echo "data: [DONE]\n\n";
2017 - flush();
2018 - } else {
2019 - wp_send_json_error([
2020 - 'error_message' => $error_message,
2021 - 'error_code' => $error_code
2022 - ]);
2023 - }
1445 +
1446 + wp_send_json_error([
1447 + 'error_message' => $error_message,
1448 + 'error_code' => $error_code
1449 + ]);
2024 1450 wp_die();
2025 1451 }
2026 -
1452 +
2027 1453 // Check if embedding is valid
2028 1454 if (!is_array($user_embedding) || empty($user_embedding)) {
2029 - $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
2030 -
2031 - // FIXED: Send error in appropriate format based on streaming mode
2032 - if ($this->is_streaming) {
2033 - echo "data: " . json_encode([
2034 - 'error' => true,
2035 - 'error_message' => $error_message,
2036 - 'error_code' => 'invalid_embedding',
2037 - 'text' => $error_message,
2038 - 'message' => $error_message
2039 - ]) . "\n\n";
2040 - echo "data: [DONE]\n\n";
2041 - flush();
2042 - } else {
2043 - wp_send_json_error([
2044 - 'error_message' => $error_message,
2045 - 'error_code' => 'invalid_embedding'
2046 - ]);
2047 - }
1455 + wp_send_json_error([
1456 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1457 + 'error_code' => 'invalid_embedding'
1458 + ]);
2048 1459 wp_die();
2049 1460 }
2050 -
1461 +
2051 1462 // Fetch intents from the database
2052 1463 $table_name = $wpdb->prefix . 'mxchat_intents';
2053 1464 if ($chat_mode === 'agent') {
2054 1465 $query = $wpdb->prepare(
@@ -2058,29 +1469,19 @@
2058 1469 $intents = $wpdb->get_results($query);
2059 1470 } else {
2060 1471 $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
2061 1472 }
2062 -
1473 +
2063 1474 if (empty($intents)) {
2064 1475 return false;
2065 1476 }
2066 -
2067 - // Prefetch individual phrase vectors from wp_mxchat_intent_phrases (grouped by intent_id)
2068 - $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
2069 - $phrases_by_intent = [];
2070 - if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
2071 - $all_phrases = $wpdb->get_results("SELECT intent_id, phrase, embedding_vector FROM $phrases_table");
2072 - foreach ($all_phrases as $p) {
2073 - $phrases_by_intent[$p->intent_id][] = $p;
2074 - }
2075 - }
2076 -
1477 +
2077 1478 $highest_similarity = -INF;
2078 1479 $matched_intent = null;
2079 -
2080 - // Array to store action analysis for testing panel
1480 +
1481 + // NEW: Array to store action analysis for testing panel
2081 1482 $action_analysis = [];
2082 -
1483 +
2083 1484 foreach ($intents as $intent) {
2084 1485 // Additional check for enabled state
2085 1486 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
2086 1487 if (!$is_enabled) {
@@ -2085,57 +1486,22 @@
2085 1486 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
2086 1487 if (!$is_enabled) {
2087 1488 continue;
2088 1489 }
2089 -
2090 - // Check if this action is enabled for the current bot
2091 - if (!$this->is_action_enabled_for_bot($intent, $current_bot_id)) {
2092 - continue;
2093 - }
2094 -
2095 - $best_similarity = -INF;
2096 - $matched_phrase_text = '';
2097 -
2098 - // Check legacy embedding vector (existing behavior)
1490 +
2099 1491 $intent_embedding_serialized = $intent->embedding_vector;
2100 1492 $intent_embedding = $intent_embedding_serialized
2101 1493 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
2102 1494 : null;
2103 -
2104 - if (is_array($intent_embedding) && !empty($intent_embedding)) {
2105 - $legacy_similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
2106 - if ($legacy_similarity > $best_similarity) {
2107 - $best_similarity = $legacy_similarity;
2108 - $matched_phrase_text = 'legacy';
2109 - }
2110 - }
2111 -
2112 - // Check individual phrase vectors
2113 - if (isset($phrases_by_intent[$intent->id])) {
2114 - foreach ($phrases_by_intent[$intent->id] as $phrase_row) {
2115 - $phrase_embedding = $phrase_row->embedding_vector
2116 - ? unserialize($phrase_row->embedding_vector, ['allowed_classes' => false])
2117 - : null;
2118 - if (!is_array($phrase_embedding)) {
2119 - continue;
2120 - }
2121 - $phrase_similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $phrase_embedding);
2122 - if ($phrase_similarity > $best_similarity) {
2123 - $best_similarity = $phrase_similarity;
2124 - $matched_phrase_text = $phrase_row->phrase;
2125 - }
2126 - }
2127 - }
2128 -
2129 - // Skip if no valid embedding was found at all
2130 - if ($best_similarity === -INF) {
1495 +
1496 + if (!is_array($intent_embedding)) {
2131 1497 continue;
2132 1498 }
2133 -
2134 - $similarity = $best_similarity;
1499 +
1500 + $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
2135 1501 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
2136 -
2137 - // Store action analysis data for testing panel
1502 +
1503 + // NEW: Store action analysis data for testing panel
2138 1504 $action_analysis[] = [
2139 1505 'intent_label' => $intent->intent_label,
2140 1506 'callback_function' => $intent->callback_function,
2141 1507 'similarity' => round($similarity, 4),
@@ -2142,12 +1508,11 @@
2142 1508 'similarity_percentage' => round($similarity * 100, 2),
2143 1509 'threshold' => $intent_threshold,
2144 1510 'threshold_percentage' => round($intent_threshold * 100, 2),
2145 1511 'above_threshold' => $similarity >= $intent_threshold,
2146 - 'matched_phrase' => $matched_phrase_text,
2147 1512 'triggered' => false // Will be updated below if this intent is triggered
2148 1513 ];
2149 -
1514 +
2150 1515 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
2151 1516 $highest_similarity = $similarity;
2152 1517 $matched_intent = $intent;
2153 1518 }
@@ -2152,9 +1517,9 @@
2152 1517 $matched_intent = $intent;
2153 1518 }
2154 1519 }
2155 1520
2156 - // Mark the triggered action if any
1521 + // NEW: Mark the triggered action if any
2157 1522 if ($matched_intent) {
2158 1523 foreach ($action_analysis as &$action) {
2159 1524 if ($action['intent_label'] === $matched_intent->intent_label) {
2160 1525 $action['triggered'] = true;
@@ -2162,9 +1527,9 @@
2162 1527 }
2163 1528 }
2164 1529 }
2165 1530
2166 - // Sort actions by similarity (highest first) and store for testing panel
1531 + // NEW: Sort actions by similarity (highest first) and store for testing panel
2167 1532 usort($action_analysis, function($a, $b) {
2168 1533 return $b['similarity'] <=> $a['similarity'];
2169 1534 });
2170 1535
@@ -2170,9 +1535,8 @@
2170 1535
2171 1536 // Store action analysis for testing panel capture
2172 1537 $this->last_action_analysis = $action_analysis;
2173 1538
2174 - // Around line 715 in your mxchat_check_intent_and_invoke_callback function
2175 1539 if ($matched_intent) {
2176 1540 // If the callback is a method on this instance (core callback), call it directly
2177 1541 if (method_exists($this, $matched_intent->callback_function)) {
2178 1542 $callback_result = call_user_func(
@@ -2186,9 +1550,9 @@
2186 1550 } else {
2187 1551 // Otherwise, use apply_filters for add-on callbacks
2188 1552 $callback_result = apply_filters(
2189 1553 $matched_intent->callback_function,
2190 - false,
1554 + false, // default return value
2191 1555 $message,
2192 1556 $user_id,
2193 1557 $session_id,
2194 1558 $matched_intent
@@ -2194,18 +1558,11 @@
2194 1558 $matched_intent
2195 1559 );
2196 1560 }
2197 1561
2198 - // Handle the callback result properly
2199 1562 if ($callback_result !== false) {
2200 - // If callback returned an array with chat_mode, use it directly
2201 - if (is_array($callback_result) && isset($callback_result['chat_mode'])) {
2202 - $this->fallbackResponse = $callback_result;
2203 - return $callback_result; // Return the full array
2204 - } else {
2205 - $this->fallbackResponse = $callback_result;
2206 - return true;
2207 - }
1563 + $this->fallbackResponse = $callback_result;
1564 + return true;
2208 1565 }
2209 1566 }
2210 1567
2211 1568 return false;
@@ -2210,34 +1567,8 @@
2210 1567
2211 1568 return false;
2212 1569 }
2213 1570
2214 -/**
2215 - * Check if an action is enabled for a specific bot
2216 - */
2217 -private function is_action_enabled_for_bot($intent, $bot_id) {
2218 - // If enabled_bots column doesn't exist or is null, assume it's enabled for all bots (backward compatibility)
2219 - if (!isset($intent->enabled_bots) || empty($intent->enabled_bots)) {
2220 - return true;
2221 - }
2222 -
2223 - $enabled_bots = json_decode($intent->enabled_bots, true);
2224 -
2225 - // If JSON decode fails or returns empty array, assume enabled for all (backward compatibility)
2226 - if (!is_array($enabled_bots) || empty($enabled_bots)) {
2227 - return true;
2228 - }
2229 -
2230 - // Admin testing tab uses bot_id "testing" — treat it as "default" so all
2231 - // default-bot actions are testable from the admin panel
2232 - if ($bot_id === 'testing') {
2233 - $bot_id = 'default';
2234 - }
2235 -
2236 - // Check if the current bot is in the enabled bots list
2237 - return in_array($bot_id, $enabled_bots);
2238 -}
2239 -
2240 1571 // Helper function to clear PDF and Word document related transients
2241 1572 private function clear_pdf_transients($session_id) {
2242 1573 // PDF transients
2243 1574 delete_transient('mxchat_pdf_url_' . $session_id);
@@ -2271,23 +1602,18 @@
2271 1602 }
2272 1603
2273 1604 public function mxchat_generate_image($message, $user_id, $session_id) {
2274 1605 //error_log("Starting image generation for message: " . $message);
2275 -
2276 - // Prepare a prompt for OpenAI image generation
1606 +
1607 + // Prepare a prompt for DALL-E
2277 1608 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2278 -
2279 - // Opt-in routing: when 'custom_provider_for_images' is on, route image gen
2280 - // through the configured Custom (OpenAI-compatible) /images/generations route.
2281 - if (!empty($this->options['custom_provider_for_images']) && $this->options['custom_provider_for_images'] === 'on') {
2282 - $image_response = $this->mxchat_generate_custom_image($prompt);
2283 - } else {
2284 - // Use the existing OpenAI API key
2285 - $openai_api_key = sanitize_text_field($this->options['api_key']);
2286 - // Call OpenAI GPT Image to generate an image
2287 - $image_response = $this->mxchat_generate_openai_image($prompt, $openai_api_key);
2288 - }
2289 1609
1610 + // Use the existing OpenAI API key
1611 + $openai_api_key = sanitize_text_field($this->options['api_key']);
1612 +
1613 + // Call DALL-E to generate an image
1614 + $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1615 +
2290 1616 // Check if the response contains an image URL
2291 1617 if (isset($image_response['imageUrl'])) {
2292 1618 $image_url = esc_url_raw($image_response['imageUrl']);
2293 1619
@@ -2330,103 +1656,24 @@
2330 1656 // Return the response directly instead of relying on the property
2331 1657 return $this->fallbackResponse;
2332 1658 }
2333 1659 }
2334 -
2335 -public function mxchat_generate_gemini_image($message, $user_id, $session_id) {
2336 - $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2337 -
2338 - $gemini_api_key = sanitize_text_field($this->options['gemini_api_key'] ?? '');
2339 - if (empty($gemini_api_key)) {
2340 - $response_text = esc_html__("Gemini API key is not configured.", 'mxchat');
2341 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2342 - return ['text' => $response_text, 'html' => '', 'images' => []];
2343 - }
2344 -
2345 - $image_response = $this->mxchat_generate_imagen_image($prompt, $gemini_api_key);
2346 -
2347 - if (isset($image_response['imageUrl'])) {
2348 - $image_url = esc_url_raw($image_response['imageUrl']);
2349 -
2350 - $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
2351 - $response_text = esc_html__('Here is the image I generated:', 'mxchat');
2352 -
2353 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2354 - $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
2355 -
2356 - $this->fallbackResponse = [
2357 - 'text' => $response_text,
2358 - 'html' => $response_html,
2359 - 'images' => [$image_url]
2360 - ];
2361 -
2362 - return $this->fallbackResponse;
2363 - } else {
2364 - $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
2365 -
2366 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2367 -
2368 - $this->fallbackResponse = [
2369 - 'text' => $response_text,
2370 - 'html' => '',
2371 - 'images' => []
2372 - ];
2373 -
2374 - return $this->fallbackResponse;
2375 - }
2376 -}
2377 -
2378 -private function mxchat_save_generated_image($base64_data, $mime_type = 'image/png', $prefix = 'mxchat-generated') {
2379 - $extension = ($mime_type === 'image/jpeg') ? 'jpg' : 'png';
2380 - $filename = sanitize_file_name($prefix . '-' . wp_generate_uuid4() . '.' . $extension);
2381 - $decoded = base64_decode($base64_data);
2382 -
2383 - if ($decoded === false) {
2384 - return new \WP_Error('decode_failed', esc_html__('Failed to decode image data.', 'mxchat'));
2385 - }
2386 -
2387 - $upload = wp_upload_bits($filename, null, $decoded);
2388 -
2389 - if (!empty($upload['error'])) {
2390 - return new \WP_Error('upload_failed', $upload['error']);
2391 - }
2392 -
2393 - $attach_id = wp_insert_attachment([
2394 - 'post_mime_type' => $mime_type,
2395 - 'post_title' => $prefix,
2396 - 'post_content' => '',
2397 - 'post_status' => 'inherit',
2398 - ], $upload['file']);
2399 -
2400 - if (is_wp_error($attach_id)) {
2401 - return $attach_id;
2402 - }
2403 -
2404 - require_once ABSPATH . 'wp-admin/includes/image.php';
2405 - $metadata = wp_generate_attachment_metadata($attach_id, $upload['file']);
2406 - wp_update_attachment_metadata($attach_id, $metadata);
2407 -
2408 - return esc_url_raw(wp_get_attachment_url($attach_id));
2409 -}
2410 -
2411 -private function mxchat_generate_openai_image($prompt, $api_key, $model = 'gpt-image-1', $timeout = 60) {
1660 +private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
2412 1661 $api_url = 'https://api.openai.com/v1/images/generations';
2413 1662 $body = json_encode([
2414 - 'prompt' => sanitize_text_field($prompt),
2415 - 'n' => 1,
2416 - 'size' => '1024x1024',
2417 - 'quality' => 'medium',
2418 - 'output_format' => 'png',
2419 - 'model' => sanitize_text_field($model),
1663 + 'prompt' => sanitize_text_field($prompt),
1664 + 'n' => 1,
1665 + 'size' => '1024x1024',
1666 + 'model' => sanitize_text_field($model),
2420 1667 ]);
2421 1668
2422 1669 $args = [
2423 - 'body' => $body,
1670 + 'body' => $body,
2424 1671 'headers' => [
2425 - 'Content-Type' => 'application/json',
1672 + 'Content-Type' => 'application/json',
2426 1673 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
2427 1674 ],
2428 - 'method' => 'POST',
1675 + 'method' => 'POST',
2429 1676 'timeout' => absint($timeout),
2430 1677 ];
2431 1678
2432 1679 $response = wp_remote_post($api_url, $args);
@@ -2431,114 +1678,23 @@
2431 1678
2432 1679 $response = wp_remote_post($api_url, $args);
2433 1680
2434 1681 if (is_wp_error($response)) {
1682 + //error_log("DALL-E request failed: " . $response->get_error_message());
2435 1683 return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2436 1684 }
2437 1685
2438 1686 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2439 1687
2440 - $b64 = $response_body['data'][0]['b64_json'] ?? $response_body['data'][0]['b64'] ?? null;
2441 - if ($b64) {
2442 - $saved_url = $this->mxchat_save_generated_image($b64, 'image/png', 'mxchat-openai');
2443 - if (is_wp_error($saved_url)) {
2444 - return ['error' => $saved_url->get_error_message()];
2445 - }
2446 - return ['imageUrl' => $saved_url];
1688 + if (isset($response_body['data'][0]['url'])) {
1689 + return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])];
2447 1690 } else {
1691 + //error_log("DALL-E response error: " . wp_remote_retrieve_body($response));
2448 1692 return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
2449 1693 }
2450 1694 }
2451 1695
2452 1696 /**
2453 - * Generate an image via a Custom (OpenAI-compatible) provider's /images/generations route.
2454 - * Only called when the opt-in 'custom_provider_for_images' setting is on.
2455 - */
2456 -private function mxchat_generate_custom_image($prompt, $timeout = 90) {
2457 - $cfg = $this->mxchat_resolve_custom_provider();
2458 - if (empty($cfg['base_url'])) {
2459 - return ['error' => esc_html__('Custom provider Base URL is not configured.', 'mxchat')];
2460 - }
2461 - $url = $cfg['base_url'] . '/images/generations';
2462 - if (!empty($cfg['api_version'])) {
2463 - $url .= (strpos($url, '?') === false ? '?' : '&') . 'api-version=' . rawurlencode($cfg['api_version']);
2464 - }
2465 - $body = wp_json_encode([
2466 - 'prompt' => sanitize_text_field($prompt),
2467 - 'n' => 1,
2468 - 'size' => '1024x1024',
2469 - 'model' => $cfg['model'],
2470 - ]);
2471 - $response = wp_remote_post($url, [
2472 - 'headers' => $this->mxchat_custom_provider_assoc_headers($cfg),
2473 - 'body' => $body,
2474 - 'method' => 'POST',
2475 - 'timeout' => absint($timeout),
2476 - ]);
2477 - if (is_wp_error($response)) {
2478 - return ['error' => esc_html__('Error generating image (custom provider): ', 'mxchat') . $response->get_error_message()];
2479 - }
2480 - $resp = json_decode(wp_remote_retrieve_body($response), true);
2481 - // Try b64 first (matches OpenAI shape), then url-based fallback.
2482 - $b64 = $resp['data'][0]['b64_json'] ?? $resp['data'][0]['b64'] ?? null;
2483 - if ($b64) {
2484 - $saved = $this->mxchat_save_generated_image($b64, 'image/png', 'mxchat-custom');
2485 - if (is_wp_error($saved)) {
2486 - return ['error' => $saved->get_error_message()];
2487 - }
2488 - return ['imageUrl' => $saved];
2489 - }
2490 - $remote_url = $resp['data'][0]['url'] ?? null;
2491 - if ($remote_url) {
2492 - return ['imageUrl' => esc_url_raw($remote_url)];
2493 - }
2494 - $err_msg = $resp['error']['message'] ?? esc_html__('Custom provider did not return an image.', 'mxchat');
2495 - return ['error' => esc_html($err_msg)];
2496 -}
2497 -
2498 -private function mxchat_generate_imagen_image($prompt, $api_key, $timeout = 60) {
2499 - $api_url = 'https://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-generate-001:predict';
2500 -
2501 - $body = json_encode([
2502 - 'instances' => [['prompt' => sanitize_text_field($prompt)]],
2503 - 'parameters' => [
2504 - 'sampleCount' => 1,
2505 - 'aspectRatio' => '1:1',
2506 - ],
2507 - ]);
2508 -
2509 - $args = [
2510 - 'body' => $body,
2511 - 'headers' => [
2512 - 'Content-Type' => 'application/json',
2513 - 'x-goog-api-key' => sanitize_text_field($api_key),
2514 - ],
2515 - 'method' => 'POST',
2516 - 'timeout' => absint($timeout),
2517 - ];
2518 -
2519 - $response = wp_remote_post($api_url, $args);
2520 -
2521 - if (is_wp_error($response)) {
2522 - return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2523 - }
2524 -
2525 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
2526 -
2527 - $b64 = $response_body['predictions'][0]['bytesBase64Encoded'] ?? $response_body['predictions'][0]['imageBytes'] ?? null;
2528 - if ($b64) {
2529 - $mime = $response_body['predictions'][0]['mimeType'] ?? 'image/png';
2530 - $saved_url = $this->mxchat_save_generated_image($b64, $mime, 'mxchat-gemini');
2531 - if (is_wp_error($saved_url)) {
2532 - return ['error' => $saved_url->get_error_message()];
2533 - }
2534 - return ['imageUrl' => $saved_url];
2535 - } else {
2536 - return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
2537 - }
2538 -}
2539 -
2540 -/**
2541 1697 * Handle web search requests.
2542 1698 *
2543 1699 * Sends the refined search query to the Brave Search API and uses the
2544 1700 * results to generate a conversational response with the AI model.
@@ -2586,10 +1742,10 @@
2586 1742 $transient_key = 'mxchat_search_' . md5($refined_search_query);
2587 1743 $results = get_transient($transient_key);
2588 1744
2589 1745 if (false === $results) {
2590 - // SECURITY FIX: Changed to wp_safe_remote_get
2591 - $response = wp_safe_remote_get(
1746 + // Fetch new results from the Brave Search API
1747 + $response = wp_remote_get(
2592 1748 $api_url,
2593 1749 array(
2594 1750 'headers' => array(
2595 1751 'Accept' => 'application/json',
@@ -2728,10 +1884,9 @@
2728 1884 ],
2729 1885 'timeout' => 10,
2730 1886 ];
2731 1887
2732 - // SECURITY FIX: Changed to wp_safe_remote_get
2733 - $response = wp_safe_remote_get($api_url, $args);
1888 + $response = wp_remote_get($api_url, $args);
2734 1889
2735 1890 if (is_wp_error($response)) {
2736 1891 return array(
2737 1892 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
@@ -2801,22 +1956,17 @@
2801 1956 * @return string The refined search query
2802 1957 */
2803 1958 public function mxchat_interpret_search_query($user_query) {
2804 1959 $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');
2805 -
1960 +
2806 1961 // Get options and determine the selected model
2807 1962 $options = $this->options ?? get_option('mxchat_options');
2808 - $selected_model = isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest';
2809 -
2810 - // Custom (OpenAI-compatible) provider routes by model id, not prefix.
2811 - if ($selected_model === 'custom-provider') {
2812 - return $this->interpret_query_with_custom($user_query, $system_prompt);
2813 - }
2814 -
1963 + $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
1964 +
2815 1965 // Extract model prefix to determine the provider
2816 1966 $model_parts = explode('-', $selected_model);
2817 1967 $provider = strtolower($model_parts[0]);
2818 -
1968 +
2819 1969 // Determine which API key to use based on the provider
2820 1970 switch ($provider) {
2821 1971 case 'gemini':
2822 1972 $api_key = isset($options['gemini_api_key']) ? sanitize_text_field($options['gemini_api_key']) : '';
@@ -2857,60 +2007,11 @@
2857 2007 }
2858 2008 }
2859 2009
2860 2010 /**
2861 - * Interpret query against the configured Custom (OpenAI-compatible) provider.
2862 - * Uses the same base URL + auth scheme as the chat dispatcher.
2863 - */
2864 -private function interpret_query_with_custom($user_query, $system_prompt) {
2865 - $cfg = $this->mxchat_resolve_custom_provider();
2866 - if (empty($cfg['base_url'])) {
2867 - return sanitize_text_field($user_query);
2868 - }
2869 - $args = [
2870 - 'headers' => $this->mxchat_custom_provider_assoc_headers($cfg),
2871 - 'body' => wp_json_encode([
2872 - 'model' => $cfg['model'],
2873 - 'messages' => [
2874 - ['role' => 'system', 'content' => $system_prompt],
2875 - ['role' => 'user', 'content' => sanitize_text_field($user_query)],
2876 - ],
2877 - 'temperature' => 0.2,
2878 - 'max_tokens' => 20,
2879 - ]),
2880 - 'method' => 'POST',
2881 - 'timeout' => 15,
2882 - ];
2883 - $response = wp_remote_post($cfg['chat_url'], $args);
2884 - if (is_wp_error($response)) {
2885 - return sanitize_text_field($user_query);
2886 - }
2887 - $body = json_decode(wp_remote_retrieve_body($response), true);
2888 - return isset($body['choices'][0]['message']['content'])
2889 - ? sanitize_text_field(trim($body['choices'][0]['message']['content']))
2890 - : sanitize_text_field($user_query);
2891 -}
2892 -
2893 -/**
2894 - * Convert the colon-style header list returned by mxchat_resolve_custom_provider
2895 - * into the assoc-array form wp_remote_post expects.
2896 - */
2897 -private function mxchat_custom_provider_assoc_headers($cfg) {
2898 - $headers = ['Content-Type' => 'application/json'];
2899 - if (!empty($cfg['api_key'])) {
2900 - if (($cfg['auth_scheme'] ?? 'bearer') === 'api-key') {
2901 - $headers['api-key'] = $cfg['api_key'];
2902 - } else {
2903 - $headers['Authorization'] = 'Bearer ' . $cfg['api_key'];
2904 - }
2905 - }
2906 - return $headers;
2907 -}
2908 -
2909 -/**
2910 2011 * Interpret query using OpenAI models
2911 2012 */
2912 -private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-5.1-chat-latest') {
2013 +private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-4o') {
2913 2014 $url = 'https://api.openai.com/v1/chat/completions';
2914 2015 $args = [
2915 2016 'headers' => [
2916 2017 'Authorization' => 'Bearer ' . $api_key,
@@ -2981,13 +2082,13 @@
2981 2082 /**
2982 2083 * Interpret query using Gemini models
2983 2084 */
2984 2085 private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
2985 - // Use v1beta for preview models, v1 for stable models
2986 - $api_version = (strpos($model, 'preview') !== false || strpos($model, 'exp') !== false) ? 'v1beta' : 'v1';
2987 -
2988 - $url = "https://generativelanguage.googleapis.com/{$api_version}/models/{$model}:generateContent?key=" . urlencode($api_key);
2086 + // Strip "gemini-" prefix for the API
2087 + $model_version = str_replace('gemini-', '', $model);
2989 2088
2089 + $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
2090 +
2990 2091 $args = [
2991 2092 'headers' => [
2992 2093 'Content-Type' => 'application/json',
2993 2094 ],
@@ -3183,9 +2284,9 @@
3183 2284 }
3184 2285
3185 2286
3186 2287 /**
3187 - * Enhanced fetch_and_split_pdf_pages with SSRF protection
2288 + * Enhanced fetch_and_split_pdf_pages with detailed debugging
3188 2289 */
3189 2290 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
3190 2291 // CLEAR DEBUG LOGGING
3191 2292 //error_log("=== MXCHAT PDF PROCESSING START ===");
@@ -3240,19 +2341,10 @@
3240 2341 // (I'll include the key parts with debug logging)
3241 2342
3242 2343 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
3243 2344 //error_log("Downloading PDF from URL...");
3244 -
3245 - // SECURITY FIX: Validate URL before processing
3246 - if (!$this->mxchat_is_safe_pdf_url($pdf_source)) {
3247 - //error_log("❌ SECURITY: Blocked unsafe PDF URL");
3248 - return false;
3249 - }
3250 -
3251 2345 $temp_file = wp_tempnam($pdf_source);
3252 -
3253 - // SECURITY FIX: Changed from wp_remote_get to wp_safe_remote_get
3254 - $response = wp_safe_remote_get($pdf_source, [
2346 + $response = wp_remote_get($pdf_source, [
3255 2347 'timeout' => 60,
3256 2348 'headers' => ['User-Agent' => 'MxChat PDF Processor']
3257 2349 ]);
3258 2350
@@ -3261,14 +2353,9 @@
3261 2353 //error_log("❌ BASIC PROCESSING: Failed to download PDF: " . $error_message);
3262 2354 return false;
3263 2355 }
3264 2356
3265 - global $wp_filesystem;
3266 - if (empty($wp_filesystem)) {
3267 - require_once ABSPATH . 'wp-admin/includes/file.php';
3268 - WP_Filesystem();
3269 - }
3270 - $wp_filesystem->put_contents($temp_file, wp_remote_retrieve_body($response), FS_CHMOD_FILE);
2357 + file_put_contents($temp_file, wp_remote_retrieve_body($response));
3271 2358 //error_log("✅ PDF downloaded successfully");
3272 2359 } else {
3273 2360 $temp_file = $pdf_source;
3274 2361 //error_log("Using local PDF file: " . $temp_file);
@@ -3275,9 +2362,8 @@
3275 2362 }
3276 2363
3277 2364 // Parse PDF
3278 2365 //error_log("Parsing PDF with basic parser...");
3279 - mxchat_load_pdf_parser();
3280 2366 $parser = new \Smalot\PdfParser\Parser();
3281 2367 $pdf = $parser->parseFile($temp_file);
3282 2368 $pages = $pdf->getPages();
3283 2369
@@ -3340,33 +2426,8 @@
3340 2426 return false;
3341 2427 }
3342 2428 }
3343 2429
3344 -
3345 -/**
3346 - * Validate PDF URL for security
3347 - * Prevents SSRF attacks by blocking dangerous URLs
3348 - */
3349 -
3350 -private function mxchat_is_safe_pdf_url($url) {
3351 - // Use WordPress core function for comprehensive validation
3352 - // This blocks localhost, private IPs, and reserved IP ranges
3353 - $validated_url = wp_http_validate_url($url);
3354 -
3355 - if ($validated_url === false) {
3356 - return false;
3357 - }
3358 -
3359 - // Additional check: only allow HTTP/HTTPS schemes
3360 - $parsed = parse_url($url);
3361 - if (!isset($parsed['scheme']) || !in_array($parsed['scheme'], ['http', 'https'], true)) {
3362 - return false;
3363 - }
3364 -
3365 - return true;
3366 -}
3367 -
3368 -
3369 2430 private function mxchat_clean_text($text) {
3370 2431 // Remove excessive whitespace
3371 2432 $text = preg_replace('/\s+/', ' ', $text);
3372 2433
@@ -3405,10 +2466,9 @@
3405 2466 }
3406 2467
3407 2468 return [];
3408 2469 }
3409 -
3410 -
2470 +// Add this to your class
3411 2471 public function handle_pdf_upload() {
3412 2472 check_ajax_referer('mxchat_chat_nonce', 'nonce');
3413 2473
3414 2474 if (!isset($_FILES['pdf_file']) || !isset($_POST['session_id'])) {
@@ -3415,29 +2475,12 @@
3415 2475 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
3416 2476 return;
3417 2477 }
3418 2478
3419 - // SECURITY FIX: Check if PDF uploads are enabled in settings
3420 - $options = get_option('mxchat_options', array());
3421 - $show_pdf_button = isset($options['show_pdf_upload_button']) ? $options['show_pdf_upload_button'] : 'on';
3422 -
3423 - if ($show_pdf_button !== 'on') {
3424 - wp_send_json_error(esc_html__('PDF uploads are currently disabled.', 'mxchat'));
3425 - return;
3426 - }
3427 -
3428 2479 $file = $_FILES['pdf_file'];
3429 2480 $session_id = sanitize_text_field($_POST['session_id']);
3430 2481 $original_filename = sanitize_text_field($file['name']);
3431 2482
3432 - // Update session owner if it changed (e.g. IP changed due to network switch)
3433 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
3434 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
3435 -
3436 - if (!$session_owner || $session_owner !== $current_user_identifier) {
3437 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
3438 - }
3439 -
3440 2483 $file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
3441 2484 if ($file_type['type'] !== 'application/pdf') {
3442 2485 wp_send_json_error(esc_html__('Invalid file type. Only PDF files are allowed.', 'mxchat'));
3443 2486 return;
@@ -3443,12 +2486,9 @@
3443 2486 return;
3444 2487 }
3445 2488
3446 2489 $upload_dir = wp_upload_dir();
3447 -
3448 - // SECURITY FIX: Generate random filename without exposing session_id
3449 - $random_string = wp_generate_password(20, false, false); // 20 char alphanumeric string
3450 - $pdf_filename = 'mxchat_' . $random_string . '_' . time() . '.pdf';
2490 + $pdf_filename = 'mxchat_' . $session_id . '_' . time() . '.pdf';
3451 2491 $pdf_path = $upload_dir['path'] . '/' . $pdf_filename;
3452 2492
3453 2493 if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
3454 2494 wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
@@ -3479,9 +2519,8 @@
3479 2519 return;
3480 2520 }
3481 2521
3482 2522 if (!empty($embeddings)) {
3483 - // Store the mapping between session and the random filename
3484 2523 set_transient('mxchat_pdf_url_' . $session_id, $pdf_path, HOUR_IN_SECONDS);
3485 2524 set_transient('mxchat_pdf_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
3486 2525 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
3487 2526 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
@@ -3525,8 +2564,10 @@
3525 2564 wp_die();
3526 2565 }
3527 2566
3528 2567
2568 +
2569 +
3529 2570 function mxchat_fetch_new_messages() {
3530 2571 $session_id = sanitize_text_field($_POST['session_id']);
3531 2572 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
3532 2573 $persistence_enabled = $_POST['persistence_enabled'] === 'true';
@@ -3539,31 +2580,14 @@
3539 2580 }
3540 2581
3541 2582 $history = get_option("mxchat_history_{$session_id}", []);
3542 2583
3543 - //error_log("MxChat WhatsApp DEBUG: Fetch new messages for session {$session_id}");
3544 - //error_log("MxChat WhatsApp DEBUG: last_seen_id = " . var_export($last_seen_id, true));
3545 - //error_log("MxChat WhatsApp DEBUG: History count = " . count($history));
3546 - //error_log("MxChat WhatsApp DEBUG: Full history = " . print_r($history, true));
3547 -
3548 2584 $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
3549 - //error_log("MxChat WhatsApp DEBUG: Checking message - ID: " . ($message['id'] ?? 'NO_ID') . ", Role: " . ($message['role'] ?? 'NO_ROLE'));
3550 -
3551 2585 // If persistence is enabled, show all new messages
3552 2586 if ($persistence_enabled) {
3553 - $has_id = !empty($message['id']);
3554 - $is_agent = $message['role'] === 'agent';
3555 -
3556 - // If last_seen_id is empty, 'NaN', or invalid, show all agent messages
3557 - if (empty($last_seen_id) || $last_seen_id === 'NaN' || $last_seen_id === 'undefined') {
3558 - $is_newer = true;
3559 - } else {
3560 - $is_newer = strcmp($message['id'] ?? '', $last_seen_id) > 0;
3561 - }
3562 -
3563 - //error_log("MxChat WhatsApp DEBUG: has_id={$has_id}, is_newer={$is_newer}, is_agent={$is_agent}");
3564 -
3565 - return $has_id && $is_newer && $is_agent;
2587 + return !empty($message['id']) &&
2588 + strcmp($message['id'], $last_seen_id) > 0 &&
2589 + $message['role'] === 'agent';
3566 2590 }
3567 2591
3568 2592 // If persistence is disabled, only show messages after initial timestamp
3569 2593 return !empty($message['id']) &&
@@ -3570,16 +2594,12 @@
3570 2594 $message['role'] === 'agent' &&
3571 2595 $message['timestamp'] > $initial_timestamp;
3572 2596 });
3573 2597
3574 - //error_log("MxChat WhatsApp DEBUG: Filtered messages count = " . count($new_messages));
2598 + //error_log(esc_html__("New agent messages fetched for session $session_id. Last seen ID: $last_seen_id", 'mxchat'));
3575 2599
3576 - // Include current chat mode so frontend can detect agent→AI transitions
3577 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
3578 -
3579 2600 wp_send_json_success([
3580 - 'new_messages' => array_values($new_messages),
3581 - 'chat_mode' => $chat_mode
2601 + 'new_messages' => array_values($new_messages)
3582 2602 ]);
3583 2603 wp_die();
3584 2604 }
3585 2605 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
@@ -3864,393 +2884,9 @@
3864 2884
3865 2885 //error_log("[DEBUG] Generated channel name: {$channel_name}");
3866 2886 return $channel_name;
3867 2887 }
3868 -
3869 -/**
3870 - * Telegram Live Agent Handover
3871 - * Creates a forum topic in the Telegram group and notifies agents
3872 - */
3873 -public function mxchat_telegram_live_agent_handover($message, $user_id, $session_id) {
3874 - // Check if Telegram agents are available
3875 - $telegram_available = $this->options['telegram_status'] ?? 'off';
3876 - if ($telegram_available !== 'on') {
3877 - $away_message = $this->options['telegram_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
3878 - $this->fallbackResponse = [
3879 - 'text' => $away_message,
3880 - 'html' => '',
3881 - 'images' => [],
3882 - 'chat_mode' => 'ai'
3883 - ];
3884 - wp_send_json([
3885 - 'text' => $away_message,
3886 - 'html' => '',
3887 - 'chat_mode' => 'ai',
3888 - 'session_id' => $session_id
3889 - ]);
3890 - wp_die();
3891 - }
3892 -
3893 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
3894 - $telegram_group_id = $this->options['telegram_group_id'] ?? '';
3895 -
3896 - if (empty($telegram_bot_token) || empty($telegram_group_id)) {
3897 - return false;
3898 - }
3899 -
3900 - // Check if topic already exists for this session
3901 - $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
3902 -
3903 - if (empty($topic_id)) {
3904 - // Generate topic name
3905 - $topic_name = $this->generate_telegram_topic_name($session_id);
3906 -
3907 - // Random icon color (Telegram forum topic colors)
3908 - $icon_colors = [0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98, 0xFF93B2, 0xFB6F5F];
3909 - $icon_color = $icon_colors[array_rand($icon_colors)];
3910 -
3911 - // Create forum topic
3912 - $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/createForumTopic", [
3913 - 'headers' => ['Content-Type' => 'application/json'],
3914 - 'body' => json_encode([
3915 - 'chat_id' => $telegram_group_id,
3916 - 'name' => $topic_name,
3917 - 'icon_color' => $icon_color
3918 - ])
3919 - ]);
3920 -
3921 - if (!is_wp_error($response)) {
3922 - $response_body = wp_remote_retrieve_body($response);
3923 - $response_data = json_decode($response_body, true);
3924 -
3925 - if (isset($response_data['ok']) && $response_data['ok']) {
3926 - $topic_id = $response_data['result']['message_thread_id'];
3927 - update_option("mxchat_telegram_topic_{$session_id}", $topic_id);
3928 - update_option("mxchat_telegram_group_{$session_id}", $telegram_group_id);
3929 - }
3930 - }
3931 -
3932 - if (empty($topic_id)) {
3933 - return false; // Failed to create topic
3934 - }
3935 - }
3936 -
3937 - // Get recent chat history
3938 - $history = get_option("mxchat_history_{$session_id}", []);
3939 - $recent_history = array_slice($history, -5);
3940 -
3941 - // Format conversation context for Telegram (HTML format)
3942 - $conversation_context = "";
3943 - if (!empty($recent_history)) {
3944 - $conversation_context = "<b>Recent Conversation:</b>\n";
3945 - foreach ($recent_history as $hist_message) {
3946 - $role_display = $hist_message['role'] === 'user' ? '👤 User' : '🤖 AI';
3947 - $escaped_content = htmlspecialchars($hist_message['content'], ENT_QUOTES, 'UTF-8');
3948 - $conversation_context .= "{$role_display}: {$escaped_content}\n";
3949 - }
3950 - $conversation_context .= "\n";
3951 - }
3952 -
3953 - // Get user info
3954 - $user_email = get_option("mxchat_email_{$session_id}", 'Not provided');
3955 - $user_name = get_option("mxchat_name_{$session_id}", 'Anonymous');
3956 -
3957 - // Update session mode
3958 - update_option("mxchat_mode_{$session_id}", 'agent');
3959 -
3960 - // Send initial message to topic
3961 - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
3962 - $topic_message = "🔔 <b>New Live Agent Request</b>\n\n";
3963 - $topic_message .= "<b>Session ID:</b> <code>{$session_id}</code>\n";
3964 - $topic_message .= "<b>User:</b> {$user_name}\n";
3965 - $topic_message .= "<b>Email:</b> {$user_email}\n\n";
3966 -
3967 - if (!empty($conversation_context)) {
3968 - $topic_message .= $conversation_context;
3969 - }
3970 -
3971 - $topic_message .= "<b>Current Message:</b>\n{$escaped_message}\n\n";
3972 - $topic_message .= "<i>Reply in this topic - messages will be sent to the user</i>\n";
3973 - $topic_message .= "<i>Type #close, #end, #disconnect, or #done to end the session</i>";
3974 -
3975 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
3976 - 'headers' => ['Content-Type' => 'application/json'],
3977 - 'body' => json_encode([
3978 - 'chat_id' => $telegram_group_id,
3979 - 'message_thread_id' => $topic_id,
3980 - 'text' => $topic_message,
3981 - 'parse_mode' => 'HTML'
3982 - ])
3983 - ]);
3984 -
3985 - $success_message = $this->options['telegram_notification_message'] ?? "I've notified a support agent. Please allow a moment for them to respond.";
3986 - $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
3987 -
3988 - $this->fallbackResponse = [
3989 - 'text' => $success_message,
3990 - 'html' => '',
3991 - 'images' => [],
3992 - 'chat_mode' => 'agent'
3993 - ];
3994 -
3995 - wp_send_json([
3996 - 'success' => true,
3997 - 'text' => $success_message,
3998 - 'html' => '',
3999 - 'chat_mode' => 'agent',
4000 - 'session_id' => $session_id,
4001 - 'fallbackResponse' => $this->fallbackResponse
4002 - ]);
4003 - wp_die();
4004 -}
4005 -
4006 -/**
4007 - * Generate topic name for Telegram forum
4008 - */
4009 -private function generate_telegram_topic_name($session_id) {
4010 - $name = null;
4011 - $email = null;
4012 -
4013 - // Check logged in user
4014 - if (is_user_logged_in()) {
4015 - $current_user = wp_get_current_user();
4016 - if (!empty($current_user->display_name)) {
4017 - $name = $current_user->display_name;
4018 - }
4019 - if (!empty($current_user->user_email)) {
4020 - $email = $current_user->user_email;
4021 - }
4022 - }
4023 -
4024 - // Check session data
4025 - if (empty($name)) {
4026 - $name = get_option("mxchat_name_{$session_id}");
4027 - }
4028 - if (empty($email)) {
4029 - $email = get_option("mxchat_email_{$session_id}");
4030 - }
4031 -
4032 - // Generate topic name
4033 - $session_suffix = substr($session_id, -6);
4034 -
4035 - if (!empty($name)) {
4036 - // Clean name for topic (max 128 chars in Telegram)
4037 - $clean_name = preg_replace('/[^\p{L}\p{N}\s\-]/u', '', $name);
4038 - $clean_name = trim($clean_name);
4039 - if (strlen($clean_name) > 50) {
4040 - $clean_name = substr($clean_name, 0, 50);
4041 - }
4042 - return "Chat - {$clean_name} ({$session_suffix})";
4043 - } elseif (!empty($email)) {
4044 - // Use email prefix
4045 - $email_prefix = explode('@', $email)[0];
4046 - if (strlen($email_prefix) > 30) {
4047 - $email_prefix = substr($email_prefix, 0, 30);
4048 - }
4049 - return "Chat - {$email_prefix} ({$session_suffix})";
4050 - }
4051 -
4052 - return "Chat - {$session_suffix}";
4053 -}
4054 -
4055 -/**
4056 - * Send user message to Telegram agent
4057 - */
4058 -public function mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id) {
4059 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4060 - $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
4061 - $group_id = get_option("mxchat_telegram_group_{$session_id}", '');
4062 -
4063 - if (empty($telegram_bot_token) || empty($topic_id) || empty($group_id)) {
4064 - return false;
4065 - }
4066 -
4067 - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
4068 - $user_message = "👤 <b>User:</b> {$escaped_message}";
4069 -
4070 - $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4071 - 'headers' => ['Content-Type' => 'application/json'],
4072 - 'body' => json_encode([
4073 - 'chat_id' => $group_id,
4074 - 'message_thread_id' => $topic_id,
4075 - 'text' => $user_message,
4076 - 'parse_mode' => 'HTML'
4077 - ])
4078 - ]);
4079 -
4080 - return !is_wp_error($response);
4081 -}
4082 -
4083 -/**
4084 - * Handle incoming Telegram webhook
4085 - */
4086 -public function handle_telegram_webhook(WP_REST_Request $request) {
4087 - $body = $request->get_body();
4088 - $data = json_decode($body, true);
4089 -
4090 - //error_log('[MxChat Telegram DEBUG] Webhook received: ' . $body);
4091 -
4092 - // Handle message events from forum topics
4093 - if (isset($data['message'])) {
4094 - $message_data = $data['message'];
4095 -
4096 - // Skip if not from a forum topic
4097 - if (!isset($message_data['message_thread_id'])) {
4098 - //error_log('[MxChat Telegram DEBUG] Skipped: No message_thread_id (not a forum topic message)');
4099 - return new WP_REST_Response(['ok' => true]);
4100 - }
4101 -
4102 - // Skip bot messages
4103 - if (isset($message_data['from']['is_bot']) && $message_data['from']['is_bot']) {
4104 - //error_log('[MxChat Telegram DEBUG] Skipped: Message from bot');
4105 - return new WP_REST_Response(['ok' => true]);
4106 - }
4107 -
4108 - $chat_id = $message_data['chat']['id'] ?? '';
4109 - $topic_id = $message_data['message_thread_id'];
4110 - $message_text = $message_data['text'] ?? '';
4111 - $message_id = $message_data['message_id'] ?? '';
4112 - $from = $message_data['from'] ?? [];
4113 - $agent_name = trim(($from['first_name'] ?? '') . ' ' . ($from['last_name'] ?? ''));
4114 - if (empty($agent_name)) {
4115 - $agent_name = $from['username'] ?? 'Agent';
4116 - }
4117 -
4118 - //error_log("[MxChat Telegram DEBUG] Parsed: chat_id={$chat_id}, topic_id={$topic_id}, agent={$agent_name}, text={$message_text}");
4119 -
4120 - // Skip empty messages
4121 - if (empty($message_text)) {
4122 - //error_log('[MxChat Telegram DEBUG] Skipped: Empty message text');
4123 - return new WP_REST_Response(['ok' => true]);
4124 - }
4125 -
4126 - // Find session ID by topic ID - cast to string for comparison
4127 - global $wpdb;
4128 - $topic_id_str = strval($topic_id);
4129 - $session_option = $wpdb->get_var(
4130 - $wpdb->prepare(
4131 - "SELECT option_name FROM {$wpdb->options}
4132 - WHERE option_name LIKE %s
4133 - AND option_value = %s",
4134 - 'mxchat_telegram_topic_%',
4135 - $topic_id_str
4136 - )
4137 - );
4138 -
4139 - //error_log("[MxChat Telegram DEBUG] Looking for topic_id={$topic_id_str} in options, found: " . ($session_option ?: 'NULL'));
4140 -
4141 - if ($session_option) {
4142 - $session_id = str_replace('mxchat_telegram_topic_', '', $session_option);
4143 - //error_log("[MxChat Telegram DEBUG] Session ID: {$session_id}");
4144 -
4145 - // Verify the group ID matches
4146 - $stored_group_id = get_option("mxchat_telegram_group_{$session_id}", '');
4147 - //error_log("[MxChat Telegram DEBUG] Stored group_id={$stored_group_id}, received chat_id={$chat_id}");
4148 -
4149 - if (strval($stored_group_id) != strval($chat_id)) {
4150 - //error_log('[MxChat Telegram DEBUG] Skipped: Group ID mismatch');
4151 - return new WP_REST_Response(['ok' => true]);
4152 - }
4153 -
4154 - // Check for closure commands
4155 - $lower_text = strtolower(trim($message_text));
4156 - if (in_array($lower_text, ['#close', '#end', '#disconnect', '#done'])) {
4157 - //error_log("[MxChat Telegram DEBUG] Closure command received: {$lower_text}");
4158 - // End the live agent session
4159 - update_option("mxchat_mode_{$session_id}", 'ai');
4160 -
4161 - // Save disconnect message
4162 - $disconnect_message = "Live agent session ended. You're now chatting with the AI assistant.";
4163 - $this->mxchat_save_chat_message($session_id, 'bot', $disconnect_message);
4164 -
4165 - // Notify in Telegram
4166 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4167 - if (!empty($telegram_bot_token)) {
4168 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4169 - 'headers' => ['Content-Type' => 'application/json'],
4170 - 'body' => json_encode([
4171 - 'chat_id' => $chat_id,
4172 - 'message_thread_id' => $topic_id,
4173 - 'text' => "✅ Session closed. User returned to AI chatbot.",
4174 - 'parse_mode' => 'HTML'
4175 - ])
4176 - ]);
4177 -
4178 - // Optionally close the topic
4179 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/closeForumTopic", [
4180 - 'headers' => ['Content-Type' => 'application/json'],
4181 - 'body' => json_encode([
4182 - 'chat_id' => $chat_id,
4183 - 'message_thread_id' => $topic_id
4184 - ])
4185 - ]);
4186 - }
4187 -
4188 - return new WP_REST_Response(['ok' => true]);
4189 - }
4190 -
4191 - // Deduplicate messages
4192 - $message_key = md5($session_id . $message_id . $message_text);
4193 - $processed_messages = get_transient('mxchat_telegram_messages_' . $session_id) ?: [];
4194 -
4195 - if (in_array($message_key, $processed_messages)) {
4196 - //error_log('[MxChat Telegram DEBUG] Skipped: Duplicate message');
4197 - return new WP_REST_Response(['ok' => true]);
4198 - }
4199 -
4200 - $processed_messages[] = $message_key;
4201 - if (count($processed_messages) > 50) {
4202 - $processed_messages = array_slice($processed_messages, -50);
4203 - }
4204 - set_transient('mxchat_telegram_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
4205 -
4206 - // Save the agent message - format with agent name prefix for proper parsing
4207 - $formatted_message = "Agent: {$agent_name} - {$message_text}";
4208 - //error_log("[MxChat Telegram DEBUG] Saving agent message: {$formatted_message}");
4209 -
4210 - $this->mxchat_save_chat_message($session_id, 'agent', $formatted_message);
4211 -
4212 - // Verify the message was saved to history
4213 - $history = get_option("mxchat_history_{$session_id}", []);
4214 - $last_message = end($history);
4215 - //error_log("[MxChat Telegram DEBUG] History after save - count: " . count($history) . ", last message role: " . ($last_message['role'] ?? 'none'));
4216 -
4217 - // Send confirmation back to Telegram
4218 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4219 - if (!empty($telegram_bot_token)) {
4220 - $confirm_key = 'mxchat_telegram_confirm_' . $message_key;
4221 - if (!get_transient($confirm_key)) {
4222 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4223 - 'headers' => ['Content-Type' => 'application/json'],
4224 - 'body' => json_encode([
4225 - 'chat_id' => $chat_id,
4226 - 'message_thread_id' => $topic_id,
4227 - 'text' => "✅ <i>Message sent to user</i>",
4228 - 'parse_mode' => 'HTML',
4229 - 'reply_to_message_id' => $message_id
4230 - ])
4231 - ]);
4232 - set_transient($confirm_key, true, 300);
4233 - }
4234 - }
4235 - } else {
4236 - //error_log("[MxChat Telegram DEBUG] No session found for topic_id={$topic_id}");
4237 - }
4238 - } else {
4239 - //error_log('[MxChat Telegram DEBUG] No message in webhook data');
4240 - }
4241 -
4242 - return new WP_REST_Response(['ok' => true]);
4243 -}
4244 -
4245 2888 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
4246 - // Check if this is a Telegram agent session
4247 - $telegram_topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
4248 - if (!empty($telegram_topic_id)) {
4249 - return $this->mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id);
4250 - }
4251 -
4252 - // Otherwise, try Slack
4253 2889 $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4254 2890 $channel_id = get_option("mxchat_channel_{$session_id}", '');
4255 2891
4256 2892 if (empty($slack_bot_token) || empty($channel_id)) {
@@ -4407,26 +3043,22 @@
4407 3043 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
4408 3044 ], 200);
4409 3045 }
4410 3046 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
4411 - // Update mode to AI
3047 + //error_log(esc_html__("Switching back to chatbot mode via intent.", 'mxchat'));
3048 +
3049 + // Just update mode to AI
4412 3050 update_option("mxchat_mode_{$session_id}", 'ai');
4413 -
4414 - // Clear any existing PDF context to start fresh
4415 - $this->clear_pdf_transients($session_id);
4416 -
4417 - // Set the response with explicit chat_mode
4418 - $this->fallbackResponse = [
4419 - 'text' => esc_html__('You are now chatting with the AI chatbot.', 'mxchat'),
4420 - 'html' => '',
4421 - 'images' => [],
4422 - 'chat_mode' => 'ai' // Ensure this is set
4423 - ];
4424 -
4425 - // Return the complete response array instead of just true
4426 - return $this->fallbackResponse;
3051 +
3052 + // Initialize states
3053 + $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
3054 + $this->productCardHtml = '';
3055 +
3056 + // Set the response message
3057 + $this->fallbackResponse['text'] = esc_html__('You are now chatting with the AI chatbot.', 'mxchat');
3058 +
3059 + return true; // Intent was handled
4427 3060 }
4428 -
4429 3061 public function handle_slack_messages(WP_REST_Request $request) {
4430 3062 // Log the incoming request for debugging
4431 3063 //error_log('Slack events request received: ' . $request->get_body());
4432 3064
@@ -4476,33 +3108,33 @@
4476 3108
4477 3109 $channel_id = $event['channel'];
4478 3110 $message_text = $event['text'] ?? '';
4479 3111 $message_ts = $event['ts'] ?? '';
4480 -
3112 +
4481 3113 // Find session ID by looking for matching channel
4482 3114 global $wpdb;
4483 3115 $session_option = $wpdb->get_var(
4484 3116 $wpdb->prepare(
4485 - "SELECT option_name FROM {$wpdb->options}
4486 - WHERE option_name LIKE 'mxchat_channel_%'
3117 + "SELECT option_name FROM {$wpdb->options}
3118 + WHERE option_name LIKE 'mxchat_channel_%'
4487 3119 AND option_value = %s",
4488 3120 $channel_id
4489 3121 )
4490 3122 );
4491 -
3123 +
4492 3124 if ($session_option) {
4493 3125 $session_id = str_replace('mxchat_channel_', '', $session_option);
4494 -
3126 +
4495 3127 // Create a unique key for this specific message
4496 3128 $message_key = md5($session_id . $message_ts . $message_text);
4497 3129 $processed_messages = get_transient('mxchat_processed_messages_' . $session_id) ?: [];
4498 -
3130 +
4499 3131 // Check if we've already processed this exact message
4500 3132 if (in_array($message_key, $processed_messages)) {
4501 3133 //error_log("Duplicate message detected for session $session_id");
4502 3134 return new WP_REST_Response(['ok' => true]);
4503 3135 }
4504 -
3136 +
4505 3137 // Add to processed messages
4506 3138 $processed_messages[] = $message_key;
4507 3139 // Keep only last 50 messages per session
4508 3140 if (count($processed_messages) > 50) {
@@ -4508,46 +3140,14 @@
4508 3140 if (count($processed_messages) > 50) {
4509 3141 $processed_messages = array_slice($processed_messages, -50);
4510 3142 }
4511 3143 set_transient('mxchat_processed_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
4512 -
4513 - $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4514 -
4515 - // Handle agent ending the chat — transfer back to AI
4516 - // Format: "!endchat" or "!endchat <custom message to user>"
4517 - if (preg_match('/^!endchat\b/i', trim($message_text))) {
4518 - update_option("mxchat_mode_{$session_id}", 'ai');
4519 -
4520 - // Extract custom message after !endchat, or use empty string
4521 - $custom_message = trim(preg_replace('/^!endchat\s*/i', '', trim($message_text)));
4522 -
4523 - // Send the agent's custom farewell message if provided
4524 - if (!empty($custom_message)) {
4525 - $this->mxchat_save_chat_message($session_id, 'agent', $custom_message);
4526 - }
4527 -
4528 - // Confirm in Slack channel
4529 - if (!empty($slack_bot_token)) {
4530 - wp_remote_post('https://slack.com/api/chat.postMessage', [
4531 - 'headers' => [
4532 - 'Content-Type' => 'application/json',
4533 - 'Authorization' => 'Bearer ' . $slack_bot_token
4534 - ],
4535 - 'body' => json_encode([
4536 - 'channel' => $channel_id,
4537 - 'text' => "✅ *Chat ended.* User has been transferred back to AI mode.",
4538 - 'mrkdwn' => true
4539 - ])
4540 - ]);
4541 - }
4542 -
4543 - return new WP_REST_Response(['ok' => true]);
4544 - }
4545 -
3144 +
4546 3145 // Save the agent message
4547 3146 $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
4548 -
3147 +
4549 3148 // Send confirmation back to Slack (only once)
3149 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4550 3150 if (!empty($slack_bot_token)) {
4551 3151 // Use a transient to prevent duplicate confirmations
4552 3152 $confirm_key = 'mxchat_confirm_' . $message_key;
4553 3153 if (!get_transient($confirm_key)) {
@@ -4557,9 +3157,9 @@
4557 3157 'Authorization' => 'Bearer ' . $slack_bot_token
4558 3158 ],
4559 3159 'body' => json_encode([
4560 3160 'channel' => $channel_id,
4561 - 'text' => "✅ _Message sent to user_",
3161 + 'text' => "✅ _Message sent to user_",
4562 3162 'thread_ts' => $event['ts'] // Reply in thread
4563 3163 ])
4564 3164 ]);
4565 3165 // Set transient to prevent duplicate confirmations
@@ -4599,15 +3199,9 @@
4599 3199 try {
4600 3200 // Get options and selected model
4601 3201 $options = get_option('mxchat_options');
4602 3202 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4603 -
4604 - // Opt-in: route embeddings through the Custom (OpenAI-compatible) provider.
4605 - // Off by default so existing sites see byte-identical behavior.
4606 - if (!empty($options['custom_provider_for_embeddings']) && $options['custom_provider_for_embeddings'] === 'on') {
4607 - return $this->mxchat_generate_embedding_custom($text);
4608 - }
4609 -
3203 +
4610 3204 // Determine endpoint and API key based on model
4611 3205 if (strpos($selected_model, 'voyage') === 0) {
4612 3206 $endpoint = 'https://api.voyageai.com/v1/embeddings';
4613 3207 $api_key = $options['voyage_api_key'] ?? '';
@@ -4803,617 +3397,251 @@
4803 3397 ];
4804 3398 }
4805 3399 }
4806 3400
3401 +private function mxchat_find_relevant_content($user_embedding) {
3402 + //error_log('MXChat Vector Search: Starting content search...');
4807 3403
4808 -/**
4809 - * Generate embedding via a Custom (OpenAI-compatible) provider's /embeddings route.
4810 - * Only called when the opt-in 'custom_provider_for_embeddings' setting is on.
4811 - * Returns a numeric array (the embedding vector) on success, or ['error','error_code'] on failure.
4812 - */
4813 -private function mxchat_generate_embedding_custom($text) {
4814 - if (empty($text)) {
4815 - return ['error' => esc_html__('No text provided for embedding generation', 'mxchat'), 'error_code' => 'empty_embedding_text'];
4816 - }
4817 - $cfg = $this->mxchat_resolve_custom_provider();
4818 - if (empty($cfg['base_url'])) {
4819 - return ['error' => esc_html__('Custom provider Base URL is not configured.', 'mxchat'), 'error_code' => 'missing_custom_provider_base_url'];
4820 - }
3404 + // Retrieve the add-on settings from the database.
3405 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
4821 3406
4822 - $options = get_option('mxchat_options');
4823 - $embed_url = $cfg['base_url'] . '/embeddings';
4824 - if (!empty($cfg['api_version'])) {
4825 - $embed_url .= (strpos($embed_url, '?') === false ? '?' : '&') . 'api-version=' . rawurlencode($cfg['api_version']);
4826 - }
4827 - $model = isset($options['custom_provider_embedding_model']) && trim((string) $options['custom_provider_embedding_model']) !== ''
4828 - ? trim((string) $options['custom_provider_embedding_model'])
4829 - : $cfg['model'];
3407 + // Determine whether Pinecone is enabled.
3408 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
4830 3409
4831 - $response = wp_remote_post($embed_url, [
4832 - 'headers' => $this->mxchat_custom_provider_assoc_headers($cfg),
4833 - 'body' => wp_json_encode(['input' => $text, 'model' => $model]),
4834 - 'timeout' => 60,
4835 - ]);
4836 - if (is_wp_error($response)) {
4837 - return [
4838 - 'error' => esc_html__('Connection error when generating embeddings (custom provider): ', 'mxchat') . esc_html($response->get_error_message()),
4839 - 'error_code' => 'embedding_custom_connection_error',
4840 - ];
4841 - }
4842 - $status = wp_remote_retrieve_response_code($response);
4843 - $body = json_decode(wp_remote_retrieve_body($response), true);
4844 - if ($status !== 200) {
4845 - $msg = isset($body['error']['message']) ? $body['error']['message'] : 'HTTP ' . $status;
4846 - return [
4847 - 'error' => esc_html__('Custom embedding endpoint error: ', 'mxchat') . esc_html($msg),
4848 - 'error_code' => 'embedding_custom_api_error',
4849 - 'status_code' => $status,
4850 - ];
4851 - }
4852 - if (isset($body['data'][0]['embedding']) && is_array($body['data'][0]['embedding'])) {
4853 - return $body['data'][0]['embedding'];
4854 - }
4855 - return [
4856 - 'error' => esc_html__('Invalid embedding response from custom provider.', 'mxchat'),
4857 - 'error_code' => 'embedding_custom_invalid_response',
4858 - ];
4859 -}
3410 + //error_log('Pinecone enabled flag: ' . $use_pinecone);
4860 3411
4861 -private function mxchat_find_relevant_content($user_embedding, $bot_id = 'default', $user_query = '') {
4862 - //error_log("MXCHAT DEBUG: find_relevant_content called with bot_id: " . $bot_id);
4863 -
4864 - // Check for OpenAI Vector Store first (takes priority when enabled)
4865 - $bot_vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
4866 -
4867 - if ($bot_vectorstore_config['use_vectorstore']) {
4868 - // Get current model to verify it's an OpenAI model
4869 - $bot_options = $this->get_bot_options($bot_id);
4870 - $mxchat_options = get_option('mxchat_options', array());
4871 - $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
4872 - $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
4873 -
4874 - if ($this->is_openai_chat_model($selected_model)) {
4875 - //error_log("MXCHAT DEBUG: Using OpenAI Vector Store for knowledge retrieval");
4876 - return $this->find_relevant_content_openai_vectorstore($user_query, $bot_id, $bot_vectorstore_config);
4877 - } else {
4878 - //error_log("MXCHAT DEBUG: Vector Store enabled but model is not OpenAI (" . $selected_model . "), skipping Vector Store");
4879 - }
4880 - }
4881 -
4882 - // Get bot-specific Pinecone configuration
4883 - $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
4884 -
4885 - // Debug: Log the Pinecone configuration
4886 - //error_log("MXCHAT DEBUG: Pinecone config for bot '$bot_id':");
4887 - //error_log(" - use_pinecone: " . ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false'));
4888 - //error_log(" - api_key: " . (empty($bot_pinecone_config['api_key']) ? 'EMPTY' : 'SET (hidden)'));
4889 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'NOT SET'));
4890 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'NOT SET'));
4891 -
4892 - // Determine whether to use Pinecone based on bot configuration
4893 - $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
4894 -
4895 - //error_log("MXCHAT DEBUG: Using " . ($use_pinecone ? "Pinecone" : "WordPress Database") . " for knowledge retrieval");
4896 -
4897 - if ($use_pinecone) {
4898 - return $this->find_relevant_content_pinecone($user_embedding, $bot_id, $bot_pinecone_config);
3412 + if ($use_pinecone === 1) {
3413 + //error_log('MXChat Vector Search: Using Pinecone database');
3414 + return $this->find_relevant_content_pinecone($user_embedding);
4899 3415 } else {
4900 - return $this->find_relevant_content_wordpress($user_embedding, $bot_id);
3416 + //error_log('MXChat Vector Search: Using WordPress database');
3417 + return $this->find_relevant_content_wordpress($user_embedding);
4901 3418 }
4902 3419 }
4903 3420
4904 -private function find_relevant_content_wordpress($user_embedding, $bot_id = 'default') {
3421 +private function find_relevant_content_wordpress($user_embedding) {
4905 3422 global $wpdb;
4906 3423 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3424 + $cache_key = 'mxchat_system_prompt_embeddings';
3425 + $batch_size = 500;
3426 +
4907 3427 // Initialize similarity analysis storage
4908 3428 $this->last_similarity_analysis = [
4909 3429 'knowledge_base_type' => 'WordPress Database',
4910 - 'bot_id' => $bot_id,
4911 3430 'top_matches' => [],
4912 3431 'threshold_used' => 0,
4913 3432 'total_checked' => 0
4914 3433 ];
4915 3434
4916 - // NEW: Initialize valid URLs array
4917 - $valid_urls = [];
3435 + // Retrieve embeddings from cache or database
3436 + $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3437 + if ($embeddings === false) {
3438 + // Cache miss - load embeddings from database WITH CONTENT and ROLE RESTRICTION for testing
3439 + $embeddings = [];
3440 + $offset = 0;
4918 3441
4919 - // Get bot-specific options for similarity threshold
4920 - $bot_options = $this->get_bot_options($bot_id);
4921 - $current_options = !empty($bot_options) ? $bot_options : $this->options;
3442 + do {
3443 + $query = $wpdb->prepare(
3444 + "SELECT id, embedding_vector, article_content, source_url, role_restriction
3445 + FROM {$system_prompt_table}
3446 + LIMIT %d OFFSET %d",
3447 + $batch_size,
3448 + $offset
3449 + );
4922 3450
4923 - // Get knowledge manager instance for role checking
4924 - $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
3451 + $batch = $wpdb->get_results($query);
3452 + if (empty($batch)) {
3453 + break;
3454 + }
4925 3455
4926 - // Get base similarity threshold from bot options or default options
4927 - $similarity_threshold = isset($current_options['similarity_threshold'])
4928 - ? ((int) $current_options['similarity_threshold']) / 100
4929 - : 0.35;
4930 - $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3456 + $embeddings = array_merge($embeddings, $batch);
3457 + $offset += $batch_size;
3458 + unset($batch);
3459 + } while (true);
4931 3460
4932 - // Precompute bot_filter once, outside the streaming loop
4933 - $bot_filter = '';
4934 - if ($bot_id !== 'default') {
4935 - $column_exists = $wpdb->get_var("SHOW COLUMNS FROM {$system_prompt_table} LIKE 'bot_metadata'");
4936 - if ($column_exists) {
4937 - $bot_filter = $wpdb->prepare(" AND (bot_metadata = %s OR bot_metadata IS NULL OR bot_metadata = '')", $bot_id);
3461 + if (empty($embeddings)) {
3462 + return '';
4938 3463 }
3464 +
3465 + // Cache embeddings for future use (but note: this now includes content and role restrictions)
3466 + wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
4939 3467 }
4940 3468
4941 - // ===== STREAMING TOP-K PASS =====
4942 - // Stream rows in small batches, compute cosine similarity per row, and keep only:
4943 - // - top 10 by raw similarity (for the testing/debug display panel)
4944 - // - candidates above threshold with access (capped) for context assembly
4945 - // This bounds peak memory regardless of knowledge base size and avoids loading
4946 - // article_content for every row. article_content is fetched in Phase 2 for winners only.
4947 - $batch_size = 250;
4948 - $max_candidates = 200; // safety cap, well above rag_sources_limit * max_chunks_per_source
4949 - $top_display = [];
4950 - $candidates = [];
4951 - $total_checked = 0;
4952 - $offset = 0;
3469 + // NEW: Get knowledge manager instance for role checking
3470 + $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
4953 3471
4954 - do {
4955 - $batch = $wpdb->get_results($wpdb->prepare(
4956 - "SELECT id, embedding_vector, source_url, role_restriction
4957 - FROM {$system_prompt_table}
4958 - WHERE 1=1 {$bot_filter}
4959 - LIMIT %d OFFSET %d",
4960 - $batch_size,
4961 - $offset
4962 - ));
4963 -
4964 - if (empty($batch)) {
4965 - break;
4966 - }
4967 -
4968 - foreach ($batch as $row) {
4969 - $database_embedding = $row->embedding_vector
4970 - ? unserialize($row->embedding_vector, ['allowed_classes' => false])
4971 - : null;
4972 -
4973 - if (!is_array($database_embedding) || !is_array($user_embedding)) {
4974 - unset($database_embedding);
4975 - continue;
4976 - }
4977 -
3472 + // Get configuration options
3473 + $main_options = get_option('mxchat_options', []);
3474 +
3475 + // Get base similarity threshold (default 75%)
3476 + $similarity_threshold = isset($main_options['similarity_threshold'])
3477 + ? ((int) $main_options['similarity_threshold']) / 100
3478 + : 0.75;
3479 +
3480 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3481 +
3482 + // Calculate similarities and build results array
3483 + $all_similarities = [];
3484 + $relevant_results = [];
3485 +
3486 + foreach ($embeddings as $embedding) {
3487 + $database_embedding = $embedding->embedding_vector
3488 + ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3489 + : null;
3490 +
3491 + if (is_array($database_embedding) && is_array($user_embedding)) {
4978 3492 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
4979 - unset($database_embedding);
4980 -
4981 - $role_restriction = $row->role_restriction ?? 'public';
3493 +
3494 + // NEW: Check role access
3495 + $role_restriction = $embedding->role_restriction ?? 'public';
4982 3496 $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
4983 - $source_url = $row->source_url ?? '';
4984 -
4985 - // Maintain top 10 display buffer (insert-if-beats-worst)
4986 - if (count($top_display) < 10) {
4987 - $top_display[] = [
4988 - 'id' => $row->id,
4989 - 'similarity' => $similarity,
4990 - 'source_url' => $source_url,
4991 - 'role_restriction' => $role_restriction,
4992 - 'has_access' => $has_access,
4993 - ];
4994 - usort($top_display, function ($a, $b) {
4995 - return $b['similarity'] <=> $a['similarity'];
4996 - });
4997 - } elseif ($similarity > $top_display[9]['similarity']) {
4998 - $top_display[9] = [
4999 - 'id' => $row->id,
5000 - 'similarity' => $similarity,
5001 - 'source_url' => $source_url,
5002 - 'role_restriction' => $role_restriction,
5003 - 'has_access' => $has_access,
5004 - ];
5005 - usort($top_display, function ($a, $b) {
5006 - return $b['similarity'] <=> $a['similarity'];
5007 - });
3497 +
3498 + // Store ALL similarities for testing (top 10)
3499 + $source_display = '';
3500 + if (!empty($embedding->source_url) && $embedding->source_url !== '#') {
3501 + $source_display = $embedding->source_url;
3502 + } else {
3503 + $content_preview = strip_tags($embedding->article_content ?? '');
3504 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3505 + $source_display = substr(trim($content_preview), 0, 50) . '...';
5008 3506 }
5009 -
5010 - // Track candidates for context assembly (above threshold + has access)
3507 +
3508 + $all_similarities[] = [
3509 + 'document_id' => $embedding->id,
3510 + 'similarity' => $similarity,
3511 + 'similarity_percentage' => round($similarity * 100, 2),
3512 + 'above_threshold' => $similarity >= $similarity_threshold,
3513 + 'source_display' => $source_display,
3514 + 'content_preview' => substr(strip_tags($embedding->article_content ?? ''), 0, 100) . '...',
3515 + 'used_for_context' => false, // Initialize as false, we'll update this later
3516 + 'role_restriction' => $role_restriction, // NEW: Include role info for testing
3517 + 'has_access' => $has_access, // NEW: Include access info for testing
3518 + 'filtered_out' => !$has_access // NEW: Mark if filtered out by role
3519 + ];
3520 +
3521 + // Only consider results above threshold AND with access for actual content retrieval
5011 3522 if ($similarity >= $similarity_threshold && $has_access) {
5012 - $candidates[] = [
5013 - 'id' => $row->id,
5014 - 'similarity' => $similarity,
5015 - 'source_url' => $source_url,
3523 + $relevant_results[] = [
3524 + 'id' => $embedding->id,
3525 + 'similarity' => $similarity
5016 3526 ];
5017 3527 }
5018 -
5019 - $total_checked++;
5020 3528 }
5021 -
5022 - unset($batch);
5023 -
5024 - // Trim candidates periodically to cap memory during long scans
5025 - if (count($candidates) > $max_candidates) {
5026 - usort($candidates, function ($a, $b) {
5027 - return $b['similarity'] <=> $a['similarity'];
5028 - });
5029 - $candidates = array_slice($candidates, 0, $max_candidates);
5030 - }
5031 -
5032 - $offset += $batch_size;
5033 - } while (true);
5034 -
5035 - if ($total_checked === 0) {
5036 - $this->current_valid_urls = [];
5037 - return '';
3529 +
3530 + unset($database_embedding);
5038 3531 }
5039 3532
5040 - // Final candidates sort (best first)
5041 - if (count($candidates) > 1) {
5042 - usort($candidates, function ($a, $b) {
5043 - return $b['similarity'] <=> $a['similarity'];
5044 - });
5045 - }
5046 -
5047 - // ===== PHASE 2: FETCH ARTICLE CONTENT ONLY FOR WINNERS =====
5048 - // Gather unique IDs we actually need (top_display + candidates) and pull
5049 - // article_content in bounded IN() batches. This avoids loading content for
5050 - // every row during the similarity scan.
5051 - $needed_ids = [];
5052 - foreach ($top_display as $item) {
5053 - $needed_ids[$item['id']] = true;
5054 - }
5055 - foreach ($candidates as $item) {
5056 - $needed_ids[$item['id']] = true;
5057 - }
5058 - $needed_ids = array_keys($needed_ids);
5059 -
5060 - $content_map = [];
5061 - if (!empty($needed_ids)) {
5062 - foreach (array_chunk($needed_ids, 250) as $chunk_ids) {
5063 - $placeholders = implode(',', array_fill(0, count($chunk_ids), '%d'));
5064 - $rows = $wpdb->get_results($wpdb->prepare(
5065 - "SELECT id, article_content FROM {$system_prompt_table} WHERE id IN ($placeholders)",
5066 - ...$chunk_ids
5067 - ));
5068 - foreach ($rows as $r) {
5069 - $content_map[$r->id] = $r->article_content;
5070 - }
5071 - unset($rows);
5072 - }
5073 - }
5074 -
5075 - // Build the all_similarities display array from the top 10
5076 - $all_similarities = [];
5077 - foreach ($top_display as $item) {
5078 - $article_content_for_parse = $content_map[$item['id']] ?? '';
5079 - $parsed_for_display = MxChat_Chunker::parse_stored_chunk($article_content_for_parse);
5080 - $is_chunk = $parsed_for_display['is_chunked'];
5081 - $chunk_meta = $parsed_for_display['metadata'];
5082 -
5083 - if (!empty($item['source_url']) && $item['source_url'] !== '#') {
5084 - $source_display = $item['source_url'];
5085 - } else {
5086 - $content_preview = strip_tags($article_content_for_parse);
5087 - $content_preview = preg_replace('/\s+/', ' ', $content_preview);
5088 - $source_display = substr(trim($content_preview), 0, 50) . '...';
5089 - }
5090 -
5091 - $all_similarities[] = [
5092 - 'document_id' => $item['id'],
5093 - 'similarity' => $item['similarity'],
5094 - 'similarity_percentage' => round($item['similarity'] * 100, 2),
5095 - 'above_threshold' => $item['similarity'] >= $similarity_threshold,
5096 - 'source_display' => $source_display,
5097 - 'content_preview' => substr(strip_tags($parsed_for_display['text'] ?? ''), 0, 100) . '...',
5098 - 'used_for_context' => false,
5099 - 'role_restriction' => $item['role_restriction'],
5100 - 'has_access' => $item['has_access'],
5101 - 'filtered_out' => !$item['has_access'],
5102 - 'is_chunk' => $is_chunk,
5103 - 'chunk_index' => $is_chunk ? ($chunk_meta['chunk_index'] ?? 0) : null,
5104 - 'total_chunks' => $is_chunk ? ($chunk_meta['total_chunks'] ?? 1) : null
5105 - ];
5106 - }
5107 -
5108 - // Build url_groups from candidates for chunk reassembly
5109 - $url_groups = array();
5110 - foreach ($candidates as $cand) {
5111 - $article_content = $content_map[$cand['id']] ?? '';
5112 - $parsed = MxChat_Chunker::parse_stored_chunk($article_content);
5113 - $is_chunked = $parsed['is_chunked'];
5114 - $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
5115 - $text_content = $parsed['text'];
5116 -
5117 - $source_url = $cand['source_url'];
5118 - $group_key = !empty($source_url) ? $source_url : '_manual_' . $cand['id'];
5119 -
5120 - if (!isset($url_groups[$group_key])) {
5121 - $url_groups[$group_key] = array(
5122 - 'source_url' => $source_url,
5123 - 'best_score' => 0,
5124 - 'is_chunked' => $is_chunked,
5125 - 'chunks' => array(),
5126 - 'single_text' => '',
5127 - 'single_id' => null
5128 - );
5129 - }
5130 -
5131 - if ($cand['similarity'] > $url_groups[$group_key]['best_score']) {
5132 - $url_groups[$group_key]['best_score'] = $cand['similarity'];
5133 - }
5134 -
5135 - if ($is_chunked) {
5136 - $url_groups[$group_key]['is_chunked'] = true;
5137 - $url_groups[$group_key]['chunks'][] = array(
5138 - 'id' => $cand['id'],
5139 - 'score' => $cand['similarity'],
5140 - 'chunk_index' => $chunk_index,
5141 - 'text' => $text_content
5142 - );
5143 - } else {
5144 - $url_groups[$group_key]['single_text'] = $text_content;
5145 - $url_groups[$group_key]['single_id'] = $cand['id'];
5146 - }
5147 - }
5148 -
5149 3533 // Sort ALL similarities for testing display (highest first)
5150 3534 usort($all_similarities, function ($a, $b) {
5151 3535 return $b['similarity'] <=> $a['similarity'];
5152 3536 });
5153 -
5154 - // Sort URL groups by best score (highest first)
5155 - uasort($url_groups, function($a, $b) {
5156 - return $b['best_score'] <=> $a['best_score'];
3537 +
3538 + // Sort relevant results by similarity (highest first)
3539 + usort($relevant_results, function ($a, $b) {
3540 + return $b['similarity'] <=> $a['similarity'];
5157 3541 });
5158 -
5159 - // Get RAG sources limit from options (default 6, min 3, max 10)
5160 - $rag_sources_limit = isset($current_options['rag_sources_limit']) ? intval($current_options['rag_sources_limit']) : 3;
5161 - if ($rag_sources_limit < 3) $rag_sources_limit = 3;
5162 - if ($rag_sources_limit > 10) $rag_sources_limit = 10;
5163 -
5164 - // Take top N unique URLs based on user setting
5165 - $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
5166 -
5167 - // Track which document IDs are used for context
3542 +
3543 + // Get top 5 results for actual content (standard approach)
3544 + $top_results = array_slice($relevant_results, 0, 5);
3545 +
3546 + // NOW mark which documents are actually used for context
5168 3547 $used_document_ids = [];
5169 - foreach ($top_urls as $group) {
5170 - if ($group['is_chunked']) {
5171 - foreach ($group['chunks'] as $chunk) {
5172 - $used_document_ids[] = $chunk['id'];
5173 - }
5174 - } elseif ($group['single_id']) {
5175 - $used_document_ids[] = $group['single_id'];
5176 - }
3548 + foreach ($top_results as $result) {
3549 + $used_document_ids[] = $result['id'];
5177 3550 }
5178 -
3551 +
5179 3552 // Update the all_similarities array to mark which were actually used
5180 3553 foreach ($all_similarities as &$similarity_item) {
5181 3554 $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
5182 3555 }
5183 -
5184 - // Store top 10 for testing panel
3556 +
3557 + // Store top 10 for testing panel (now with correct used_for_context flags and role info)
5185 3558 $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
5186 - $this->last_similarity_analysis['total_checked'] = $total_checked;
5187 -
3559 + $this->last_similarity_analysis['total_checked'] = count($embeddings);
3560 +
3561 + //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " top matches for testing");
3562 +
5188 3563 // Initialize final content
5189 3564 $content = '';
5190 - $matches_used = 0;
5191 - $total_chunks_used = 0;
5192 - $max_total_chunks = isset($current_options['rag_chunks_limit']) ? intval($current_options['rag_chunks_limit']) : 15;
5193 - if ($max_total_chunks < 8) $max_total_chunks = 8;
5194 - if ($max_total_chunks > 20) $max_total_chunks = 20;
5195 - $max_chunks_per_source = 5; // Cap per individual source to limit token usage
5196 -
5197 - // Check if citation links are enabled (default to 'on' for backwards compatibility)
5198 - // Use fresh options to ensure we get the latest setting value
5199 - $fresh_options = get_option('mxchat_options', []);
5200 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
5201 -
5202 - // Build content from top sources
5203 - foreach ($top_urls as $group_key => $group) {
5204 - $source_url = $group['source_url']; // Use actual source_url, not the group key
5205 -
5206 - // Stop if we've hit the total chunk limit
5207 - if ($total_chunks_used >= $max_total_chunks) {
5208 - break;
3565 +
3566 + // Track document IDs to avoid duplicates
3567 + $added_document_ids = [];
3568 +
3569 + // Fetch and format content for each selected result
3570 + foreach ($top_results as $index => $result) {
3571 + if (in_array($result['id'], $added_document_ids)) {
3572 + continue;
5209 3573 }
5210 -
5211 - $full_text = '';
5212 - $chunks_in_this_source = 1; // Default for non-chunked content
5213 -
5214 - if ($group['is_chunked']) {
5215 - // Calculate how many chunks we can still use (respect both total and per-source caps)
5216 - $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
5217 -
5218 - // Fetch chunks for this URL with limit
5219 - $full_text = $this->reassemble_chunks_from_wordpress($source_url, $chunks_remaining, $chunks_in_this_source);
5220 -
5221 - // If fetching all chunks fails, fall back to matched chunks
5222 - if (empty($full_text)) {
5223 - // Sort matched chunks by index and concatenate
5224 - usort($group['chunks'], function($a, $b) {
5225 - return $a['chunk_index'] <=> $b['chunk_index'];
5226 - });
5227 -
5228 - $chunk_texts = array();
5229 - $chunks_in_this_source = 0;
5230 - foreach ($group['chunks'] as $chunk) {
5231 - if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
5232 - break;
5233 - }
5234 - $chunk_texts[] = $chunk['text'];
5235 - $chunks_in_this_source++;
3574 +
3575 + $chunk_content = $this->fetch_content_with_product_links($result['id']);
3576 + $added_document_ids[] = $result['id'];
3577 +
3578 + $content .= "## Reference " . ($index + 1) . " ##\n";
3579 + $content .= $chunk_content . "\n\n";
3580 +
3581 + // PDF surrounding pages logic (unchanged)
3582 + if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
3583 + $surrounding_content = $wpdb->get_results($wpdb->prepare(
3584 + "SELECT id, article_content, role_restriction FROM {$system_prompt_table}
3585 + WHERE id IN (
3586 + (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
3587 + (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
3588 + )",
3589 + $result['id'],
3590 + $result['id']
3591 + ));
3592 +
3593 + // NEW: Check role access for surrounding content too
3594 + if (!empty($surrounding_content[0])) {
3595 + $surrounding_role = $surrounding_content[0]->role_restriction ?? 'public';
3596 + if ($knowledge_manager->mxchat_user_has_content_access($surrounding_role)) {
3597 + $content .= "## Related Content ##\n";
3598 + $content .= $surrounding_content[0]->article_content . "\n\n";
3599 + $added_document_ids[] = $surrounding_content[0]->id;
5236 3600 }
5237 - $full_text = implode("\n\n", $chunk_texts);
5238 3601 }
5239 - } else {
5240 - $full_text = $group['single_text'];
5241 - $chunks_in_this_source = 1;
5242 - }
5243 -
5244 - if (!empty($full_text)) {
5245 - // Strip URLs from content if citation links are disabled
5246 - if (!$citation_links_enabled) {
5247 - $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
5248 - $full_text = preg_replace('/\s+/', ' ', trim($full_text)); // Clean up extra spaces
5249 - }
5250 -
5251 - // Use numbered reference for URL-based entries, plain info label for manual entries
5252 - // Manual entries are stored with an internal mxchat:// placeholder URL — never expose them as citations
5253 - if (!empty($source_url) && $source_url !== '#' && strpos($source_url, 'mxchat://') !== 0) {
5254 - $matches_used++;
5255 - $content .= "## Reference " . $matches_used . " ##\n";
5256 - $content .= $full_text . "\n\n";
5257 -
5258 - // Only include citation URLs if citation links are enabled
5259 - if ($citation_links_enabled) {
5260 - $valid_urls[] = $source_url;
5261 - $content .= "URL: " . $source_url . "\n\n";
3602 +
3603 + if (!empty($surrounding_content[1])) {
3604 + $surrounding_role = $surrounding_content[1]->role_restriction ?? 'public';
3605 + if ($knowledge_manager->mxchat_user_has_content_access($surrounding_role)) {
3606 + $content .= "## Related Content ##\n";
3607 + $content .= $surrounding_content[1]->article_content . "\n\n";
3608 + $added_document_ids[] = $surrounding_content[1]->id;
5262 3609 }
5263 - } else {
5264 - // Manual entry — no reference number, no citation
5265 - $content .= "## Information ##\n";
5266 - $content .= $full_text . "\n\n";
5267 3610 }
5268 -
5269 - // Extract any URLs from the text content itself (only if citation links enabled)
5270 - if ($citation_links_enabled) {
5271 - preg_match_all(
5272 - '#\bhttps?://[^\s<>"\']+#i',
5273 - $full_text,
5274 - $content_urls
5275 - );
5276 - if (!empty($content_urls[0])) {
5277 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5278 - }
5279 - }
5280 -
5281 - $total_chunks_used += $chunks_in_this_source;
5282 3611 }
5283 3612 }
5284 -
5285 - // NEW: Store unique valid URLs for validation
5286 - $this->current_valid_urls = array_unique($valid_urls);
5287 -
5288 - // Store sources and chunks counts for testing/transcript display
5289 - $this->last_similarity_analysis['sources_used'] = $matches_used;
5290 - $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
5291 -
5292 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
5293 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
5294 -
3613 +
5295 3614 // Add response guidelines
5296 - if (empty($top_urls)) {
3615 + if (empty($top_results)) {
5297 3616 $content = "No reference information was found for this query.\n\n";
5298 3617 } else {
5299 - // Build response guidelines based on citation links setting
5300 3618 $content .= "\n## Response Guidelines ##\n" .
5301 3619 "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
5302 3620 "Be conversational and friendly, but never mention your knowledge base or training data. " .
5303 3621 "If you don't have specific information or are uncertain about any details, it's always " .
5304 3622 "better to honestly say you don't know rather than making up or guessing at answers. " .
5305 - "When information is incomplete, let them know you are unsure.\n\n";
5306 -
5307 - // Only add hyperlink instructions if citation links are enabled
5308 - if ($citation_links_enabled) {
5309 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
5310 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
5311 - "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
5312 - } else {
5313 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
5314 - "Simply provide helpful answers based on the reference information without citing sources.";
5315 - }
3623 + "When information is incomplete, let them know you are unsure.";
5316 3624 }
5317 3625
5318 3626 return trim($content);
5319 3627 }
5320 3628
5321 -/**
5322 - * Fetch and reassemble chunks for a URL from WordPress database
5323 - *
5324 - * @param string $source_url The source URL to fetch chunks for
5325 - * @param int $max_chunks Maximum number of chunks to return (0 = unlimited)
5326 - * @param int &$chunk_count Reference to store the actual number of chunks returned
5327 - * @return string Reassembled content from chunks
5328 - */
5329 -private function reassemble_chunks_from_wordpress($source_url, $max_chunks = 0, &$chunk_count = 0) {
5330 - global $wpdb;
5331 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
5332 -
5333 - // Fetch all rows with this source_url
5334 - $rows = $wpdb->get_results($wpdb->prepare(
5335 - "SELECT article_content FROM {$table}
5336 - WHERE source_url = %s
5337 - ORDER BY id ASC",
5338 - $source_url
5339 - ));
5340 -
5341 - if (empty($rows)) {
5342 - $chunk_count = 0;
5343 - return '';
5344 - }
5345 -
5346 - // Parse and sort chunks by index
5347 - $chunks = array();
5348 - foreach ($rows as $row) {
5349 - $parsed = MxChat_Chunker::parse_stored_chunk($row->article_content);
5350 -
5351 - if ($parsed['is_chunked']) {
5352 - $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
5353 - $chunks[$chunk_index] = $parsed['text'];
5354 - } else {
5355 - // Non-chunked content - just return it
5356 - $chunks[] = $parsed['text'];
5357 - }
5358 - }
5359 -
5360 - // Sort by chunk index
5361 - ksort($chunks);
5362 -
5363 - // Apply chunk limit if specified
5364 - if ($max_chunks > 0 && count($chunks) > $max_chunks) {
5365 - $chunks = array_slice($chunks, 0, $max_chunks, true);
5366 - }
5367 -
5368 - // Store actual chunk count
5369 - $chunk_count = count($chunks);
5370 -
5371 - // Reassemble content
5372 - return implode("\n\n", $chunks);
5373 -}
5374 -
5375 -private function find_relevant_content_pinecone($user_embedding, $bot_id = 'default', $bot_config = null) {
5376 - global $wpdb;
3629 +private function find_relevant_content_pinecone($user_embedding) {
3630 + global $wpdb; // For single role lookups
3631 + $options = get_option('mxchat_pinecone_addon_options', array());
3632 + $api_key = $options['mxchat_pinecone_api_key'] ?? '';
3633 + $host = $options['mxchat_pinecone_host'] ?? '';
5377 3634
5378 - //error_log("MXCHAT DEBUG: find_relevant_content_pinecone called");
5379 - //error_log(" - bot_id: " . $bot_id);
5380 - //error_log(" - user_embedding is array: " . (is_array($user_embedding) ? 'yes' : 'no'));
5381 - //error_log(" - user_embedding count: " . (is_array($user_embedding) ? count($user_embedding) : 'N/A'));
5382 -
5383 - // Use bot-specific config or fall back to default
5384 - if ($bot_config === null) {
5385 - $bot_config = $this->get_bot_pinecone_config($bot_id);
5386 - }
5387 -
5388 - $api_key = $bot_config['api_key'] ?? '';
5389 - $host = $bot_config['host'] ?? '';
5390 - $namespace = $bot_config['namespace'] ?? '';
5391 -
5392 - //error_log("MXCHAT DEBUG: Pinecone query parameters:");
5393 - //error_log(" - API Key: " . (empty($api_key) ? 'EMPTY - ERROR!' : 'Present (length: ' . strlen($api_key) . ')'));
5394 - //error_log(" - Host: " . (empty($host) ? 'EMPTY - ERROR!' : $host));
5395 - //error_log(" - Namespace: " . (empty($namespace) ? 'EMPTY (will use default)' : $namespace));
5396 -
5397 3635 // Initialize similarity analysis storage
5398 3636 $this->last_similarity_analysis = [
5399 3637 'knowledge_base_type' => 'Pinecone',
5400 - 'bot_id' => $bot_id,
5401 - 'namespace' => $namespace,
5402 3638 'top_matches' => [],
5403 3639 'threshold_used' => 0,
5404 3640 'total_checked' => 0
5405 3641 ];
5406 3642
5407 - // NEW: Initialize valid URLs array
5408 - $valid_urls = [];
5409 -
5410 3643 if (empty($host) || empty($api_key)) {
5411 - //error_log("MXCHAT DEBUG ERROR: Missing Pinecone host or API key!");
5412 - //error_log(" - Host empty: " . (empty($host) ? 'YES' : 'NO'));
5413 - //error_log(" - API key empty: " . (empty($api_key) ? 'YES' : 'NO'));
5414 - // Store empty array for valid URLs since we can't proceed
5415 - $this->current_valid_urls = [];
5416 3644 return '';
5417 3645 }
5418 3646
5419 3647 // Get knowledge manager instance for role checking
@@ -5418,37 +3646,26 @@
5418 3646
5419 3647 // Get knowledge manager instance for role checking
5420 3648 $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
5421 3649
5422 - // Get the similarity threshold from the bot options or main options
5423 - $bot_options = $this->get_bot_options($bot_id);
5424 - $current_options = !empty($bot_options) ? $bot_options : get_option('mxchat_options', []);
3650 + // Get the similarity threshold from the main options
3651 + $main_options = get_option('mxchat_options', []);
3652 + $similarity_threshold = isset($main_options['similarity_threshold'])
3653 + ? ((int) $main_options['similarity_threshold']) / 100
3654 + : 0.75;
5425 3655
5426 - $similarity_threshold = isset($current_options['similarity_threshold'])
5427 - ? ((int) $current_options['similarity_threshold']) / 100
5428 - : 0.35;
5429 -
5430 3656 $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
5431 3657
5432 - // Prepare the query request for Pinecone
3658 + // Prepare the query request for Pinecone (request more for testing)
5433 3659 $api_endpoint = "https://{$host}/query";
5434 3660
5435 3661 $request_body = array(
5436 3662 'vector' => $user_embedding,
5437 - 'topK' => 50, // Increased for chunked content grouping - need more candidates to find top N unique URLs
3663 + 'topK' => 20, // Request more to get good testing data
5438 3664 'includeMetadata' => true,
5439 3665 'includeValues' => true
5440 3666 );
5441 3667
5442 - // Add namespace if specified for this bot
5443 - if (!empty($namespace)) {
5444 - $request_body['namespace'] = $namespace;
5445 - }
5446 -
5447 - //error_log("MXCHAT DEBUG: About to call Pinecone API");
5448 - //error_log(" - Endpoint: " . $api_endpoint);
5449 - //error_log(" - Namespace in request: " . (!empty($namespace) ? $namespace : 'NOT SET'));
5450 -
5451 3668 $response = wp_remote_post($api_endpoint, array(
5452 3669 'headers' => array(
5453 3670 'Api-Key' => $api_key,
5454 3671 'accept' => 'application/json',
@@ -5458,242 +3675,62 @@
5458 3675 'timeout' => 30
5459 3676 ));
5460 3677
5461 3678 if (is_wp_error($response)) {
5462 - //error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
5463 - // Store empty array for valid URLs
5464 - $this->current_valid_urls = [];
5465 3679 return '';
5466 3680 }
5467 3681
5468 3682 $response_code = wp_remote_retrieve_response_code($response);
5469 - //error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
5470 -
5471 3683 if ($response_code !== 200) {
5472 - $response_body = wp_remote_retrieve_body($response);
5473 - //error_log("MXCHAT DEBUG ERROR: Pinecone API error response: " . substr($response_body, 0, 500));
5474 - // Store empty array for valid URLs
5475 - $this->current_valid_urls = [];
5476 3684 return '';
5477 3685 }
5478 3686
5479 - // ADD DETAILED DEBUG SECTION HERE
5480 - $response_body = wp_remote_retrieve_body($response);
5481 - //error_log("MXCHAT DEBUG: Raw Pinecone response length: " . strlen($response_body));
5482 -
5483 - $results = json_decode($response_body, true);
5484 -
5485 - if (json_last_error() !== JSON_ERROR_NONE) {
5486 - //error_log("MXCHAT DEBUG ERROR: JSON decode error: " . json_last_error_msg());
5487 - //error_log("MXCHAT DEBUG: First 500 chars of response: " . substr($response_body, 0, 500));
5488 - // Store empty array for valid URLs
5489 - $this->current_valid_urls = [];
5490 - return '';
5491 - }
5492 -
5493 - //error_log("MXCHAT DEBUG: Pinecone response structure:");
5494 - //error_log(" - Has 'matches' key: " . (isset($results['matches']) ? 'yes' : 'no'));
5495 - //error_log(" - Has 'namespace' key: " . (isset($results['namespace']) ? 'yes (' . $results['namespace'] . ')' : 'no'));
5496 -
3687 + $results = json_decode(wp_remote_retrieve_body($response), true);
5497 3688 if (empty($results['matches'])) {
5498 - //error_log("MXCHAT DEBUG: No matches found in Pinecone response");
5499 - //error_log("MXCHAT DEBUG: Response keys: " . implode(', ', array_keys($results)));
5500 - // Store empty array for valid URLs
5501 - $this->current_valid_urls = [];
5502 3689 return '';
5503 3690 }
5504 3691
5505 - //error_log("MXCHAT DEBUG: Found " . count($results['matches']) . " matches in Pinecone");
5506 -
5507 - // Log first match details for debugging
5508 - if (!empty($results['matches'][0])) {
5509 - $first_match = $results['matches'][0];
5510 - //error_log("MXCHAT DEBUG: First match details:");
5511 - //error_log(" - Score: " . ($first_match['score'] ?? 'no score'));
5512 - //error_log(" - Has metadata: " . (isset($first_match['metadata']) ? 'yes' : 'no'));
5513 - if (isset($first_match['metadata'])) {
5514 - //error_log(" - Metadata keys: " . implode(', ', array_keys($first_match['metadata'])));
5515 - }
5516 - }
5517 -
5518 3692 // Initialize the final content
5519 3693 $content = '';
5520 3694 $matches_used = 0;
5521 3695 $matches_used_for_context = [];
5522 - $total_chunks_used = 0;
5523 - $max_total_chunks = isset($current_options['rag_chunks_limit']) ? intval($current_options['rag_chunks_limit']) : 15;
5524 - if ($max_total_chunks < 8) $max_total_chunks = 8;
5525 - if ($max_total_chunks > 20) $max_total_chunks = 20;
5526 - $max_chunks_per_source = 5; // Cap per individual source to limit token usage
5527 -
5528 - // Check if citation links are enabled (default to 'on' for backwards compatibility)
5529 - // Use fresh options to ensure we get the latest setting value
5530 - $fresh_options = get_option('mxchat_options', []);
5531 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
5532 -
5533 - // NEW CHUNKING LOGIC: Group results by source_url for chunk reassembly
5534 - $url_groups = array();
5535 -
3696 +
3697 + // Process each match for actual content generation (lazy role checking)
5536 3698 foreach ($results['matches'] as $index => $match) {
5537 3699 // Skip if similarity is below threshold
5538 3700 if ($match['score'] < $similarity_threshold) {
5539 3701 continue;
5540 3702 }
5541 -
5542 - $metadata = $match['metadata'] ?? array();
5543 - $source_url = $metadata['source_url'] ?? '';
5544 - $match_id = $match['id'] ?? '';
5545 -
5546 - // LAZY ROLE CHECK: Only check role for content we're actually considering
5547 - $role_restriction = $this->get_single_vector_role($match_id, $metadata);
5548 - $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
5549 -
5550 - // Skip if user doesn't have access
5551 - if (!$has_access) {
5552 - continue;
5553 - }
5554 -
5555 - // Use a unique key for manual entries without a source URL
5556 - $group_key = !empty($source_url) ? $source_url : '_manual_' . $match_id;
5557 -
5558 - // Group by source URL (or unique key for manual entries)
5559 - if (!isset($url_groups[$group_key])) {
5560 - $url_groups[$group_key] = array(
5561 - 'source_url' => $source_url,
5562 - 'best_score' => 0,
5563 - 'is_chunked' => isset($metadata['is_chunked']) && $metadata['is_chunked'],
5564 - 'chunks' => array(),
5565 - 'single_text' => ''
5566 - );
5567 - }
5568 -
5569 - // Track best score for this group
5570 - if ($match['score'] > $url_groups[$group_key]['best_score']) {
5571 - $url_groups[$group_key]['best_score'] = $match['score'];
5572 - }
5573 -
5574 - // Store chunk info or single text
5575 - if ($url_groups[$group_key]['is_chunked']) {
5576 - $url_groups[$group_key]['chunks'][] = array(
5577 - 'id' => $match_id,
5578 - 'score' => $match['score'],
5579 - 'chunk_index' => $metadata['chunk_index'] ?? 0,
5580 - 'text' => $metadata['text'] ?? ''
5581 - );
5582 - } else {
5583 - // Non-chunked content - just store the text
5584 - $url_groups[$group_key]['single_text'] = $metadata['text'] ?? '';
5585 - $url_groups[$group_key]['single_id'] = $match_id;
5586 - }
5587 - }
5588 -
5589 - // Sort URL groups by best score (highest first)
5590 - uasort($url_groups, function($a, $b) {
5591 - return $b['best_score'] <=> $a['best_score'];
5592 - });
5593 -
5594 - // Get RAG sources limit from options (default 6, min 3, max 10)
5595 - $rag_sources_limit = isset($current_options['rag_sources_limit']) ? intval($current_options['rag_sources_limit']) : 3;
5596 - if ($rag_sources_limit < 3) $rag_sources_limit = 3;
5597 - if ($rag_sources_limit > 10) $rag_sources_limit = 10;
5598 -
5599 - // Take top N unique URLs based on user setting
5600 - $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
5601 -
5602 - // Track which match IDs are actually used for context
5603 - foreach ($top_urls as $group) {
5604 - if ($group['is_chunked']) {
5605 - foreach ($group['chunks'] as $chunk) {
5606 - $matches_used_for_context[] = $chunk['id'];
5607 - }
5608 - } elseif (!empty($group['single_id'])) {
5609 - $matches_used_for_context[] = $group['single_id'];
5610 - }
5611 - }
5612 -
5613 - // Build content from top sources
5614 - foreach ($top_urls as $group_key => $group) {
5615 - $source_url = $group['source_url']; // Use actual source_url, not the group key
5616 -
5617 - // Stop if we've hit the total chunk limit
5618 - if ($total_chunks_used >= $max_total_chunks) {
3703 +
3704 + // Limit to top 5 matches above threshold
3705 + if ($matches_used >= 5) {
5619 3706 break;
5620 3707 }
5621 -
5622 - $full_text = '';
5623 - $chunks_in_this_source = 1; // Default for non-chunked content
5624 -
5625 - if ($group['is_chunked']) {
5626 - // Calculate how many chunks we can still use (respect both total and per-source caps)
5627 - $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
5628 -
5629 - // Fetch chunks for this URL with limit
5630 - $full_text = $this->reassemble_chunks_from_pinecone($source_url, $bot_config, $chunks_remaining, $chunks_in_this_source);
5631 -
5632 - // If fetching all chunks fails, fall back to matched chunks
5633 - if (empty($full_text)) {
5634 - // Sort matched chunks by index and concatenate
5635 - usort($group['chunks'], function($a, $b) {
5636 - return $a['chunk_index'] <=> $b['chunk_index'];
5637 - });
5638 -
5639 - $chunk_texts = array();
5640 - $chunks_in_this_source = 0;
5641 - foreach ($group['chunks'] as $chunk) {
5642 - if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
5643 - break;
5644 - }
5645 - $chunk_texts[] = $chunk['text'];
5646 - $chunks_in_this_source++;
5647 - }
5648 - $full_text = implode("\n\n", $chunk_texts);
3708 +
3709 + if (!empty($match['metadata']['text'])) {
3710 + // LAZY ROLE CHECK: Only check role for content we're actually considering
3711 + $match_id = $match['id'] ?? '';
3712 + $role_restriction = $this->get_single_vector_role($match_id, $match['metadata']);
3713 + $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
3714 +
3715 + // Skip if user doesn't have access
3716 + if (!$has_access) {
3717 + continue;
5649 3718 }
5650 - } else {
5651 - $full_text = $group['single_text'];
5652 - $chunks_in_this_source = 1;
5653 - }
5654 -
5655 - if (!empty($full_text)) {
5656 - // Strip URLs from content if citation links are disabled
5657 - if (!$citation_links_enabled) {
5658 - $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
5659 - $full_text = preg_replace('/\s+/', ' ', trim($full_text)); // Clean up extra spaces
3719 +
3720 + // User has access - add to content
3721 + $content .= "## Reference " . ($matches_used + 1) . " ##\n";
3722 + $content .= $match['metadata']['text'] . "\n\n";
3723 +
3724 + if (!empty($match['metadata']['source_url'])) {
3725 + $content .= "URL: " . $match['metadata']['source_url'] . "\n\n";
5660 3726 }
5661 -
5662 - // Use numbered reference for URL-based entries, plain info label for manual entries
5663 - // Manual entries are stored with an internal mxchat:// placeholder URL — never expose them as citations
5664 - if (!empty($source_url) && $source_url !== '#' && strpos($source_url, 'mxchat://') !== 0) {
5665 - $matches_used++;
5666 - $content .= "## Reference " . $matches_used . " ##\n";
5667 - $content .= $full_text . "\n\n";
5668 -
5669 - // Only include citation URLs if citation links are enabled
5670 - if ($citation_links_enabled) {
5671 - $valid_urls[] = $source_url;
5672 - $content .= "URL: " . $source_url . "\n\n";
5673 - }
5674 - } else {
5675 - // Manual entry — no reference number, no citation
5676 - $content .= "## Information ##\n";
5677 - $content .= $full_text . "\n\n";
5678 - }
5679 -
5680 - // Extract any URLs from the text content itself (only if citation links enabled)
5681 - if ($citation_links_enabled) {
5682 - preg_match_all(
5683 - '#\bhttps?://[^\s<>"\']+#i',
5684 - $full_text,
5685 - $content_urls
5686 - );
5687 - if (!empty($content_urls[0])) {
5688 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5689 - }
5690 - }
5691 -
5692 - $total_chunks_used += $chunks_in_this_source;
3727 +
3728 + $matches_used_for_context[] = $match['id'] ?? $index;
3729 + $matches_used++;
5693 3730 }
5694 3731 }
5695 -
3732 +
5696 3733 // Process ALL matches for testing data (top 10) - with role checking for testing display
5697 3734 $all_matches = [];
5698 3735 foreach ($results['matches'] as $index => $match) {
5699 3736 if ($index >= 10) break; // Limit to top 10 for testing
@@ -5713,19 +3750,9 @@
5713 3750 $source_display = substr(trim($content_preview), 0, 50) . '...';
5714 3751 }
5715 3752
5716 3753 $match_id_for_display = $match['id'] ?? $index;
5717 -
5718 - // Check for chunk metadata in Pinecone
5719 - $is_chunk = isset($match['metadata']['is_chunked']) && $match['metadata']['is_chunked'];
5720 - $chunk_index = isset($match['metadata']['chunk_index']) ? intval($match['metadata']['chunk_index']) : null;
5721 - $total_chunks = isset($match['metadata']['total_chunks']) ? intval($match['metadata']['total_chunks']) : null;
5722 -
5723 - // Also detect chunk from vector ID pattern: {hash}_chunk_{index}
5724 - if (!$is_chunk && MxChat_Chunker::is_chunk_vector_id($match_id_for_display)) {
5725 - $is_chunk = true;
5726 - }
5727 -
3754 +
5728 3755 $all_matches[] = [
5729 3756 'document_id' => $match_id_for_display,
5730 3757 'similarity' => $match['score'],
5731 3758 'similarity_percentage' => round($match['score'] * 100, 2),
@@ -5734,12 +3761,9 @@
5734 3761 'content_preview' => substr(strip_tags($match['metadata']['text'] ?? ''), 0, 100) . '...',
5735 3762 'used_for_context' => in_array($match_id_for_display, $matches_used_for_context),
5736 3763 'role_restriction' => $role_restriction,
5737 3764 'has_access' => $has_access,
5738 - 'filtered_out' => !$has_access,
5739 - 'is_chunk' => $is_chunk,
5740 - 'chunk_index' => $chunk_index,
5741 - 'total_chunks' => $total_chunks
3765 + 'filtered_out' => !$has_access
5742 3766 ];
5743 3767 }
5744 3768
5745 3769 // Store for testing panel
@@ -5744,40 +3768,23 @@
5744 3768
5745 3769 // Store for testing panel
5746 3770 $this->last_similarity_analysis['top_matches'] = $all_matches;
5747 3771 $this->last_similarity_analysis['total_checked'] = count($results['matches']);
5748 - $this->last_similarity_analysis['sources_used'] = $matches_used;
5749 - $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
5750 -
5751 - // NEW: Store unique valid URLs for validation
5752 - $this->current_valid_urls = array_unique($valid_urls);
5753 -
5754 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
5755 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
5756 -
3772 +
3773 + //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " Pinecone matches for testing");
3774 +
5757 3775 // Add response guidelines
5758 3776 if ($matches_used === 0) {
5759 3777 $content = "No reference information was found for this query.\n\n";
5760 3778 } else {
5761 - // Build response guidelines based on citation links setting
5762 - $content .= "\n## Response Guidelines ##\n" .
5763 - "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
5764 - "Be conversational and friendly, but never mention your knowledge base or training data. " .
5765 - "If you don't have specific information or are uncertain about any details, it's always " .
5766 - "better to honestly say you don't know rather than making up or guessing at answers. " .
5767 - "When information is incomplete, let them know you are unsure.\n\n";
5768 -
5769 - // Only add hyperlink instructions if citation links are enabled
5770 - if ($citation_links_enabled) {
5771 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
5772 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
5773 - "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
5774 - } else {
5775 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
5776 - "Simply provide helpful answers based on the reference information without citing sources.";
5777 - }
3779 + $content .= "\n## Response Guidelines ##\n" .
3780 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3781 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
3782 + "If you don't have specific information or are uncertain about any details, it's always " .
3783 + "better to honestly say you don't know rather than making up or guessing at answers. " .
3784 + "When information is incomplete, let them know you are unsure.";
5778 3785 }
5779 -
3786 +
5780 3787 return trim($content);
5781 3788 }
5782 3789
5783 3790 /**
@@ -5817,518 +3824,12 @@
5817 3824 }
5818 3825
5819 3826 // Cache individual role for 1 hour
5820 3827 wp_cache_set($cache_key, $role_restriction, 'mxchat_vector_roles', 3600);
5821 -
3828 +
5822 3829 return $role_restriction;
5823 3830 }
5824 3831
5825 -/**
5826 - * Fetch and reassemble all chunks for a URL from Pinecone
5827 - *
5828 - * @param string $source_url The source URL to fetch chunks for
5829 - * @param array $bot_config Bot-specific Pinecone configuration
5830 - * @return string Reassembled content from all chunks
5831 - */
5832 -private function reassemble_chunks_from_pinecone($source_url, $bot_config, $max_chunks = 0, &$chunk_count = 0) {
5833 - $api_key = $bot_config['api_key'] ?? '';
5834 - $host = $bot_config['host'] ?? '';
5835 - $namespace = $bot_config['namespace'] ?? '';
5836 -
5837 - if (empty($host) || empty($api_key)) {
5838 - $chunk_count = 0;
5839 - return '';
5840 - }
5841 -
5842 - $base_hash = md5($source_url);
5843 -
5844 - // Use Pinecone list API to find all chunk vectors with this prefix
5845 - $list_url = "https://{$host}/vectors/list";
5846 -
5847 - // Limit to max_chunks if specified, otherwise fetch up to 100
5848 - $fetch_limit = ($max_chunks > 0 && $max_chunks < 100) ? $max_chunks : 100;
5849 -
5850 - $list_body = array(
5851 - 'prefix' => $base_hash . '_chunk_',
5852 - 'limit' => $fetch_limit
5853 - );
5854 -
5855 - if (!empty($namespace)) {
5856 - $list_body['namespace'] = $namespace;
5857 - }
5858 -
5859 - $list_response = wp_remote_post($list_url, array(
5860 - 'headers' => array(
5861 - 'Api-Key' => $api_key,
5862 - 'accept' => 'application/json',
5863 - 'content-type' => 'application/json'
5864 - ),
5865 - 'body' => wp_json_encode($list_body),
5866 - 'timeout' => 30
5867 - ));
5868 -
5869 - if (is_wp_error($list_response)) {
5870 - //error_log('[MXCHAT-CHUNK] List API error: ' . $list_response->get_error_message());
5871 - return '';
5872 - }
5873 -
5874 - $list_data = json_decode(wp_remote_retrieve_body($list_response), true);
5875 -
5876 - if (empty($list_data['vectors'])) {
5877 - //error_log('[MXCHAT-CHUNK] No chunk vectors found for URL: ' . $source_url);
5878 - return '';
5879 - }
5880 -
5881 - // Extract vector IDs
5882 - $vector_ids = array();
5883 - foreach ($list_data['vectors'] as $vector) {
5884 - if (isset($vector['id'])) {
5885 - $vector_ids[] = $vector['id'];
5886 - }
5887 - }
5888 -
5889 - if (empty($vector_ids)) {
5890 - return '';
5891 - }
5892 -
5893 - // Fetch all chunk content
5894 - $fetch_url = "https://{$host}/vectors/fetch";
5895 -
5896 - $fetch_body = array(
5897 - 'ids' => $vector_ids
5898 - );
5899 -
5900 - if (!empty($namespace)) {
5901 - $fetch_body['namespace'] = $namespace;
5902 - }
5903 -
5904 - $fetch_response = wp_remote_post($fetch_url, array(
5905 - 'headers' => array(
5906 - 'Api-Key' => $api_key,
5907 - 'accept' => 'application/json',
5908 - 'content-type' => 'application/json'
5909 - ),
5910 - 'body' => wp_json_encode($fetch_body),
5911 - 'timeout' => 30
5912 - ));
5913 -
5914 - if (is_wp_error($fetch_response)) {
5915 - //error_log('[MXCHAT-CHUNK] Fetch API error: ' . $fetch_response->get_error_message());
5916 - return '';
5917 - }
5918 -
5919 - $fetch_data = json_decode(wp_remote_retrieve_body($fetch_response), true);
5920 -
5921 - if (empty($fetch_data['vectors'])) {
5922 - return '';
5923 - }
5924 -
5925 - // Sort chunks by index and reassemble
5926 - $chunks = array();
5927 - foreach ($fetch_data['vectors'] as $id => $vector) {
5928 - $metadata = $vector['metadata'] ?? array();
5929 - $chunk_index = $metadata['chunk_index'] ?? 0;
5930 - $text = $metadata['text'] ?? '';
5931 -
5932 - // Store chunk with its index
5933 - $chunks[$chunk_index] = $text;
5934 - }
5935 -
5936 - // Sort by chunk index
5937 - ksort($chunks);
5938 -
5939 - // Apply chunk limit if specified
5940 - if ($max_chunks > 0 && count($chunks) > $max_chunks) {
5941 - $chunks = array_slice($chunks, 0, $max_chunks, true);
5942 - }
5943 -
5944 - // Store actual chunk count
5945 - $chunk_count = count($chunks);
5946 -
5947 - // Reassemble content
5948 - return implode("\n\n", $chunks);
5949 -}
5950 -
5951 -/**
5952 - * Search for relevant content using OpenAI Vector Store (File Search)
5953 - *
5954 - * @param string $user_query The user's query text
5955 - * @param string $bot_id The bot ID
5956 - * @param array $vectorstore_config Vector Store configuration
5957 - * @return string Formatted context string with references
5958 - */
5959 -private function find_relevant_content_openai_vectorstore($user_query, $bot_id = 'default', $vectorstore_config = array()) {
5960 - //error_log("MXCHAT DEBUG: find_relevant_content_openai_vectorstore called");
5961 - //error_log(" - bot_id: " . $bot_id);
5962 - //error_log(" - user_query length: " . strlen($user_query));
5963 -
5964 - // Get OpenAI API key
5965 - $mxchat_options = get_option('mxchat_options', array());
5966 - $api_key = $mxchat_options['api_key'] ?? '';
5967 -
5968 - // Reset vectorstore error tracking
5969 - $this->last_vectorstore_error = null;
5970 -
5971 - if (empty($api_key)) {
5972 - //error_log("MXCHAT DEBUG ERROR: OpenAI API key not configured");
5973 - $this->last_vectorstore_error = 'Vector Store search failed: OpenAI API key is not configured.';
5974 - $this->current_valid_urls = [];
5975 - return '';
5976 - }
5977 -
5978 - // Get Vector Store configuration
5979 - if (empty($vectorstore_config)) {
5980 - $vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
5981 - }
5982 -
5983 - $vectorstore_ids_string = $vectorstore_config['vectorstore_ids'] ?? '';
5984 - $max_results = $vectorstore_config['max_results'] ?? 5;
5985 -
5986 - if (empty($vectorstore_ids_string)) {
5987 - //error_log("MXCHAT DEBUG ERROR: No Vector Store IDs configured");
5988 - $this->last_vectorstore_error = 'Vector Store search failed: No Vector Store IDs are configured for this bot.';
5989 - $this->current_valid_urls = [];
5990 - return '';
5991 - }
5992 -
5993 - // Parse Vector Store IDs
5994 - $vectorstore_ids = array_map('trim', explode(',', $vectorstore_ids_string));
5995 - $vectorstore_ids = array_filter($vectorstore_ids); // Remove empty values
5996 -
5997 - //error_log("MXCHAT DEBUG: Vector Store IDs: " . implode(', ', $vectorstore_ids));
5998 - //error_log("MXCHAT DEBUG: Max results: " . $max_results);
5999 -
6000 - // Initialize similarity analysis storage
6001 - $this->last_similarity_analysis = [
6002 - 'knowledge_base_type' => 'OpenAI Vector Store',
6003 - 'bot_id' => $bot_id,
6004 - 'vectorstore_ids' => $vectorstore_ids,
6005 - 'top_matches' => [],
6006 - 'threshold_used' => 0,
6007 - 'total_checked' => 0
6008 - ];
6009 -
6010 - $valid_urls = [];
6011 -
6012 - // Get the selected model
6013 - $bot_options = $this->get_bot_options($bot_id);
6014 - $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
6015 - $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
6016 -
6017 - // Verify it's an OpenAI model
6018 - if (!$this->is_openai_chat_model($selected_model)) {
6019 - //error_log("MXCHAT DEBUG ERROR: Vector Store search requires OpenAI model. Current: " . $selected_model);
6020 - $this->last_vectorstore_error = 'Vector Store search requires an OpenAI model. Current model: ' . $selected_model;
6021 - $this->current_valid_urls = [];
6022 - return '';
6023 - }
6024 -
6025 - // Use OpenAI Responses API with file_search tool
6026 - $request_body = array(
6027 - 'model' => $selected_model,
6028 - 'input' => $user_query,
6029 - 'tools' => array(
6030 - array(
6031 - 'type' => 'file_search',
6032 - 'vector_store_ids' => $vectorstore_ids,
6033 - 'max_num_results' => intval($max_results)
6034 - )
6035 - ),
6036 - 'include' => array('output[*].file_search_call.search_results')
6037 - );
6038 -
6039 - //error_log("MXCHAT VECTORSTORE: ========== REQUEST START ==========");
6040 - //error_log("MXCHAT VECTORSTORE: Model: " . $selected_model);
6041 - //error_log("MXCHAT VECTORSTORE: Query: " . substr($user_query, 0, 200));
6042 - //error_log("MXCHAT VECTORSTORE: Vector Store IDs: " . implode(', ', $vectorstore_ids));
6043 - //error_log("MXCHAT VECTORSTORE: Max Results: " . $max_results);
6044 - //error_log("MXCHAT VECTORSTORE: Request body: " . wp_json_encode($request_body));
6045 -
6046 - $response = wp_remote_post('https://api.openai.com/v1/responses', array(
6047 - 'headers' => array(
6048 - 'Authorization' => 'Bearer ' . $api_key,
6049 - 'Content-Type' => 'application/json'
6050 - ),
6051 - 'body' => wp_json_encode($request_body),
6052 - 'timeout' => 60
6053 - ));
6054 -
6055 - if (is_wp_error($response)) {
6056 - //error_log("MXCHAT VECTORSTORE ERROR: WP Error: " . $response->get_error_message());
6057 - $this->last_vectorstore_error = 'Vector Store API request failed: ' . $response->get_error_message();
6058 - $this->current_valid_urls = [];
6059 - return '';
6060 - }
6061 -
6062 - $response_code = wp_remote_retrieve_response_code($response);
6063 - //error_log("MXCHAT VECTORSTORE: Response code: " . $response_code);
6064 -
6065 - $response_body = wp_remote_retrieve_body($response);
6066 - //error_log("MXCHAT VECTORSTORE: Raw response (first 2000 chars): " . substr($response_body, 0, 2000));
6067 -
6068 - if ($response_code !== 200) {
6069 - //error_log("MXCHAT VECTORSTORE ERROR: API error response: " . $response_body);
6070 - $api_error_detail = '';
6071 - $decoded_error = json_decode($response_body, true);
6072 - if (isset($decoded_error['error']['message'])) {
6073 - $api_error_detail = $decoded_error['error']['message'];
6074 - }
6075 - $this->last_vectorstore_error = 'Vector Store API returned HTTP ' . $response_code . ($api_error_detail ? ': ' . $api_error_detail : '');
6076 - $this->current_valid_urls = [];
6077 - return '';
6078 - }
6079 - $result = json_decode($response_body, true);
6080 -
6081 - if (json_last_error() !== JSON_ERROR_NONE) {
6082 - //error_log("MXCHAT VECTORSTORE ERROR: JSON decode error: " . json_last_error_msg());
6083 - $this->last_vectorstore_error = 'Vector Store response could not be parsed: ' . json_last_error_msg();
6084 - $this->current_valid_urls = [];
6085 - return '';
6086 - }
6087 -
6088 - // Debug: Log the structure of the result
6089 - //error_log("MXCHAT VECTORSTORE: Result keys: " . implode(', ', array_keys($result)));
6090 - if (isset($result['output'])) {
6091 - //error_log("MXCHAT VECTORSTORE: Output count: " . count($result['output']));
6092 - foreach ($result['output'] as $idx => $out) {
6093 - //error_log("MXCHAT VECTORSTORE: Output[$idx] type: " . ($out['type'] ?? 'unknown'));
6094 - //error_log("MXCHAT VECTORSTORE: Output[$idx] keys: " . implode(', ', array_keys($out)));
6095 - }
6096 - } else {
6097 - //error_log("MXCHAT VECTORSTORE: No 'output' key in result!");
6098 - }
6099 -
6100 - // Extract file search results from the response
6101 - $content = '';
6102 - $matches_used = 0;
6103 - $all_matches = [];
6104 -
6105 - // The Responses API returns output array with tool results
6106 - if (isset($result['output']) && is_array($result['output'])) {
6107 - foreach ($result['output'] as $output_item) {
6108 - // Look for file_search_call results
6109 - if (isset($output_item['type']) && $output_item['type'] === 'file_search_call') {
6110 - //error_log("MXCHAT VECTORSTORE: Found file_search_call output item");
6111 - //error_log("MXCHAT VECTORSTORE: file_search_call keys: " . implode(', ', array_keys($output_item)));
6112 -
6113 - // Check for search_results in the output item directly
6114 - $search_results = $output_item['search_results'] ?? $output_item['results'] ?? [];
6115 - //error_log("MXCHAT VECTORSTORE: Search results count: " . count($search_results));
6116 -
6117 - if (empty($search_results)) {
6118 - //error_log("MXCHAT VECTORSTORE: No search results found in file_search_call");
6119 - //error_log("MXCHAT VECTORSTORE: file_search_call content: " . wp_json_encode($output_item));
6120 - }
6121 -
6122 - foreach ($search_results as $index => $search_result) {
6123 - $filename = $search_result['filename'] ?? '';
6124 - $score = $search_result['score'] ?? 0;
6125 - $text_content = '';
6126 -
6127 - // Extract text content from the result
6128 - // The text can be directly on the result OR nested under content array
6129 - if (isset($search_result['text']) && !empty($search_result['text'])) {
6130 - // Direct text field (OpenAI's actual format)
6131 - $text_content = $search_result['text'];
6132 - //error_log("MXCHAT VECTORSTORE: Found text directly on result[$index], length: " . strlen($text_content));
6133 - } elseif (isset($search_result['content']) && is_array($search_result['content'])) {
6134 - // Nested content array format
6135 - foreach ($search_result['content'] as $content_item) {
6136 - if (isset($content_item['text'])) {
6137 - $text_content .= $content_item['text'] . "\n";
6138 - }
6139 - }
6140 - //error_log("MXCHAT VECTORSTORE: Found text in content array for result[$index], length: " . strlen($text_content));
6141 - } else {
6142 - //error_log("MXCHAT VECTORSTORE: No text found for result[$index]. Keys: " . implode(', ', array_keys($search_result)));
6143 - }
6144 -
6145 - if (!empty($text_content)) {
6146 - $content .= "## Reference " . ($matches_used + 1) . " ##\n";
6147 - $content .= trim($text_content) . "\n\n";
6148 -
6149 - if (!empty($filename)) {
6150 - $content .= "Source: " . $filename . "\n\n";
6151 - }
6152 -
6153 - // Extract URLs from content
6154 - preg_match_all(
6155 - '#\bhttps?://[^\s<>"\']+#i',
6156 - $text_content,
6157 - $content_urls
6158 - );
6159 - if (!empty($content_urls[0])) {
6160 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
6161 - }
6162 -
6163 - $matches_used++;
6164 - }
6165 -
6166 - // Store for similarity analysis
6167 - $all_matches[] = [
6168 - 'document_id' => $filename ?: ('result_' . $index),
6169 - 'similarity' => $score,
6170 - 'similarity_percentage' => round($score * 100, 2),
6171 - 'above_threshold' => true,
6172 - 'source_display' => $filename,
6173 - 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
6174 - 'used_for_context' => true,
6175 - 'role_restriction' => 'public',
6176 - 'has_access' => true,
6177 - 'filtered_out' => false
6178 - ];
6179 - }
6180 - }
6181 -
6182 - // Also check for message content with annotations (citations)
6183 - if (isset($output_item['type']) && $output_item['type'] === 'message') {
6184 - if (isset($output_item['content']) && is_array($output_item['content'])) {
6185 - foreach ($output_item['content'] as $content_block) {
6186 - if (isset($content_block['annotations']) && is_array($content_block['annotations'])) {
6187 - foreach ($content_block['annotations'] as $annotation) {
6188 - if (isset($annotation['filename'])) {
6189 - $filename = $annotation['filename'];
6190 - $score = $annotation['score'] ?? 0;
6191 - $text_content = '';
6192 -
6193 - if (isset($annotation['content']) && is_array($annotation['content'])) {
6194 - foreach ($annotation['content'] as $ann_content) {
6195 - if (isset($ann_content['text'])) {
6196 - $text_content .= $ann_content['text'] . "\n";
6197 - }
6198 - }
6199 - }
6200 -
6201 - if (!empty($text_content) && $matches_used < $max_results) {
6202 - $content .= "## Reference " . ($matches_used + 1) . " ##\n";
6203 - $content .= trim($text_content) . "\n\n";
6204 - $content .= "Source: " . $filename . "\n\n";
6205 -
6206 - preg_match_all(
6207 - '#\bhttps?://[^\s<>"\']+#i',
6208 - $text_content,
6209 - $content_urls
6210 - );
6211 - if (!empty($content_urls[0])) {
6212 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
6213 - }
6214 -
6215 - $matches_used++;
6216 -
6217 - $all_matches[] = [
6218 - 'document_id' => $filename,
6219 - 'similarity' => $score,
6220 - 'similarity_percentage' => round($score * 100, 2),
6221 - 'above_threshold' => true,
6222 - 'source_display' => $filename,
6223 - 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
6224 - 'used_for_context' => true,
6225 - 'role_restriction' => 'public',
6226 - 'has_access' => true,
6227 - 'filtered_out' => false
6228 - ];
6229 - }
6230 - }
6231 - }
6232 - }
6233 - }
6234 - }
6235 - }
6236 - }
6237 - }
6238 -
6239 - // Store for testing panel
6240 - $this->last_similarity_analysis['top_matches'] = $all_matches;
6241 - $this->last_similarity_analysis['total_checked'] = count($all_matches);
6242 -
6243 - // Store unique valid URLs for validation
6244 - $this->current_valid_urls = array_unique($valid_urls);
6245 -
6246 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
6247 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
6248 -
6249 - //error_log("MXCHAT VECTORSTORE: ========== SEARCH COMPLETE ==========");
6250 - //error_log("MXCHAT VECTORSTORE: Matches used: " . $matches_used);
6251 - //error_log("MXCHAT VECTORSTORE: All matches count: " . count($all_matches));
6252 - //error_log("MXCHAT VECTORSTORE: Content length: " . strlen($content));
6253 - if ($matches_used > 0) {
6254 - //error_log("MXCHAT VECTORSTORE: Content preview: " . substr($content, 0, 500));
6255 - }
6256 -
6257 - // Check if citation links are enabled
6258 - $citation_links_enabled = ($mxchat_options['citation_links_toggle'] ?? 'on') === 'on';
6259 -
6260 - // Add response guidelines
6261 - if ($matches_used === 0) {
6262 - //error_log("MXCHAT VECTORSTORE: No matches found - returning empty reference message");
6263 - $content = "No reference information was found for this query.\n\n";
6264 - } else {
6265 - // Build response guidelines based on citation links setting
6266 - $content .= "\n## Response Guidelines ##\n" .
6267 - "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
6268 - "Be conversational and friendly, but never mention your knowledge base or training data. " .
6269 - "If you don't have specific information or are uncertain about any details, it's always " .
6270 - "better to honestly say you don't know rather than making up or guessing at answers. " .
6271 - "When information is incomplete, let them know you are unsure.\n\n";
6272 -
6273 - // Only add hyperlink instructions if citation links are enabled
6274 - if ($citation_links_enabled) {
6275 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
6276 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about.";
6277 - } else {
6278 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
6279 - "Simply provide helpful answers based on the reference information without citing sources.";
6280 - }
6281 - }
6282 -
6283 - //error_log("MXCHAT DEBUG: Vector Store search complete. Matches used: " . $matches_used);
6284 -
6285 - return trim($content);
6286 -}
6287 -
6288 -/**
6289 - * Check if the given model is an OpenAI chat model
6290 - *
6291 - * @param string $model The model ID
6292 - * @return bool True if it's an OpenAI model
6293 - */
6294 -private function is_openai_chat_model($model) {
6295 - $openai_prefixes = array('gpt-', 'o1-', 'o3-');
6296 - foreach ($openai_prefixes as $prefix) {
6297 - if (strpos($model, $prefix) === 0) {
6298 - return true;
6299 - }
6300 - }
6301 - return false;
6302 -}
6303 -
6304 -/**
6305 - * Get bot-specific Vector Store configuration
6306 - *
6307 - * @param string $bot_id The bot ID
6308 - * @return array Configuration array
6309 - */
6310 -private function get_bot_vectorstore_config($bot_id = 'default') {
6311 - $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
6312 -
6313 - // Default global settings
6314 - $default_config = array(
6315 - 'use_vectorstore' => ($vectorstore_options['mxchat_use_openai_vectorstore'] ?? '0') === '1',
6316 - 'vectorstore_ids' => $vectorstore_options['mxchat_vectorstore_ids'] ?? '',
6317 - 'max_results' => $vectorstore_options['mxchat_vectorstore_max_results'] ?? 5
6318 - );
6319 -
6320 - // Allow multi-bot plugin to override with bot-specific settings
6321 - $bot_config = apply_filters('mxchat_get_bot_vectorstore_config', $default_config, $bot_id);
6322 -
6323 - // Preserve max_results from global settings if not set in bot config
6324 - if (!isset($bot_config['max_results'])) {
6325 - $bot_config['max_results'] = $default_config['max_results'];
6326 - }
6327 -
6328 - return $bot_config;
6329 -}
6330 -
6331 3832 private function mxchat_find_relevant_products($user_embedding) {
6332 3833 //error_log('MXChat Vector Search: Starting product search...');
6333 3834
6334 3835 // Retrieve the add-on settings from the database
@@ -6349,75 +3850,73 @@
6349 3850 }
6350 3851 private function find_relevant_products_wordpress($user_embedding) {
6351 3852 global $wpdb;
6352 3853 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3854 + $cache_key = 'mxchat_system_prompt_embeddings';
3855 + $batch_size = 500;
6353 3856
6354 - if (!is_array($user_embedding)) {
6355 - return '';
6356 - }
3857 + // Original WordPress database search logic
3858 + // [Previous implementation remains the same]
3859 + $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3860 + if ($embeddings === false) {
3861 + $embeddings = [];
3862 + $offset = 0;
6357 3863
6358 - // Streaming top-K pass: scan rows in small batches, keep only the top 3
6359 - // results above the similarity threshold. Peak memory is bounded by
6360 - // $batch_size embedding rows plus a 3-element top list.
6361 - $batch_size = 250;
6362 - $similarity_threshold = 0.85;
6363 - $top_k = 3;
6364 - $top_results = [];
6365 - $offset = 0;
3864 + do {
3865 + $query = $wpdb->prepare(
3866 + "SELECT id, embedding_vector
3867 + FROM {$system_prompt_table}
3868 + LIMIT %d OFFSET %d",
3869 + $batch_size,
3870 + $offset
3871 + );
6366 3872
6367 - do {
6368 - $batch = $wpdb->get_results($wpdb->prepare(
6369 - "SELECT id, embedding_vector
6370 - FROM {$system_prompt_table}
6371 - LIMIT %d OFFSET %d",
6372 - $batch_size,
6373 - $offset
6374 - ));
3873 + $batch = $wpdb->get_results($query);
3874 + if (empty($batch)) {
3875 + break;
3876 + }
6375 3877
6376 - if (empty($batch)) {
6377 - break;
6378 - }
3878 + $embeddings = array_merge($embeddings, $batch);
3879 + $offset += $batch_size;
6379 3880
6380 - foreach ($batch as $row) {
6381 - $database_embedding = $row->embedding_vector
6382 - ? unserialize($row->embedding_vector, ['allowed_classes' => false])
6383 - : null;
3881 + unset($batch);
6384 3882
6385 - if (!is_array($database_embedding)) {
6386 - unset($database_embedding);
6387 - continue;
6388 - }
3883 + } while (true);
6389 3884
3885 + if (empty($embeddings)) {
3886 + return '';
3887 + }
3888 + wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3889 + }
3890 +
3891 + $relevant_results = [];
3892 + foreach ($embeddings as $embedding) {
3893 + $database_embedding = $embedding->embedding_vector
3894 + ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3895 + : null;
3896 + if (is_array($database_embedding) && is_array($user_embedding)) {
6390 3897 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
6391 - unset($database_embedding);
6392 -
6393 - if ($similarity < $similarity_threshold) {
6394 - continue;
6395 - }
6396 -
6397 - // Insert into bounded top-K (kept sorted descending)
6398 - if (count($top_results) < $top_k) {
6399 - $top_results[] = ['id' => $row->id, 'similarity' => $similarity];
6400 - usort($top_results, function ($a, $b) {
6401 - return $b['similarity'] <=> $a['similarity'];
6402 - });
6403 - } elseif ($similarity > $top_results[$top_k - 1]['similarity']) {
6404 - $top_results[$top_k - 1] = ['id' => $row->id, 'similarity' => $similarity];
6405 - usort($top_results, function ($a, $b) {
6406 - return $b['similarity'] <=> $a['similarity'];
6407 - });
6408 - }
3898 + $relevant_results[] = [
3899 + 'id' => $embedding->id,
3900 + 'similarity' => $similarity
3901 + ];
6409 3902 }
3903 + unset($database_embedding);
3904 + }
6410 3905
6411 - unset($batch);
6412 - $offset += $batch_size;
6413 - } while (true);
3906 + // Use fixed threshold for products
3907 + $similarity_threshold = 0.85;
6414 3908
6415 - if (empty($top_results)) {
6416 - return '';
6417 - }
3909 + $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
3910 + return $result['similarity'] >= $similarity_threshold;
3911 + });
3912 + usort($relevant_results, function ($a, $b) {
3913 + return $b['similarity'] <=> $a['similarity'];
3914 + });
6418 3915
3916 + $top_results = array_slice($relevant_results, 0, 5);
6419 3917 $content = '';
3918 +
6420 3919 foreach ($top_results as $result) {
6421 3920 $chunk_content = $this->fetch_content_with_product_links($result['id']);
6422 3921 $content .= $chunk_content . "\n\n";
6423 3922 }
@@ -6423,10 +3922,8 @@
6423 3922 }
6424 3923
6425 3924 return trim($content);
6426 3925 }
6427 -
6428 -
6429 3926 private function find_relevant_products_pinecone($user_embedding) {
6430 3927 //error_log('Starting Pinecone product search...');
6431 3928
6432 3929 $options = get_option('mxchat_pinecone_addon_options', array());
@@ -6501,10 +3998,8 @@
6501 3998 }
6502 3999
6503 4000 return trim($content);
6504 4001 }
6505 -
6506 -
6507 4002 private function fetch_content_with_product_links($most_relevant_id) {
6508 4003 global $wpdb;
6509 4004 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
6510 4005
@@ -6524,85 +4019,12 @@
6524 4019 return null;
6525 4020 }
6526 4021
6527 4022 /**
6528 - * Get system instructions for a specific bot or default
6529 - * Checks for multi-bot add-on and uses bot-specific instructions if available
6530 - * Automatically strips URLs if citation links are disabled
6531 - * Replaces {visitor_name} placeholder with actual visitor name if available
6532 - *
6533 - * @param string $bot_id The bot ID to get instructions for
6534 - * @param string $session_id Optional session ID to lookup visitor name
4023 + * Modified streaming functions to include testing data
6535 4024 */
6536 -private function get_system_instructions($bot_id = 'default', $session_id = '') {
6537 - $instructions = '';
6538 4025
6539 - // Check if multi-bot add-on is active
6540 - if (class_exists('MxChat_Multi_Bot_Core_Manager') && $bot_id !== 'default') {
6541 - // Get bot-specific options from multi-bot add-on
6542 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
6543 -
6544 - // If bot has custom system instructions, use those
6545 - if (!empty($bot_options['system_prompt_instructions'])) {
6546 - $instructions = $bot_options['system_prompt_instructions'];
6547 - }
6548 - }
6549 -
6550 - // Fall back to default system instructions
6551 - if (empty($instructions)) {
6552 - $instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
6553 - }
6554 -
6555 - // Check if citation links are disabled - if so, strip URLs from instructions
6556 - $fresh_options = get_option('mxchat_options', []);
6557 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
6558 -
6559 - if (!$citation_links_enabled && !empty($instructions)) {
6560 - $instructions = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $instructions);
6561 - $instructions = preg_replace('/\s+/', ' ', trim($instructions)); // Clean up extra spaces
6562 - }
6563 -
6564 - // Replace {visitor_name} placeholder with actual visitor name if available
6565 - if (!empty($instructions) && !empty($session_id) && stripos($instructions, '{visitor_name}') !== false) {
6566 - $name_option_key = "mxchat_name_{$session_id}";
6567 - $visitor_name = get_option($name_option_key, '');
6568 -
6569 - if (!empty($visitor_name)) {
6570 - $instructions = str_ireplace('{visitor_name}', sanitize_text_field($visitor_name), $instructions);
6571 - } else {
6572 - // Remove placeholder if no name is available
6573 - $instructions = str_ireplace('{visitor_name}', '', $instructions);
6574 - $instructions = preg_replace('/\s{2,}/', ' ', trim($instructions)); // Clean up extra spaces
6575 - }
6576 - }
6577 -
6578 - // Allow developers to filter system instructions and process shortcodes
6579 - $instructions = apply_filters('mxchat_system_instructions', $instructions, $bot_id, $session_id);
6580 - $instructions = do_shortcode($instructions);
6581 -
6582 - return $instructions;
6583 -}
6584 -/**
6585 - * Get the current bot ID from session or request context
6586 - */
6587 -private function get_current_bot_id($session_id = '') {
6588 - // First, check if bot_id is passed in the current request
6589 - if (isset($_POST['bot_id']) && !empty($_POST['bot_id'])) {
6590 - return sanitize_key($_POST['bot_id']);
6591 - }
6592 -
6593 - // If not in POST, try to get it from session data
6594 - if (!empty($session_id)) {
6595 - $bot_id = get_option("mxchat_session_bot_{$session_id}", '');
6596 - if (!empty($bot_id)) {
6597 - return $bot_id;
6598 - }
6599 - }
6600 -
6601 - // Fall back to default
6602 - return 'default';
6603 -}
6604 -private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $gemini_api_key, $openrouter_api_key, $conversation_history, $streaming = false, $session_id = '', $testing_data = null, $selected_model = 'gpt-5.1-chat-latest') {
4026 +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) {
6605 4027 try {
6606 4028 if (!$relevant_content) {
6607 4029 $error_response = [
6608 4030 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
@@ -6608,74 +4030,25 @@
6608 4030 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
6609 4031 'error_code' => 'no_relevant_content'
6610 4032 ];
6611 4033
4034 + // Add testing data to error response if available
6612 4035 if ($testing_data !== null) {
6613 4036 $error_response['testing_data'] = $testing_data;
4037 + //error_log("MxChat Testing: Added testing data to no_relevant_content error");
6614 4038 }
6615 4039
6616 4040 return $error_response;
6617 4041 }
6618 4042
4043 + // Ensure conversation_history is an array
6619 4044 if (!is_array($conversation_history)) {
6620 4045 $conversation_history = array();
6621 4046 }
6622 4047
6623 - // Check if this is an OpenRouter model
6624 - if ($selected_model === 'openrouter') {
6625 - // Get the actual OpenRouter model from options
6626 - $openrouter_selected_model = $this->options['openrouter_selected_model'] ?? '';
6627 -
6628 - if (empty($openrouter_selected_model)) {
6629 - $error_response = [
6630 - 'error' => esc_html__('No OpenRouter model selected. Please select a model in settings.', 'mxchat'),
6631 - 'error_code' => 'no_openrouter_model_selected'
6632 - ];
6633 - if ($testing_data !== null) {
6634 - $error_response['testing_data'] = $testing_data;
6635 - }
6636 - return $error_response;
6637 - }
6638 -
6639 - if (empty($openrouter_api_key)) {
6640 - $error_response = [
6641 - 'error' => esc_html__('OpenRouter API key is not configured', 'mxchat'),
6642 - 'error_code' => 'missing_openrouter_api_key'
6643 - ];
6644 - if ($testing_data !== null) {
6645 - $error_response['testing_data'] = $testing_data;
6646 - }
6647 - return $error_response;
6648 - }
6649 -
6650 - if ($streaming) {
6651 - return $this->mxchat_generate_response_openrouter_stream(
6652 - $openrouter_selected_model,
6653 - $openrouter_api_key,
6654 - $conversation_history,
6655 - $relevant_content,
6656 - $session_id,
6657 - $testing_data
6658 - );
6659 - } else {
6660 - $response = $this->mxchat_generate_response_openrouter(
6661 - $openrouter_selected_model,
6662 - $openrouter_api_key,
6663 - $conversation_history,
6664 - $relevant_content
6665 - );
6666 - }
6667 -
6668 - if (is_array($response) && isset($response['error'])) {
6669 - if ($testing_data !== null) {
6670 - $response['testing_data'] = $testing_data;
6671 - }
6672 - return $response;
6673 - }
6674 -
6675 - return $response;
6676 - }
6677 -
4048 + // Get selected model with default fallback
4049 + $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
4050 +
6678 4051 // Extract model prefix to determine the provider
6679 4052 $model_parts = explode('-', $selected_model);
6680 4053 $provider = strtolower($model_parts[0]);
6681 4054
@@ -6717,9 +4090,9 @@
6717 4090 $claude_api_key,
6718 4091 $conversation_history,
6719 4092 $relevant_content,
6720 4093 $session_id,
6721 - $testing_data
4094 + $testing_data // Pass testing data
6722 4095 );
6723 4096 } else {
6724 4097 $response = $this->mxchat_generate_response_claude(
6725 4098 $selected_model,
@@ -6747,9 +4120,9 @@
6747 4120 $xai_api_key,
6748 4121 $conversation_history,
6749 4122 $relevant_content,
6750 4123 $session_id,
6751 - $testing_data
4124 + $testing_data // Pass testing data
6752 4125 );
6753 4126 } else {
6754 4127 $response = $this->mxchat_generate_response_xai(
6755 4128 $selected_model,
@@ -6777,9 +4150,9 @@
6777 4150 $deepseek_api_key,
6778 4151 $conversation_history,
6779 4152 $relevant_content,
6780 4153 $session_id,
6781 - $testing_data
4154 + $testing_data // Pass testing data
6782 4155 );
6783 4156 } else {
6784 4157 $response = $this->mxchat_generate_response_deepseek(
6785 4158 $selected_model,
@@ -6789,38 +4162,8 @@
6789 4162 );
6790 4163 }
6791 4164 break;
6792 4165
6793 - case 'custom':
6794 - // Custom (OpenAI-compatible) provider — Ollama, LM Studio, vLLM, llama.cpp, Azure OpenAI
6795 - $cp_base_url = isset($this->options['custom_provider_base_url']) ? trim((string) $this->options['custom_provider_base_url']) : '';
6796 - if (empty($cp_base_url)) {
6797 - $error_response = [
6798 - 'error' => esc_html__('Custom provider is not configured. Set Base URL in MxChat → API Keys → Custom Provider.', 'mxchat'),
6799 - 'error_code' => 'missing_custom_provider_base_url'
6800 - ];
6801 - if ($testing_data !== null) {
6802 - $error_response['testing_data'] = $testing_data;
6803 - }
6804 - return $error_response;
6805 - }
6806 - if ($streaming) {
6807 - return $this->mxchat_generate_response_custom_stream(
6808 - $selected_model,
6809 - $conversation_history,
6810 - $relevant_content,
6811 - $session_id,
6812 - $testing_data
6813 - );
6814 - } else {
6815 - $response = $this->mxchat_generate_response_custom(
6816 - $selected_model,
6817 - $conversation_history,
6818 - $relevant_content
6819 - );
6820 - }
6821 - break;
6822 -
6823 4166 case 'gpt':
6824 4167 case 'o1':
6825 4168 if (empty($api_key)) {
6826 4169 $error_response = [
@@ -6831,27 +4174,9 @@
6831 4174 $error_response['testing_data'] = $testing_data;
6832 4175 }
6833 4176 return $error_response;
6834 4177 }
6835 -
6836 - // Check if web search is enabled for this OpenAI model
6837 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
6838 - // Models that don't support web search
6839 - $unsupported_web_search_models = array('gpt-4.1-nano');
6840 - $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
6841 -
6842 - if ($web_search_enabled && $model_supports_web_search) {
6843 - // Use Responses API (required for some models, or when web search is enabled)
6844 - return $this->mxchat_generate_response_openai_web_search(
6845 - $selected_model,
6846 - $api_key,
6847 - $conversation_history,
6848 - $relevant_content,
6849 - $session_id,
6850 - $testing_data,
6851 - $streaming
6852 - );
6853 - } elseif ($streaming) {
4178 + if ($streaming) {
6854 4179 return $this->mxchat_generate_response_openai_stream(
6855 4180 $selected_model,
6856 4181 $api_key,
6857 4182 $conversation_history,
@@ -6856,9 +4181,9 @@
6856 4181 $api_key,
6857 4182 $conversation_history,
6858 4183 $relevant_content,
6859 4184 $session_id,
6860 - $testing_data
4185 + $testing_data // Pass testing data
6861 4186 );
6862 4187 } else {
6863 4188 $response = $this->mxchat_generate_response_openai(
6864 4189 $selected_model,
@@ -6869,8 +4194,9 @@
6869 4194 }
6870 4195 break;
6871 4196
6872 4197 default:
4198 + // Default to OpenAI for custom models or unrecognized prefixes
6873 4199 if (empty($api_key)) {
6874 4200 $error_response = [
6875 4201 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
6876 4202 'error_code' => 'missing_openai_api_key'
@@ -6879,25 +4205,9 @@
6879 4205 $error_response['testing_data'] = $testing_data;
6880 4206 }
6881 4207 return $error_response;
6882 4208 }
6883 -
6884 - // Check if web search is enabled (default case also handles OpenAI models)
6885 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
6886 - $unsupported_web_search_models = array('gpt-4.1-nano');
6887 - $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
6888 -
6889 - if ($web_search_enabled && $model_supports_web_search) {
6890 - return $this->mxchat_generate_response_openai_web_search(
6891 - $selected_model,
6892 - $api_key,
6893 - $conversation_history,
6894 - $relevant_content,
6895 - $session_id,
6896 - $testing_data,
6897 - $streaming
6898 - );
6899 - } elseif ($streaming) {
4209 + if ($streaming) {
6900 4210 return $this->mxchat_generate_response_openai_stream(
6901 4211 $selected_model,
6902 4212 $api_key,
6903 4213 $conversation_history,
@@ -6902,9 +4212,9 @@
6902 4212 $api_key,
6903 4213 $conversation_history,
6904 4214 $relevant_content,
6905 4215 $session_id,
6906 - $testing_data
4216 + $testing_data // Pass testing data
6907 4217 );
6908 4218 } else {
6909 4219 $response = $this->mxchat_generate_response_openai(
6910 4220 $selected_model,
@@ -6915,18 +4225,24 @@
6915 4225 }
6916 4226 break;
6917 4227 }
6918 4228
4229 + // Check if the response is an error array from the provider-specific function
6919 4230 if (is_array($response) && isset($response['error'])) {
4231 + // Add testing data to error response if available
6920 4232 if ($testing_data !== null) {
6921 4233 $response['testing_data'] = $testing_data;
4234 + //error_log("MxChat Testing: Added testing data to provider error response");
6922 4235 }
6923 - return $response;
4236 + return $response; // Pass through the error with testing data
6924 4237 }
6925 4238
4239 + // For successful non-streaming responses, we don't add testing data here
4240 + // because it will be added in the main handler
6926 4241 return $response;
6927 4242
6928 4243 } catch (Exception $e) {
4244 + //error_log('MXChat Error: ' . $e->getMessage());
6929 4245 $error_response = [
6930 4246 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
6931 4247 'error_code' => 'system_exception',
6932 4248 'exception_details' => $e->getMessage()
@@ -6931,230 +4247,23 @@
6931 4247 'error_code' => 'system_exception',
6932 4248 'exception_details' => $e->getMessage()
6933 4249 ];
6934 4250
4251 + // Add testing data to exception response if available
6935 4252 if ($testing_data !== null) {
6936 4253 $error_response['testing_data'] = $testing_data;
4254 + //error_log("MxChat Testing: Added testing data to exception response");
6937 4255 }
6938 4256
6939 4257 return $error_response;
6940 4258 }
6941 4259 }
6942 -private function mxchat_generate_response_openrouter_stream($selected_model, $openrouter_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
6943 - try {
6944 - $bot_id = $this->get_current_bot_id($session_id);
6945 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
6946 -
6947 - if (!is_array($conversation_history)) {
6948 - $conversation_history = array();
6949 - }
6950 4260
6951 - $formatted_conversation = array();
6952 -
6953 - $formatted_conversation[] = array(
6954 - 'role' => 'system',
6955 - 'content' => $system_prompt_instructions . " " . $relevant_content
6956 - );
6957 -
6958 - foreach ($conversation_history as $message) {
6959 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
6960 - $role = $message['role'];
6961 - if ($role === 'bot' || $role === 'agent') {
6962 - $role = 'assistant';
6963 - }
6964 - if (!in_array($role, ['system', 'assistant', 'user'])) {
6965 - $role = 'user';
6966 - }
6967 - $formatted_conversation[] = array(
6968 - 'role' => $role,
6969 - 'content' => $message['content']
6970 - );
6971 - }
6972 - }
6973 -
6974 - if (headers_sent() || !function_exists('curl_init')) {
6975 - $regular_response = $this->mxchat_generate_response_openrouter(
6976 - $selected_model,
6977 - $openrouter_api_key,
6978 - $conversation_history,
6979 - $relevant_content
6980 - );
6981 -
6982 - // Save bot response to transcript
6983 - if (!empty($regular_response) && !empty($session_id)) {
6984 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
6985 - }
6986 -
6987 - $response_data = [
6988 - 'text' => $regular_response,
6989 - 'html' => '',
6990 - 'session_id' => $session_id
6991 - ];
6992 -
6993 - if ($testing_data !== null) {
6994 - $response_data['testing_data'] = $testing_data;
6995 - }
6996 -
6997 - header('Content-Type: application/json');
6998 - echo json_encode($response_data);
6999 - return true;
7000 - }
7001 -
7002 - $body = json_encode([
7003 - 'model' => $selected_model,
7004 - 'messages' => $formatted_conversation,
7005 - 'temperature' => 1,
7006 - 'stream' => true
7007 - ]);
7008 -
7009 - // Setup streaming headers now that we know we're actually streaming
7010 - $this->setup_streaming_headers();
7011 -
7012 - $ch = curl_init();
7013 - curl_setopt($ch, CURLOPT_URL, 'https://openrouter.ai/api/v1/chat/completions');
7014 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
7015 - curl_setopt($ch, CURLOPT_POST, true);
7016 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
7017 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
7018 - 'Content-Type: application/json',
7019 - 'Authorization: Bearer ' . $openrouter_api_key,
7020 - 'HTTP-Referer: ' . home_url(),
7021 - 'X-Title: ' . get_bloginfo('name')
7022 - ));
7023 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7024 - curl_setopt($ch, CURLOPT_TIMEOUT, 60);
7025 -
7026 - $full_response = '';
7027 - $stream_started = false;
7028 - $buffer = '';
7029 -
7030 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
7031 - if (!$stream_started && $testing_data !== null) {
7032 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
7033 - flush();
7034 - $stream_started = true;
7035 - }
7036 -
7037 - $buffer .= $data;
7038 - $lines = explode("\n", $buffer);
7039 - $buffer = array_pop($lines);
7040 -
7041 - foreach ($lines as $line) {
7042 - if (trim($line) === '') {
7043 - continue;
7044 - }
7045 -
7046 - if (strpos($line, 'data: ') !== 0) {
7047 - continue;
7048 - }
7049 -
7050 - $json_str = substr($line, 6);
7051 -
7052 - if (trim($json_str) === '[DONE]') {
7053 - echo "data: [DONE]\n\n";
7054 - flush();
7055 - continue;
7056 - }
7057 -
7058 - $json = json_decode(trim($json_str), true);
7059 - if ($json && isset($json['choices'][0]['delta']['content'])) {
7060 - $content = $json['choices'][0]['delta']['content'];
7061 - $full_response .= $content;
7062 -
7063 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
7064 - flush();
7065 - }
7066 - }
7067 -
7068 - return strlen($data);
7069 - });
7070 -
7071 - $response = curl_exec($ch);
7072 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7073 -
7074 - if (curl_errno($ch) || $http_code !== 200) {
7075 - curl_close($ch);
7076 -
7077 - $regular_response = $this->mxchat_generate_response_openrouter(
7078 - $selected_model,
7079 - $openrouter_api_key,
7080 - $conversation_history,
7081 - $relevant_content
7082 - );
7083 -
7084 - $response_data = [
7085 - 'text' => $regular_response,
7086 - 'html' => '',
7087 - 'session_id' => $session_id
7088 - ];
7089 -
7090 - if ($testing_data !== null) {
7091 - $response_data['testing_data'] = $testing_data;
7092 - }
7093 -
7094 - header('Content-Type: application/json');
7095 - echo json_encode($response_data);
7096 - return true;
7097 - }
7098 -
7099 - curl_close($ch);
7100 -
7101 - if (!empty($full_response) && !empty($session_id)) {
7102 - // Prepare RAG context for streaming response
7103 - $rag_context_for_storage = null;
7104 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7105 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7106 -
7107 - if ($has_rag_data || $has_action_data) {
7108 - $rag_context_for_storage = [];
7109 -
7110 - if ($has_rag_data) {
7111 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7112 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7113 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7114 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7115 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7116 - }
7117 -
7118 - if ($has_action_data) {
7119 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7120 - }
7121 - }
7122 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
7123 - }
7124 -
7125 - return true;
7126 -
7127 - } catch (Exception $e) {
7128 - $regular_response = $this->mxchat_generate_response_openrouter(
7129 - $selected_model,
7130 - $openrouter_api_key,
7131 - $conversation_history,
7132 - $relevant_content
7133 - );
7134 -
7135 - $response_data = [
7136 - 'text' => $regular_response,
7137 - 'html' => '',
7138 - 'session_id' => $session_id
7139 - ];
7140 -
7141 - if ($testing_data !== null) {
7142 - $response_data['testing_data'] = $testing_data;
7143 - }
7144 -
7145 - header('Content-Type: application/json');
7146 - echo json_encode($response_data);
7147 - return true;
7148 - }
7149 -}
7150 4261 private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
7151 4262 try {
7152 - $bot_id = $this->get_current_bot_id($session_id);
4263 + // Get system prompt instructions from options
4264 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
7153 4265
7154 - // Get system prompt instructions using centralized function
7155 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
7156 -
7157 4266 // Ensure conversation_history is an array
7158 4267 if (!is_array($conversation_history)) {
7159 4268 $conversation_history = array();
7160 4269 }
@@ -7185,8 +4294,9 @@
7185 4294
7186 4295 // Check if we can actually stream
7187 4296 if (headers_sent() || !function_exists('curl_init')) {
7188 4297 // Fallback to regular response with testing data
4298 + //error_log("MxChat: OpenAI streaming not possible, falling back to regular response");
7189 4299 $regular_response = $this->mxchat_generate_response_openai(
7190 4300 $selected_model,
7191 4301 $api_key,
7192 4302 $conversation_history,
@@ -7192,13 +4302,8 @@
7192 4302 $conversation_history,
7193 4303 $relevant_content
7194 4304 );
7195 4305
7196 - // Save bot response to transcript
7197 - if (!empty($regular_response) && !empty($session_id)) {
7198 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
7199 - }
7200 -
7201 4306 $response_data = [
7202 4307 'text' => $regular_response,
7203 4308 'html' => '',
7204 4309 'session_id' => $session_id
@@ -7205,8 +4310,9 @@
7205 4310 ];
7206 4311
7207 4312 if ($testing_data !== null) {
7208 4313 $response_data['testing_data'] = $testing_data;
4314 + //error_log("MxChat Testing: Added testing data to OpenAI fallback response");
7209 4315 }
7210 4316
7211 4317 header('Content-Type: application/json');
7212 4318 echo json_encode($response_data);
@@ -7212,47 +4318,16 @@
7212 4318 echo json_encode($response_data);
7213 4319 return true;
7214 4320 }
7215 4321
7216 - // Check if this is a GPT-5 model (supports reasoning_effort parameter)
7217 - $is_gpt5_model = (
7218 - strpos($selected_model, 'gpt-5') === 0 ||
7219 - $selected_model === 'gpt-5.2' ||
7220 - $selected_model === 'gpt-5.1-2025-11-13' ||
7221 - $selected_model === 'gpt-5' ||
7222 - $selected_model === 'gpt-5-mini' ||
7223 - $selected_model === 'gpt-5-nano'
7224 - );
7225 -
7226 - // Build request body with optimal settings for fast streaming
7227 - $request_body = [
4322 + // Prepare the request body with stream: true
4323 + $body = json_encode([
7228 4324 'model' => $selected_model,
7229 4325 'messages' => $formatted_conversation,
7230 4326 'temperature' => 1,
7231 4327 'stream' => true
7232 - ];
4328 + ]);
7233 4329
7234 - // Add reasoning_effort only for GPT-5 models that support it
7235 - // These chat models don't support reasoning_effort parameter
7236 - $no_reasoning_models = array('gpt-5.2', 'gpt-5.1-chat-latest', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
7237 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
7238 - // GPT-5.1 uses 'low' instead of 'minimal'
7239 - if ($selected_model === 'gpt-5.1-2025-11-13') {
7240 - $request_body['reasoning_effort'] = 'low';
7241 - } elseif ($selected_model === 'gpt-5.5') {
7242 - $request_body['reasoning_effort'] = 'none';
7243 - } elseif ($selected_model === 'gpt-5.4') {
7244 - $request_body['reasoning_effort'] = 'none';
7245 - } else {
7246 - $request_body['reasoning_effort'] = 'minimal';
7247 - }
7248 - }
7249 -
7250 - $body = json_encode($request_body);
7251 -
7252 - // Setup streaming headers now that we know we're actually streaming
7253 - $this->setup_streaming_headers();
7254 -
7255 4330 // Use cURL for streaming support
7256 4331 $ch = curl_init();
7257 4332 curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
7258 4333 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -7266,54 +4341,39 @@
7266 4341 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
7267 4342
7268 4343 $full_response = ''; // Accumulate full response for saving
7269 4344 $stream_started = false;
7270 - $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
7271 4345
7272 4346 // Buffer control for real-time streaming
7273 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
4347 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
7274 4348 // Send testing data as the first event if available
7275 4349 if (!$stream_started && $testing_data !== null) {
7276 4350 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
7277 4351 flush();
7278 4352 $stream_started = true;
4353 + //error_log("MxChat Testing: Sent testing data in OpenAI stream");
7279 4354 }
7280 4355
7281 - // CRITICAL FIX: Append new data to buffer
7282 - $buffer .= $data;
4356 + // Process each chunk of data
4357 + $lines = explode("\n", $data);
7283 4358
7284 - // Process complete lines only
7285 - $lines = explode("\n", $buffer);
7286 -
7287 - // CRITICAL FIX: Keep the last incomplete line in the buffer
7288 - // The last element might be incomplete, so keep it in buffer
7289 - $buffer = array_pop($lines);
7290 -
7291 4359 foreach ($lines as $line) {
7292 - // Skip empty lines
7293 - if (trim($line) === '') {
4360 + if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
7294 4361 continue;
7295 4362 }
7296 4363
7297 - // Only process lines that start with "data: "
7298 - if (strpos($line, 'data: ') !== 0) {
7299 - continue;
7300 - }
7301 -
7302 4364 $json_str = substr($line, 6); // Remove 'data: ' prefix
7303 4365
7304 - if (trim($json_str) === '[DONE]') {
4366 + if ($json_str === '[DONE]') {
7305 4367 echo "data: [DONE]\n\n";
7306 4368 flush();
7307 4369 continue;
7308 4370 }
7309 4371
7310 - // Try to decode JSON
7311 - $json = json_decode(trim($json_str), true);
7312 - if ($json && isset($json['choices'][0]['delta']['content'])) {
4372 + $json = json_decode($json_str, true);
4373 + if (isset($json['choices'][0]['delta']['content'])) {
7313 4374 $content = $json['choices'][0]['delta']['content'];
7314 - $full_response .= $content; // Accumulate the full response
7315 -
4375 + $full_response .= $content; // Accumulate
7316 4376 // Send as SSE format
7317 4377 echo "data: " . json_encode(['content' => $content]) . "\n\n";
7318 4378 flush();
7319 4379 }
@@ -7325,12 +4385,12 @@
7325 4385 $response = curl_exec($ch);
7326 4386 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7327 4387
7328 4388 if (curl_errno($ch) || $http_code !== 200) {
7329 - $curl_error = curl_error($ch);
7330 4389 curl_close($ch);
7331 -
4390 +
7332 4391 // Fallback to regular response
4392 + //error_log("MxChat: OpenAI streaming failed, falling back");
7333 4393 $regular_response = $this->mxchat_generate_response_openai(
7334 4394 $selected_model,
7335 4395 $api_key,
7336 4396 $conversation_history,
@@ -7335,34 +4395,20 @@
7335 4395 $api_key,
7336 4396 $conversation_history,
7337 4397 $relevant_content
7338 4398 );
7339 -
7340 - // FIXED: Check if regular response returned an error
7341 - if (is_array($regular_response) && isset($regular_response['error'])) {
7342 - // Send error in SSE format since we're in streaming mode
7343 - echo "data: " . json_encode([
7344 - 'error' => true,
7345 - 'error_message' => $regular_response['error'],
7346 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7347 - 'text' => $regular_response['error'],
7348 - 'message' => $regular_response['error']
7349 - ]) . "\n\n";
7350 - echo "data: [DONE]\n\n";
7351 - flush();
7352 - return true;
7353 - }
7354 -
4399 +
7355 4400 $response_data = [
7356 4401 'text' => $regular_response,
7357 4402 'html' => '',
7358 4403 'session_id' => $session_id
7359 4404 ];
7360 -
4405 +
7361 4406 if ($testing_data !== null) {
7362 4407 $response_data['testing_data'] = $testing_data;
4408 + //error_log("MxChat Testing: Added testing data to OpenAI error fallback");
7363 4409 }
7364 -
4410 +
7365 4411 header('Content-Type: application/json');
7366 4412 echo json_encode($response_data);
7367 4413 return true;
7368 4414 }
@@ -7367,37 +4413,19 @@
7367 4413 return true;
7368 4414 }
7369 4415
7370 4416 curl_close($ch);
7371 -
4417 +
7372 4418 // Save the complete response to maintain chat persistence
7373 4419 if (!empty($full_response) && !empty($session_id)) {
7374 - // Prepare RAG context for streaming response
7375 - $rag_context_for_storage = null;
7376 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7377 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7378 -
7379 - if ($has_rag_data || $has_action_data) {
7380 - $rag_context_for_storage = [];
7381 -
7382 - if ($has_rag_data) {
7383 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7384 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7385 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7386 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7387 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7388 - }
7389 -
7390 - if ($has_action_data) {
7391 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7392 - }
7393 - }
7394 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
4420 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
7395 4421 }
7396 -
4422 +
7397 4423 return true; // Indicate streaming completed successfully
7398 -
4424 +
7399 4425 } catch (Exception $e) {
4426 + //error_log("MxChat OpenAI streaming exception: " . $e->getMessage());
4427 +
7400 4428 // Fallback to regular response
7401 4429 $regular_response = $this->mxchat_generate_response_openai(
7402 4430 $selected_model,
7403 4431 $api_key,
@@ -7403,634 +4431,30 @@
7403 4431 $api_key,
7404 4432 $conversation_history,
7405 4433 $relevant_content
7406 4434 );
7407 -
7408 - // FIXED: Check if regular response returned an error
7409 - if (is_array($regular_response) && isset($regular_response['error'])) {
7410 - // Send error in SSE format since we're in streaming mode
7411 - echo "data: " . json_encode([
7412 - 'error' => true,
7413 - 'error_message' => $regular_response['error'],
7414 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7415 - 'text' => $regular_response['error'],
7416 - 'message' => $regular_response['error']
7417 - ]) . "\n\n";
7418 - echo "data: [DONE]\n\n";
7419 - flush();
7420 - return true;
7421 - }
7422 -
4435 +
7423 4436 $response_data = [
7424 4437 'text' => $regular_response,
7425 4438 'html' => '',
7426 4439 'session_id' => $session_id
7427 4440 ];
7428 -
4441 +
7429 4442 if ($testing_data !== null) {
7430 4443 $response_data['testing_data'] = $testing_data;
4444 + //error_log("MxChat Testing: Added testing data to OpenAI exception fallback");
7431 4445 }
7432 -
4446 +
7433 4447 header('Content-Type: application/json');
7434 4448 echo json_encode($response_data);
7435 4449 return true;
7436 4450 }
7437 4451 }
7438 -
7439 -/**
7440 - * Resolve custom (OpenAI-compatible) provider config from settings.
7441 - * Returns ['base_url','api_key','model','auth_scheme','api_version','chat_url','headers'].
7442 - */
7443 -private function mxchat_resolve_custom_provider() {
7444 - $base_url = isset($this->options['custom_provider_base_url']) ? rtrim(trim((string) $this->options['custom_provider_base_url']), '/') : '';
7445 - $api_key = isset($this->options['custom_provider_api_key']) ? trim((string) $this->options['custom_provider_api_key']) : '';
7446 - $model = isset($this->options['custom_provider_model']) ? trim((string) $this->options['custom_provider_model']) : '';
7447 - $auth_scheme = isset($this->options['custom_provider_auth_scheme']) ? $this->options['custom_provider_auth_scheme'] : 'bearer';
7448 - $api_version = isset($this->options['custom_provider_api_version']) ? trim((string) $this->options['custom_provider_api_version']) : '';
7449 -
7450 - $chat_url = $base_url . '/chat/completions';
7451 - if (!empty($api_version)) {
7452 - $chat_url .= (strpos($chat_url, '?') === false ? '?' : '&') . 'api-version=' . rawurlencode($api_version);
7453 - }
7454 -
7455 - $headers = array('Content-Type: application/json');
7456 - if (!empty($api_key)) {
7457 - if ($auth_scheme === 'api-key') {
7458 - $headers[] = 'api-key: ' . $api_key;
7459 - } else {
7460 - $headers[] = 'Authorization: Bearer ' . $api_key;
7461 - }
7462 - }
7463 -
7464 - return array(
7465 - 'base_url' => $base_url,
7466 - 'api_key' => $api_key,
7467 - 'model' => $model !== '' ? $model : 'default',
7468 - 'auth_scheme' => $auth_scheme,
7469 - 'api_version' => $api_version,
7470 - 'chat_url' => $chat_url,
7471 - 'headers' => $headers,
7472 - );
7473 -}
7474 -
7475 -/**
7476 - * Streaming chat completion against an OpenAI-compatible custom provider
7477 - * (Ollama, LM Studio, vLLM, llama.cpp, Azure OpenAI, etc.).
7478 - * Mirrors mxchat_generate_response_openai_stream but with parameterized URL/auth/model.
7479 - */
7480 -private function mxchat_generate_response_custom_stream($selected_model, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4452 +private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
7481 4453 try {
7482 - $cfg = $this->mxchat_resolve_custom_provider();
7483 - if (empty($cfg['base_url'])) {
7484 - return array('error' => esc_html__('Custom provider Base URL is not configured.', 'mxchat'), 'error_code' => 'missing_custom_provider_base_url');
7485 - }
4454 + // Get system prompt instructions from options
4455 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
7486 4456
7487 - $bot_id = $this->get_current_bot_id($session_id);
7488 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
7489 - if (!is_array($conversation_history)) {
7490 - $conversation_history = array();
7491 - }
7492 -
7493 - $formatted_conversation = array();
7494 - $formatted_conversation[] = array(
7495 - 'role' => 'system',
7496 - 'content' => $system_prompt_instructions . ' ' . $relevant_content,
7497 - );
7498 - foreach ($conversation_history as $message) {
7499 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
7500 - $role = $message['role'];
7501 - if ($role === 'bot' || $role === 'agent') { $role = 'assistant'; }
7502 - if (!in_array($role, array('system', 'assistant', 'user', 'function', 'tool'))) { $role = 'user'; }
7503 - $formatted_conversation[] = array('role' => $role, 'content' => $message['content']);
7504 - }
7505 - }
7506 -
7507 - if (headers_sent() || !function_exists('curl_init')) {
7508 - // No streaming capability — fall through to non-stream wrapper
7509 - $regular = $this->mxchat_generate_response_custom($selected_model, $conversation_history, $relevant_content);
7510 - if (!empty($regular) && !empty($session_id) && is_string($regular)) {
7511 - $this->mxchat_save_chat_message($session_id, 'bot', $regular);
7512 - }
7513 - $response_data = array('text' => is_string($regular) ? $regular : '', 'html' => '', 'session_id' => $session_id);
7514 - if ($testing_data !== null) { $response_data['testing_data'] = $testing_data; }
7515 - header('Content-Type: application/json');
7516 - echo json_encode($response_data);
7517 - return true;
7518 - }
7519 -
7520 - $request_body = array(
7521 - 'model' => $cfg['model'],
7522 - 'messages' => $formatted_conversation,
7523 - 'stream' => true,
7524 - );
7525 - $body = json_encode($request_body);
7526 -
7527 - $this->setup_streaming_headers();
7528 -
7529 - $ch = curl_init();
7530 - curl_setopt($ch, CURLOPT_URL, $cfg['chat_url']);
7531 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
7532 - curl_setopt($ch, CURLOPT_POST, true);
7533 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
7534 - curl_setopt($ch, CURLOPT_HTTPHEADER, $cfg['headers']);
7535 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7536 - curl_setopt($ch, CURLOPT_TIMEOUT, 120);
7537 -
7538 - $full_response = '';
7539 - $stream_started = false;
7540 - $buffer = '';
7541 -
7542 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
7543 - if (!$stream_started && $testing_data !== null) {
7544 - echo "data: " . json_encode(array('testing_data' => $testing_data)) . "\n\n";
7545 - flush();
7546 - $stream_started = true;
7547 - }
7548 - $buffer .= $data;
7549 - $lines = explode("\n", $buffer);
7550 - $buffer = array_pop($lines);
7551 - foreach ($lines as $line) {
7552 - if (trim($line) === '') { continue; }
7553 - if (strpos($line, 'data: ') !== 0) { continue; }
7554 - $json_str = substr($line, 6);
7555 - if (trim($json_str) === '[DONE]') {
7556 - echo "data: [DONE]\n\n";
7557 - flush();
7558 - continue;
7559 - }
7560 - $json = json_decode(trim($json_str), true);
7561 - if ($json && isset($json['choices'][0]['delta']['content'])) {
7562 - $content = $json['choices'][0]['delta']['content'];
7563 - $full_response .= $content;
7564 - echo "data: " . json_encode(array('content' => $content)) . "\n\n";
7565 - flush();
7566 - }
7567 - }
7568 - return strlen($data);
7569 - });
7570 -
7571 - $response = curl_exec($ch);
7572 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7573 -
7574 - if (curl_errno($ch) || $http_code !== 200) {
7575 - $curl_error = curl_error($ch);
7576 - curl_close($ch);
7577 - $regular = $this->mxchat_generate_response_custom($selected_model, $conversation_history, $relevant_content);
7578 - if (is_array($regular) && isset($regular['error'])) {
7579 - echo "data: " . json_encode(array(
7580 - 'error' => true,
7581 - 'error_message' => $regular['error'],
7582 - 'error_code' => $regular['error_code'] ?? 'custom_provider_error',
7583 - 'text' => $regular['error'],
7584 - 'message' => $regular['error'],
7585 - )) . "\n\n";
7586 - echo "data: [DONE]\n\n";
7587 - flush();
7588 - return true;
7589 - }
7590 - $response_data = array('text' => is_string($regular) ? $regular : '', 'html' => '', 'session_id' => $session_id);
7591 - if ($testing_data !== null) { $response_data['testing_data'] = $testing_data; }
7592 - header('Content-Type: application/json');
7593 - echo json_encode($response_data);
7594 - return true;
7595 - }
7596 -
7597 - curl_close($ch);
7598 -
7599 - if (!empty($full_response) && !empty($session_id)) {
7600 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
7601 - }
7602 - return true;
7603 -
7604 - } catch (Exception $e) {
7605 - return array('error' => sprintf(esc_html__('Custom provider error: %s', 'mxchat'), $e->getMessage()), 'error_code' => 'custom_provider_exception');
7606 - }
7607 -}
7608 -
7609 -/**
7610 - * Non-streaming chat completion against a custom OpenAI-compatible provider.
7611 - * Returns string content on success, array['error'=>...] on failure.
7612 - */
7613 -private function mxchat_generate_response_custom($selected_model, $conversation_history, $relevant_content) {
7614 - $cfg = $this->mxchat_resolve_custom_provider();
7615 - if (empty($cfg['base_url'])) {
7616 - return array('error' => esc_html__('Custom provider Base URL is not configured.', 'mxchat'), 'error_code' => 'missing_custom_provider_base_url');
7617 - }
7618 -
7619 - $bot_id = $this->get_current_bot_id(null);
7620 - $system_prompt_instructions = $this->get_system_instructions($bot_id, null);
7621 - if (!is_array($conversation_history)) {
7622 - $conversation_history = array();
7623 - }
7624 -
7625 - $messages = array(array(
7626 - 'role' => 'system',
7627 - 'content' => $system_prompt_instructions . ' ' . $relevant_content,
7628 - ));
7629 - foreach ($conversation_history as $message) {
7630 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
7631 - $role = $message['role'];
7632 - if ($role === 'bot' || $role === 'agent') { $role = 'assistant'; }
7633 - if (!in_array($role, array('system', 'assistant', 'user', 'function', 'tool'))) { $role = 'user'; }
7634 - $messages[] = array('role' => $role, 'content' => $message['content']);
7635 - }
7636 - }
7637 -
7638 - $headers_assoc = array('Content-Type' => 'application/json');
7639 - if (!empty($cfg['api_key'])) {
7640 - if ($cfg['auth_scheme'] === 'api-key') {
7641 - $headers_assoc['api-key'] = $cfg['api_key'];
7642 - } else {
7643 - $headers_assoc['Authorization'] = 'Bearer ' . $cfg['api_key'];
7644 - }
7645 - }
7646 -
7647 - $response = wp_remote_post($cfg['chat_url'], array(
7648 - 'headers' => $headers_assoc,
7649 - 'body' => wp_json_encode(array(
7650 - 'model' => $cfg['model'],
7651 - 'messages' => $messages,
7652 - )),
7653 - 'timeout' => 120,
7654 - ));
7655 -
7656 - if (is_wp_error($response)) {
7657 - return array('error' => sprintf(esc_html__('Custom provider request failed: %s', 'mxchat'), $response->get_error_message()), 'error_code' => 'custom_provider_network_error');
7658 - }
7659 - $code = (int) wp_remote_retrieve_response_code($response);
7660 - if ($code < 200 || $code >= 300) {
7661 - return array('error' => sprintf(esc_html__('Custom provider returned HTTP %d.', 'mxchat'), $code), 'error_code' => 'custom_provider_http_error');
7662 - }
7663 - $body = json_decode(wp_remote_retrieve_body($response), true);
7664 - if (isset($body['choices'][0]['message']['content'])) {
7665 - return (string) $body['choices'][0]['message']['content'];
7666 - }
7667 - return array('error' => esc_html__('Custom provider returned an unexpected response shape.', 'mxchat'), 'error_code' => 'custom_provider_response_shape');
7668 -}
7669 -
7670 -/**
7671 - * Generate response using OpenAI Responses API with web search tool
7672 - * This uses the newer Responses API which supports web search functionality
7673 - */
7674 -private function mxchat_generate_response_openai_web_search($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null, $streaming = false) {
7675 - try {
7676 - $bot_id = $this->get_current_bot_id($session_id);
7677 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
7678 -
7679 - if (!is_array($conversation_history)) {
7680 - $conversation_history = array();
7681 - }
7682 -
7683 - // Build the input for Responses API
7684 - // The Responses API uses a different format - we need to construct the input properly
7685 - $input_parts = [];
7686 -
7687 - // Add system instructions as context
7688 - $system_context = $system_prompt_instructions . "\n\n" . $relevant_content;
7689 -
7690 - // Build conversation as input items for Responses API
7691 - foreach ($conversation_history as $message) {
7692 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
7693 - $role = $message['role'];
7694 - if ($role === 'bot' || $role === 'agent') {
7695 - $role = 'assistant';
7696 - }
7697 - if (!in_array($role, ['assistant', 'user'])) {
7698 - $role = 'user';
7699 - }
7700 - $input_parts[] = [
7701 - 'type' => 'message',
7702 - 'role' => $role,
7703 - 'content' => $message['content']
7704 - ];
7705 - }
7706 - }
7707 -
7708 - // Build request body for Responses API
7709 - $request_body = [
7710 - 'model' => $selected_model,
7711 - 'input' => $input_parts,
7712 - 'instructions' => $system_context,
7713 - 'stream' => $streaming
7714 - ];
7715 -
7716 - // Only add web search tool if web search is enabled in settings
7717 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
7718 - if ($web_search_enabled) {
7719 - $request_body['tools'] = [
7720 - ['type' => 'web_search']
7721 - ];
7722 - }
7723 -
7724 - // Add reasoning effort for supported models
7725 - $is_gpt5_model = strpos($selected_model, 'gpt-5') === 0;
7726 - $no_reasoning_web = array('gpt-5.2', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
7727 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_web, true)) {
7728 - if ($selected_model === 'gpt-5.1-2025-11-13') {
7729 - $request_body['reasoning'] = ['effort' => 'low'];
7730 - } elseif ($selected_model === 'gpt-5.5') {
7731 - $request_body['reasoning'] = ['effort' => 'low'];
7732 - } elseif ($selected_model === 'gpt-5.4') {
7733 - $request_body['reasoning'] = ['effort' => 'low'];
7734 - }
7735 - }
7736 -
7737 - //error_log("MXCHAT WEB SEARCH: Request body: " . json_encode($request_body));
7738 -
7739 - if ($streaming) {
7740 - return $this->mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data);
7741 - } else {
7742 - return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7743 - }
7744 -
7745 - } catch (Exception $e) {
7746 - //error_log("MXCHAT WEB SEARCH ERROR: " . $e->getMessage());
7747 - return [
7748 - 'error' => sprintf(esc_html__('Web search error: %s', 'mxchat'), esc_html($e->getMessage())),
7749 - 'error_code' => 'web_search_exception'
7750 - ];
7751 - }
7752 -}
7753 -
7754 -/**
7755 - * Handle non-streaming web search response
7756 - */
7757 -private function mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data) {
7758 - $request_body['stream'] = false;
7759 -
7760 - $response = wp_remote_post('https://api.openai.com/v1/responses', array(
7761 - 'headers' => array(
7762 - 'Authorization' => 'Bearer ' . $api_key,
7763 - 'Content-Type' => 'application/json'
7764 - ),
7765 - 'body' => json_encode($request_body),
7766 - 'timeout' => 90
7767 - ));
7768 -
7769 - if (is_wp_error($response)) {
7770 - //error_log("MXCHAT WEB SEARCH ERROR: WP Error: " . $response->get_error_message());
7771 - return [
7772 - 'error' => esc_html__('Failed to connect to OpenAI web search API', 'mxchat'),
7773 - 'error_code' => 'web_search_connection_error'
7774 - ];
7775 - }
7776 -
7777 - $response_code = wp_remote_retrieve_response_code($response);
7778 - $response_body = wp_remote_retrieve_body($response);
7779 -
7780 - //error_log("MXCHAT WEB SEARCH: Response code: " . $response_code);
7781 - //error_log("MXCHAT WEB SEARCH: Response body (first 2000): " . substr($response_body, 0, 2000));
7782 -
7783 - if ($response_code !== 200) {
7784 - $error_data = json_decode($response_body, true);
7785 - $error_message = $error_data['error']['message'] ?? 'Unknown API error';
7786 - return [
7787 - 'error' => sprintf(esc_html__('OpenAI API error: %s', 'mxchat'), esc_html($error_message)),
7788 - 'error_code' => 'web_search_api_error'
7789 - ];
7790 - }
7791 -
7792 - $result = json_decode($response_body, true);
7793 -
7794 - if (json_last_error() !== JSON_ERROR_NONE) {
7795 - return [
7796 - 'error' => esc_html__('Invalid response from OpenAI', 'mxchat'),
7797 - 'error_code' => 'web_search_json_error'
7798 - ];
7799 - }
7800 -
7801 - // Extract the response text and citations from Responses API format
7802 - $output_text = '';
7803 - $citations = [];
7804 -
7805 - if (isset($result['output'])) {
7806 - foreach ($result['output'] as $output_item) {
7807 - if ($output_item['type'] === 'message' && isset($output_item['content'])) {
7808 - foreach ($output_item['content'] as $content_item) {
7809 - if ($content_item['type'] === 'output_text') {
7810 - $output_text .= $content_item['text'];
7811 -
7812 - // Extract citations/annotations
7813 - if (isset($content_item['annotations'])) {
7814 - foreach ($content_item['annotations'] as $annotation) {
7815 - if ($annotation['type'] === 'url_citation') {
7816 - $citations[] = [
7817 - 'url' => $annotation['url'],
7818 - 'title' => $annotation['title'] ?? ''
7819 - ];
7820 - }
7821 - }
7822 - }
7823 - }
7824 - }
7825 - }
7826 - }
7827 - }
7828 -
7829 - // If we have citations, append them to the response
7830 - if (!empty($citations)) {
7831 - $output_text .= "\n\n**Sources:**\n";
7832 - $seen_urls = [];
7833 - foreach ($citations as $citation) {
7834 - if (!in_array($citation['url'], $seen_urls)) {
7835 - $seen_urls[] = $citation['url'];
7836 - $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
7837 - $output_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
7838 - }
7839 - }
7840 - }
7841 -
7842 - // Transcript save is handled by the main handler (mxchat_handle_chat_request)
7843 - // which includes rag_context for the "sources" link in transcripts.
7844 -
7845 - return $output_text;
7846 -}
7847 -
7848 -/**
7849 - * Handle streaming web search response using Responses API
7850 - */
7851 -private function mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data) {
7852 - $request_body['stream'] = true;
7853 -
7854 - // Check if we can stream
7855 - if (headers_sent() || !function_exists('curl_init')) {
7856 - // Fallback to non-streaming
7857 - return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7858 - }
7859 -
7860 - // Setup streaming headers
7861 - $this->setup_streaming_headers();
7862 -
7863 - $ch = curl_init();
7864 - curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/responses');
7865 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
7866 - curl_setopt($ch, CURLOPT_POST, true);
7867 - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($request_body));
7868 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
7869 - 'Content-Type: application/json',
7870 - 'Authorization: Bearer ' . $api_key
7871 - ));
7872 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7873 - curl_setopt($ch, CURLOPT_TIMEOUT, 120);
7874 -
7875 - $full_response = '';
7876 - $stream_started = false;
7877 - $buffer = '';
7878 - $citations = [];
7879 -
7880 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$citations, $testing_data) {
7881 - // Send testing data as first event if available
7882 - if (!$stream_started && $testing_data !== null) {
7883 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
7884 - flush();
7885 - $stream_started = true;
7886 - }
7887 -
7888 - $buffer .= $data;
7889 - $lines = explode("\n", $buffer);
7890 - $buffer = array_pop($lines);
7891 -
7892 - foreach ($lines as $line) {
7893 - if (trim($line) === '') continue;
7894 - if (strpos($line, 'data: ') !== 0) continue;
7895 -
7896 - $json_str = substr($line, 6);
7897 -
7898 - if (trim($json_str) === '[DONE]') {
7899 - // Append citations if we have any
7900 - if (!empty($citations)) {
7901 - $citation_text = "\n\n**Sources:**\n";
7902 - $seen_urls = [];
7903 - foreach ($citations as $citation) {
7904 - if (!in_array($citation['url'], $seen_urls)) {
7905 - $seen_urls[] = $citation['url'];
7906 - $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
7907 - $citation_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
7908 - }
7909 - }
7910 - echo "data: " . json_encode(['content' => $citation_text]) . "\n\n";
7911 - $full_response .= $citation_text;
7912 - flush();
7913 - }
7914 - echo "data: [DONE]\n\n";
7915 - flush();
7916 - continue;
7917 - }
7918 -
7919 - $json = json_decode(trim($json_str), true);
7920 - if (!$json) continue;
7921 -
7922 - // Handle Responses API streaming events
7923 - // The format is different from Chat Completions
7924 - if (isset($json['type'])) {
7925 - switch ($json['type']) {
7926 - case 'response.output_text.delta':
7927 - // Text content delta
7928 - if (isset($json['delta'])) {
7929 - $content = $json['delta'];
7930 - $full_response .= $content;
7931 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
7932 - flush();
7933 - }
7934 - break;
7935 -
7936 - case 'response.output_item.done':
7937 - // Check for citations in completed items
7938 - if (isset($json['item']['content'])) {
7939 - foreach ($json['item']['content'] as $content_item) {
7940 - if (isset($content_item['annotations'])) {
7941 - foreach ($content_item['annotations'] as $annotation) {
7942 - if ($annotation['type'] === 'url_citation') {
7943 - $citations[] = [
7944 - 'url' => $annotation['url'],
7945 - 'title' => $annotation['title'] ?? ''
7946 - ];
7947 - }
7948 - }
7949 - }
7950 - }
7951 - }
7952 - break;
7953 - }
7954 - }
7955 - }
7956 -
7957 - return strlen($data);
7958 - });
7959 -
7960 - $response = curl_exec($ch);
7961 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7962 -
7963 - if (curl_errno($ch) || $http_code !== 200) {
7964 - $curl_error = curl_error($ch);
7965 - curl_close($ch);
7966 -
7967 - //error_log("MXCHAT WEB SEARCH STREAM ERROR: HTTP $http_code, cURL error: $curl_error");
7968 -
7969 - // Fallback to non-streaming
7970 - $fallback_response = $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7971 -
7972 - if (is_array($fallback_response) && isset($fallback_response['error'])) {
7973 - echo "data: " . json_encode([
7974 - 'error' => true,
7975 - 'error_message' => $fallback_response['error'],
7976 - 'error_code' => $fallback_response['error_code'] ?? 'web_search_error'
7977 - ]) . "\n\n";
7978 - echo "data: [DONE]\n\n";
7979 - flush();
7980 - return true;
7981 - }
7982 -
7983 - $response_data = [
7984 - 'text' => $fallback_response,
7985 - 'html' => '',
7986 - 'session_id' => $session_id
7987 - ];
7988 - if ($testing_data !== null) {
7989 - $response_data['testing_data'] = $testing_data;
7990 - }
7991 - header('Content-Type: application/json');
7992 - echo json_encode($response_data);
7993 - return true;
7994 - }
7995 -
7996 - curl_close($ch);
7997 -
7998 - // Save the complete response with RAG context so the "sources" link
7999 - // appears in transcripts — mirrors the pattern used by Claude/OpenAI streaming.
8000 - if (!empty($full_response) && !empty($session_id)) {
8001 - $rag_context_for_storage = null;
8002 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8003 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8004 -
8005 - if ($has_rag_data || $has_action_data) {
8006 - $rag_context_for_storage = [];
8007 -
8008 - if ($has_rag_data) {
8009 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8010 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8011 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8012 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8013 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8014 - }
8015 -
8016 - if ($has_action_data) {
8017 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8018 - }
8019 - }
8020 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
8021 - }
8022 -
8023 - return true;
8024 -}
8025 -
8026 -private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
8027 - try {
8028 - // Get bot ID from session or request
8029 - $bot_id = $this->get_current_bot_id($session_id);
8030 -
8031 - // Get system prompt instructions using centralized function
8032 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8033 4457 // Ensure conversation_history is an array
8034 4458 if (!is_array($conversation_history)) {
8035 4459 $conversation_history = array();
8036 4460 }
@@ -8082,13 +4506,8 @@
8082 4506 array_slice($conversation_history, 0, -1), // Remove the added content
8083 4507 $relevant_content
8084 4508 );
8085 4509
8086 - // Save bot response to transcript
8087 - if (!empty($regular_response) && !empty($session_id)) {
8088 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
8089 - }
8090 -
8091 4510 // Return as JSON with testing data
8092 4511 $response_data = [
8093 4512 'text' => $regular_response,
8094 4513 'html' => '',
@@ -8107,11 +4526,8 @@
8107 4526 echo json_encode($response_data);
8108 4527 return true; // Indicate we handled the response
8109 4528 }
8110 4529
8111 - // Setup streaming headers now that we know we're actually streaming
8112 - $this->setup_streaming_headers();
8113 -
8114 4530 // Use cURL for streaming support
8115 4531 $ch = curl_init();
8116 4532 curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
8117 4533 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -8126,12 +4542,11 @@
8126 4542 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
8127 4543
8128 4544 $full_response = ''; // Accumulate full response for saving
8129 4545 $stream_started = false;
8130 - $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
8131 4546
8132 4547 // Buffer control for real-time streaming
8133 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
4548 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
8134 4549 // Send testing data as the first event if available
8135 4550 if (!$stream_started && $testing_data !== null) {
8136 4551 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
8137 4552 flush();
@@ -8138,17 +4553,10 @@
8138 4553 $stream_started = true;
8139 4554 //error_log("MxChat Testing: Sent testing data in Claude stream");
8140 4555 }
8141 4556
8142 - // CRITICAL FIX: Append new data to buffer
8143 - $buffer .= $data;
8144 -
8145 - // Process complete lines only
8146 - $lines = explode("\n", $buffer);
8147 -
8148 - // CRITICAL FIX: Keep the last incomplete line in the buffer
8149 - // The last element might be incomplete, so keep it in buffer
8150 - $buffer = array_pop($lines);
4557 + // Process each chunk of data
4558 + $lines = explode("\n", $data);
8151 4559
8152 4560 foreach ($lines as $line) {
8153 4561 if (trim($line) === '') {
8154 4562 continue;
@@ -8162,9 +4570,9 @@
8162 4570
8163 4571 if (strpos($line, 'data: ') === 0) {
8164 4572 $json_str = substr($line, 6); // Remove 'data: ' prefix
8165 4573
8166 - $json = json_decode(trim($json_str), true);
4574 + $json = json_decode($json_str, true);
8167 4575 if (json_last_error() !== JSON_ERROR_NONE) {
8168 4576 continue;
8169 4577 }
8170 4578
@@ -8216,35 +4624,20 @@
8216 4624 $claude_api_key,
8217 4625 array_slice($conversation_history, 0, -1), // Remove the added content
8218 4626 $relevant_content
8219 4627 );
8220 -
8221 - // FIXED: Check if regular response returned an error
8222 - if (is_array($regular_response) && isset($regular_response['error'])) {
8223 - // Send error in SSE format since we're in streaming mode
8224 - echo "data: " . json_encode([
8225 - 'error' => true,
8226 - 'error_message' => $regular_response['error'],
8227 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
8228 - 'text' => $regular_response['error'],
8229 - 'message' => $regular_response['error']
8230 - ]) . "\n\n";
8231 - echo "data: [DONE]\n\n";
8232 - flush();
8233 - return true;
8234 - }
8235 -
4628 +
8236 4629 $response_data = [
8237 4630 'text' => $regular_response,
8238 4631 'html' => '',
8239 4632 'session_id' => $session_id
8240 4633 ];
8241 -
4634 +
8242 4635 if ($testing_data !== null) {
8243 4636 $response_data['testing_data'] = $testing_data;
8244 4637 //error_log("MxChat Testing: Added testing data to Claude error fallback");
8245 4638 }
8246 -
4639 +
8247 4640 header('Content-Type: application/json');
8248 4641 echo json_encode($response_data);
8249 4642 return true;
8250 4643 }
@@ -8250,29 +4643,9 @@
8250 4643 }
8251 4644
8252 4645 // Save the complete response to maintain chat persistence
8253 4646 if (!empty($full_response) && !empty($session_id)) {
8254 - // Prepare RAG context for streaming response
8255 - $rag_context_for_storage = null;
8256 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8257 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8258 -
8259 - if ($has_rag_data || $has_action_data) {
8260 - $rag_context_for_storage = [];
8261 -
8262 - if ($has_rag_data) {
8263 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8264 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8265 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8266 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8267 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8268 - }
8269 -
8270 - if ($has_action_data) {
8271 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8272 - }
8273 - }
8274 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
4647 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
8275 4648 }
8276 4649
8277 4650 return true; // Indicate streaming completed successfully
8278 4651
@@ -8277,9 +4650,9 @@
8277 4650 return true; // Indicate streaming completed successfully
8278 4651
8279 4652 } catch (Exception $e) {
8280 4653 //error_log("MxChat Claude streaming exception: " . $e->getMessage());
8281 -
4654 +
8282 4655 // Fallback to regular response on exception
8283 4656 $regular_response = $this->mxchat_generate_response_claude(
8284 4657 $selected_model,
8285 4658 $claude_api_key,
@@ -8285,35 +4658,20 @@
8285 4658 $claude_api_key,
8286 4659 $conversation_history,
8287 4660 $relevant_content
8288 4661 );
8289 -
8290 - // FIXED: Check if regular response returned an error
8291 - if (is_array($regular_response) && isset($regular_response['error'])) {
8292 - // Send error in SSE format since we're in streaming mode
8293 - echo "data: " . json_encode([
8294 - 'error' => true,
8295 - 'error_message' => $regular_response['error'],
8296 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
8297 - 'text' => $regular_response['error'],
8298 - 'message' => $regular_response['error']
8299 - ]) . "\n\n";
8300 - echo "data: [DONE]\n\n";
8301 - flush();
8302 - return true;
8303 - }
8304 -
4662 +
8305 4663 $response_data = [
8306 4664 'text' => $regular_response,
8307 4665 'html' => '',
8308 4666 'session_id' => $session_id
8309 4667 ];
8310 -
4668 +
8311 4669 if ($testing_data !== null) {
8312 4670 $response_data['testing_data'] = $testing_data;
8313 4671 //error_log("MxChat Testing: Added testing data to Claude exception fallback");
8314 4672 }
8315 -
4673 +
8316 4674 header('Content-Type: application/json');
8317 4675 echo json_encode($response_data);
8318 4676 return true;
8319 4677 }
@@ -8319,14 +4677,11 @@
8319 4677 }
8320 4678 }
8321 4679 private function mxchat_generate_response_xai_stream($selected_model, $xai_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
8322 4680 try {
8323 - // Get bot ID from session or request
8324 - $bot_id = $this->get_current_bot_id($session_id);
4681 + // Get system prompt instructions from options
4682 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
8325 4683
8326 - // Get system prompt instructions using centralized function
8327 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8328 -
8329 4684 // Ensure conversation_history is an array
8330 4685 if (!is_array($conversation_history)) {
8331 4686 $conversation_history = array();
8332 4687 }
@@ -8365,13 +4720,8 @@
8365 4720 $conversation_history,
8366 4721 $relevant_content
8367 4722 );
8368 4723
8369 - // Save bot response to transcript
8370 - if (!empty($regular_response) && !empty($session_id)) {
8371 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
8372 - }
8373 -
8374 4724 $response_data = [
8375 4725 'text' => $regular_response,
8376 4726 'html' => '',
8377 4727 'session_id' => $session_id
@@ -8394,11 +4744,8 @@
8394 4744 'temperature' => 0.8,
8395 4745 'stream' => true
8396 4746 ]);
8397 4747
8398 - // Setup streaming headers now that we know we're actually streaming
8399 - $this->setup_streaming_headers();
8400 -
8401 4748 // Use cURL for streaming support
8402 4749 $ch = curl_init();
8403 4750 curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
8404 4751 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -8412,12 +4759,11 @@
8412 4759 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
8413 4760
8414 4761 $full_response = ''; // Accumulate full response for saving
8415 4762 $stream_started = false;
8416 - $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
8417 4763
8418 4764 // Buffer control for real-time streaming
8419 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
4765 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
8420 4766 // Send testing data as the first event if available
8421 4767 if (!$stream_started && $testing_data !== null) {
8422 4768 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
8423 4769 flush();
@@ -8424,40 +4770,26 @@
8424 4770 $stream_started = true;
8425 4771 //error_log("MxChat Testing: Sent testing data in X.AI stream");
8426 4772 }
8427 4773
8428 - // CRITICAL FIX: Append new data to buffer
8429 - $buffer .= $data;
4774 + // Process each chunk of data
4775 + $lines = explode("\n", $data);
8430 4776
8431 - // Process complete lines only
8432 - $lines = explode("\n", $buffer);
8433 -
8434 - // CRITICAL FIX: Keep the last incomplete line in the buffer
8435 - // The last element might be incomplete, so keep it in buffer
8436 - $buffer = array_pop($lines);
8437 -
8438 4777 foreach ($lines as $line) {
8439 - // Skip empty lines
8440 - if (trim($line) === '') {
4778 + if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
8441 4779 continue;
8442 4780 }
8443 4781
8444 - // Only process lines that start with "data: "
8445 - if (strpos($line, 'data: ') !== 0) {
8446 - continue;
8447 - }
8448 -
8449 4782 $json_str = substr($line, 6); // Remove 'data: ' prefix
8450 4783
8451 - if (trim($json_str) === '[DONE]') {
4784 + if ($json_str === '[DONE]') {
8452 4785 echo "data: [DONE]\n\n";
8453 4786 flush();
8454 4787 continue;
8455 4788 }
8456 4789
8457 - // Try to decode JSON
8458 - $json = json_decode(trim($json_str), true);
8459 - if ($json && isset($json['choices'][0]['delta']['content'])) {
4790 + $json = json_decode($json_str, true);
4791 + if (isset($json['choices'][0]['delta']['content'])) {
8460 4792 $content = $json['choices'][0]['delta']['content'];
8461 4793 $full_response .= $content; // Accumulate
8462 4794 // Send as SSE format
8463 4795 echo "data: " . json_encode(['content' => $content]) . "\n\n";
@@ -8499,39 +4831,19 @@
8499 4831 return true;
8500 4832 }
8501 4833
8502 4834 curl_close($ch);
8503 -
4835 +
8504 4836 // Save the complete response to maintain chat persistence
8505 4837 if (!empty($full_response) && !empty($session_id)) {
8506 - // Prepare RAG context for streaming response
8507 - $rag_context_for_storage = null;
8508 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8509 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8510 -
8511 - if ($has_rag_data || $has_action_data) {
8512 - $rag_context_for_storage = [];
8513 -
8514 - if ($has_rag_data) {
8515 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8516 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8517 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8518 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8519 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8520 - }
8521 -
8522 - if ($has_action_data) {
8523 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8524 - }
8525 - }
8526 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
4838 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
8527 4839 }
8528 -
4840 +
8529 4841 return true; // Indicate streaming completed successfully
8530 -
4842 +
8531 4843 } catch (Exception $e) {
8532 4844 //error_log("MxChat X.AI streaming exception: " . $e->getMessage());
8533 -
4845 +
8534 4846 // Fallback to regular response
8535 4847 $regular_response = $this->mxchat_generate_response_xai(
8536 4848 $selected_model,
8537 4849 $xai_api_key,
@@ -8556,14 +4868,11 @@
8556 4868 }
8557 4869 }
8558 4870 private function mxchat_generate_response_deepseek_stream($selected_model, $deepseek_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
8559 4871 try {
8560 - // Get bot ID from session or request
8561 - $bot_id = $this->get_current_bot_id($session_id);
4872 + // Get system prompt instructions from options
4873 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
8562 4874
8563 - // Get system prompt instructions using centralized function
8564 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8565 -
8566 4875 // Ensure conversation_history is an array
8567 4876 if (!is_array($conversation_history)) {
8568 4877 $conversation_history = array();
8569 4878 }
@@ -8602,13 +4911,8 @@
8602 4911 $conversation_history,
8603 4912 $relevant_content
8604 4913 );
8605 4914
8606 - // Save bot response to transcript
8607 - if (!empty($regular_response) && !empty($session_id)) {
8608 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
8609 - }
8610 -
8611 4915 $response_data = [
8612 4916 'text' => $regular_response,
8613 4917 'html' => '',
8614 4918 'session_id' => $session_id
@@ -8631,11 +4935,8 @@
8631 4935 'temperature' => 0.8,
8632 4936 'stream' => true
8633 4937 ]);
8634 4938
8635 - // Setup streaming headers now that we know we're actually streaming
8636 - $this->setup_streaming_headers();
8637 -
8638 4939 // Use cURL for streaming support
8639 4940 $ch = curl_init();
8640 4941 curl_setopt($ch, CURLOPT_URL, 'https://api.deepseek.com/v1/chat/completions');
8641 4942 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -8649,12 +4950,11 @@
8649 4950 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
8650 4951
8651 4952 $full_response = ''; // Accumulate full response for saving
8652 4953 $stream_started = false;
8653 - $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
8654 4954
8655 4955 // Buffer control for real-time streaming
8656 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
4956 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
8657 4957 // Send testing data as the first event if available
8658 4958 if (!$stream_started && $testing_data !== null) {
8659 4959 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
8660 4960 flush();
@@ -8661,43 +4961,28 @@
8661 4961 $stream_started = true;
8662 4962 //error_log("MxChat Testing: Sent testing data in DeepSeek stream");
8663 4963 }
8664 4964
8665 - // CRITICAL FIX: Append new data to buffer
8666 - $buffer .= $data;
4965 + // Process each chunk of data
4966 + $lines = explode("\n", $data);
8667 4967
8668 - // Process complete lines only
8669 - $lines = explode("\n", $buffer);
8670 -
8671 - // CRITICAL FIX: Keep the last incomplete line in the buffer
8672 - // The last element might be incomplete, so keep it in buffer
8673 - $buffer = array_pop($lines);
8674 -
8675 4968 foreach ($lines as $line) {
8676 - // Skip empty lines
8677 - if (trim($line) === '') {
4969 + if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
8678 4970 continue;
8679 4971 }
8680 4972
8681 - // Only process lines that start with "data: "
8682 - if (strpos($line, 'data: ') !== 0) {
8683 - continue;
8684 - }
8685 -
8686 4973 $json_str = substr($line, 6); // Remove 'data: ' prefix
8687 4974
8688 - if (trim($json_str) === '[DONE]') {
4975 + if ($json_str === '[DONE]') {
8689 4976 echo "data: [DONE]\n\n";
8690 4977 flush();
8691 4978 continue;
8692 4979 }
8693 4980
8694 - // Try to decode JSON
8695 - $json = json_decode(trim($json_str), true);
8696 - if ($json && isset($json['choices'][0]['delta']['content'])) {
4981 + $json = json_decode($json_str, true);
4982 + if (isset($json['choices'][0]['delta']['content'])) {
8697 4983 $content = $json['choices'][0]['delta']['content'];
8698 - $full_response .= $content; // Accumulate the full response
8699 -
4984 + $full_response .= $content; // Accumulate
8700 4985 // Send as SSE format
8701 4986 echo "data: " . json_encode(['content' => $content]) . "\n\n";
8702 4987 flush();
8703 4988 }
@@ -8750,39 +5035,19 @@
8750 5035 return true;
8751 5036 }
8752 5037
8753 5038 curl_close($ch);
8754 -
5039 +
8755 5040 // Save the complete response to maintain chat persistence
8756 5041 if (!empty($full_response) && !empty($session_id)) {
8757 - // Prepare RAG context for streaming response
8758 - $rag_context_for_storage = null;
8759 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8760 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8761 -
8762 - if ($has_rag_data || $has_action_data) {
8763 - $rag_context_for_storage = [];
8764 -
8765 - if ($has_rag_data) {
8766 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8767 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8768 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8769 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8770 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8771 - }
8772 -
8773 - if ($has_action_data) {
8774 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8775 - }
8776 - }
8777 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
5042 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
8778 5043 }
8779 -
5044 +
8780 5045 return true; // Indicate streaming completed successfully
8781 -
5046 +
8782 5047 } catch (Exception $e) {
8783 5048 //error_log("MxChat DeepSeek streaming exception: " . $e->getMessage());
8784 -
5049 +
8785 5050 // Fallback to regular response
8786 5051 $regular_response = $this->mxchat_generate_response_deepseek(
8787 5052 $selected_model,
8788 5053 $deepseek_api_key,
@@ -8816,120 +5081,12 @@
8816 5081 return true;
8817 5082 }
8818 5083 }
8819 5084
5085 +private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
5086 + // Get system prompt instructions from options
5087 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
8820 5088
8821 -private function mxchat_generate_response_openrouter($selected_model, $openrouter_api_key, $conversation_history, $relevant_content) {
8822 - try {
8823 - if (!is_array($conversation_history)) {
8824 - $conversation_history = array();
8825 - }
8826 -
8827 - $bot_id = $this->get_current_bot_id('');
8828 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8829 -
8830 - $formatted_conversation = array();
8831 -
8832 - $formatted_conversation[] = array(
8833 - 'role' => 'system',
8834 - 'content' => $system_prompt_instructions . " " . $relevant_content
8835 - );
8836 -
8837 - foreach ($conversation_history as $message) {
8838 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
8839 - $role = $message['role'];
8840 -
8841 - if ($role === 'bot' || $role === 'agent') {
8842 - $role = 'assistant';
8843 - }
8844 - if (!in_array($role, ['system', 'assistant', 'user'])) {
8845 - $role = 'user';
8846 - }
8847 -
8848 - $formatted_conversation[] = array(
8849 - 'role' => $role,
8850 - 'content' => $message['content']
8851 - );
8852 - }
8853 - }
8854 -
8855 - $body = json_encode([
8856 - 'model' => $selected_model,
8857 - 'messages' => $formatted_conversation,
8858 - 'temperature' => 1,
8859 - ]);
8860 -
8861 - $args = [
8862 - 'body' => $body,
8863 - 'headers' => [
8864 - 'Content-Type' => 'application/json',
8865 - 'Authorization' => 'Bearer ' . $openrouter_api_key,
8866 - 'HTTP-Referer' => home_url(),
8867 - 'X-Title' => get_bloginfo('name'),
8868 - ],
8869 - 'timeout' => 60,
8870 - 'redirection' => 5,
8871 - 'blocking' => true,
8872 - 'httpversion' => '1.0',
8873 - 'sslverify' => true,
8874 - ];
8875 -
8876 - $response = wp_remote_post('https://openrouter.ai/api/v1/chat/completions', $args);
8877 -
8878 - if (is_wp_error($response)) {
8879 - $error_message = $response->get_error_message();
8880 - return [
8881 - 'error' => esc_html__('Connection error when contacting OpenRouter: ', 'mxchat') . esc_html($error_message),
8882 - 'error_code' => 'openrouter_connection_error',
8883 - 'provider' => 'openrouter'
8884 - ];
8885 - }
8886 -
8887 - $status_code = wp_remote_retrieve_response_code($response);
8888 - if ($status_code !== 200) {
8889 - $response_body = wp_remote_retrieve_body($response);
8890 - $decoded_response = json_decode($response_body, true);
8891 -
8892 - $error_message = isset($decoded_response['error']['message'])
8893 - ? $decoded_response['error']['message']
8894 - : 'HTTP Error ' . $status_code;
8895 -
8896 - return [
8897 - 'error' => esc_html__('OpenRouter API error: ', 'mxchat') . esc_html($error_message),
8898 - 'error_code' => 'openrouter_api_error',
8899 - 'provider' => 'openrouter',
8900 - 'status_code' => $status_code
8901 - ];
8902 - }
8903 -
8904 - $response_body = wp_remote_retrieve_body($response);
8905 - $decoded_response = json_decode($response_body, true);
8906 -
8907 - if (isset($decoded_response['choices'][0]['message']['content'])) {
8908 - return trim($decoded_response['choices'][0]['message']['content']);
8909 - } else {
8910 - return [
8911 - 'error' => esc_html__('Unexpected response format from OpenRouter.', 'mxchat'),
8912 - 'error_code' => 'openrouter_response_format_error',
8913 - 'provider' => 'openrouter'
8914 - ];
8915 - }
8916 - } catch (Exception $e) {
8917 - return [
8918 - 'error' => esc_html__('System error when processing OpenRouter request: ', 'mxchat') . esc_html($e->getMessage()),
8919 - 'error_code' => 'openrouter_exception',
8920 - 'provider' => 'openrouter'
8921 - ];
8922 - }
8923 -}
8924 -private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
8925 -
8926 - // Get bot ID from session or request
8927 - $bot_id = $this->get_current_bot_id($session_id);
8928 -
8929 - // Get system prompt instructions using centralized function
8930 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8931 -
8932 5089 // Clean and validate conversation history
8933 5090 foreach ($conversation_history as &$message) {
8934 5091 // Convert bot and agent roles to assistant
8935 5092 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
@@ -9031,14 +5188,11 @@
9031 5188 if (!is_array($conversation_history)) {
9032 5189 $conversation_history = array();
9033 5190 }
9034 5191
9035 - // Get bot ID from session or request
9036 - $bot_id = $this->get_current_bot_id('');
9037 -
9038 - // Get system prompt instructions using centralized function
9039 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
9040 -
5192 + // Get system prompt instructions from options
5193 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5194 +
9041 5195 // Create a new array for the formatted conversation
9042 5196 $formatted_conversation = array();
9043 5197
9044 5198 // Add system message first
@@ -9066,44 +5220,15 @@
9066 5220 );
9067 5221 }
9068 5222 }
9069 5223
9070 - // Check if this is a GPT-5 model (supports reasoning_effort parameter)
9071 - $is_gpt5_model = (
9072 - strpos($selected_model, 'gpt-5') === 0 ||
9073 - $selected_model === 'gpt-5.2' ||
9074 - $selected_model === 'gpt-5.1-2025-11-13' ||
9075 - $selected_model === 'gpt-5' ||
9076 - $selected_model === 'gpt-5-mini' ||
9077 - $selected_model === 'gpt-5-nano'
9078 - );
9079 -
9080 - // Build request body with optimal settings for fast responses
9081 - $request_body = [
5224 + $body = json_encode([
9082 5225 'model' => $selected_model,
9083 5226 'messages' => $formatted_conversation,
9084 5227 'temperature' => 1,
9085 5228 'stream' => false
9086 - ];
5229 + ]);
9087 5230
9088 - // Add reasoning_effort only for GPT-5 models that support it
9089 - // These chat models don't support reasoning_effort parameter
9090 - $no_reasoning_models = array('gpt-5.2', 'gpt-5.1-chat-latest', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
9091 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
9092 - // GPT-5.1 uses 'low' instead of 'minimal'
9093 - if ($selected_model === 'gpt-5.1-2025-11-13') {
9094 - $request_body['reasoning_effort'] = 'low';
9095 - } elseif ($selected_model === 'gpt-5.5') {
9096 - $request_body['reasoning_effort'] = 'none';
9097 - } elseif ($selected_model === 'gpt-5.4') {
9098 - $request_body['reasoning_effort'] = 'none';
9099 - } else {
9100 - $request_body['reasoning_effort'] = 'minimal';
9101 - }
9102 - }
9103 -
9104 - $body = json_encode($request_body);
9105 -
9106 5231 $args = [
9107 5232 'body' => $body,
9108 5233 'headers' => [
9109 5234 'Content-Type' => 'application/json',
@@ -9119,8 +5244,9 @@
9119 5244 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
9120 5245
9121 5246 if (is_wp_error($response)) {
9122 5247 $error_message = $response->get_error_message();
5248 + //error_log('OpenAI API Error: ' . $error_message);
9123 5249 return [
9124 5250 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
9125 5251 'error_code' => 'openai_connection_error',
9126 5252 'provider' => 'openai'
@@ -9139,8 +5265,10 @@
9139 5265 $error_type = isset($decoded_response['error']['type'])
9140 5266 ? $decoded_response['error']['type']
9141 5267 : 'unknown';
9142 5268
5269 + //error_log('OpenAI API HTTP Error: ' . $status_code . ' - ' . $error_message);
5270 +
9143 5271 // Handle specific error types
9144 5272 switch ($error_type) {
9145 5273 case 'invalid_request_error':
9146 5274 if (strpos($error_message, 'API key') !== false) {
@@ -9188,8 +5316,9 @@
9188 5316
9189 5317 if (isset($decoded_response['choices'][0]['message']['content'])) {
9190 5318 return trim($decoded_response['choices'][0]['message']['content']);
9191 5319 } else {
5320 + //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
9192 5321 return [
9193 5322 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
9194 5323 'error_code' => 'openai_response_format_error',
9195 5324 'provider' => 'openai'
@@ -9195,8 +5324,9 @@
9195 5324 'provider' => 'openai'
9196 5325 ];
9197 5326 }
9198 5327 } catch (Exception $e) {
5328 + //error_log('OpenAI Exception: ' . $e->getMessage());
9199 5329 return [
9200 5330 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
9201 5331 'error_code' => 'openai_exception',
9202 5332 'provider' => 'openai'
@@ -9202,17 +5332,13 @@
9202 5332 'provider' => 'openai'
9203 5333 ];
9204 5334 }
9205 5335 }
9206 -
9207 5336 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
9208 5337 try {
9209 - // Get bot ID from session or request
9210 - $bot_id = $this->get_current_bot_id($session_id);
9211 -
9212 - // Get system prompt instructions using centralized function
9213 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
9214 -
5338 + // Get system prompt instructions from options
5339 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5340 +
9215 5341 // Add system prompt to relevant content
9216 5342 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
9217 5343
9218 5344 // Prepend system instructions to the conversation history
@@ -9404,14 +5530,11 @@
9404 5530 if (!is_array($conversation_history)) {
9405 5531 $conversation_history = array();
9406 5532 }
9407 5533
9408 - // Get bot ID from session or request
9409 - $bot_id = $this->get_current_bot_id($session_id);
9410 -
9411 - // Get system prompt instructions using centralized function
9412 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
9413 -
5534 + // Get system prompt instructions from options
5535 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5536 +
9414 5537 // Create a new array for the formatted conversation
9415 5538 $formatted_conversation = array();
9416 5539
9417 5540 // Add system message first
@@ -9563,14 +5686,11 @@
9563 5686 ];
9564 5687 }
9565 5688 }
9566 5689 private function mxchat_generate_response_gemini($selected_model, $gemini_api_key, $conversation_history, $relevant_content) {
9567 - // Get bot ID from session or request
9568 - $bot_id = $this->get_current_bot_id($session_id);
9569 -
9570 - // Get system prompt instructions using centralized function
9571 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
9572 -
5690 + // Get system prompt instructions from options
5691 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5692 +
9573 5693 // Add system prompt to relevant content
9574 5694 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
9575 5695
9576 5696 // Format messages for Gemini API
@@ -9665,11 +5785,9 @@
9665 5785 ]
9666 5786 ]);
9667 5787
9668 5788 // Prepare the API endpoint
9669 - // Use v1beta for preview models (Gemini 3, experimental), v1 for stable models
9670 - $api_version = (strpos($selected_model, 'preview') !== false || strpos($selected_model, 'exp') !== false) ? 'v1beta' : 'v1';
9671 - $api_endpoint = 'https://generativelanguage.googleapis.com/' . $api_version . '/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
5789 + $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
9672 5790
9673 5791 // Set up the API request
9674 5792 $args = [
9675 5793 'body' => $body,
@@ -9708,12 +5826,11 @@
9708 5826 return "Sorry, I couldn't process that request. The response format was unexpected.";
9709 5827 }
9710 5828 }
9711 5829
9712 -
9713 5830 public function test_streaming_request() {
9714 5831 $options = get_option('mxchat_options', []);
9715 - $model = $options['model'] ?? 'gpt-5.1-chat-latest';
5832 + $model = $options['model'] ?? 'gpt-4o';
9716 5833
9717 5834 // Detect provider from model prefix
9718 5835 $provider = strtolower(explode('-', $model)[0]);
9719 5836
@@ -9839,8 +5956,9 @@
9839 5956
9840 5957 return true;
9841 5958 }
9842 5959
5960 +
9843 5961 public function mxchat_dismiss_pre_chat_message() {
9844 5962 // Get and sanitize the user identifier
9845 5963 $user_id = $this->mxchat_get_user_identifier();
9846 5964 $user_id = sanitize_key($user_id);
@@ -9894,56 +6012,36 @@
9894 6012
9895 6013 return $dotProduct / ($normA * $normB);
9896 6014 }
9897 6015
9898 -
9899 6016 public function mxchat_enqueue_scripts_styles() {
9900 - // Fetch options from the database first to check loading strategy
9901 - $this->options = get_option('mxchat_options');
9902 - $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
9903 -
9904 - // Always enqueue CSS immediately
6017 + // Define version numbers for the styles and scripts
6018 + $chat_style_version = '2.3.9';
6019 + $chat_script_version = '2.3.9';
6020 + // Enqueue the script
6021 + wp_enqueue_script(
6022 + 'mxchat-chat-js',
6023 + plugin_dir_url(__FILE__) . '../js/chat-script.js',
6024 + array('jquery'),
6025 + $chat_script_version,
6026 + true
6027 + );
6028 + // Enqueue the CSS
9905 6029 wp_enqueue_style(
9906 6030 'mxchat-chat-css',
9907 6031 plugin_dir_url(__FILE__) . '../css/chat-style.css',
9908 6032 array(),
9909 - MXCHAT_VERSION
6033 + $chat_style_version
9910 6034 );
9911 -
9912 - // Handle script loading based on strategy
9913 - if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
9914 - // Enqueue the script normally
9915 - wp_enqueue_script(
9916 - 'mxchat-chat-js',
9917 - plugin_dir_url(__FILE__) . '../js/chat-script.js',
9918 - array('jquery'),
9919 - MXCHAT_VERSION,
9920 - true
9921 - );
9922 -
9923 - // Add defer attribute if strategy is 'defer'
9924 - if ($loading_strategy === 'defer') {
9925 - wp_script_add_data('mxchat-chat-js', 'strategy', 'defer');
9926 - }
9927 - } else {
9928 - // For delay or interaction-based loading, we'll use a custom loader
9929 - // Don't enqueue the main script - we'll load it dynamically
9930 - add_action('wp_footer', array($this, 'mxchat_output_delayed_script_loader'), 99);
9931 - }
9932 -
6035 + // Fetch options from the database
6036 + $this->options = get_option('mxchat_options');
9933 6037 $prompts_options = get_option('mxchat_prompts_options', array());
9934 -
9935 - // Check if AI theme is active - if so, skip inline colors in JavaScript
9936 - $theme_options = get_option('mxchat_theme_options', array());
9937 - $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
9938 - $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
9939 - $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
9940 -
6038 +
9941 6039 // Prepare settings for JavaScript
9942 6040 $style_settings = array(
9943 6041 'ajax_url' => admin_url('admin-ajax.php'),
9944 6042 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
9945 - 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest',
6043 + 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-4o',
9946 6044 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
9947 6045 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
9948 6046 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
9949 6047 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
@@ -9970,172 +6068,15 @@
9970 6068 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
9971 6069 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off', // FIXED
9972 6070 'initial_email_state' => null, // Also fixed this undefined variable
9973 6071 'skip_email_check' => true,
9974 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
9975 - 'skip_inline_colors' => $skip_inline_colors,
9976 - 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array(),
9977 - 'print_button_enabled' => $this->options['print_button_enabled'] ?? 'on',
9978 - 'print_button_label' => esc_html__('Download Transcript', 'mxchat'),
9979 - 'print_header_title' => esc_html(get_bloginfo('name')) . ' — ' . esc_html__('Chat transcript', 'mxchat'),
9980 - 'satisfaction_rating_enabled' => apply_filters(
9981 - 'mxchat_satisfaction_rating_enabled',
9982 - ($this->options['satisfaction_rating_enabled'] ?? 'off') === 'on'
9983 - ),
9984 - 'satisfaction_rating_idle_seconds' => max(5, min(600, intval($this->options['satisfaction_rating_idle_seconds'] ?? 60))),
9985 - 'satisfaction_rating_copy' => array(
9986 - 'question' => !empty($this->options['satisfaction_rating_question']) ? esc_html($this->options['satisfaction_rating_question']) : esc_html__('Was this helpful?', 'mxchat'),
9987 - 'helpful' => esc_html__('Helpful', 'mxchat'),
9988 - 'not_helpful' => esc_html__('Not helpful', 'mxchat'),
9989 - 'dismiss' => esc_html__('Dismiss', 'mxchat'),
9990 - 'thanks' => !empty($this->options['satisfaction_rating_thanks']) ? esc_html($this->options['satisfaction_rating_thanks']) : esc_html__('Thanks! Anything we should improve? (optional)', 'mxchat'),
9991 - 'placeholder' => !empty($this->options['satisfaction_rating_placeholder']) ? esc_html($this->options['satisfaction_rating_placeholder']) : esc_html__('Tell us what could be better…', 'mxchat'),
9992 - 'send' => esc_html__('Send', 'mxchat'),
9993 - 'skip' => esc_html__('Skip', 'mxchat'),
9994 - 'saved' => !empty($this->options['satisfaction_rating_saved']) ? esc_html($this->options['satisfaction_rating_saved']) : esc_html__('Thanks for the feedback.', 'mxchat'),
9995 - ),
6072 + 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
9996 6073 );
9997 -
9998 - // For normal/defer loading, use wp_localize_script
9999 - // For delayed loading, we store settings in a transient to be output inline
10000 - if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
10001 - wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
10002 - } else {
10003 - // Store settings for the delayed loader to use
10004 - set_transient('mxchat_delayed_settings_' . get_current_user_id(), $style_settings, 60);
10005 - }
6074 + // Pass the settings to the script
6075 + wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
10006 6076 }
10007 6077
10008 -/**
10009 - * Output the delayed script loader for performance optimization
10010 - */
10011 -public function mxchat_output_delayed_script_loader() {
10012 - $this->options = get_option('mxchat_options');
10013 - $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
10014 - $script_url = plugin_dir_url(__FILE__) . '../js/chat-script.js?ver=' . MXCHAT_VERSION;
10015 6078
10016 - // Get the stored settings
10017 - $prompts_options = get_option('mxchat_prompts_options', array());
10018 - $theme_options = get_option('mxchat_theme_options', array());
10019 - $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
10020 - $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
10021 - $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
10022 -
10023 - $style_settings = array(
10024 - 'ajax_url' => admin_url('admin-ajax.php'),
10025 - 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
10026 - 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest',
10027 - 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
10028 - 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
10029 - 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
10030 - 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
10031 - 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
10032 - 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
10033 - 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
10034 - 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
10035 - 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
10036 - 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
10037 - 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
10038 - 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
10039 - 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
10040 - 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
10041 - 'icon_color' => $this->options['icon_color'] ?? '#fff',
10042 - 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
10043 - 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
10044 - 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
10045 - 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
10046 - 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
10047 - 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
10048 - 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
10049 - 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
10050 - 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
10051 - 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
10052 - 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off',
10053 - 'initial_email_state' => null,
10054 - 'skip_email_check' => true,
10055 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
10056 - 'skip_inline_colors' => $skip_inline_colors,
10057 - 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array(),
10058 - 'print_button_enabled' => $this->options['print_button_enabled'] ?? 'on',
10059 - 'print_button_label' => esc_html__('Download Transcript', 'mxchat'),
10060 - 'print_header_title' => esc_html(get_bloginfo('name')) . ' — ' . esc_html__('Chat transcript', 'mxchat'),
10061 - 'satisfaction_rating_enabled' => apply_filters(
10062 - 'mxchat_satisfaction_rating_enabled',
10063 - ($this->options['satisfaction_rating_enabled'] ?? 'off') === 'on'
10064 - ),
10065 - 'satisfaction_rating_idle_seconds' => max(5, min(600, intval($this->options['satisfaction_rating_idle_seconds'] ?? 60))),
10066 - 'satisfaction_rating_copy' => array(
10067 - 'question' => !empty($this->options['satisfaction_rating_question']) ? esc_html($this->options['satisfaction_rating_question']) : esc_html__('Was this helpful?', 'mxchat'),
10068 - 'helpful' => esc_html__('Helpful', 'mxchat'),
10069 - 'not_helpful' => esc_html__('Not helpful', 'mxchat'),
10070 - 'dismiss' => esc_html__('Dismiss', 'mxchat'),
10071 - 'thanks' => !empty($this->options['satisfaction_rating_thanks']) ? esc_html($this->options['satisfaction_rating_thanks']) : esc_html__('Thanks! Anything we should improve? (optional)', 'mxchat'),
10072 - 'placeholder' => !empty($this->options['satisfaction_rating_placeholder']) ? esc_html($this->options['satisfaction_rating_placeholder']) : esc_html__('Tell us what could be better…', 'mxchat'),
10073 - 'send' => esc_html__('Send', 'mxchat'),
10074 - 'skip' => esc_html__('Skip', 'mxchat'),
10075 - 'saved' => !empty($this->options['satisfaction_rating_saved']) ? esc_html($this->options['satisfaction_rating_saved']) : esc_html__('Thanks for the feedback.', 'mxchat'),
10076 - ),
10077 - );
10078 -
10079 - // Determine delay time based on strategy
10080 - $delay_ms = 0;
10081 - switch ($loading_strategy) {
10082 - case 'delay_1s':
10083 - $delay_ms = 1000;
10084 - break;
10085 - case 'delay_3s':
10086 - $delay_ms = 3000;
10087 - break;
10088 - case 'delay_5s':
10089 - $delay_ms = 5000;
10090 - break;
10091 - }
10092 -
10093 - ?>
10094 - <script type="text/javascript">
10095 - (function() {
10096 - var mxchatLoaded = false;
10097 - var mxchatChat = <?php echo wp_json_encode($style_settings); ?>;
10098 - window.mxchatChat = mxchatChat;
10099 -
10100 - function loadMxChatScript() {
10101 - if (mxchatLoaded) return;
10102 - mxchatLoaded = true;
10103 -
10104 - function appendChatScript() {
10105 - var script = document.createElement('script');
10106 - script.src = <?php echo wp_json_encode($script_url); ?>;
10107 - script.type = 'text/javascript';
10108 - document.body.appendChild(script);
10109 - }
10110 -
10111 - if (typeof jQuery !== 'undefined') {
10112 - appendChatScript();
10113 - } else {
10114 - var jq = document.createElement('script');
10115 - jq.src = <?php echo wp_json_encode(includes_url('js/jquery/jquery.min.js')); ?>;
10116 - jq.onload = appendChatScript;
10117 - document.body.appendChild(jq);
10118 - }
10119 - }
10120 -
10121 - <?php if ($loading_strategy === 'on_interaction'): ?>
10122 - // Load on user interaction
10123 - var events = ['scroll', 'mousemove', 'touchstart', 'keydown', 'click'];
10124 - events.forEach(function(evt) {
10125 - window.addEventListener(evt, loadMxChatScript, {once: true, passive: true});
10126 - });
10127 - // Fallback: load after 8 seconds if no interaction
10128 - setTimeout(loadMxChatScript, 8000);
10129 - <?php else: ?>
10130 - // Load after specified delay
10131 - setTimeout(loadMxChatScript, <?php echo intval($delay_ms); ?>);
10132 - <?php endif; ?>
10133 - })();
10134 - </script>
10135 - <?php
10136 -}
10137 -
10138 6079 /**
10139 6080 * Setup the cron jobs for rate limits with guard against multiple calls
10140 6081 */
10141 6082 public function setup_rate_limit_cron_jobs() {
@@ -10273,9 +6214,9 @@
10273 6214 update_option('mxchat_next_rate_limit_check', time() + $check_interval);
10274 6215 }
10275 6216 }
10276 6217 /**
10277 - * Enhanced rate limit check that includes fallback cleanup and bot-specific rate limits
6218 + * Enhanced rate limit check that includes fallback cleanup
10278 6219 */
10279 6220 public function check_rate_limit() {
10280 6221 // Check if we need to run fallback cleanup
10281 6222 $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
@@ -10285,18 +6226,11 @@
10285 6226 $this->mxchat_reset_rate_limits();
10286 6227 update_option('mxchat_next_rate_limit_check', time() + 3600); // Next hour
10287 6228 }
10288 6229
10289 - // Get bot ID from current request context
10290 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
6230 + // Continue with your existing rate limit logic...
6231 + $all_options = get_option('mxchat_options', []);
10291 6232
10292 - // Get bot-specific options (includes rate limits if overridden)
10293 - $bot_options = $this->get_bot_options($bot_id);
10294 - $current_options = !empty($bot_options) ? $bot_options : $this->options;
10295 -
10296 - // Use bot-specific rate limits if available, otherwise fall back to default
10297 - $rate_limits_source = isset($current_options['rate_limits']) ? $current_options['rate_limits'] : get_option('mxchat_options', [])['rate_limits'] ?? [];
10298 -
10299 6233 // Determine user role or if logged out
10300 6234 if (is_user_logged_in()) {
10301 6235 $user = wp_get_current_user();
10302 6236 $user_id = $user->ID;
@@ -10316,13 +6250,13 @@
10316 6250 $user_id = $this->get_client_ip();
10317 6251 }
10318 6252
10319 6253 // Check if rate limits are configured for this role
10320 - if (!isset($rate_limits_source[$role])) {
6254 + if (!isset($all_options['rate_limits'][$role])) {
10321 6255 return true; // No limit set for this role
10322 6256 }
10323 6257
10324 - $limit = $rate_limits_source[$role]['limit'];
6258 + $limit = $all_options['rate_limits'][$role]['limit'];
10325 6259
10326 6260 // If unlimited, return true immediately
10327 6261 if ($limit === 'unlimited') {
10328 6262 return true;
@@ -10327,16 +6261,13 @@
10327 6261 if ($limit === 'unlimited') {
10328 6262 return true;
10329 6263 }
10330 6264
10331 - // Get the option name for this user/role with safer naming (include bot_id for bot-specific limits)
6265 + // Get the option name for this user/role with safer naming
10332 6266 $safe_role = preg_replace('/[^a-zA-Z0-9_]/', '_', $role);
10333 6267 $safe_user_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $user_id);
10334 - $safe_bot_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $bot_id);
6268 + $option_name = 'mxchat_chat_limit_' . $safe_role . '_' . $safe_user_id;
10335 6269
10336 - // Include bot_id in option name so each bot has separate rate limits
10337 - $option_name = 'mxchat_chat_limit_' . $safe_bot_id . '_' . $safe_role . '_' . $safe_user_id;
10338 -
10339 6270 // Get the counter data
10340 6271 $limit_data = get_option($option_name, ['count' => 0, 'timestamp' => time()]);
10341 6272
10342 6273 // If first request or counter reset needed, set the initial timestamp
@@ -10345,10 +6276,10 @@
10345 6276 update_option($option_name, $limit_data);
10346 6277 }
10347 6278
10348 6279 // Get the timeframe
10349 - $timeframe = isset($rate_limits_source[$role]['timeframe']) ?
10350 - $rate_limits_source[$role]['timeframe'] : 'daily';
6280 + $timeframe = isset($all_options['rate_limits'][$role]['timeframe']) ?
6281 + $all_options['rate_limits'][$role]['timeframe'] : 'daily';
10351 6282
10352 6283 // Check if the counter needs to be reset based on timeframe
10353 6284 $current_time = time();
10354 6285 $timestamp = $limit_data['timestamp'];
@@ -10377,10 +6308,10 @@
10377 6308
10378 6309 // Check if user has exceeded their limit
10379 6310 if ($limit_data['count'] >= intval($limit)) {
10380 6311 // Get the custom message for this role
10381 - $message = !empty($rate_limits_source[$role]['message'])
10382 - ? $rate_limits_source[$role]['message']
6312 + $message = !empty($all_options['rate_limits'][$role]['message'])
6313 + ? $all_options['rate_limits'][$role]['message']
10383 6314 : __('Rate limit exceeded. Please try again later.', 'mxchat');
10384 6315
10385 6316 // Add timeframe information to the message if placeholders exist
10386 6317 $timeframe_label = '';
@@ -10670,11 +6601,8 @@
10670 6601
10671 6602 /**
10672 6603 * AJAX handler to get system information for testing panel
10673 6604 */
10674 -/**
10675 - * AJAX handler to get system information for testing panel
10676 - */
10677 6605 public function mxchat_get_system_info() {
10678 6606 // Verify nonce for security
10679 6607 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
10680 6608 wp_send_json_error(['message' => 'Invalid nonce']);
@@ -10692,24 +6620,10 @@
10692 6620 ? $this->options['system_prompt_instructions']
10693 6621 : 'No system prompt configured';
10694 6622
10695 6623 // Get selected model
10696 - $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest';
6624 + $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
10697 6625
10698 - // Check if OpenRouter is being used
10699 - $is_openrouter = ($selected_model === 'openrouter');
10700 - $openrouter_model = '';
10701 -
10702 - if ($is_openrouter) {
10703 - // Get the actual OpenRouter model that's selected
10704 - $openrouter_model = isset($this->options['openrouter_selected_model'])
10705 - ? $this->options['openrouter_selected_model']
10706 - : 'No OpenRouter model selected';
10707 -
10708 - // Update selected_model display to show both
10709 - $selected_model = 'OpenRouter: ' . $openrouter_model;
10710 - }
10711 -
10712 6626 // Get API key status (just check if they exist, don't expose the keys)
10713 6627 $api_status = [];
10714 6628 $api_status['openai'] = !empty($this->options['api_key']);
10715 6629 $api_status['claude'] = !empty($this->options['claude_api_key']);
@@ -10715,15 +6629,12 @@
10715 6629 $api_status['claude'] = !empty($this->options['claude_api_key']);
10716 6630 $api_status['gemini'] = !empty($this->options['gemini_api_key']);
10717 6631 $api_status['xai'] = !empty($this->options['xai_api_key']);
10718 6632 $api_status['deepseek'] = !empty($this->options['deepseek_api_key']);
10719 - $api_status['openrouter'] = !empty($this->options['openrouter_api_key']);
10720 6633
10721 6634 wp_send_json_success([
10722 6635 'system_prompt' => $system_prompt,
10723 6636 'selected_model' => $selected_model,
10724 - 'is_openrouter' => $is_openrouter,
10725 - 'openrouter_model' => $openrouter_model,
10726 6637 'api_status' => $api_status
10727 6638 ]);
10728 6639 }
10729 6640
@@ -10742,12 +6653,12 @@
10742 6653 wp_send_json_error(['message' => 'Unauthorized']);
10743 6654 return;
10744 6655 }
10745 6656
10746 - // Get similarity threshold from main options (default 35%)
6657 + // Get similarity threshold from main options (default 75%)
10747 6658 $similarity_threshold = isset($this->options['similarity_threshold'])
10748 6659 ? ((int) $this->options['similarity_threshold']) / 100
10749 - : 0.35;
6660 + : 0.75;
10750 6661
10751 6662 wp_send_json_success([
10752 6663 'threshold' => $similarity_threshold,
10753 6664 'threshold_percentage' => ($similarity_threshold * 100) . '%'
@@ -10762,42 +6673,24 @@
10762 6673 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
10763 6674 wp_send_json_error(['message' => 'Invalid nonce']);
10764 6675 return;
10765 6676 }
10766 -
6677 +
10767 6678 // Only allow admin users
10768 6679 if (!current_user_can('administrator')) {
10769 6680 wp_send_json_error(['message' => 'Unauthorized']);
10770 6681 return;
10771 6682 }
10772 -
10773 - // Check OpenAI Vector Store first (takes priority)
10774 - $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
10775 - $use_vectorstore = (isset($vectorstore_options['mxchat_use_openai_vectorstore']) && $vectorstore_options['mxchat_use_openai_vectorstore'] === '1');
10776 -
10777 - if ($use_vectorstore) {
10778 - $vectorstore_ids = $vectorstore_options['mxchat_openai_vectorstore_ids'] ?? '';
10779 - $id_count = !empty($vectorstore_ids) ? count(array_filter(array_map('trim', explode(',', $vectorstore_ids)))) : 0;
10780 -
10781 - $kb_info = [
10782 - 'type' => 'OpenAI Vector Store',
10783 - 'status' => 'Active',
10784 - 'documents' => $id_count > 0 ? $id_count . ' vector store' . ($id_count > 1 ? 's' : '') . ' configured' : 'No vector stores configured'
10785 - ];
10786 -
10787 - wp_send_json_success($kb_info);
10788 - return;
10789 - }
10790 -
6683 +
10791 6684 // Check Pinecone vs WordPress
10792 6685 $addon_options = get_option('mxchat_pinecone_addon_options', array());
10793 6686 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
10794 -
6687 +
10795 6688 $kb_info = [
10796 6689 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
10797 6690 'status' => 'Active'
10798 6691 ];
10799 -
6692 +
10800 6693 // Get document count
10801 6694 if ($use_pinecone) {
10802 6695 $kb_info['documents'] = 'Connected to Pinecone';
10803 6696 $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
@@ -10807,9 +6700,9 @@
10807 6700 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
10808 6701 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
10809 6702 $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
10810 6703 }
10811 -
6704 +
10812 6705 wp_send_json_success($kb_info);
10813 6706 }
10814 6707
10815 6708 /**
@@ -10891,13 +6784,9 @@
10891 6784 // Clear any other session-specific transients
10892 6785 delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
10893 6786 delete_transient("mxchat_include_pdf_in_context_{$session_id}");
10894 6787 delete_transient("mxchat_include_word_in_context_{$session_id}");
10895 -
10896 - // Clear form addon state (pending forms and submitted forms)
10897 - delete_option("mxchat_pending_form_{$session_id}");
10898 - delete_option("mxchat_submitted_forms_{$session_id}");
10899 -
6788 +
10900 6789 //error_log("MxChat: Cleared all data for session: {$session_id}");
10901 6790 }
10902 6791
10903 6792 /**
@@ -10932,15 +6821,15 @@
10932 6821 $testing_data = [
10933 6822 'query' => $message,
10934 6823 'timestamp' => time(),
10935 6824 'top_matches' => [],
10936 - 'action_matches' => [] // Add action matches
6825 + 'action_matches' => [] // NEW: Add action matches
10937 6826 ];
10938 6827
10939 6828 // Get similarity threshold
10940 6829 $similarity_threshold = isset($this->options['similarity_threshold'])
10941 6830 ? ((int) $this->options['similarity_threshold']) / 100
10942 - : 0.35;
6831 + : 0.75;
10943 6832
10944 6833 $testing_data['similarity_threshold'] = $similarity_threshold;
10945 6834
10946 6835 // Use the real similarity analysis if available
@@ -10955,9 +6844,9 @@
10955 6844
10956 6845 $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
10957 6846 }
10958 6847
10959 - // Include action analysis if available
6848 + // NEW: Include action analysis if available
10960 6849 if (isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
10961 6850 $testing_data['action_matches'] = $this->last_action_analysis;
10962 6851
10963 6852 // Clear it after capturing to avoid stale data
@@ -10968,9 +6857,9 @@
10968 6857 }
10969 6858
10970 6859
10971 6860 /**
10972 - * Track URL clicks from chatbot responses
6861 + * NEW: Track URL clicks from chatbot responses
10973 6862 */
10974 6863 public function mxchat_track_url_click() {
10975 6864 // Verify nonce for security
10976 6865 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
@@ -11007,9 +6896,9 @@
11007 6896 wp_die();
11008 6897 }
11009 6898
11010 6899 /**
11011 - * Get URL click analytics for a session
6900 + * NEW: Get URL click analytics for a session
11012 6901 */
11013 6902 public function mxchat_get_url_clicks($session_id) {
11014 6903 global $wpdb;
11015 6904 $table_name = $wpdb->prefix . 'mxchat_url_clicks';
@@ -11021,9 +6910,9 @@
11021 6910
11022 6911 return $clicks;
11023 6912 }
11024 6913 /**
11025 - * Track the originating page where chat was started
6914 + * NEW: Track the originating page where chat was started
11026 6915 */
11027 6916 public function mxchat_track_originating_page() {
11028 6917 // Verify nonce
11029 6918 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
@@ -11072,195 +6961,8 @@
11072 6961 wp_send_json_success(['message' => 'Originating page tracked']);
11073 6962 wp_die();
11074 6963 }
11075 6964
11076 -/**
11077 - * Validate and clean URLs from AI response
11078 - * Removes any URLs that aren't in the knowledge base
11079 - *
11080 - * @param string $response_text The AI-generated response
11081 - * @param array $valid_urls Array of URLs from the knowledge base
11082 - * @return string Cleaned response with invalid URLs removed/flagged
11083 - */
11084 -private function validate_and_clean_urls($response_text, $valid_urls) {
11085 - // DEBUG: Log what we're working with
11086 - //error_log("=== MxChat URL Validation Debug ===");
11087 - //error_log("Valid URLs count: " . count($valid_urls));
11088 - //error_log("Valid URLs: " . print_r($valid_urls, true));
11089 - //error_log("Response text length: " . strlen($response_text));
11090 - //error_log("Response text preview: " . substr($response_text, 0, 500));
11091 -
11092 - // If no valid URLs provided or empty response, return as-is
11093 - if (empty($valid_urls) || empty($response_text)) {
11094 - //error_log("Validation skipped - empty valid_urls or response");
11095 - return $response_text;
11096 - }
11097 -
11098 - // Extract all URLs from the AI response
11099 - // This regex matches http:// and https:// URLs
11100 - preg_match_all(
11101 - '#\bhttps?://[^\s<>"\')\]]+#i',
11102 - $response_text,
11103 - $matches
11104 - );
11105 -
11106 - // If no URLs found in response, return as-is
11107 - if (empty($matches[0])) {
11108 - //error_log("No URLs found in response");
11109 - return $response_text;
11110 - }
11111 -
11112 - $found_urls = $matches[0];
11113 - $cleaned_response = $response_text;
11114 - $removed_count = 0;
11115 -
11116 - // Normalize valid URLs for comparison (remove trailing slashes, fragments, etc.)
11117 - $normalized_valid_urls = array_map(function($url) {
11118 - // Remove trailing slash
11119 - $url = rtrim($url, '/');
11120 - // Remove URL fragments (#section)
11121 - $url = preg_replace('/#.*$/', '', $url);
11122 - // Remove trailing punctuation that might have been captured
11123 - $url = rtrim($url, '.,;:!?');
11124 - return $url;
11125 - }, $valid_urls);
11126 -
11127 - //error_log("Normalized valid URLs: " . print_r($normalized_valid_urls, true));
11128 -
11129 - foreach ($found_urls as $found_url) {
11130 - // Clean up the found URL (remove trailing punctuation that might have been captured)
11131 - $clean_found_url = rtrim($found_url, '.,;:!?)');
11132 -
11133 - // DEBUG: Log each URL being checked
11134 - //error_log("Checking found URL: " . $found_url);
11135 -
11136 - // Normalize for comparison
11137 - $normalized_found = rtrim($clean_found_url, '/');
11138 - $normalized_found = preg_replace('/#.*$/', '', $normalized_found);
11139 -
11140 - //error_log("Normalized found URL: " . $normalized_found);
11141 -
11142 - // Check if this URL exists in our valid URLs list
11143 - $is_valid = false;
11144 -
11145 - //error_log("Starting validation checks for: " . $normalized_found);
11146 -
11147 - // First, try exact match
11148 - if (in_array($normalized_found, $normalized_valid_urls)) {
11149 - $is_valid = true;
11150 - //error_log("EXACT MATCH FOUND");
11151 - } else {
11152 - //error_log("No exact match, checking variations...");
11153 - // If no exact match, check if it's a variation (with query params, etc.)
11154 - foreach ($normalized_valid_urls as $valid_url) {
11155 - //error_log(" Comparing against valid URL: " . $valid_url);
11156 -
11157 - // Check if the found URL starts with a valid URL (handles query params)
11158 - if (strpos($normalized_found, $valid_url) === 0) {
11159 - // Check what comes after the valid URL
11160 - $remainder = substr($normalized_found, strlen($valid_url));
11161 -
11162 - // Only valid if:
11163 - // 1. Exact match (remainder is empty)
11164 - // 2. Query params (starts with ?)
11165 - // 3. Fragment (starts with #)
11166 - if (empty($remainder) || $remainder[0] === '?' || $remainder[0] === '#') {
11167 - $is_valid = true;
11168 - //error_log(" MATCH: Found URL is valid variation of base URL");
11169 - break;
11170 - } else {
11171 - //error_log(" NOT A MATCH: Found URL extends path beyond valid URL (remainder: " . $remainder . ")");
11172 - }
11173 - }
11174 - // Also check the reverse (in case valid URL has query params)
11175 - if (strpos($valid_url, $normalized_found) === 0) {
11176 - $is_valid = true;
11177 - //error_log(" MATCH: Valid URL starts with found URL");
11178 - break;
11179 - }
11180 - }
11181 -
11182 - if (!$is_valid) {
11183 - //error_log("NO MATCH FOUND - URL should be removed");
11184 - }
11185 - }
11186 -
11187 - // If URL is not valid, remove it from the response
11188 - if (!$is_valid) {
11189 - // Log the removal for debugging
11190 - //error_log("MxChat: Removed hallucinated URL: " . $found_url);
11191 - //error_log("MxChat: Valid URLs were: " . implode(', ', array_slice($normalized_valid_urls, 0, 5)));
11192 -
11193 - $removed_count++;
11194 -
11195 - // Check if URL is part of a markdown link: [text](url)
11196 - $markdown_pattern = '/\[([^\]]+)\]\(' . preg_quote($found_url, '/') . '\)/';
11197 - if (preg_match($markdown_pattern, $cleaned_response)) {
11198 - //error_log("Found markdown link, removing but keeping text");
11199 - // Remove the markdown link but keep the text
11200 - $cleaned_response = preg_replace($markdown_pattern, '$1', $cleaned_response);
11201 - }
11202 - // Check if URL is part of an HTML link: <a href="url">text</a>
11203 - else if (preg_match('/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>(.*?)<\/a>/i', $cleaned_response, $link_match)) {
11204 - //error_log("Found HTML link, removing but keeping text");
11205 - // Remove the HTML link but keep the text
11206 - $link_text = $link_match[1];
11207 - $cleaned_response = preg_replace(
11208 - '/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>.*?<\/a>/i',
11209 - $link_text,
11210 - $cleaned_response
11211 - );
11212 - }
11213 - // Otherwise just remove the bare URL
11214 - else {
11215 - //error_log("Removing bare URL");
11216 - $cleaned_response = str_replace($found_url, '', $cleaned_response);
11217 - }
11218 - }
11219 - }
11220 -
11221 - // Log summary if any URLs were removed
11222 - if ($removed_count > 0) {
11223 - //error_log("MxChat: URL Validation Summary - Removed {$removed_count} hallucinated URL(s)");
11224 - } else {
11225 - //error_log("MxChat: URL Validation Summary - No URLs removed, all were valid");
11226 - }
11227 -
11228 - // Clean up any double spaces or awkward punctuation left behind
11229 - // IMPORTANT: Only collapse horizontal whitespace (spaces/tabs), preserve newlines for markdown formatting
11230 - $cleaned_response = preg_replace('/[^\S\n]+/', ' ', $cleaned_response); // Collapse spaces/tabs but NOT newlines
11231 - $cleaned_response = preg_replace('/[^\S\n]+([.,;:!?])/', '$1', $cleaned_response); // Same for punctuation cleanup
11232 -
11233 - //error_log("Final cleaned response: " . $cleaned_response);
11234 -
11235 - return trim($cleaned_response);
11236 -}
11237 -
11238 -/**
11239 - * AJAX handler to get current chat mode for a session
11240 - */
11241 -public function mxchat_get_current_chat_mode() {
11242 - // Verify nonce for security
11243 - if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
11244 - wp_send_json_error(['message' => 'Invalid nonce']);
11245 - wp_die();
11246 - }
11247 -
11248 - $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
11249 -
11250 - if (empty($session_id)) {
11251 - wp_send_json_error(['message' => 'Session ID missing']);
11252 - wp_die();
11253 - }
11254 -
11255 - // Get the current chat mode for this session
11256 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
11257 -
11258 - wp_send_json_success([
11259 - 'chat_mode' => $chat_mode
11260 - ]);
11261 - wp_die();
11262 -}
11263 6965
11264 6966
11265 6967
11266 6968 }