PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.3.6
MxChat – AI Chatbot & Content Generation for WordPress v2.3.6
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 +1274 -5584 3.2.22.3.6 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,40 +76,23 @@
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 - //error_log('MxChat Core: check_actions_for_addons called with message: ' . $message);
90 + error_log('MxChat Core: check_actions_for_addons called with message: ' . $message);
138 91
139 92 $result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
140 93
141 - //error_log('MxChat Core: Intent check result = ' . ($result === false ? 'false' : 'true'));
94 + error_log('MxChat Core: Intent check result = ' . ($result === false ? 'false' : 'true'));
142 95
143 96 return $result;
144 97 }
145 98
@@ -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
@@ -365,42 +282,8 @@
365 282 // Compare signatures
366 283 return hash_equals($my_signature, $slack_signature);
367 284 }
368 285
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 286 public function mxchat_stream_events(WP_REST_Request $request) {
404 287 header('Content-Type: text/event-stream');
405 288 header('Cache-Control: no-cache');
406 289 header('Connection: keep-alive');
@@ -434,9 +317,9 @@
434 317
435 318
436 319
437 320
438 -private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null, $rag_context = null) {
321 +private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null) {
439 322 global $wpdb;
440 323 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
441 324 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
442 325
@@ -448,26 +331,14 @@
448 331 $session_id
449 332 ));
450 333 $is_new_session = ($existing_messages == 0);
451 334
452 - // Log for debugging
335 + // NEW: Log for debugging
453 336 if ($is_new_session) {
454 - //error_log("[DEBUG] This is a NEW session - first message");
337 + error_log("[DEBUG] This is a NEW session - first message");
455 338 }
456 339 }
457 340
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 341 // 1) Extract agent name if present
471 342 $agent_name = '';
472 343 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
473 344 $agent_name = $matches[1];
@@ -499,33 +370,18 @@
499 370 $email_option_key = "mxchat_email_{$session_id}";
500 371 $saved_email = get_option($email_option_key);
501 372 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for email_option_key: {$email_option_key}, found: {$saved_email}");
502 373
503 - // Check for a saved name in wp_options
504 - $name_option_key = "mxchat_name_{$session_id}";
505 - $saved_name = get_option($name_option_key);
506 - //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for name_option_key: {$name_option_key}, found: {$saved_name}");
507 -
508 - // If found, update DB user_email and user_name
509 - if ($saved_email || $saved_name) {
510 - $update_data = [];
511 - if ($saved_email) {
512 - $update_data['user_email'] = $saved_email;
513 - }
514 - if ($saved_name) {
515 - $update_data['user_name'] = $saved_name;
516 - }
517 -
518 - if (!empty($update_data)) {
519 - $update_res = $wpdb->update(
520 - $table_name,
521 - $update_data,
522 - ['session_id' => $session_id],
523 - array_fill(0, count($update_data), '%s'),
524 - ['%s']
525 - );
526 - //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email/user_name update for session_id {$session_id}. update_res: {$update_res}");
527 - }
374 + // If found, update DB user_email
375 + if ($saved_email) {
376 + $update_res = $wpdb->update(
377 + $table_name,
378 + ['user_email' => $saved_email],
379 + ['session_id' => $session_id],
380 + ['%s'],
381 + ['%s']
382 + );
383 + //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email update for session_id {$session_id}. update_res: {$update_res}");
528 384 }
529 385
530 386 // 7) Save to session history in wp_options
531 387 $history_key = "mxchat_history_{$session_id}";
@@ -544,9 +400,8 @@
544 400 $insert_data = [
545 401 'user_id' => $user_id,
546 402 'user_identifier'=> $user_identifier,
547 403 'user_email' => $saved_email ?: $user_email,
548 - 'user_name' => $saved_name ?: '', // Add name to insert data
549 404 'session_id' => $session_id,
550 405 'role' => $role,
551 406 'message' => $message,
552 407 'timestamp' => current_time('mysql', 1),
@@ -563,9 +418,9 @@
563 418 if ($originating_page && !empty($originating_page['url'])) {
564 419 $insert_data['originating_page_url'] = $originating_page['url'];
565 420 $insert_data['originating_page_title'] = $originating_page['title'] ?? '';
566 421
567 - //error_log("[DEBUG] Setting originating page from parameter: " . $originating_page['url']);
422 + error_log("[DEBUG] Setting originating page from parameter: " . $originating_page['url']);
568 423 }
569 424 // Otherwise check if it's stored in the instance property
570 425 else if (isset($this->pending_originating_page) && !empty($this->pending_originating_page['url'])) {
571 426 $insert_data['originating_page_url'] = $this->pending_originating_page['url'];
@@ -570,9 +425,9 @@
570 425 else if (isset($this->pending_originating_page) && !empty($this->pending_originating_page['url'])) {
571 426 $insert_data['originating_page_url'] = $this->pending_originating_page['url'];
572 427 $insert_data['originating_page_title'] = $this->pending_originating_page['title'] ?? '';
573 428
574 - //error_log("[DEBUG] Setting originating page from pending_originating_page: " . $this->pending_originating_page['url']);
429 + error_log("[DEBUG] Setting originating page from pending_originating_page: " . $this->pending_originating_page['url']);
575 430
576 431 // Clear after using
577 432 unset($this->pending_originating_page);
578 433 }
@@ -591,9 +446,9 @@
591 446 $title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
592 447 $insert_data['originating_page_title'] = ucwords(trim($title));
593 448 }
594 449
595 - //error_log("[DEBUG] Setting originating page from HTTP_REFERER: " . $referer_url);
450 + error_log("[DEBUG] Setting originating page from HTTP_REFERER: " . $referer_url);
596 451 }
597 452
598 453 // Store for this session so all messages have the same originating page
599 454 if (!empty($insert_data['originating_page_url'])) {
@@ -610,17 +465,9 @@
610 465 $insert_data['originating_page_title'] = $stored_originating['title'] ?? '';
611 466 }
612 467 }
613 468 }
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 -
469 +
623 470 $wpdb->insert($table_name, $insert_data);
624 471 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
625 472
626 473 // 9) Send notification email if this is the first user message in a new session
@@ -631,13 +478,8 @@
631 478 'ip' => $_SERVER['REMOTE_ADDR']
632 479 ));
633 480 }
634 481
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 482 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
641 483 return $message_id;
642 484 }
643 485
@@ -684,200 +526,11 @@
684 526 // Send email
685 527 return wp_mail($to, $subject, $message);
686 528 }
687 529
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 530 public function mxchat_handle_save_email_and_response() {
875 531 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
876 - //error_log('DEBUG: POST data: ' . print_r($_POST, true));
877 532
878 - nocache_headers();
879 -
880 533 // Validate nonce
881 534 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
882 535 //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
883 536 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
@@ -885,41 +538,22 @@
885 538 }
886 539
887 540 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
888 541 $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
889 - $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
890 542
891 - //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}, name: {$name}");
543 + //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}");
892 544
893 - if (empty($session_id) || $session_id === 'null' || empty($email)) {
545 + if (empty($session_id) || empty($email)) {
894 546 //error_log("[ERROR] Missing session_id or email: session_id={$session_id}, email={$email}");
895 547 wp_send_json_error(['message' => esc_html__('Session ID or email is missing.', 'mxchat')]);
896 548 wp_die();
897 549 }
898 550
899 - // Validate name if provided (check if name field is enabled and name is required)
900 - $options = get_option('mxchat_options', []);
901 - $name_field_enabled = isset($options['enable_name_field']) &&
902 - ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
903 -
904 - if ($name_field_enabled && (empty($name) || strlen(trim($name)) < 2 || strlen(trim($name)) > 100)) {
905 - //error_log("[ERROR] Invalid name: {$name} (enabled: {$name_field_enabled})");
906 - wp_send_json_error(['message' => esc_html__('Name must be between 2 and 100 characters.', 'mxchat')]);
907 - wp_die();
908 - }
551 + // 1) Always store in wp_options
552 + $option_key = "mxchat_email_{$session_id}";
553 + update_option($option_key, $email);
554 + //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$option_key} => {$email}");
909 555
910 - // 1) Always store email in wp_options
911 - $email_option_key = "mxchat_email_{$session_id}";
912 - update_option($email_option_key, $email, 'no');
913 - //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$email_option_key} => {$email}");
914 -
915 - // Store name in wp_options if provided
916 - if (!empty($name)) {
917 - $name_option_key = "mxchat_name_{$session_id}";
918 - update_option($name_option_key, $name, 'no');
919 - //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$name_option_key} => {$name}");
920 - }
921 -
922 556 // 2) (Optional) Also store in DB if a row already exists
923 557 global $wpdb;
924 558 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
925 559
@@ -929,30 +563,21 @@
929 563
930 564 //error_log("[DEBUG] handle_save_email_and_response -> session_count for {$session_id}: {$session_count} (SQL: {$sql})");
931 565
932 566 if ($session_count) {
933 - // Update both user_email and user_name if row(s) exist
934 - if (!empty($name)) {
935 - $update_sql = $wpdb->prepare(
936 - "UPDATE {$table_name} SET user_email = %s, user_name = %s WHERE session_id = %s",
937 - $email,
938 - $name,
939 - $session_id
940 - );
941 - } else {
942 - $update_sql = $wpdb->prepare(
943 - "UPDATE {$table_name} SET user_email = %s WHERE session_id = %s",
944 - $email,
945 - $session_id
946 - );
947 - }
567 + // Update user_email if row(s) exist
568 + $update_sql = $wpdb->prepare(
569 + "UPDATE {$table_name} SET user_email = %s WHERE session_id = %s",
570 + $email,
571 + $session_id
572 + );
948 573 $wpdb->query($update_sql);
949 574 //error_log("[DEBUG] handle_save_email_and_response -> DB updated: {$update_sql}");
950 575 } else {
951 - //error_log("[INFO] handle_save_email_and_response -> No DB entry for {$session_id}, so email/name is only in wp_options.");
576 + //error_log("[INFO] handle_save_email_and_response -> No DB entry for {$session_id}, so email is only in wp_options.");
952 577 }
953 578
954 - // Provide success response (same as original)
579 + // Provide success response
955 580 $bot_message = __('Thanks for providing your email! You can continue chatting now.', 'mxchat');
956 581 //error_log("[DEBUG] handle_save_email_and_response -> success, returning bot_message: {$bot_message}");
957 582 wp_send_json_success(['message' => $bot_message]);
958 583 wp_die();
@@ -960,10 +585,8 @@
960 585
961 586 public function mxchat_check_email_provided() {
962 587 //error_log('[DEBUG] ---------- mxchat_check_email_provided START ----------');
963 588
964 - nocache_headers();
965 -
966 589 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
967 590 //error_log('[ERROR] Invalid nonce in mxchat_check_email_provided');
968 591 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
969 592 }
@@ -968,9 +591,9 @@
968 591 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
969 592 }
970 593
971 594 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
972 - if (empty($session_id) || $session_id === 'null') {
595 + if (empty($session_id)) {
973 596 //error_log('[ERROR] No session ID provided in mxchat_check_email_provided');
974 597 wp_send_json_error(['message' => esc_html__('No session ID provided', 'mxchat')]);
975 598 }
976 599
@@ -977,109 +600,49 @@
977 600 // Check if the user is logged in
978 601 if (is_user_logged_in()) {
979 602 $current_user = wp_get_current_user();
980 603 //error_log("[DEBUG] User is logged in as {$current_user->user_email}");
981 -
982 - // Get user's display name for logged in users
983 - $user_name = !empty($current_user->display_name) ? $current_user->display_name :
984 - (!empty($current_user->first_name) ? $current_user->first_name : '');
985 -
986 - $response_data = ['logged_in' => true, 'email' => $current_user->user_email];
987 - if (!empty($user_name)) {
988 - $response_data['name'] = $user_name;
989 - }
990 -
991 - wp_send_json_success($response_data);
604 + wp_send_json_success(['logged_in' => true, 'email' => $current_user->user_email]);
992 605 }
993 606
994 - // Check if name field is required
995 - $options = get_option('mxchat_options', []);
996 - $name_field_enabled = isset($options['enable_name_field']) &&
997 - ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
607 + $option_key = "mxchat_email_{$session_id}";
608 + $stored_email = get_option($option_key, '');
998 609
999 - $email_option_key = "mxchat_email_{$session_id}";
1000 - $stored_email = get_option($email_option_key, '');
1001 -
1002 - // Check for stored name
1003 - $name_option_key = "mxchat_name_{$session_id}";
1004 - $stored_name = get_option($name_option_key, '');
610 + //error_log("[DEBUG] mxchat_check_email_provided -> Checking option: {$option_key}, found: {$stored_email}");
1005 611
1006 - //error_log("[DEBUG] mxchat_check_email_provided -> Checking email option: {$email_option_key}, found: {$stored_email}");
1007 - //error_log("[DEBUG] mxchat_check_email_provided -> Checking name option: {$name_option_key}, found: {$stored_name}, required: " . ($name_field_enabled ? 'yes' : 'no'));
1008 -
1009 - // Check if we have email and name (if name is required)
1010 - $has_required_info = !empty($stored_email);
1011 -
1012 - if ($name_field_enabled) {
1013 - $has_required_info = $has_required_info && !empty($stored_name);
1014 - }
1015 -
1016 - if ($has_required_info) {
1017 - //error_log("[DEBUG] mxchat_check_email_provided -> Required info found, returning success");
1018 -
1019 - $response_data = ['email' => $stored_email];
1020 - if (!empty($stored_name)) {
1021 - $response_data['name'] = $stored_name;
1022 - }
1023 -
1024 - wp_send_json_success($response_data);
612 + if (!empty($stored_email)) {
613 + //error_log("[DEBUG] mxchat_check_email_provided -> Email found, returning success");
614 + wp_send_json_success(['email' => $stored_email]);
1025 615 } else {
1026 - //error_log("[DEBUG] mxchat_check_email_provided -> Required info missing, returning error");
616 + //error_log("[DEBUG] mxchat_check_email_provided -> No email found, returning error");
1027 617 wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
1028 618 }
1029 619 }
1030 620
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 621 public function mxchat_handle_chat_request() {
1059 622 global $wpdb;
1060 623
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);
624 + // NEW: Check if this is a streaming request
625 + $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat';
1065 626
1066 - // Get bot-specific options
1067 - $bot_options = $this->get_bot_options($bot_id);
1068 - $current_options = !empty($bot_options) ? $bot_options : $this->options;
627 + // NEW: Set streaming headers if needed
628 + if ($is_streaming) {
629 + // Disable output buffering
630 + while (ob_get_level()) {
631 + ob_end_flush(); // Changed from ob_end_clean()
632 + }
633 +
634 + // Set headers for SSE
635 + header('Content-Type: text/event-stream');
636 + header('Cache-Control: no-cache');
637 + header('Connection: keep-alive');
638 + header('X-Accel-Buffering: no');
639 +
640 + // Add these new lines:
641 + ob_implicit_flush(true);
642 + flush();
643 + }
1069 644
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 645 // Check if MX Chat Moderation is active
1083 646 if (class_exists('MX_Chat_Moderation')) {
1084 647 // Get user email and IP
1085 648 $user_email = '';
@@ -1149,17 +712,8 @@
1149 712 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
1150 713 wp_die();
1151 714 }
1152 715
1153 - // Update session owner if it changed (e.g. IP changed due to network switch)
1154 - // The session ID itself is the authentication — if the client has it, they own it
1155 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
1156 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
1157 -
1158 - if (!$session_owner || $session_owner !== $current_user_identifier) {
1159 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
1160 - }
1161 -
1162 716 // Validate and sanitize the incoming message
1163 717 if (empty($_POST['message'])) {
1164 718 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
1165 719 wp_die();
@@ -1165,191 +719,209 @@
1165 719 wp_die();
1166 720 }
1167 721
1168 722
1169 - // Track originating page for first message in session
1170 - $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
723 + // NEW: Track originating page for first message in session
724 +$table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1171 725
1172 - // Check if originating page columns exist
1173 - $columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
726 +// Check if originating page columns exist
727 +$columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
1174 728
1175 - if ($columns_exist) {
1176 - // Check if this session already has messages
1177 - $message_count = $wpdb->get_var($wpdb->prepare(
1178 - "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
1179 - $session_id
1180 - ));
729 +if ($columns_exist) {
730 + // Check if this session already has messages
731 + $message_count = $wpdb->get_var($wpdb->prepare(
732 + "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
733 + $session_id
734 + ));
735 +
736 + // If this is the first message in the session
737 + if ($message_count == 0) {
738 + // Get originating page from JavaScript (preferred) or HTTP_REFERER (fallback)
739 + $originating_url = '';
740 + $originating_title = '';
1181 741
1182 - // If this is the first message in the session
1183 - if ($message_count == 0) {
1184 - // Get originating page from JavaScript (preferred) or HTTP_REFERER (fallback)
1185 - $originating_url = '';
1186 - $originating_title = '';
742 + // Try to get from POST data first (sent by JavaScript)
743 + if (isset($_POST['current_page_url'])) {
744 + $originating_url = esc_url_raw($_POST['current_page_url']);
745 + $originating_title = isset($_POST['current_page_title'])
746 + ? sanitize_text_field($_POST['current_page_title'])
747 + : '';
748 + }
749 + // Fallback to HTTP_REFERER if not provided by JavaScript
750 + else if (isset($_SERVER['HTTP_REFERER'])) {
751 + $originating_url = esc_url_raw($_SERVER['HTTP_REFERER']);
752 + }
753 +
754 + // Generate title if we have URL but no title
755 + if ($originating_url && empty($originating_title)) {
756 + $parsed_url = parse_url($originating_url);
757 + $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
1187 758
1188 - // Try to get from POST data first (sent by JavaScript)
1189 - if (isset($_POST['current_page_url'])) {
1190 - $originating_url = esc_url_raw($_POST['current_page_url']);
1191 - $originating_title = isset($_POST['current_page_title'])
1192 - ? sanitize_text_field($_POST['current_page_title'])
1193 - : '';
759 + if (empty($path) || $path === 'index.php' || $path === 'index.html') {
760 + $originating_title = 'Homepage';
761 + } else {
762 + // Clean up the path to make a readable title
763 + $originating_title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
764 + $originating_title = ucwords(trim($originating_title));
1194 765 }
1195 - // Fallback to HTTP_REFERER if not provided by JavaScript
1196 - else if (isset($_SERVER['HTTP_REFERER'])) {
1197 - $originating_url = esc_url_raw($_SERVER['HTTP_REFERER']);
1198 - }
1199 -
1200 - // Generate title if we have URL but no title
1201 - if ($originating_url && empty($originating_title)) {
1202 - $parsed_url = parse_url($originating_url);
1203 - $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
1204 -
1205 - if (empty($path) || $path === 'index.php' || $path === 'index.html') {
1206 - $originating_title = 'Homepage';
1207 - } else {
1208 - // Clean up the path to make a readable title
1209 - $originating_title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
1210 - $originating_title = ucwords(trim($originating_title));
1211 - }
1212 - }
1213 -
1214 - // Store for later use when saving the message
1215 - $this->pending_originating_page = [
1216 - 'url' => $originating_url,
1217 - 'title' => $originating_title
1218 - ];
1219 766 }
767 +
768 + // Store for later use when saving the message
769 + $this->pending_originating_page = [
770 + 'url' => $originating_url,
771 + 'title' => $originating_title
772 + ];
1220 773 }
774 +}
775 +
776 +
777 +
778 + // NEW: Get page context if provided
779 + $page_context = null;
780 + if (isset($_POST['page_context']) && !empty($_POST['page_context'])) {
781 + $page_context_raw = stripslashes($_POST['page_context']);
782 + $page_context = json_decode($page_context_raw, true);
1221 783
1222 -
1223 -
1224 - // Get page context if provided
1225 - $page_context = null;
1226 - if (isset($_POST['page_context']) && !empty($_POST['page_context'])) {
1227 - $page_context_raw = stripslashes($_POST['page_context']);
1228 - $page_context = json_decode($page_context_raw, true);
784 + // Validate page context structure
785 + if (is_array($page_context) &&
786 + isset($page_context['url']) &&
787 + isset($page_context['title']) &&
788 + isset($page_context['content'])) {
1229 789
1230 - // Validate page context structure
1231 - if (is_array($page_context) &&
1232 - isset($page_context['url']) &&
1233 - isset($page_context['title']) &&
1234 - isset($page_context['content'])) {
1235 -
1236 - // Sanitize page context
1237 - $page_context['url'] = esc_url_raw($page_context['url']);
1238 - $page_context['title'] = sanitize_text_field($page_context['title']);
1239 - $page_context['content'] = wp_kses_post($page_context['content']);
1240 - } else {
1241 - $page_context = null;
1242 - }
790 + // Sanitize page context
791 + $page_context['url'] = esc_url_raw($page_context['url']);
792 + $page_context['title'] = sanitize_text_field($page_context['title']);
793 + $page_context['content'] = wp_kses_post($page_context['content']);
794 + } else {
795 + $page_context = null;
1243 796 }
797 + }
1244 798
1245 - // Modify the message sanitization to preserve PHP tags in code blocks
1246 - $allowed_tags = [
1247 - 'pre' => [],
1248 - 'code' => ['class' => true],
1249 - 'span' => ['class' => true],
1250 - 'div' => ['class' => true],
1251 - ];
799 + // Modify the message sanitization to preserve PHP tags in code blocks
800 + $allowed_tags = [
801 + 'pre' => [],
802 + 'code' => ['class' => true],
803 + 'span' => ['class' => true],
804 + 'div' => ['class' => true],
805 + ];
1252 806
1253 - // First preserve code blocks
1254 - $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
1255 - return htmlspecialchars_decode($matches[0]);
1256 - }, $_POST['message']);
807 + // First preserve code blocks
808 + $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
809 + return htmlspecialchars_decode($matches[0]);
810 + }, $_POST['message']);
1257 811
1258 - // Then apply sanitization
1259 - $message = wp_kses($message, $allowed_tags);
812 + // Then apply sanitization
813 + $message = wp_kses($message, $allowed_tags);
1260 814
1261 - // Preserve code blocks from markdown conversion
1262 - $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
1263 - $message = apply_filters('mxchat_filter_message', $message, 'prompt', $session_id);
815 + // Preserve code blocks from markdown conversion
816 + $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
817 + $message = apply_filters('mxchat_filter_message', $message, 'prompt', $session_id);
1264 818
1265 - // ===== SIMPLIFIED TESTING PANEL INITIALIZATION =====
1266 - // Always initialize testing data for admins (no toggle needed)
1267 - $testing_data = null;
1268 - if (current_user_can('administrator')) {
1269 - // For vision messages, use the original user message for the query display
1270 - $query_for_testing = $message;
1271 - if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
1272 - $query_for_testing = sanitize_textarea_field($_POST['original_user_message']);
1273 - }
1274 -
1275 - $testing_data = [
1276 - 'query' => $query_for_testing,
1277 - 'timestamp' => time(),
1278 - 'top_matches' => [],
1279 - 'action_matches' => [], // Initialize action matches array
1280 - 'page_context' => $page_context, // Include page context in testing data
1281 - 'is_vision' => isset($_POST['vision_processed']) && $_POST['vision_processed'],
1282 - 'bot_id' => $bot_id // Include bot ID in testing data
1283 - ];
1284 -
1285 - // Get similarity threshold from bot options or default options
1286 - $similarity_threshold = isset($current_options['similarity_threshold'])
1287 - ? ((int) $current_options['similarity_threshold']) / 100
1288 - : 0.35;
1289 -
1290 - $testing_data['similarity_threshold'] = $similarity_threshold;
1291 -
1292 - // Determine knowledge base type using bot-specific config
1293 - $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
1294 - $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
1295 - $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
819 +// ===== SIMPLIFIED TESTING PANEL INITIALIZATION =====
820 + // Always initialize testing data for admins (no toggle needed)
821 + $testing_data = null;
822 + if (current_user_can('administrator')) {
823 + // For vision messages, use the original user message for the query display
824 + $query_for_testing = $message;
825 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
826 + $query_for_testing = sanitize_textarea_field($_POST['original_user_message']);
1296 827 }
1297 - // ===== END SIMPLIFIED TESTING INITIALIZATION =====
828 +
829 + $testing_data = [
830 + 'query' => $query_for_testing,
831 + 'timestamp' => time(),
832 + 'top_matches' => [],
833 + 'action_matches' => [], // NEW: Initialize action matches array
834 + 'page_context' => $page_context, // NEW: Include page context in testing data
835 + 'is_vision' => isset($_POST['vision_processed']) && $_POST['vision_processed']
836 + ];
837 +
838 + // Get similarity threshold
839 + $similarity_threshold = isset($this->options['similarity_threshold'])
840 + ? ((int) $this->options['similarity_threshold']) / 100
841 + : 0.75;
842 +
843 + $testing_data['similarity_threshold'] = $similarity_threshold;
844 +
845 + // Determine knowledge base type
846 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
847 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
848 + $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
849 + }
850 + // ===== END SIMPLIFIED TESTING INITIALIZATION =====
1298 851
1299 - // Add debug before and after:
1300 - //error_log('MxChat Core: About to call mxchat_pre_process_message filter with message: ' . $message);
1301 - $pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
1302 - //error_log('MxChat Core: Filter returned: ' . (is_array($pre_processed_result) ? 'array' : $pre_processed_result));
852 +// Add debug before and after:
853 +error_log('MxChat Core: About to call mxchat_pre_process_message filter with message: ' . $message);
854 +$pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
855 +error_log('MxChat Core: Filter returned: ' . (is_array($pre_processed_result) ? 'array' : $pre_processed_result));
1303 856
1304 857
1305 - // If the pre-processing returned a result (not the original message), use it directly
1306 - if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
1307 - // Save the AI response
1308 - $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
1309 -
1310 - // Save HTML content if provided
1311 - if (!empty($pre_processed_result['html'])) {
1312 - $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
1313 - }
1314 -
1315 - // Add testing data if admin
1316 - $response_data = [
1317 - 'text' => $pre_processed_result['text'],
1318 - 'html' => $pre_processed_result['html'] ?? '',
1319 - 'session_id' => $session_id
1320 - ];
1321 -
1322 - if ($testing_data !== null) {
1323 - $response_data['testing_data'] = $testing_data;
1324 - }
1325 -
1326 - wp_send_json($response_data);
1327 - wp_die();
858 + // If the pre-processing returned a result (not the original message), use it directly
859 + if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
860 + // Save the AI response
861 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
862 +
863 + // Save HTML content if provided
864 + if (!empty($pre_processed_result['html'])) {
865 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
1328 866 }
867 +
868 + // Add testing data if admin
869 + $response_data = [
870 + 'text' => $pre_processed_result['text'],
871 + 'html' => $pre_processed_result['html'] ?? '',
872 + 'session_id' => $session_id
873 + ];
874 +
875 + if ($testing_data !== null) {
876 + $response_data['testing_data'] = $testing_data;
877 + }
878 +
879 + wp_send_json($response_data);
880 + wp_die();
881 + }
1329 882
1330 - // Save the user's message - handle vision processed messages differently
1331 - if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
1332 - // For vision messages, save the original user message with image indicator
1333 - $original_message = sanitize_textarea_field($_POST['original_user_message']);
1334 - if (isset($_POST['vision_images_count']) && $_POST['vision_images_count'] > 0) {
1335 - $image_count = intval($_POST['vision_images_count']);
1336 - $original_message .= " [{$image_count} image(s)]";
1337 - }
1338 - $this->mxchat_save_chat_message($session_id, 'user', $original_message);
1339 - } else {
1340 - // Regular message - save as normal
1341 - $this->mxchat_save_chat_message($session_id, 'user', $message);
883 + // Save the user's message - handle vision processed messages differently
884 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
885 + // For vision messages, save the original user message with image indicator
886 + $original_message = sanitize_textarea_field($_POST['original_user_message']);
887 + if (isset($_POST['vision_images_count']) && $_POST['vision_images_count'] > 0) {
888 + $image_count = intval($_POST['vision_images_count']);
889 + $original_message .= " [{$image_count} image(s)]";
1342 890 }
891 + $this->mxchat_save_chat_message($session_id, 'user', $original_message);
892 + } else {
893 + // Regular message - save as normal
894 + $this->mxchat_save_chat_message($session_id, 'user', $message);
895 + }
1343 896
897 +
898 +if (is_email($message)) {
899 + // Add the email to Loops
900 + $this->add_email_to_loops($message);
1344 901
1345 - if (is_email($message)) {
1346 - // Add the email to Loops
1347 - $this->add_email_to_loops($message);
902 + // Get the user's success message instruction
903 + $user_success_message = $this->options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
904 +
905 + // Set instruction for AI using the user's success message
906 + $this->current_action_instruction = $user_success_message;
907 +
908 + // Clear the email capture transient since we got the email
909 + delete_transient('mxchat_email_capture_' . $user_id);
910 + }
911 +
912 + // NEW: Check if we're in an email capture flow but user hasn't provided email yet
913 + elseif (get_transient('mxchat_email_capture_' . $user_id)) {
914 + // Check if the message contains an email (not the whole message being an email)
915 + if (preg_match('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', $message, $matches)) {
916 + $extracted_email = $matches[0];
1348 917
1349 - // Get the user's success message instruction using current_options
1350 - $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
918 + // Add the extracted email to Loops
919 + $this->add_email_to_loops($extracted_email);
1351 920
921 + // Get the user's success message instruction
922 + $user_success_message = $this->options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
923 +
1352 924 // Set instruction for AI using the user's success message
1353 925 $this->current_action_instruction = $user_success_message;
1354 926
1355 927 // Clear the email capture transient since we got the email
@@ -1354,692 +926,464 @@
1354 926
1355 927 // Clear the email capture transient since we got the email
1356 928 delete_transient('mxchat_email_capture_' . $user_id);
1357 929 }
1358 -
1359 - // Check if we're in an email capture flow but user hasn't provided email yet
1360 - elseif (get_transient('mxchat_email_capture_' . $user_id)) {
1361 - // Check if the message contains an email (not the whole message being an email)
1362 - if (preg_match('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', $message, $matches)) {
1363 - $extracted_email = $matches[0];
1364 -
1365 - // Add the extracted email to Loops
1366 - $this->add_email_to_loops($extracted_email);
1367 -
1368 - // Get the user's success message instruction using current_options
1369 - $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
1370 -
1371 - // Set instruction for AI using the user's success message
1372 - $this->current_action_instruction = $user_success_message;
1373 -
1374 - // Clear the email capture transient since we got the email
1375 - delete_transient('mxchat_email_capture_' . $user_id);
1376 - }
1377 - // If no email found but we're in capture mode, remind them
1378 - else {
1379 - // Get the original instruction to remind them using current_options
1380 - $original_instruction = $current_options['triggered_phrase_response'] ?? __("Please provide your email address.", 'mxchat');
1381 - $this->current_action_instruction = $original_instruction;
1382 - }
930 + // If no email found but we're in capture mode, remind them
931 + else {
932 + // Get the original instruction to remind them
933 + $original_instruction = $this->options['triggered_phrase_response'] ?? __("Please provide your email address.", 'mxchat');
934 + $this->current_action_instruction = $original_instruction;
1383 935 }
936 + }
1384 937
1385 - $intent_info = '';
938 + $intent_info = '';
1386 939
1387 - // Check chat mode
1388 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
940 + // Check chat mode
941 + $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
1389 942
1390 - // Handle agent mode
1391 943 // Handle agent mode
1392 - if ($chat_mode === 'agent') {
1393 - // First, check for switch intent before doing anything else
1394 - $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
944 +// Handle agent mode
945 + if ($chat_mode === 'agent') {
946 + // First, check for switch intent before doing anything else
947 + $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1395 948
1396 - // Capture action analysis for testing panel after intent check
1397 - if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1398 - $testing_data['action_matches'] = $this->last_action_analysis;
949 + // NEW: Capture action analysis for testing panel after intent check
950 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
951 + $testing_data['action_matches'] = $this->last_action_analysis;
952 + }
953 +
954 + // If we matched an intent and it's the switch intent, handle it
955 + if ($intent_matched && !empty($this->fallbackResponse['text'])) {
956 + // Update chat mode first
957 + update_option("mxchat_mode_{$session_id}", 'ai');
958 +
959 + // Clear any existing PDF context to start fresh
960 + $this->clear_pdf_transients($session_id);
961 +
962 + // Prepare clean switch response
963 + $response_data = [
964 + 'text' => $this->fallbackResponse['text'],
965 + 'html' => '',
966 + 'session_id' => $session_id,
967 + 'chat_mode' => 'ai'
968 + ];
969 +
970 + if ($testing_data !== null) {
971 + $response_data['testing_data'] = $testing_data;
1399 972 }
1400 -
1401 - // Around line 506, in the agent mode handling section:
1402 - if ($intent_matched && !empty($this->fallbackResponse['text'])) {
1403 - // Update chat mode first
1404 - update_option("mxchat_mode_{$session_id}", 'ai');
1405 -
1406 - // Clear any existing PDF context to start fresh
1407 - $this->clear_pdf_transients($session_id);
1408 -
1409 - // Prepare clean switch response with explicit chat_mode
1410 - $response_data = [
1411 - 'text' => $this->fallbackResponse['text'],
1412 - 'html' => $this->fallbackResponse['html'] ?? '',
1413 - 'session_id' => $session_id,
1414 - 'chat_mode' => 'ai' // EXPLICITLY SET THIS
973 +
974 + // Save the mode switch message
975 + $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
976 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
977 +
978 + // Send response and exit
979 + wp_send_json($response_data);
980 + wp_die();
981 + } elseif (!$intent_matched) {
982 + // No intent matched, handle live agent message
983 + try {
984 + $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
985 +
986 + $agent_response = [
987 + 'status' => 'waiting_for_agent',
988 + 'message' => esc_html__('Message sent to live agent.', 'mxchat')
1415 989 ];
1416 -
990 +
1417 991 if ($testing_data !== null) {
1418 - $response_data['testing_data'] = $testing_data;
992 + $agent_response['testing_data'] = $testing_data;
1419 993 }
1420 -
1421 - // Save the mode switch message
1422 - $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
1423 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1424 -
1425 - // Send response and exit
1426 - wp_send_json($response_data);
1427 - wp_die();
1428 - } elseif (!$intent_matched) {
1429 - // No intent matched, handle live agent message
1430 - try {
1431 - $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
1432 994
1433 - $agent_response = [
1434 - 'status' => 'waiting_for_agent',
1435 - 'message' => esc_html__('Message sent to live agent.', 'mxchat')
1436 - ];
1437 -
1438 - if ($testing_data !== null) {
1439 - $agent_response['testing_data'] = $testing_data;
1440 - }
1441 -
1442 - wp_send_json_success($agent_response);
1443 - } catch (\Exception $e) {
1444 - wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
1445 - }
1446 - wp_die();
995 + wp_send_json_success($agent_response);
996 + } catch (\Exception $e) {
997 + wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
1447 998 }
999 + wp_die();
1448 1000 }
1001 + }
1449 1002
1450 - // Step 1: Check for new PDF URL in the message
1451 - if (!isset($_POST['vision_processed']) && preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1452 - $new_pdf_url = $matches[0];
1003 + // Step 1: Check for new PDF URL in the message
1004 + if (!isset($_POST['vision_processed']) && preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1005 + $new_pdf_url = $matches[0];
1453 1006
1454 - // Check if this is likely a PDF-related request
1455 - $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
1456 - $is_pdf_request = false;
1007 + // Check if this is likely a PDF-related request
1008 + $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
1009 + $is_pdf_request = false;
1457 1010
1458 - foreach ($pdf_keywords as $keyword) {
1459 - if (stripos($message, $keyword) !== false) {
1460 - $is_pdf_request = true;
1461 - break;
1462 - }
1011 + foreach ($pdf_keywords as $keyword) {
1012 + if (stripos($message, $keyword) !== false) {
1013 + $is_pdf_request = true;
1014 + break;
1463 1015 }
1016 + }
1464 1017
1465 - // If it looks like a PDF request or we're waiting for a PDF URL
1466 - if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
1467 - // Validate HTTPS
1468 - if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
1469 - // Extract filename from URL
1470 - $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1018 + // If it looks like a PDF request or we're waiting for a PDF URL
1019 + if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
1020 + // Validate HTTPS
1021 + if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
1022 + // Extract filename from URL
1023 + $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1471 1024
1472 - // Clear previous PDF transients
1473 - $this->clear_pdf_transients($session_id);
1025 + // Clear previous PDF transients
1026 + $this->clear_pdf_transients($session_id);
1474 1027
1475 - // Process new PDF using current_options
1476 - $max_pages = $current_options['pdf_max_pages'] ?? 69;
1477 - $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
1028 + // Process new PDF
1029 + $max_pages = $this->options['pdf_max_pages'] ?? 69;
1030 + $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
1478 1031
1479 - if ($embeddings === 'too_many_pages') {
1480 - $error_text = sprintf(
1481 - $current_options['pdf_intent_error_text'] ??
1482 - esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
1483 - $max_pages
1484 - );
1485 - $this->fallbackResponse['text'] = $error_text;
1486 - } elseif ($embeddings) {
1487 - // Store new PDF information
1488 - $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1032 + if ($embeddings === 'too_many_pages') {
1033 + $error_text = sprintf(
1034 + $this->options['pdf_intent_error_text'] ??
1035 + esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
1036 + $max_pages
1037 + );
1038 + $this->fallbackResponse['text'] = $error_text;
1039 + } elseif ($embeddings) {
1040 + // Store new PDF information
1041 + $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1489 1042
1490 - // If the filename is generic, create a more descriptive one
1491 - if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
1492 - strpos($pdf_filename, '.php') !== false) {
1493 - $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
1494 - }
1043 + // If the filename is generic, create a more descriptive one
1044 + if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
1045 + strpos($pdf_filename, '.php') !== false) {
1046 + $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
1047 + }
1495 1048
1496 - set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
1497 - set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
1498 - set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
1499 - set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
1049 + set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
1050 + set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
1051 + set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
1052 + set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
1500 1053
1501 - $success_text = $current_options['pdf_intent_success_text'] ??
1502 - esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
1054 + $success_text = $this->options['pdf_intent_success_text'] ??
1055 + esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
1503 1056
1504 - $pdf_response = [
1505 - 'success' => true,
1506 - 'message' => $success_text,
1507 - 'data' => [
1508 - 'filename' => $pdf_filename
1509 - ]
1510 - ];
1511 -
1512 - if ($testing_data !== null) {
1513 - $pdf_response['testing_data'] = $testing_data;
1514 - }
1515 -
1516 - wp_send_json($pdf_response);
1517 - wp_die();
1518 - } else {
1519 - $error_text = $current_options['pdf_intent_error_text'] ??
1520 - esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
1521 - $this->fallbackResponse['text'] = $error_text;
1522 - }
1523 -
1524 - $pdf_error_response = [
1525 - 'success' => false,
1526 - 'message' => $this->fallbackResponse['text']
1057 + $pdf_response = [
1058 + 'success' => true,
1059 + 'message' => $success_text,
1060 + 'data' => [
1061 + 'filename' => $pdf_filename
1062 + ]
1527 1063 ];
1528 1064
1529 1065 if ($testing_data !== null) {
1530 - $pdf_error_response['testing_data'] = $testing_data;
1066 + $pdf_response['testing_data'] = $testing_data;
1531 1067 }
1532 1068
1533 - wp_send_json($pdf_error_response);
1069 + wp_send_json($pdf_response);
1534 1070 wp_die();
1071 + } else {
1072 + $error_text = $this->options['pdf_intent_error_text'] ??
1073 + esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
1074 + $this->fallbackResponse['text'] = $error_text;
1535 1075 }
1536 - }
1537 - }
1538 1076
1539 -
1540 - // Step 2: Detect intent and handle intent-based responses
1541 - $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1542 -
1543 - // Capture action analysis for testing panel after intent check
1544 - if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1545 - $testing_data['action_matches'] = $this->last_action_analysis;
1546 - }
1547 -
1548 - // Step 3: Handle the intent result appropriately
1549 - if ($intent_result !== false) {
1550 - // Intent was matched - ALWAYS send as JSON response, never streaming
1551 -
1552 - if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
1553 - // Intent returned a direct response array
1554 - $response_data = [
1555 - 'text' => $intent_result['text'] ?? '',
1556 - 'html' => $intent_result['html'] ?? '',
1557 - 'session_id' => $session_id
1077 + $pdf_error_response = [
1078 + 'success' => false,
1079 + 'message' => $this->fallbackResponse['text']
1558 1080 ];
1559 -
1560 - // IMPORTANT: Include chat_mode if present (for WhatsApp, Slack, etc.)
1561 - if (isset($intent_result['chat_mode'])) {
1562 - $response_data['chat_mode'] = $intent_result['chat_mode'];
1563 - }
1564 -
1081 +
1565 1082 if ($testing_data !== null) {
1566 - $response_data['testing_data'] = $testing_data;
1083 + $pdf_error_response['testing_data'] = $testing_data;
1567 1084 }
1568 1085
1569 - wp_send_json($response_data);
1086 + wp_send_json($pdf_error_response);
1570 1087 wp_die();
1571 - } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1572 - // Intent returned true and set fallbackResponse
1573 -
1574 - // SAVE TO TRANSCRIPT
1575 - if (!empty($this->fallbackResponse['text'])) {
1576 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1577 - }
1578 - // Save action HTML (product cards, featured products, etc.) so it renders in transcripts
1579 - if (!empty($this->fallbackResponse['html'])) {
1580 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1581 - }
1582 -
1583 - $response_data = [
1584 - 'text' => $this->fallbackResponse['text'] ?? '',
1585 - 'html' => $this->fallbackResponse['html'] ?? '',
1586 - 'session_id' => $session_id
1587 - ];
1588 -
1589 - if (isset($this->fallbackResponse['chat_mode'])) {
1590 - $response_data['chat_mode'] = $this->fallbackResponse['chat_mode'];
1591 - }
1592 -
1593 - if ($testing_data !== null) {
1594 - $response_data['testing_data'] = $testing_data;
1595 - }
1596 -
1597 - wp_send_json($response_data);
1598 - wp_die();
1599 1088 }
1600 1089 }
1090 + }
1601 1091
1602 - // If we get here, no intent matched OR the intent didn't provide a usable response
1603 -
1604 - // Step 4: Generate AI response
1605 - // Get session start timestamp - when persistence is OFF, only include messages from this page load
1606 - $session_start_timestamp = isset($_POST['session_start_timestamp']) ? intval($_POST['session_start_timestamp']) : 0;
1607 - $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id, $session_start_timestamp);
1608 - $this->mxchat_increment_chat_count();
1092 + // Check if there's an active recommendation flow session
1093 + $flow_state = get_option("mxchat_sr_flow_state_{$session_id}", array());
1094 + if (!empty($flow_state) && isset($flow_state['flow_id'])) {
1095 + // Create a dummy intent object that matches the original intent
1096 + $dummy_intent = new stdClass();
1097 + $dummy_intent->intent_label = 'Recommendation Flow ' . $flow_state['flow_id'];
1098 + $dummy_intent->phrases = ''; // Empty phrases to avoid matching the original trigger
1609 1099
1610 - // Generate embedding for the user's query - USE BOT-SPECIFIC API KEY
1611 - $api_key = $current_options['api_key'] ?? $this->options['api_key'];
1612 - $user_message_embedding = $this->mxchat_generate_embedding($message, $api_key);
1100 + // Call the recommendation flow handler directly
1101 + $response_data = apply_filters('mxchat_sr_recommendation_flow', false, $message, $user_id, $session_id, $dummy_intent);
1613 1102
1614 - // Check if the embedding generation returned an error
1615 - if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1616 - $error_message = $user_message_embedding['error'];
1617 - $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
1618 -
1619 - // FIXED: Send error in appropriate format based on streaming mode
1620 - if ($is_streaming) {
1621 - echo "data: " . json_encode([
1622 - 'error' => true,
1623 - 'error_message' => $error_message,
1624 - 'error_code' => $error_code,
1625 - 'text' => $error_message,
1626 - 'message' => $error_message
1627 - ]) . "\n\n";
1628 - echo "data: [DONE]\n\n";
1629 - flush();
1630 - } else {
1631 - wp_send_json_error([
1632 - 'error_message' => $error_message,
1633 - 'error_code' => $error_code
1634 - ]);
1103 + // If the handler returned a response, send it
1104 + if (is_array($response_data) && (isset($response_data['text']) || isset($response_data['html']))) {
1105 + // Save the bot's response to the chat history
1106 + if (!empty($response_data['text'])) {
1107 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['text']);
1635 1108 }
1109 + if (!empty($response_data['html'])) {
1110 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['html']);
1111 + }
1112 +
1113 + if ($testing_data !== null) {
1114 + $response_data['testing_data'] = $testing_data;
1115 + }
1116 +
1117 + // Send the response
1118 + wp_send_json($response_data);
1636 1119 wp_die();
1637 1120 }
1121 + }
1638 1122
1639 - // Check if the embedding is valid
1640 - if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1641 - $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
1123 + // Step 2: Detect intent and handle intent-based responses
1124 + $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1642 1125
1643 - // FIXED: Send error in appropriate format based on streaming mode
1126 + // NEW: Capture action analysis for testing panel after intent check
1127 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1128 + $testing_data['action_matches'] = $this->last_action_analysis;
1129 + }
1130 +
1131 + // Step 3: Handle the intent result appropriately
1132 + if ($intent_result !== false) {
1133 + // Intent was matched - ALWAYS send as JSON response, never streaming
1134 +
1135 + if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
1136 + // Intent returned a direct response array
1137 + $response_data = [
1138 + 'text' => $intent_result['text'] ?? '',
1139 + 'html' => $intent_result['html'] ?? '',
1140 + 'session_id' => $session_id
1141 + ];
1142 +
1143 + if ($testing_data !== null) {
1144 + $response_data['testing_data'] = $testing_data;
1145 + }
1146 +
1147 + // Clear streaming headers if they were set
1644 1148 if ($is_streaming) {
1645 - echo "data: " . json_encode([
1646 - 'error' => true,
1647 - 'error_message' => $error_message,
1648 - 'error_code' => 'invalid_embedding',
1649 - 'text' => $error_message,
1650 - 'message' => $error_message
1651 - ]) . "\n\n";
1652 - echo "data: [DONE]\n\n";
1653 - flush();
1654 - } else {
1655 - wp_send_json_error([
1656 - 'error_message' => $error_message,
1657 - 'error_code' => 'invalid_embedding'
1658 - ]);
1149 + header_remove('Content-Type');
1150 + header_remove('Cache-Control');
1151 + header_remove('Connection');
1152 + header_remove('X-Accel-Buffering');
1153 + header('Content-Type: application/json');
1659 1154 }
1155 +
1156 + wp_send_json($response_data);
1660 1157 wp_die();
1661 - }
1662 -
1663 - // Build context with both knowledge base and PDF content if available
1664 - $context_content = "User asked: '{$message}'\n\n";
1665 -
1666 - // Add action instruction if present (add this right after the above line)
1667 - if (!empty($this->current_action_instruction)) {
1668 - $context_content .= "===== SPECIAL INSTRUCTION =====\n";
1669 - $context_content .= "IMPORTANT: " . $this->current_action_instruction . "\n";
1670 - $context_content .= "Respond naturally and conversationally while following this instruction.\n";
1671 - $context_content .= "===== END SPECIAL INSTRUCTION =====\n\n";
1158 + } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1159 + // Intent returned true and set fallbackResponse
1672 1160
1673 - // Clear the instruction after using it
1674 - $this->current_action_instruction = null;
1675 - }
1676 -
1677 -
1678 - // Add page context if available and contextual awareness is enabled using current_options
1679 - if ($page_context && isset($current_options['contextual_awareness_toggle']) && $current_options['contextual_awareness_toggle'] === 'on') {
1680 - $context_content .= "===== CURRENT PAGE CONTEXT =====\n";
1681 - $context_content .= "Page URL: " . $page_context['url'] . "\n";
1682 - $context_content .= "Page Title: " . $page_context['title'] . "\n";
1683 - $context_content .= "Page Content: " . $page_context['content'] . "\n";
1684 - $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1685 - }
1686 -
1687 - // Get relevant content from knowledge base - PASS BOT_ID and MESSAGE for Vector Store
1688 - $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding, $bot_id, $message);
1689 -
1690 - // NEW: Also extract URLs from system instructions (only if citation links enabled)
1691 - // Use fresh options to ensure we get the latest setting value
1692 - $fresh_options = get_option('mxchat_options', []);
1693 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
1694 -
1695 - $system_instructions = $this->get_system_instructions($bot_id, $session_id);
1696 - if ($citation_links_enabled && !empty($system_instructions)) {
1697 - preg_match_all(
1698 - '#\bhttps?://[^\s<>"\']+#i',
1699 - $system_instructions,
1700 - $system_instruction_urls
1701 - );
1702 -
1703 - if (!empty($system_instruction_urls[0])) {
1704 - // Merge with existing valid URLs
1705 - $this->current_valid_urls = array_merge(
1706 - $this->current_valid_urls,
1707 - $system_instruction_urls[0]
1708 - );
1709 - // Remove duplicates
1710 - $this->current_valid_urls = array_unique($this->current_valid_urls);
1711 -
1712 - //error_log("Added " . count($system_instruction_urls[0]) . " URLs from system instructions");
1161 + // SAVE TO TRANSCRIPT FIRST
1162 + if (!empty($this->fallbackResponse['text'])) {
1163 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1713 1164 }
1714 - }
1715 -
1716 -// ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1717 -if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1718 - // Update testing data with the REAL similarity analysis
1719 - $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1720 - $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1721 - $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1722 - $testing_data['sources_used'] = $this->last_similarity_analysis['sources_used'] ?? 0;
1723 - $testing_data['total_chunks_used'] = $this->last_similarity_analysis['total_chunks_used'] ?? 0;
1724 -}
1725 -// ===== END SIMILARITY DATA CAPTURE =====
1726 -
1727 -// NEW: Add valid URLs to testing data for admin panel display (AFTER similarity data)
1728 -if ($testing_data !== null && !empty($this->current_valid_urls)) {
1729 - $testing_data['approved_urls'] = array_values($this->current_valid_urls);
1730 - //error_log("Added " . count($this->current_valid_urls) . " approved URLs to testing data");
1731 -}
1732 -
1733 - if (!empty($relevant_content)) {
1734 - $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1735 - } else {
1736 - $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
1737 - }
1738 -
1739 - // NEW: Add approved URLs list to context for AI (only if citation links enabled)
1740 - if ($citation_links_enabled && !empty($this->current_valid_urls)) {
1741 - $context_content .= "===== APPROVED URLS FOR CITATIONS =====\n";
1742 - $context_content .= "You may ONLY use these exact URLs in your response:\n";
1743 - foreach ($this->current_valid_urls as $url) {
1744 - $context_content .= "- " . $url . "\n";
1165 + if (!empty($this->fallbackResponse['html'])) {
1166 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1745 1167 }
1746 - $context_content .= "\nCRITICAL: Do NOT create, modify, extend, or invent any other URLs. ";
1747 - $context_content .= "===== END APPROVED URLS =====\n\n";
1748 - }
1749 -
1750 - // Check for and include PDF content
1751 - $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
1752 - $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
1753 - $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
1754 - if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
1755 - $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
1756 - if (!empty($relevant_pdf_pages)) {
1757 - $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
1758 - foreach ($relevant_pdf_pages as $page_data) {
1759 - $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
1760 - }
1761 - $context_content .= "\n";
1168 +
1169 + $response_data = [
1170 + 'text' => $this->fallbackResponse['text'] ?? '',
1171 + 'html' => $this->fallbackResponse['html'] ?? '',
1172 + 'session_id' => $session_id
1173 + ];
1174 +
1175 + if ($testing_data !== null) {
1176 + $response_data['testing_data'] = $testing_data;
1762 1177 }
1763 - }
1764 -
1765 - // Check for and include Word content
1766 - $word_url = get_transient('mxchat_word_url_' . $session_id);
1767 - $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
1768 - $word_filename = get_transient('mxchat_word_filename_' . $session_id);
1769 - if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
1770 - $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
1771 - if (!empty($relevant_word_chunks)) {
1772 - $context_content .= "Relevant content from Word document '{$word_filename}':\n";
1773 - foreach ($relevant_word_chunks as $chunk_data) {
1774 - $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
1775 - }
1776 - $context_content .= "\n";
1178 +
1179 + // Clear streaming headers if they were set
1180 + if ($is_streaming) {
1181 + header_remove('Content-Type');
1182 + header_remove('Cache-Control');
1183 + header_remove('Connection');
1184 + header_remove('X-Accel-Buffering');
1185 + header('Content-Type: application/json');
1777 1186 }
1187 +
1188 + wp_send_json($response_data);
1189 + wp_die();
1778 1190 }
1779 -
1780 - $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
1191 + }
1781 1192
1782 - // Extract model from current options for bot-specific model support
1783 - $selected_model = isset($current_options['model']) ? $current_options['model'] : 'gpt-5.1-chat-latest';
1193 + // If we get here, no intent matched OR the intent didn't provide a usable response
1194 +
1195 + // Step 4: Generate AI response
1196 + $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
1197 + $this->mxchat_increment_chat_count();
1198 +
1199 + // Generate embedding for the user's query
1200 + $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1201 +
1202 + // Check if the embedding generation returned an error
1203 + if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1204 + $error_message = $user_message_embedding['error'];
1205 + $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
1784 1206
1785 - $response = $this->mxchat_generate_response(
1786 - $context_content,
1787 - $current_options['api_key'] ?? $this->options['api_key'],
1788 - $current_options['xai_api_key'] ?? $this->options['xai_api_key'],
1789 - $current_options['claude_api_key'] ?? $this->options['claude_api_key'],
1790 - $current_options['deepseek_api_key'] ?? $this->options['deepseek_api_key'],
1791 - $current_options['gemini_api_key'] ?? $this->options['gemini_api_key'],
1792 - $current_options['openrouter_api_key'] ?? $this->options['openrouter_api_key'],
1793 - $conversation_history,
1794 - $is_streaming,
1795 - $session_id,
1796 - $testing_data,
1797 - $selected_model
1798 - );
1799 -
1800 - // Handle streaming vs non-streaming responses
1801 - if ($is_streaming) {
1802 - // Check if streaming actually happened or if it fell back to regular response
1803 - if ($response === true) {
1804 - wp_die();
1805 - }
1806 - // If we get here, streaming fell back to regular response, continue
1807 - // But if there's an error, we need to send it as SSE format since headers are already set
1808 - if (is_array($response) && isset($response['error'])) {
1809 - $error_message = $response['error'];
1810 - $error_code = $response['error_code'] ?? 'api_error';
1811 - // Send error in SSE format that the client JS can handle
1812 - echo "data: " . json_encode([
1813 - 'error' => true,
1814 - 'error_message' => $error_message,
1815 - 'error_code' => $error_code,
1816 - 'text' => $error_message, // Also include as text for fallback handling
1817 - 'message' => $error_message
1818 - ]) . "\n\n";
1819 - echo "data: [DONE]\n\n";
1820 - flush();
1821 - wp_die();
1822 - }
1823 - }
1207 + wp_send_json_error([
1208 + 'error_message' => $error_message,
1209 + 'error_code' => $error_code
1210 + ]);
1211 + wp_die();
1212 + }
1213 +
1214 + // Check if the embedding is valid
1215 + if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1216 + wp_send_json_error([
1217 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1218 + 'error_code' => 'invalid_embedding'
1219 + ]);
1220 + wp_die();
1221 + }
1824 1222
1825 - // Check if the response is an error array (non-streaming mode)
1826 - if (is_array($response) && isset($response['error'])) {
1827 - wp_send_json_error([
1828 - 'error_message' => $response['error'],
1829 - 'error_code' => $response['error_code'] ?? 'api_error'
1830 - ]);
1831 - wp_die();
1832 - }
1223 + // Build context with both knowledge base and PDF content if available
1224 + $context_content = "User asked: '{$message}'\n\n";
1225 +
1226 + // NEW: Add action instruction if present (add this right after the above line)
1227 + if (!empty($this->current_action_instruction)) {
1228 + $context_content .= "===== SPECIAL INSTRUCTION =====\n";
1229 + $context_content .= "IMPORTANT: " . $this->current_action_instruction . "\n";
1230 + $context_content .= "Respond naturally and conversationally while following this instruction.\n";
1231 + $context_content .= "===== END SPECIAL INSTRUCTION =====\n\n";
1833 1232
1834 - // DEBUG: Check what we have
1835 - //error_log("=== BEFORE URL VALIDATION ===");
1836 - //error_log("current_valid_urls is empty? " . (empty($this->current_valid_urls) ? 'YES' : 'NO'));
1837 - //error_log("current_valid_urls count: " . count($this->current_valid_urls));
1838 - //error_log("current_valid_urls content: " . print_r($this->current_valid_urls, true));
1839 -
1840 - // If we get here, the response is valid text - now validate URLs
1841 - if (!empty($this->current_valid_urls)) {
1842 - //error_log("CALLING validate_and_clean_urls");
1843 - $response = $this->validate_and_clean_urls($response, $this->current_valid_urls);
1844 - } else {
1845 - //error_log("SKIPPING validation - current_valid_urls is empty");
1846 - }
1847 - // ===== END URL VALIDATION =====
1233 + // Clear the instruction after using it
1234 + $this->current_action_instruction = null;
1235 + }
1848 1236
1849 - // Prepare RAG context data for storage (only include documents used for context)
1850 - $rag_context_for_storage = null;
1851 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
1852 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
1853 1237
1854 - if ($has_rag_data || $has_action_data) {
1855 - $rag_context_for_storage = [];
1238 + // NEW: Add page context if available and contextual awareness is enabled
1239 + if ($page_context && isset($this->options['contextual_awareness_toggle']) && $this->options['contextual_awareness_toggle'] === 'on') {
1240 + $context_content .= "===== CURRENT PAGE CONTEXT =====\n";
1241 + $context_content .= "Page URL: " . $page_context['url'] . "\n";
1242 + $context_content .= "Page Title: " . $page_context['title'] . "\n";
1243 + $context_content .= "Page Content: " . $page_context['content'] . "\n";
1244 + $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1245 + }
1856 1246
1857 - // Add RAG/source data if available
1858 - if ($has_rag_data) {
1859 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
1860 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
1861 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
1862 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
1863 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1864 - $rag_context_for_storage['sources_used'] = $this->last_similarity_analysis['sources_used'] ?? 0;
1865 - $rag_context_for_storage['total_chunks_used'] = $this->last_similarity_analysis['total_chunks_used'] ?? 0;
1866 - }
1247 + // Get relevant content from knowledge base - THIS IS WHERE THE SIMILARITY ANALYSIS HAPPENS
1248 + $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
1249 +
1250 + // ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1251 + if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1252 + // Update testing data with the REAL similarity analysis
1253 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1254 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1255 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1256 + }
1257 + // ===== END SIMILARITY DATA CAPTURE =====
1258 +
1259 + if (!empty($relevant_content)) {
1260 + $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1261 + } else {
1262 + $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
1263 + }
1867 1264
1868 - // Add action analysis data if available
1869 - if ($has_action_data) {
1870 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
1265 + // Check for and include PDF content
1266 + $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
1267 + $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
1268 + $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
1269 + if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
1270 + $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
1271 + if (!empty($relevant_pdf_pages)) {
1272 + $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
1273 + foreach ($relevant_pdf_pages as $page_data) {
1274 + $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
1871 1275 }
1276 + $context_content .= "\n";
1872 1277 }
1278 + }
1873 1279
1874 - // Save the cleaned response with RAG context
1875 - $this->mxchat_save_chat_message($session_id, 'bot', $response, null, $rag_context_for_storage);
1876 -
1877 - // Step 5: Save additional content if available
1878 - if (!empty($this->productCardHtml)) {
1879 - $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
1280 + // Check for and include Word content
1281 + $word_url = get_transient('mxchat_word_url_' . $session_id);
1282 + $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
1283 + $word_filename = get_transient('mxchat_word_filename_' . $session_id);
1284 + if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
1285 + $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
1286 + if (!empty($relevant_word_chunks)) {
1287 + $context_content .= "Relevant content from Word document '{$word_filename}':\n";
1288 + foreach ($relevant_word_chunks as $chunk_data) {
1289 + $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
1290 + }
1291 + $context_content .= "\n";
1880 1292 }
1293 + }
1294 +
1295 + $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
1881 1296
1882 - if (!empty($this->fallbackResponse['html'])) {
1883 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1297 + // Generate response
1298 + $response = $this->mxchat_generate_response(
1299 + $context_content,
1300 + $this->options['api_key'],
1301 + $this->options['xai_api_key'],
1302 + $this->options['claude_api_key'],
1303 + $this->options['deepseek_api_key'],
1304 + $this->options['gemini_api_key'],
1305 + $conversation_history,
1306 + $is_streaming,
1307 + $session_id,
1308 + $testing_data
1309 + );
1310 +
1311 + // Handle streaming vs non-streaming responses
1312 + if ($is_streaming) {
1313 + // Check if streaming actually happened or if it fell back to regular response
1314 + if ($response === true) {
1315 + wp_die();
1884 1316 }
1885 -
1886 - // Step 6: Return the response
1887 - // DEBUG: Check if newlines exist in the response
1888 - //error_log("=== MXCHAT NON-STREAMING RESPONSE DEBUG ===");
1889 - //error_log("Response has newlines: " . (strpos($response, "\n") !== false ? 'YES' : 'NO'));
1890 - //error_log("Response first 500 chars: " . substr($response, 0, 500));
1891 -
1892 - $response_data = [
1893 - 'text' => $response,
1894 - 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1895 - 'session_id' => $session_id
1896 - ];
1897 -
1898 - // Include vectorstore error info for admin debugging (only visible to admins via testing_data)
1899 - if (!empty($this->last_vectorstore_error) && $testing_data !== null) {
1900 - $testing_data['vectorstore_error'] = $this->last_vectorstore_error;
1901 - }
1902 -
1903 - // Also pass it as a top-level field so JS can show a better error message to admins
1904 - if (!empty($this->last_vectorstore_error) && current_user_can('manage_options')) {
1905 - $response_data['vectorstore_error'] = $this->last_vectorstore_error;
1906 - }
1907 -
1908 - // Always add testing data for admins (no toggle needed)
1909 - if ($testing_data !== null) {
1910 - $response_data['testing_data'] = $testing_data;
1911 - }
1912 -
1913 - wp_send_json($response_data);
1317 + // If we get here, streaming fell back to regular response, continue
1318 + }
1319 +
1320 + // Check if the response is an error array
1321 + if (is_array($response) && isset($response['error'])) {
1322 + wp_send_json_error([
1323 + 'error_message' => $response['error'],
1324 + 'error_code' => $response['error_code'] ?? 'api_error'
1325 + ]);
1914 1326 wp_die();
1915 -}
1916 -
1917 -/**
1918 - * Get bot-specific options for multi-bot functionality
1919 - * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
1920 - */
1921 -// Also debug the bot options retrieval
1922 -private function get_bot_options($bot_id = 'default') {
1923 - //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
1924 -
1925 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1926 - //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
1927 - return array();
1928 1327 }
1929 1328
1930 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
1931 -
1932 - if (!empty($bot_options)) {
1933 - //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
1934 - if (isset($bot_options['similarity_threshold'])) {
1935 - //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
1936 - }
1329 + // If we get here, the response is valid text
1330 + $this->mxchat_save_chat_message($session_id, 'bot', $response);
1331 +
1332 + // Step 5: Save additional content if available
1333 + if (!empty($this->productCardHtml)) {
1334 + $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
1937 1335 }
1938 -
1939 - return is_array($bot_options) ? $bot_options : array();
1940 -}
1941 1336
1942 -/**
1943 - * Get bot-specific Pinecone configuration
1944 - * Used in the knowledge retrieval functions
1945 - */
1946 -// Also add debugging to your get_bot_pinecone_config function
1947 -private function get_bot_pinecone_config($bot_id = 'default') {
1948 - //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
1949 -
1950 - // If default bot or multi-bot add-on not active, use default Pinecone config
1951 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1952 - //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
1953 - $addon_options = get_option('mxchat_pinecone_addon_options', array());
1954 - $config = array(
1955 - 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
1956 - 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
1957 - 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
1958 - 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
1959 - );
1960 - //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
1961 - return $config;
1337 + if (!empty($this->fallbackResponse['html'])) {
1338 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1962 1339 }
1963 -
1964 - //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
1965 -
1966 - // Hook for multi-bot add-on to provide bot-specific Pinecone config
1967 - $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
1968 -
1969 - if (!empty($bot_pinecone_config)) {
1970 - //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
1971 - //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
1972 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
1973 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
1974 - } else {
1975 - //error_log("MXCHAT DEBUG: Filter returned empty config!");
1340 +
1341 + // Step 6: Return the response
1342 + $response_data = [
1343 + 'text' => $response,
1344 + 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1345 + 'session_id' => $session_id
1346 + ];
1347 +
1348 + // Always add testing data for admins (no toggle needed)
1349 + if ($testing_data !== null) {
1350 + $response_data['testing_data'] = $testing_data;
1976 1351 }
1977 -
1978 - return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
1352 +
1353 + wp_send_json($response_data);
1354 + wp_die();
1979 1355 }
1980 1356
1981 -
1982 1357 // Updated function to check intents and invoke the callback function
1983 1358 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
1984 1359 global $wpdb;
1985 1360 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
1986 1361
1987 - // Get the current bot_id
1988 - $current_bot_id = $this->get_current_bot_id($session_id);
1989 -
1990 1362 // Generate the user embedding
1991 1363 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1992 -
1364 +
1993 1365 // Check if embedding generation returned an error
1994 1366 if (is_array($user_embedding) && isset($user_embedding['error'])) {
1995 1367 $error_message = $user_embedding['error'];
1996 1368 $error_code = $user_embedding['error_code'] ?? 'embedding_error';
1997 -
1998 - // FIXED: Send error in appropriate format based on streaming mode
1999 - if ($this->is_streaming) {
2000 - echo "data: " . json_encode([
2001 - 'error' => true,
2002 - 'error_message' => $error_message,
2003 - 'error_code' => $error_code,
2004 - 'text' => $error_message,
2005 - 'message' => $error_message
2006 - ]) . "\n\n";
2007 - echo "data: [DONE]\n\n";
2008 - flush();
2009 - } else {
2010 - wp_send_json_error([
2011 - 'error_message' => $error_message,
2012 - 'error_code' => $error_code
2013 - ]);
2014 - }
1369 +
1370 + wp_send_json_error([
1371 + 'error_message' => $error_message,
1372 + 'error_code' => $error_code
1373 + ]);
2015 1374 wp_die();
2016 1375 }
2017 -
1376 +
2018 1377 // Check if embedding is valid
2019 1378 if (!is_array($user_embedding) || empty($user_embedding)) {
2020 - $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
2021 -
2022 - // FIXED: Send error in appropriate format based on streaming mode
2023 - if ($this->is_streaming) {
2024 - echo "data: " . json_encode([
2025 - 'error' => true,
2026 - 'error_message' => $error_message,
2027 - 'error_code' => 'invalid_embedding',
2028 - 'text' => $error_message,
2029 - 'message' => $error_message
2030 - ]) . "\n\n";
2031 - echo "data: [DONE]\n\n";
2032 - flush();
2033 - } else {
2034 - wp_send_json_error([
2035 - 'error_message' => $error_message,
2036 - 'error_code' => 'invalid_embedding'
2037 - ]);
2038 - }
1379 + wp_send_json_error([
1380 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1381 + 'error_code' => 'invalid_embedding'
1382 + ]);
2039 1383 wp_die();
2040 1384 }
2041 -
1385 +
2042 1386 // Fetch intents from the database
2043 1387 $table_name = $wpdb->prefix . 'mxchat_intents';
2044 1388 if ($chat_mode === 'agent') {
2045 1389 $query = $wpdb->prepare(
@@ -2049,29 +1393,19 @@
2049 1393 $intents = $wpdb->get_results($query);
2050 1394 } else {
2051 1395 $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
2052 1396 }
2053 -
1397 +
2054 1398 if (empty($intents)) {
2055 1399 return false;
2056 1400 }
2057 -
2058 - // Prefetch individual phrase vectors from wp_mxchat_intent_phrases (grouped by intent_id)
2059 - $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
2060 - $phrases_by_intent = [];
2061 - if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
2062 - $all_phrases = $wpdb->get_results("SELECT intent_id, phrase, embedding_vector FROM $phrases_table");
2063 - foreach ($all_phrases as $p) {
2064 - $phrases_by_intent[$p->intent_id][] = $p;
2065 - }
2066 - }
2067 -
1401 +
2068 1402 $highest_similarity = -INF;
2069 1403 $matched_intent = null;
2070 -
2071 - // Array to store action analysis for testing panel
1404 +
1405 + // NEW: Array to store action analysis for testing panel
2072 1406 $action_analysis = [];
2073 -
1407 +
2074 1408 foreach ($intents as $intent) {
2075 1409 // Additional check for enabled state
2076 1410 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
2077 1411 if (!$is_enabled) {
@@ -2076,57 +1410,22 @@
2076 1410 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
2077 1411 if (!$is_enabled) {
2078 1412 continue;
2079 1413 }
2080 -
2081 - // Check if this action is enabled for the current bot
2082 - if (!$this->is_action_enabled_for_bot($intent, $current_bot_id)) {
2083 - continue;
2084 - }
2085 -
2086 - $best_similarity = -INF;
2087 - $matched_phrase_text = '';
2088 -
2089 - // Check legacy embedding vector (existing behavior)
1414 +
2090 1415 $intent_embedding_serialized = $intent->embedding_vector;
2091 1416 $intent_embedding = $intent_embedding_serialized
2092 1417 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
2093 1418 : null;
2094 -
2095 - if (is_array($intent_embedding) && !empty($intent_embedding)) {
2096 - $legacy_similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
2097 - if ($legacy_similarity > $best_similarity) {
2098 - $best_similarity = $legacy_similarity;
2099 - $matched_phrase_text = 'legacy';
2100 - }
2101 - }
2102 -
2103 - // Check individual phrase vectors
2104 - if (isset($phrases_by_intent[$intent->id])) {
2105 - foreach ($phrases_by_intent[$intent->id] as $phrase_row) {
2106 - $phrase_embedding = $phrase_row->embedding_vector
2107 - ? unserialize($phrase_row->embedding_vector, ['allowed_classes' => false])
2108 - : null;
2109 - if (!is_array($phrase_embedding)) {
2110 - continue;
2111 - }
2112 - $phrase_similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $phrase_embedding);
2113 - if ($phrase_similarity > $best_similarity) {
2114 - $best_similarity = $phrase_similarity;
2115 - $matched_phrase_text = $phrase_row->phrase;
2116 - }
2117 - }
2118 - }
2119 -
2120 - // Skip if no valid embedding was found at all
2121 - if ($best_similarity === -INF) {
1419 +
1420 + if (!is_array($intent_embedding)) {
2122 1421 continue;
2123 1422 }
2124 -
2125 - $similarity = $best_similarity;
1423 +
1424 + $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
2126 1425 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
2127 -
2128 - // Store action analysis data for testing panel
1426 +
1427 + // NEW: Store action analysis data for testing panel
2129 1428 $action_analysis[] = [
2130 1429 'intent_label' => $intent->intent_label,
2131 1430 'callback_function' => $intent->callback_function,
2132 1431 'similarity' => round($similarity, 4),
@@ -2133,12 +1432,11 @@
2133 1432 'similarity_percentage' => round($similarity * 100, 2),
2134 1433 'threshold' => $intent_threshold,
2135 1434 'threshold_percentage' => round($intent_threshold * 100, 2),
2136 1435 'above_threshold' => $similarity >= $intent_threshold,
2137 - 'matched_phrase' => $matched_phrase_text,
2138 1436 'triggered' => false // Will be updated below if this intent is triggered
2139 1437 ];
2140 -
1438 +
2141 1439 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
2142 1440 $highest_similarity = $similarity;
2143 1441 $matched_intent = $intent;
2144 1442 }
@@ -2143,9 +1441,9 @@
2143 1441 $matched_intent = $intent;
2144 1442 }
2145 1443 }
2146 1444
2147 - // Mark the triggered action if any
1445 + // NEW: Mark the triggered action if any
2148 1446 if ($matched_intent) {
2149 1447 foreach ($action_analysis as &$action) {
2150 1448 if ($action['intent_label'] === $matched_intent->intent_label) {
2151 1449 $action['triggered'] = true;
@@ -2153,9 +1451,9 @@
2153 1451 }
2154 1452 }
2155 1453 }
2156 1454
2157 - // Sort actions by similarity (highest first) and store for testing panel
1455 + // NEW: Sort actions by similarity (highest first) and store for testing panel
2158 1456 usort($action_analysis, function($a, $b) {
2159 1457 return $b['similarity'] <=> $a['similarity'];
2160 1458 });
2161 1459
@@ -2161,9 +1459,8 @@
2161 1459
2162 1460 // Store action analysis for testing panel capture
2163 1461 $this->last_action_analysis = $action_analysis;
2164 1462
2165 - // Around line 715 in your mxchat_check_intent_and_invoke_callback function
2166 1463 if ($matched_intent) {
2167 1464 // If the callback is a method on this instance (core callback), call it directly
2168 1465 if (method_exists($this, $matched_intent->callback_function)) {
2169 1466 $callback_result = call_user_func(
@@ -2177,9 +1474,9 @@
2177 1474 } else {
2178 1475 // Otherwise, use apply_filters for add-on callbacks
2179 1476 $callback_result = apply_filters(
2180 1477 $matched_intent->callback_function,
2181 - false,
1478 + false, // default return value
2182 1479 $message,
2183 1480 $user_id,
2184 1481 $session_id,
2185 1482 $matched_intent
@@ -2185,18 +1482,11 @@
2185 1482 $matched_intent
2186 1483 );
2187 1484 }
2188 1485
2189 - // Handle the callback result properly
2190 1486 if ($callback_result !== false) {
2191 - // If callback returned an array with chat_mode, use it directly
2192 - if (is_array($callback_result) && isset($callback_result['chat_mode'])) {
2193 - $this->fallbackResponse = $callback_result;
2194 - return $callback_result; // Return the full array
2195 - } else {
2196 - $this->fallbackResponse = $callback_result;
2197 - return true;
2198 - }
1487 + $this->fallbackResponse = $callback_result;
1488 + return true;
2199 1489 }
2200 1490 }
2201 1491
2202 1492 return false;
@@ -2201,34 +1491,8 @@
2201 1491
2202 1492 return false;
2203 1493 }
2204 1494
2205 -/**
2206 - * Check if an action is enabled for a specific bot
2207 - */
2208 -private function is_action_enabled_for_bot($intent, $bot_id) {
2209 - // If enabled_bots column doesn't exist or is null, assume it's enabled for all bots (backward compatibility)
2210 - if (!isset($intent->enabled_bots) || empty($intent->enabled_bots)) {
2211 - return true;
2212 - }
2213 -
2214 - $enabled_bots = json_decode($intent->enabled_bots, true);
2215 -
2216 - // If JSON decode fails or returns empty array, assume enabled for all (backward compatibility)
2217 - if (!is_array($enabled_bots) || empty($enabled_bots)) {
2218 - return true;
2219 - }
2220 -
2221 - // Admin testing tab uses bot_id "testing" — treat it as "default" so all
2222 - // default-bot actions are testable from the admin panel
2223 - if ($bot_id === 'testing') {
2224 - $bot_id = 'default';
2225 - }
2226 -
2227 - // Check if the current bot is in the enabled bots list
2228 - return in_array($bot_id, $enabled_bots);
2229 -}
2230 -
2231 1495 // Helper function to clear PDF and Word document related transients
2232 1496 private function clear_pdf_transients($session_id) {
2233 1497 // PDF transients
2234 1498 delete_transient('mxchat_pdf_url_' . $session_id);
@@ -2263,17 +1527,17 @@
2263 1527
2264 1528 public function mxchat_generate_image($message, $user_id, $session_id) {
2265 1529 //error_log("Starting image generation for message: " . $message);
2266 1530
2267 - // Prepare a prompt for OpenAI image generation
1531 + // Prepare a prompt for DALL-E
2268 1532 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2269 -
1533 +
2270 1534 // Use the existing OpenAI API key
2271 1535 $openai_api_key = sanitize_text_field($this->options['api_key']);
2272 -
2273 - // Call OpenAI GPT Image to generate an image
2274 - $image_response = $this->mxchat_generate_openai_image($prompt, $openai_api_key);
2275 1536
1537 + // Call DALL-E to generate an image
1538 + $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1539 +
2276 1540 // Check if the response contains an image URL
2277 1541 if (isset($image_response['imageUrl'])) {
2278 1542 $image_url = esc_url_raw($image_response['imageUrl']);
2279 1543
@@ -2316,103 +1580,24 @@
2316 1580 // Return the response directly instead of relying on the property
2317 1581 return $this->fallbackResponse;
2318 1582 }
2319 1583 }
2320 -
2321 -public function mxchat_generate_gemini_image($message, $user_id, $session_id) {
2322 - $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2323 -
2324 - $gemini_api_key = sanitize_text_field($this->options['gemini_api_key'] ?? '');
2325 - if (empty($gemini_api_key)) {
2326 - $response_text = esc_html__("Gemini API key is not configured.", 'mxchat');
2327 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2328 - return ['text' => $response_text, 'html' => '', 'images' => []];
2329 - }
2330 -
2331 - $image_response = $this->mxchat_generate_imagen_image($prompt, $gemini_api_key);
2332 -
2333 - if (isset($image_response['imageUrl'])) {
2334 - $image_url = esc_url_raw($image_response['imageUrl']);
2335 -
2336 - $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
2337 - $response_text = esc_html__('Here is the image I generated:', 'mxchat');
2338 -
2339 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2340 - $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
2341 -
2342 - $this->fallbackResponse = [
2343 - 'text' => $response_text,
2344 - 'html' => $response_html,
2345 - 'images' => [$image_url]
2346 - ];
2347 -
2348 - return $this->fallbackResponse;
2349 - } else {
2350 - $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
2351 -
2352 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2353 -
2354 - $this->fallbackResponse = [
2355 - 'text' => $response_text,
2356 - 'html' => '',
2357 - 'images' => []
2358 - ];
2359 -
2360 - return $this->fallbackResponse;
2361 - }
2362 -}
2363 -
2364 -private function mxchat_save_generated_image($base64_data, $mime_type = 'image/png', $prefix = 'mxchat-generated') {
2365 - $extension = ($mime_type === 'image/jpeg') ? 'jpg' : 'png';
2366 - $filename = sanitize_file_name($prefix . '-' . wp_generate_uuid4() . '.' . $extension);
2367 - $decoded = base64_decode($base64_data);
2368 -
2369 - if ($decoded === false) {
2370 - return new \WP_Error('decode_failed', esc_html__('Failed to decode image data.', 'mxchat'));
2371 - }
2372 -
2373 - $upload = wp_upload_bits($filename, null, $decoded);
2374 -
2375 - if (!empty($upload['error'])) {
2376 - return new \WP_Error('upload_failed', $upload['error']);
2377 - }
2378 -
2379 - $attach_id = wp_insert_attachment([
2380 - 'post_mime_type' => $mime_type,
2381 - 'post_title' => $prefix,
2382 - 'post_content' => '',
2383 - 'post_status' => 'inherit',
2384 - ], $upload['file']);
2385 -
2386 - if (is_wp_error($attach_id)) {
2387 - return $attach_id;
2388 - }
2389 -
2390 - require_once ABSPATH . 'wp-admin/includes/image.php';
2391 - $metadata = wp_generate_attachment_metadata($attach_id, $upload['file']);
2392 - wp_update_attachment_metadata($attach_id, $metadata);
2393 -
2394 - return esc_url_raw(wp_get_attachment_url($attach_id));
2395 -}
2396 -
2397 -private function mxchat_generate_openai_image($prompt, $api_key, $model = 'gpt-image-1', $timeout = 60) {
1584 +private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
2398 1585 $api_url = 'https://api.openai.com/v1/images/generations';
2399 1586 $body = json_encode([
2400 - 'prompt' => sanitize_text_field($prompt),
2401 - 'n' => 1,
2402 - 'size' => '1024x1024',
2403 - 'quality' => 'medium',
2404 - 'output_format' => 'png',
2405 - 'model' => sanitize_text_field($model),
1587 + 'prompt' => sanitize_text_field($prompt),
1588 + 'n' => 1,
1589 + 'size' => '1024x1024',
1590 + 'model' => sanitize_text_field($model),
2406 1591 ]);
2407 1592
2408 1593 $args = [
2409 - 'body' => $body,
1594 + 'body' => $body,
2410 1595 'headers' => [
2411 - 'Content-Type' => 'application/json',
1596 + 'Content-Type' => 'application/json',
2412 1597 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
2413 1598 ],
2414 - 'method' => 'POST',
1599 + 'method' => 'POST',
2415 1600 'timeout' => absint($timeout),
2416 1601 ];
2417 1602
2418 1603 $response = wp_remote_post($api_url, $args);
@@ -2417,67 +1602,22 @@
2417 1602
2418 1603 $response = wp_remote_post($api_url, $args);
2419 1604
2420 1605 if (is_wp_error($response)) {
1606 + //error_log("DALL-E request failed: " . $response->get_error_message());
2421 1607 return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2422 1608 }
2423 1609
2424 1610 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2425 1611
2426 - $b64 = $response_body['data'][0]['b64_json'] ?? $response_body['data'][0]['b64'] ?? null;
2427 - if ($b64) {
2428 - $saved_url = $this->mxchat_save_generated_image($b64, 'image/png', 'mxchat-openai');
2429 - if (is_wp_error($saved_url)) {
2430 - return ['error' => $saved_url->get_error_message()];
2431 - }
2432 - return ['imageUrl' => $saved_url];
1612 + if (isset($response_body['data'][0]['url'])) {
1613 + return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])];
2433 1614 } else {
1615 + //error_log("DALL-E response error: " . wp_remote_retrieve_body($response));
2434 1616 return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
2435 1617 }
2436 1618 }
2437 1619
2438 -private function mxchat_generate_imagen_image($prompt, $api_key, $timeout = 60) {
2439 - $api_url = 'https://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-generate-001:predict';
2440 -
2441 - $body = json_encode([
2442 - 'instances' => [['prompt' => sanitize_text_field($prompt)]],
2443 - 'parameters' => [
2444 - 'sampleCount' => 1,
2445 - 'aspectRatio' => '1:1',
2446 - ],
2447 - ]);
2448 -
2449 - $args = [
2450 - 'body' => $body,
2451 - 'headers' => [
2452 - 'Content-Type' => 'application/json',
2453 - 'x-goog-api-key' => sanitize_text_field($api_key),
2454 - ],
2455 - 'method' => 'POST',
2456 - 'timeout' => absint($timeout),
2457 - ];
2458 -
2459 - $response = wp_remote_post($api_url, $args);
2460 -
2461 - if (is_wp_error($response)) {
2462 - return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2463 - }
2464 -
2465 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
2466 -
2467 - $b64 = $response_body['predictions'][0]['bytesBase64Encoded'] ?? $response_body['predictions'][0]['imageBytes'] ?? null;
2468 - if ($b64) {
2469 - $mime = $response_body['predictions'][0]['mimeType'] ?? 'image/png';
2470 - $saved_url = $this->mxchat_save_generated_image($b64, $mime, 'mxchat-gemini');
2471 - if (is_wp_error($saved_url)) {
2472 - return ['error' => $saved_url->get_error_message()];
2473 - }
2474 - return ['imageUrl' => $saved_url];
2475 - } else {
2476 - return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
2477 - }
2478 -}
2479 -
2480 1620 /**
2481 1621 * Handle web search requests.
2482 1622 *
2483 1623 * Sends the refined search query to the Brave Search API and uses the
@@ -2526,10 +1666,10 @@
2526 1666 $transient_key = 'mxchat_search_' . md5($refined_search_query);
2527 1667 $results = get_transient($transient_key);
2528 1668
2529 1669 if (false === $results) {
2530 - // SECURITY FIX: Changed to wp_safe_remote_get
2531 - $response = wp_safe_remote_get(
1670 + // Fetch new results from the Brave Search API
1671 + $response = wp_remote_get(
2532 1672 $api_url,
2533 1673 array(
2534 1674 'headers' => array(
2535 1675 'Accept' => 'application/json',
@@ -2668,10 +1808,9 @@
2668 1808 ],
2669 1809 'timeout' => 10,
2670 1810 ];
2671 1811
2672 - // SECURITY FIX: Changed to wp_safe_remote_get
2673 - $response = wp_safe_remote_get($api_url, $args);
1812 + $response = wp_remote_get($api_url, $args);
2674 1813
2675 1814 if (is_wp_error($response)) {
2676 1815 return array(
2677 1816 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
@@ -2744,9 +1883,9 @@
2744 1883 $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');
2745 1884
2746 1885 // Get options and determine the selected model
2747 1886 $options = $this->options ?? get_option('mxchat_options');
2748 - $selected_model = isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest';
1887 + $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
2749 1888
2750 1889 // Extract model prefix to determine the provider
2751 1890 $model_parts = explode('-', $selected_model);
2752 1891 $provider = strtolower($model_parts[0]);
@@ -2794,9 +1933,9 @@
2794 1933
2795 1934 /**
2796 1935 * Interpret query using OpenAI models
2797 1936 */
2798 -private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-5.1-chat-latest') {
1937 +private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-4o') {
2799 1938 $url = 'https://api.openai.com/v1/chat/completions';
2800 1939 $args = [
2801 1940 'headers' => [
2802 1941 'Authorization' => 'Bearer ' . $api_key,
@@ -2867,13 +2006,13 @@
2867 2006 /**
2868 2007 * Interpret query using Gemini models
2869 2008 */
2870 2009 private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
2871 - // Use v1beta for preview models, v1 for stable models
2872 - $api_version = (strpos($model, 'preview') !== false || strpos($model, 'exp') !== false) ? 'v1beta' : 'v1';
2873 -
2874 - $url = "https://generativelanguage.googleapis.com/{$api_version}/models/{$model}:generateContent?key=" . urlencode($api_key);
2010 + // Strip "gemini-" prefix for the API
2011 + $model_version = str_replace('gemini-', '', $model);
2875 2012
2013 + $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
2014 +
2876 2015 $args = [
2877 2016 'headers' => [
2878 2017 'Content-Type' => 'application/json',
2879 2018 ],
@@ -3069,55 +2208,55 @@
3069 2208 }
3070 2209
3071 2210
3072 2211 /**
3073 - * Enhanced fetch_and_split_pdf_pages with SSRF protection
2212 + * Enhanced fetch_and_split_pdf_pages with detailed debugging
3074 2213 */
3075 2214 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
3076 2215 // CLEAR DEBUG LOGGING
3077 - //error_log("=== MXCHAT PDF PROCESSING START ===");
3078 - //error_log("PDF Source: " . $pdf_source);
3079 - //error_log("Max Pages: " . $max_pages);
3080 - //error_log("Session ID: " . ($this->session_id ?? 'not set'));
2216 + error_log("=== MXCHAT PDF PROCESSING START ===");
2217 + error_log("PDF Source: " . $pdf_source);
2218 + error_log("Max Pages: " . $max_pages);
2219 + error_log("Session ID: " . ($this->session_id ?? 'not set'));
3081 2220
3082 2221 // Check if Advanced Claude Toolbar is available and enabled
3083 2222 $claude_available = function_exists('mxchatACT_is_advanced_claude_enabled');
3084 2223 $claude_enabled = $claude_available ? mxchatACT_is_advanced_claude_enabled() : false;
3085 2224
3086 - //error_log("Claude Function Available: " . ($claude_available ? 'YES' : 'NO'));
3087 - //error_log("Claude Enabled: " . ($claude_enabled ? 'YES' : 'NO'));
2225 + error_log("Claude Function Available: " . ($claude_available ? 'YES' : 'NO'));
2226 + error_log("Claude Enabled: " . ($claude_enabled ? 'YES' : 'NO'));
3088 2227
3089 2228 if ($claude_available && $claude_enabled) {
3090 - //error_log("🚀 ATTEMPTING CLAUDE PROCESSING...");
2229 + error_log("🚀 ATTEMPTING CLAUDE PROCESSING...");
3091 2230
3092 2231 // Attempt Claude processing first
3093 2232 $claude_result = apply_filters('mxchat_process_pdf_advanced', false, $pdf_source, $max_pages, $this->session_id);
3094 2233
3095 2234 if ($claude_result !== false && is_array($claude_result) && !empty($claude_result)) {
3096 - //error_log("✅ CLAUDE PROCESSING SUCCESSFUL!");
3097 - //error_log("Claude returned " . count($claude_result) . " processed pages");
2235 + error_log("✅ CLAUDE PROCESSING SUCCESSFUL!");
2236 + error_log("Claude returned " . count($claude_result) . " processed pages");
3098 2237
3099 2238 // Log first page details for verification
3100 2239 if (isset($claude_result[0])) {
3101 2240 $first_page = $claude_result[0];
3102 - //error_log("First page enhanced: " . (isset($first_page['enhanced']) && $first_page['enhanced'] ? 'YES' : 'NO'));
3103 - //error_log("Processing method: " . ($first_page['processing_method'] ?? 'not set'));
3104 - //error_log("First page text preview: " . substr($first_page['text'] ?? '', 0, 100) . "...");
2241 + error_log("First page enhanced: " . (isset($first_page['enhanced']) && $first_page['enhanced'] ? 'YES' : 'NO'));
2242 + error_log("Processing method: " . ($first_page['processing_method'] ?? 'not set'));
2243 + error_log("First page text preview: " . substr($first_page['text'] ?? '', 0, 100) . "...");
3105 2244 }
3106 2245
3107 - //error_log("=== MXCHAT PDF PROCESSING END (CLAUDE) ===");
2246 + error_log("=== MXCHAT PDF PROCESSING END (CLAUDE) ===");
3108 2247 return $claude_result;
3109 2248 } else {
3110 - //error_log("❌ CLAUDE PROCESSING FAILED or returned invalid result");
3111 - //error_log("Claude result type: " . gettype($claude_result));
2249 + error_log("❌ CLAUDE PROCESSING FAILED or returned invalid result");
2250 + error_log("Claude result type: " . gettype($claude_result));
3112 2251 if (is_array($claude_result)) {
3113 - //error_log("Claude result count: " . count($claude_result));
2252 + error_log("Claude result count: " . count($claude_result));
3114 2253 }
3115 2254 }
3116 2255 }
3117 2256
3118 2257 // Fallback to basic processing
3119 - //error_log("🔄 FALLING BACK TO BASIC PDF PROCESSING...");
2258 + error_log("🔄 FALLING BACK TO BASIC PDF PROCESSING...");
3120 2259
3121 2260 $upload_dir = wp_upload_dir();
3122 2261 $temp_file = null;
3123 2262
@@ -3125,20 +2264,11 @@
3125 2264 // Your existing basic processing code here...
3126 2265 // (I'll include the key parts with debug logging)
3127 2266
3128 2267 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
3129 - //error_log("Downloading PDF from URL...");
3130 -
3131 - // SECURITY FIX: Validate URL before processing
3132 - if (!$this->mxchat_is_safe_pdf_url($pdf_source)) {
3133 - //error_log("❌ SECURITY: Blocked unsafe PDF URL");
3134 - return false;
3135 - }
3136 -
2268 + error_log("Downloading PDF from URL...");
3137 2269 $temp_file = wp_tempnam($pdf_source);
3138 -
3139 - // SECURITY FIX: Changed from wp_remote_get to wp_safe_remote_get
3140 - $response = wp_safe_remote_get($pdf_source, [
2270 + $response = wp_remote_get($pdf_source, [
3141 2271 'timeout' => 60,
3142 2272 'headers' => ['User-Agent' => 'MxChat PDF Processor']
3143 2273 ]);
3144 2274
@@ -3143,35 +2273,29 @@
3143 2273 ]);
3144 2274
3145 2275 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
3146 2276 $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code($response);
3147 - //error_log("❌ BASIC PROCESSING: Failed to download PDF: " . $error_message);
2277 + error_log("❌ BASIC PROCESSING: Failed to download PDF: " . $error_message);
3148 2278 return false;
3149 2279 }
3150 2280
3151 - global $wp_filesystem;
3152 - if (empty($wp_filesystem)) {
3153 - require_once ABSPATH . 'wp-admin/includes/file.php';
3154 - WP_Filesystem();
3155 - }
3156 - $wp_filesystem->put_contents($temp_file, wp_remote_retrieve_body($response), FS_CHMOD_FILE);
3157 - //error_log("✅ PDF downloaded successfully");
2281 + file_put_contents($temp_file, wp_remote_retrieve_body($response));
2282 + error_log("✅ PDF downloaded successfully");
3158 2283 } else {
3159 2284 $temp_file = $pdf_source;
3160 - //error_log("Using local PDF file: " . $temp_file);
2285 + error_log("Using local PDF file: " . $temp_file);
3161 2286 }
3162 2287
3163 2288 // Parse PDF
3164 - //error_log("Parsing PDF with basic parser...");
3165 - mxchat_load_pdf_parser();
2289 + error_log("Parsing PDF with basic parser...");
3166 2290 $parser = new \Smalot\PdfParser\Parser();
3167 2291 $pdf = $parser->parseFile($temp_file);
3168 2292 $pages = $pdf->getPages();
3169 2293
3170 - //error_log("PDF contains " . count($pages) . " pages");
2294 + error_log("PDF contains " . count($pages) . " pages");
3171 2295
3172 2296 if (count($pages) > $max_pages) {
3173 - //error_log("❌ BASIC PROCESSING: Too many pages (" . count($pages) . " > " . $max_pages . ")");
2297 + error_log("❌ BASIC PROCESSING: Too many pages (" . count($pages) . " > " . $max_pages . ")");
3174 2298 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
3175 2299 unlink($temp_file);
3176 2300 }
3177 2301 return 'too_many_pages';
@@ -3183,9 +2307,9 @@
3183 2307 foreach ($pages as $page_number => $page) {
3184 2308 $text = $page->getText();
3185 2309
3186 2310 if (empty(trim($text))) {
3187 - //error_log("Skipping empty page: " . ($page_number + 1));
2311 + error_log("Skipping empty page: " . ($page_number + 1));
3188 2312 continue;
3189 2313 }
3190 2314
3191 2315 $text = $this->mxchat_clean_text($text);
@@ -3206,9 +2330,9 @@
3206 2330 $processed_pages++;
3207 2331 }
3208 2332 }
3209 2333
3210 - //error_log("✅ BASIC PROCESSING COMPLETE: " . $processed_pages . " pages processed");
2334 + error_log("✅ BASIC PROCESSING COMPLETE: " . $processed_pages . " pages processed");
3211 2335
3212 2336 // Cleanup
3213 2337 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
3214 2338 unlink($temp_file);
@@ -3213,46 +2337,21 @@
3213 2337 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
3214 2338 unlink($temp_file);
3215 2339 }
3216 2340
3217 - //error_log("=== MXCHAT PDF PROCESSING END (BASIC) ===");
2341 + error_log("=== MXCHAT PDF PROCESSING END (BASIC) ===");
3218 2342 return $embeddings;
3219 2343
3220 2344 } catch (\Exception $e) {
3221 - //error_log("❌ BASIC PROCESSING ERROR: " . $e->getMessage());
2345 + error_log("❌ BASIC PROCESSING ERROR: " . $e->getMessage());
3222 2346 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
3223 2347 unlink($temp_file);
3224 2348 }
3225 - //error_log("=== MXCHAT PDF PROCESSING END (ERROR) ===");
2349 + error_log("=== MXCHAT PDF PROCESSING END (ERROR) ===");
3226 2350 return false;
3227 2351 }
3228 2352 }
3229 2353
3230 -
3231 -/**
3232 - * Validate PDF URL for security
3233 - * Prevents SSRF attacks by blocking dangerous URLs
3234 - */
3235 -
3236 -private function mxchat_is_safe_pdf_url($url) {
3237 - // Use WordPress core function for comprehensive validation
3238 - // This blocks localhost, private IPs, and reserved IP ranges
3239 - $validated_url = wp_http_validate_url($url);
3240 -
3241 - if ($validated_url === false) {
3242 - return false;
3243 - }
3244 -
3245 - // Additional check: only allow HTTP/HTTPS schemes
3246 - $parsed = parse_url($url);
3247 - if (!isset($parsed['scheme']) || !in_array($parsed['scheme'], ['http', 'https'], true)) {
3248 - return false;
3249 - }
3250 -
3251 - return true;
3252 -}
3253 -
3254 -
3255 2354 private function mxchat_clean_text($text) {
3256 2355 // Remove excessive whitespace
3257 2356 $text = preg_replace('/\s+/', ' ', $text);
3258 2357
@@ -3291,10 +2390,9 @@
3291 2390 }
3292 2391
3293 2392 return [];
3294 2393 }
3295 -
3296 -
2394 +// Add this to your class
3297 2395 public function handle_pdf_upload() {
3298 2396 check_ajax_referer('mxchat_chat_nonce', 'nonce');
3299 2397
3300 2398 if (!isset($_FILES['pdf_file']) || !isset($_POST['session_id'])) {
@@ -3301,29 +2399,12 @@
3301 2399 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
3302 2400 return;
3303 2401 }
3304 2402
3305 - // SECURITY FIX: Check if PDF uploads are enabled in settings
3306 - $options = get_option('mxchat_options', array());
3307 - $show_pdf_button = isset($options['show_pdf_upload_button']) ? $options['show_pdf_upload_button'] : 'on';
3308 -
3309 - if ($show_pdf_button !== 'on') {
3310 - wp_send_json_error(esc_html__('PDF uploads are currently disabled.', 'mxchat'));
3311 - return;
3312 - }
3313 -
3314 2403 $file = $_FILES['pdf_file'];
3315 2404 $session_id = sanitize_text_field($_POST['session_id']);
3316 2405 $original_filename = sanitize_text_field($file['name']);
3317 2406
3318 - // Update session owner if it changed (e.g. IP changed due to network switch)
3319 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
3320 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
3321 -
3322 - if (!$session_owner || $session_owner !== $current_user_identifier) {
3323 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
3324 - }
3325 -
3326 2407 $file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
3327 2408 if ($file_type['type'] !== 'application/pdf') {
3328 2409 wp_send_json_error(esc_html__('Invalid file type. Only PDF files are allowed.', 'mxchat'));
3329 2410 return;
@@ -3329,12 +2410,9 @@
3329 2410 return;
3330 2411 }
3331 2412
3332 2413 $upload_dir = wp_upload_dir();
3333 -
3334 - // SECURITY FIX: Generate random filename without exposing session_id
3335 - $random_string = wp_generate_password(20, false, false); // 20 char alphanumeric string
3336 - $pdf_filename = 'mxchat_' . $random_string . '_' . time() . '.pdf';
2414 + $pdf_filename = 'mxchat_' . $session_id . '_' . time() . '.pdf';
3337 2415 $pdf_path = $upload_dir['path'] . '/' . $pdf_filename;
3338 2416
3339 2417 if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
3340 2418 wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
@@ -3365,9 +2443,8 @@
3365 2443 return;
3366 2444 }
3367 2445
3368 2446 if (!empty($embeddings)) {
3369 - // Store the mapping between session and the random filename
3370 2447 set_transient('mxchat_pdf_url_' . $session_id, $pdf_path, HOUR_IN_SECONDS);
3371 2448 set_transient('mxchat_pdf_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
3372 2449 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
3373 2450 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
@@ -3411,8 +2488,10 @@
3411 2488 wp_die();
3412 2489 }
3413 2490
3414 2491
2492 +
2493 +
3415 2494 function mxchat_fetch_new_messages() {
3416 2495 $session_id = sanitize_text_field($_POST['session_id']);
3417 2496 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
3418 2497 $persistence_enabled = $_POST['persistence_enabled'] === 'true';
@@ -3425,31 +2504,14 @@
3425 2504 }
3426 2505
3427 2506 $history = get_option("mxchat_history_{$session_id}", []);
3428 2507
3429 - //error_log("MxChat WhatsApp DEBUG: Fetch new messages for session {$session_id}");
3430 - //error_log("MxChat WhatsApp DEBUG: last_seen_id = " . var_export($last_seen_id, true));
3431 - //error_log("MxChat WhatsApp DEBUG: History count = " . count($history));
3432 - //error_log("MxChat WhatsApp DEBUG: Full history = " . print_r($history, true));
3433 -
3434 2508 $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
3435 - //error_log("MxChat WhatsApp DEBUG: Checking message - ID: " . ($message['id'] ?? 'NO_ID') . ", Role: " . ($message['role'] ?? 'NO_ROLE'));
3436 -
3437 2509 // If persistence is enabled, show all new messages
3438 2510 if ($persistence_enabled) {
3439 - $has_id = !empty($message['id']);
3440 - $is_agent = $message['role'] === 'agent';
3441 -
3442 - // If last_seen_id is empty, 'NaN', or invalid, show all agent messages
3443 - if (empty($last_seen_id) || $last_seen_id === 'NaN' || $last_seen_id === 'undefined') {
3444 - $is_newer = true;
3445 - } else {
3446 - $is_newer = strcmp($message['id'] ?? '', $last_seen_id) > 0;
3447 - }
3448 -
3449 - //error_log("MxChat WhatsApp DEBUG: has_id={$has_id}, is_newer={$is_newer}, is_agent={$is_agent}");
3450 -
3451 - return $has_id && $is_newer && $is_agent;
2511 + return !empty($message['id']) &&
2512 + strcmp($message['id'], $last_seen_id) > 0 &&
2513 + $message['role'] === 'agent';
3452 2514 }
3453 2515
3454 2516 // If persistence is disabled, only show messages after initial timestamp
3455 2517 return !empty($message['id']) &&
@@ -3456,16 +2518,12 @@
3456 2518 $message['role'] === 'agent' &&
3457 2519 $message['timestamp'] > $initial_timestamp;
3458 2520 });
3459 2521
3460 - //error_log("MxChat WhatsApp DEBUG: Filtered messages count = " . count($new_messages));
2522 + //error_log(esc_html__("New agent messages fetched for session $session_id. Last seen ID: $last_seen_id", 'mxchat'));
3461 2523
3462 - // Include current chat mode so frontend can detect agent→AI transitions
3463 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
3464 -
3465 2524 wp_send_json_success([
3466 - 'new_messages' => array_values($new_messages),
3467 - 'chat_mode' => $chat_mode
2525 + 'new_messages' => array_values($new_messages)
3468 2526 ]);
3469 2527 wp_die();
3470 2528 }
3471 2529 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
@@ -3498,9 +2556,9 @@
3498 2556 $channel_id = get_option("mxchat_channel_{$session_id}", '');
3499 2557
3500 2558 if (empty($channel_id)) {
3501 2559 // Create new channel with session ID as name
3502 - $channel_name = $this->generate_channel_name($session_id);
2560 + $channel_name = 'chat-' . strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $session_id));
3503 2561
3504 2562 //error_log("Attempting to create channel: $channel_name");
3505 2563
3506 2564 $response = wp_remote_post('https://slack.com/api/conversations.create', [
@@ -3636,507 +2694,9 @@
3636 2694 'fallbackResponse' => $this->fallbackResponse
3637 2695 ]);
3638 2696 wp_die();
3639 2697 }
3640 -
3641 -private function generate_channel_name($session_id) {
3642 - $email = null;
3643 - $name = null;
3644 -
3645 - // 1. First priority: Check if user is logged in and get their info
3646 - if (is_user_logged_in()) {
3647 - $current_user = wp_get_current_user();
3648 - if (!empty($current_user->user_email)) {
3649 - $email = $current_user->user_email;
3650 - //error_log("[DEBUG] Using logged-in user email for channel: {$email}");
3651 - }
3652 - if (!empty($current_user->display_name)) {
3653 - $name = $current_user->display_name;
3654 - //error_log("[DEBUG] Using logged-in user name for channel: {$name}");
3655 - }
3656 - }
3657 -
3658 - // 2. Second priority: Check for saved email/name from "require email to chat" option
3659 - if (empty($email)) {
3660 - $email_option_key = "mxchat_email_{$session_id}";
3661 - $saved_email = get_option($email_option_key);
3662 - if (!empty($saved_email)) {
3663 - $email = $saved_email;
3664 - //error_log("[DEBUG] Using saved email from session for channel: {$email}");
3665 - }
3666 - }
3667 -
3668 - if (empty($name)) {
3669 - $name_option_key = "mxchat_name_{$session_id}";
3670 - $saved_name = get_option($name_option_key);
3671 - if (!empty($saved_name)) {
3672 - $name = $saved_name;
3673 - //error_log("[DEBUG] Using saved name from session for channel: {$name}");
3674 - }
3675 - }
3676 -
3677 - // 3. Third priority: Check existing chat transcript for email/name
3678 - if (empty($email) || empty($name)) {
3679 - global $wpdb;
3680 - $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
3681 - $existing_data = $wpdb->get_row($wpdb->prepare(
3682 - "SELECT user_email, user_name FROM $table_name WHERE session_id = %s AND (user_email IS NOT NULL OR user_name IS NOT NULL) LIMIT 1",
3683 - $session_id
3684 - ));
3685 -
3686 - if ($existing_data) {
3687 - if (empty($email) && !empty($existing_data->user_email)) {
3688 - $email = $existing_data->user_email;
3689 - //error_log("[DEBUG] Using email from chat transcript for channel: {$email}");
3690 - }
3691 - if (empty($name) && !empty($existing_data->user_name)) {
3692 - $name = $existing_data->user_name;
3693 - //error_log("[DEBUG] Using name from chat transcript for channel: {$name}");
3694 - }
3695 - }
3696 - }
3697 -
3698 - // 4. Generate channel name based on priority: Name > Email > Session ID
3699 - $channel_name = '';
3700 -
3701 - if (!empty($name)) {
3702 - // Convert name to valid Slack channel name
3703 - $base_name = strtolower(trim($name));
3704 - // Replace spaces and invalid characters
3705 - $base_name = preg_replace('/[^a-z0-9\s]/', '', $base_name);
3706 - $base_name = preg_replace('/\s+/', '-', $base_name);
3707 - $base_name = trim($base_name, '-');
3708 -
3709 - // Get last 4 characters of session ID for uniqueness
3710 - $session_suffix = substr($session_id, -4);
3711 - $channel_name = 'chat-' . $base_name . '-' . strtolower($session_suffix);
3712 -
3713 - // Slack channel names have a 21 character limit
3714 - if (strlen($channel_name) > 21) {
3715 - // Calculate available space for name (21 - 'chat-' - '-' - session_suffix)
3716 - $available_space = 21 - 5 - 1 - strlen($session_suffix); // 'chat-' = 5, '-' = 1
3717 - $truncated_name = substr($base_name, 0, $available_space);
3718 - $truncated_name = rtrim($truncated_name, '-'); // Remove trailing hyphen
3719 - $channel_name = 'chat-' . $truncated_name . '-' . strtolower($session_suffix);
3720 - }
3721 -
3722 - //error_log("[DEBUG] Using name for channel: {$channel_name} (from name: {$name})");
3723 -
3724 - } elseif (!empty($email)) {
3725 - // Convert email to valid Slack channel name (your existing logic)
3726 - $channel_name = 'chat-' . strtolower(str_replace(['@', '.', '+', '_'], ['-at-', '-', '-plus-', '-'], $email));
3727 - // Remove any remaining invalid characters
3728 - $channel_name = preg_replace('/[^a-z0-9\-]/', '', $channel_name);
3729 - // Ensure it doesn't end with a hyphen
3730 - $channel_name = rtrim($channel_name, '-');
3731 - // Slack channel names have a 21 character limit, so truncate if needed
3732 - if (strlen($channel_name) > 21) {
3733 - $channel_name = substr($channel_name, 0, 21);
3734 - $channel_name = rtrim($channel_name, '-'); // Remove trailing hyphen if truncation created one
3735 - }
3736 -
3737 - //error_log("[DEBUG] Using email for channel: {$channel_name} (from email: {$email})");
3738 -
3739 - } else {
3740 - // Fallback to session ID if no name or email found
3741 - $channel_name = 'chat-' . strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $session_id));
3742 - //error_log("[DEBUG] No name or email found, using session ID for channel: {$channel_name}");
3743 - }
3744 -
3745 - // Final validation - ensure channel name meets Slack requirements
3746 - if (strlen($channel_name) > 21) {
3747 - $channel_name = substr($channel_name, 0, 21);
3748 - $channel_name = rtrim($channel_name, '-');
3749 - }
3750 -
3751 - //error_log("[DEBUG] Generated channel name: {$channel_name}");
3752 - return $channel_name;
3753 -}
3754 -
3755 -/**
3756 - * Telegram Live Agent Handover
3757 - * Creates a forum topic in the Telegram group and notifies agents
3758 - */
3759 -public function mxchat_telegram_live_agent_handover($message, $user_id, $session_id) {
3760 - // Check if Telegram agents are available
3761 - $telegram_available = $this->options['telegram_status'] ?? 'off';
3762 - if ($telegram_available !== 'on') {
3763 - $away_message = $this->options['telegram_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
3764 - $this->fallbackResponse = [
3765 - 'text' => $away_message,
3766 - 'html' => '',
3767 - 'images' => [],
3768 - 'chat_mode' => 'ai'
3769 - ];
3770 - wp_send_json([
3771 - 'text' => $away_message,
3772 - 'html' => '',
3773 - 'chat_mode' => 'ai',
3774 - 'session_id' => $session_id
3775 - ]);
3776 - wp_die();
3777 - }
3778 -
3779 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
3780 - $telegram_group_id = $this->options['telegram_group_id'] ?? '';
3781 -
3782 - if (empty($telegram_bot_token) || empty($telegram_group_id)) {
3783 - return false;
3784 - }
3785 -
3786 - // Check if topic already exists for this session
3787 - $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
3788 -
3789 - if (empty($topic_id)) {
3790 - // Generate topic name
3791 - $topic_name = $this->generate_telegram_topic_name($session_id);
3792 -
3793 - // Random icon color (Telegram forum topic colors)
3794 - $icon_colors = [0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98, 0xFF93B2, 0xFB6F5F];
3795 - $icon_color = $icon_colors[array_rand($icon_colors)];
3796 -
3797 - // Create forum topic
3798 - $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/createForumTopic", [
3799 - 'headers' => ['Content-Type' => 'application/json'],
3800 - 'body' => json_encode([
3801 - 'chat_id' => $telegram_group_id,
3802 - 'name' => $topic_name,
3803 - 'icon_color' => $icon_color
3804 - ])
3805 - ]);
3806 -
3807 - if (!is_wp_error($response)) {
3808 - $response_body = wp_remote_retrieve_body($response);
3809 - $response_data = json_decode($response_body, true);
3810 -
3811 - if (isset($response_data['ok']) && $response_data['ok']) {
3812 - $topic_id = $response_data['result']['message_thread_id'];
3813 - update_option("mxchat_telegram_topic_{$session_id}", $topic_id);
3814 - update_option("mxchat_telegram_group_{$session_id}", $telegram_group_id);
3815 - }
3816 - }
3817 -
3818 - if (empty($topic_id)) {
3819 - return false; // Failed to create topic
3820 - }
3821 - }
3822 -
3823 - // Get recent chat history
3824 - $history = get_option("mxchat_history_{$session_id}", []);
3825 - $recent_history = array_slice($history, -5);
3826 -
3827 - // Format conversation context for Telegram (HTML format)
3828 - $conversation_context = "";
3829 - if (!empty($recent_history)) {
3830 - $conversation_context = "<b>Recent Conversation:</b>\n";
3831 - foreach ($recent_history as $hist_message) {
3832 - $role_display = $hist_message['role'] === 'user' ? '👤 User' : '🤖 AI';
3833 - $escaped_content = htmlspecialchars($hist_message['content'], ENT_QUOTES, 'UTF-8');
3834 - $conversation_context .= "{$role_display}: {$escaped_content}\n";
3835 - }
3836 - $conversation_context .= "\n";
3837 - }
3838 -
3839 - // Get user info
3840 - $user_email = get_option("mxchat_email_{$session_id}", 'Not provided');
3841 - $user_name = get_option("mxchat_name_{$session_id}", 'Anonymous');
3842 -
3843 - // Update session mode
3844 - update_option("mxchat_mode_{$session_id}", 'agent');
3845 -
3846 - // Send initial message to topic
3847 - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
3848 - $topic_message = "🔔 <b>New Live Agent Request</b>\n\n";
3849 - $topic_message .= "<b>Session ID:</b> <code>{$session_id}</code>\n";
3850 - $topic_message .= "<b>User:</b> {$user_name}\n";
3851 - $topic_message .= "<b>Email:</b> {$user_email}\n\n";
3852 -
3853 - if (!empty($conversation_context)) {
3854 - $topic_message .= $conversation_context;
3855 - }
3856 -
3857 - $topic_message .= "<b>Current Message:</b>\n{$escaped_message}\n\n";
3858 - $topic_message .= "<i>Reply in this topic - messages will be sent to the user</i>\n";
3859 - $topic_message .= "<i>Type #close, #end, #disconnect, or #done to end the session</i>";
3860 -
3861 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
3862 - 'headers' => ['Content-Type' => 'application/json'],
3863 - 'body' => json_encode([
3864 - 'chat_id' => $telegram_group_id,
3865 - 'message_thread_id' => $topic_id,
3866 - 'text' => $topic_message,
3867 - 'parse_mode' => 'HTML'
3868 - ])
3869 - ]);
3870 -
3871 - $success_message = $this->options['telegram_notification_message'] ?? "I've notified a support agent. Please allow a moment for them to respond.";
3872 - $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
3873 -
3874 - $this->fallbackResponse = [
3875 - 'text' => $success_message,
3876 - 'html' => '',
3877 - 'images' => [],
3878 - 'chat_mode' => 'agent'
3879 - ];
3880 -
3881 - wp_send_json([
3882 - 'success' => true,
3883 - 'text' => $success_message,
3884 - 'html' => '',
3885 - 'chat_mode' => 'agent',
3886 - 'session_id' => $session_id,
3887 - 'fallbackResponse' => $this->fallbackResponse
3888 - ]);
3889 - wp_die();
3890 -}
3891 -
3892 -/**
3893 - * Generate topic name for Telegram forum
3894 - */
3895 -private function generate_telegram_topic_name($session_id) {
3896 - $name = null;
3897 - $email = null;
3898 -
3899 - // Check logged in user
3900 - if (is_user_logged_in()) {
3901 - $current_user = wp_get_current_user();
3902 - if (!empty($current_user->display_name)) {
3903 - $name = $current_user->display_name;
3904 - }
3905 - if (!empty($current_user->user_email)) {
3906 - $email = $current_user->user_email;
3907 - }
3908 - }
3909 -
3910 - // Check session data
3911 - if (empty($name)) {
3912 - $name = get_option("mxchat_name_{$session_id}");
3913 - }
3914 - if (empty($email)) {
3915 - $email = get_option("mxchat_email_{$session_id}");
3916 - }
3917 -
3918 - // Generate topic name
3919 - $session_suffix = substr($session_id, -6);
3920 -
3921 - if (!empty($name)) {
3922 - // Clean name for topic (max 128 chars in Telegram)
3923 - $clean_name = preg_replace('/[^\p{L}\p{N}\s\-]/u', '', $name);
3924 - $clean_name = trim($clean_name);
3925 - if (strlen($clean_name) > 50) {
3926 - $clean_name = substr($clean_name, 0, 50);
3927 - }
3928 - return "Chat - {$clean_name} ({$session_suffix})";
3929 - } elseif (!empty($email)) {
3930 - // Use email prefix
3931 - $email_prefix = explode('@', $email)[0];
3932 - if (strlen($email_prefix) > 30) {
3933 - $email_prefix = substr($email_prefix, 0, 30);
3934 - }
3935 - return "Chat - {$email_prefix} ({$session_suffix})";
3936 - }
3937 -
3938 - return "Chat - {$session_suffix}";
3939 -}
3940 -
3941 -/**
3942 - * Send user message to Telegram agent
3943 - */
3944 -public function mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id) {
3945 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
3946 - $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
3947 - $group_id = get_option("mxchat_telegram_group_{$session_id}", '');
3948 -
3949 - if (empty($telegram_bot_token) || empty($topic_id) || empty($group_id)) {
3950 - return false;
3951 - }
3952 -
3953 - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
3954 - $user_message = "👤 <b>User:</b> {$escaped_message}";
3955 -
3956 - $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
3957 - 'headers' => ['Content-Type' => 'application/json'],
3958 - 'body' => json_encode([
3959 - 'chat_id' => $group_id,
3960 - 'message_thread_id' => $topic_id,
3961 - 'text' => $user_message,
3962 - 'parse_mode' => 'HTML'
3963 - ])
3964 - ]);
3965 -
3966 - return !is_wp_error($response);
3967 -}
3968 -
3969 -/**
3970 - * Handle incoming Telegram webhook
3971 - */
3972 -public function handle_telegram_webhook(WP_REST_Request $request) {
3973 - $body = $request->get_body();
3974 - $data = json_decode($body, true);
3975 -
3976 - //error_log('[MxChat Telegram DEBUG] Webhook received: ' . $body);
3977 -
3978 - // Handle message events from forum topics
3979 - if (isset($data['message'])) {
3980 - $message_data = $data['message'];
3981 -
3982 - // Skip if not from a forum topic
3983 - if (!isset($message_data['message_thread_id'])) {
3984 - //error_log('[MxChat Telegram DEBUG] Skipped: No message_thread_id (not a forum topic message)');
3985 - return new WP_REST_Response(['ok' => true]);
3986 - }
3987 -
3988 - // Skip bot messages
3989 - if (isset($message_data['from']['is_bot']) && $message_data['from']['is_bot']) {
3990 - //error_log('[MxChat Telegram DEBUG] Skipped: Message from bot');
3991 - return new WP_REST_Response(['ok' => true]);
3992 - }
3993 -
3994 - $chat_id = $message_data['chat']['id'] ?? '';
3995 - $topic_id = $message_data['message_thread_id'];
3996 - $message_text = $message_data['text'] ?? '';
3997 - $message_id = $message_data['message_id'] ?? '';
3998 - $from = $message_data['from'] ?? [];
3999 - $agent_name = trim(($from['first_name'] ?? '') . ' ' . ($from['last_name'] ?? ''));
4000 - if (empty($agent_name)) {
4001 - $agent_name = $from['username'] ?? 'Agent';
4002 - }
4003 -
4004 - //error_log("[MxChat Telegram DEBUG] Parsed: chat_id={$chat_id}, topic_id={$topic_id}, agent={$agent_name}, text={$message_text}");
4005 -
4006 - // Skip empty messages
4007 - if (empty($message_text)) {
4008 - //error_log('[MxChat Telegram DEBUG] Skipped: Empty message text');
4009 - return new WP_REST_Response(['ok' => true]);
4010 - }
4011 -
4012 - // Find session ID by topic ID - cast to string for comparison
4013 - global $wpdb;
4014 - $topic_id_str = strval($topic_id);
4015 - $session_option = $wpdb->get_var(
4016 - $wpdb->prepare(
4017 - "SELECT option_name FROM {$wpdb->options}
4018 - WHERE option_name LIKE %s
4019 - AND option_value = %s",
4020 - 'mxchat_telegram_topic_%',
4021 - $topic_id_str
4022 - )
4023 - );
4024 -
4025 - //error_log("[MxChat Telegram DEBUG] Looking for topic_id={$topic_id_str} in options, found: " . ($session_option ?: 'NULL'));
4026 -
4027 - if ($session_option) {
4028 - $session_id = str_replace('mxchat_telegram_topic_', '', $session_option);
4029 - //error_log("[MxChat Telegram DEBUG] Session ID: {$session_id}");
4030 -
4031 - // Verify the group ID matches
4032 - $stored_group_id = get_option("mxchat_telegram_group_{$session_id}", '');
4033 - //error_log("[MxChat Telegram DEBUG] Stored group_id={$stored_group_id}, received chat_id={$chat_id}");
4034 -
4035 - if (strval($stored_group_id) != strval($chat_id)) {
4036 - //error_log('[MxChat Telegram DEBUG] Skipped: Group ID mismatch');
4037 - return new WP_REST_Response(['ok' => true]);
4038 - }
4039 -
4040 - // Check for closure commands
4041 - $lower_text = strtolower(trim($message_text));
4042 - if (in_array($lower_text, ['#close', '#end', '#disconnect', '#done'])) {
4043 - //error_log("[MxChat Telegram DEBUG] Closure command received: {$lower_text}");
4044 - // End the live agent session
4045 - update_option("mxchat_mode_{$session_id}", 'ai');
4046 -
4047 - // Save disconnect message
4048 - $disconnect_message = "Live agent session ended. You're now chatting with the AI assistant.";
4049 - $this->mxchat_save_chat_message($session_id, 'bot', $disconnect_message);
4050 -
4051 - // Notify in Telegram
4052 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4053 - if (!empty($telegram_bot_token)) {
4054 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4055 - 'headers' => ['Content-Type' => 'application/json'],
4056 - 'body' => json_encode([
4057 - 'chat_id' => $chat_id,
4058 - 'message_thread_id' => $topic_id,
4059 - 'text' => "✅ Session closed. User returned to AI chatbot.",
4060 - 'parse_mode' => 'HTML'
4061 - ])
4062 - ]);
4063 -
4064 - // Optionally close the topic
4065 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/closeForumTopic", [
4066 - 'headers' => ['Content-Type' => 'application/json'],
4067 - 'body' => json_encode([
4068 - 'chat_id' => $chat_id,
4069 - 'message_thread_id' => $topic_id
4070 - ])
4071 - ]);
4072 - }
4073 -
4074 - return new WP_REST_Response(['ok' => true]);
4075 - }
4076 -
4077 - // Deduplicate messages
4078 - $message_key = md5($session_id . $message_id . $message_text);
4079 - $processed_messages = get_transient('mxchat_telegram_messages_' . $session_id) ?: [];
4080 -
4081 - if (in_array($message_key, $processed_messages)) {
4082 - //error_log('[MxChat Telegram DEBUG] Skipped: Duplicate message');
4083 - return new WP_REST_Response(['ok' => true]);
4084 - }
4085 -
4086 - $processed_messages[] = $message_key;
4087 - if (count($processed_messages) > 50) {
4088 - $processed_messages = array_slice($processed_messages, -50);
4089 - }
4090 - set_transient('mxchat_telegram_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
4091 -
4092 - // Save the agent message - format with agent name prefix for proper parsing
4093 - $formatted_message = "Agent: {$agent_name} - {$message_text}";
4094 - //error_log("[MxChat Telegram DEBUG] Saving agent message: {$formatted_message}");
4095 -
4096 - $this->mxchat_save_chat_message($session_id, 'agent', $formatted_message);
4097 -
4098 - // Verify the message was saved to history
4099 - $history = get_option("mxchat_history_{$session_id}", []);
4100 - $last_message = end($history);
4101 - //error_log("[MxChat Telegram DEBUG] History after save - count: " . count($history) . ", last message role: " . ($last_message['role'] ?? 'none'));
4102 -
4103 - // Send confirmation back to Telegram
4104 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4105 - if (!empty($telegram_bot_token)) {
4106 - $confirm_key = 'mxchat_telegram_confirm_' . $message_key;
4107 - if (!get_transient($confirm_key)) {
4108 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4109 - 'headers' => ['Content-Type' => 'application/json'],
4110 - 'body' => json_encode([
4111 - 'chat_id' => $chat_id,
4112 - 'message_thread_id' => $topic_id,
4113 - 'text' => "✅ <i>Message sent to user</i>",
4114 - 'parse_mode' => 'HTML',
4115 - 'reply_to_message_id' => $message_id
4116 - ])
4117 - ]);
4118 - set_transient($confirm_key, true, 300);
4119 - }
4120 - }
4121 - } else {
4122 - //error_log("[MxChat Telegram DEBUG] No session found for topic_id={$topic_id}");
4123 - }
4124 - } else {
4125 - //error_log('[MxChat Telegram DEBUG] No message in webhook data');
4126 - }
4127 -
4128 - return new WP_REST_Response(['ok' => true]);
4129 -}
4130 -
4131 2698 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
4132 - // Check if this is a Telegram agent session
4133 - $telegram_topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
4134 - if (!empty($telegram_topic_id)) {
4135 - return $this->mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id);
4136 - }
4137 -
4138 - // Otherwise, try Slack
4139 2699 $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4140 2700 $channel_id = get_option("mxchat_channel_{$session_id}", '');
4141 2701
4142 2702 if (empty($slack_bot_token) || empty($channel_id)) {
@@ -4293,26 +2853,22 @@
4293 2853 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
4294 2854 ], 200);
4295 2855 }
4296 2856 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
4297 - // Update mode to AI
2857 + //error_log(esc_html__("Switching back to chatbot mode via intent.", 'mxchat'));
2858 +
2859 + // Just update mode to AI
4298 2860 update_option("mxchat_mode_{$session_id}", 'ai');
4299 -
4300 - // Clear any existing PDF context to start fresh
4301 - $this->clear_pdf_transients($session_id);
4302 -
4303 - // Set the response with explicit chat_mode
4304 - $this->fallbackResponse = [
4305 - 'text' => esc_html__('You are now chatting with the AI chatbot.', 'mxchat'),
4306 - 'html' => '',
4307 - 'images' => [],
4308 - 'chat_mode' => 'ai' // Ensure this is set
4309 - ];
4310 -
4311 - // Return the complete response array instead of just true
4312 - return $this->fallbackResponse;
2861 +
2862 + // Initialize states
2863 + $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
2864 + $this->productCardHtml = '';
2865 +
2866 + // Set the response message
2867 + $this->fallbackResponse['text'] = esc_html__('You are now chatting with the AI chatbot.', 'mxchat');
2868 +
2869 + return true; // Intent was handled
4313 2870 }
4314 -
4315 2871 public function handle_slack_messages(WP_REST_Request $request) {
4316 2872 // Log the incoming request for debugging
4317 2873 //error_log('Slack events request received: ' . $request->get_body());
4318 2874
@@ -4362,33 +2918,33 @@
4362 2918
4363 2919 $channel_id = $event['channel'];
4364 2920 $message_text = $event['text'] ?? '';
4365 2921 $message_ts = $event['ts'] ?? '';
4366 -
2922 +
4367 2923 // Find session ID by looking for matching channel
4368 2924 global $wpdb;
4369 2925 $session_option = $wpdb->get_var(
4370 2926 $wpdb->prepare(
4371 - "SELECT option_name FROM {$wpdb->options}
4372 - WHERE option_name LIKE 'mxchat_channel_%'
2927 + "SELECT option_name FROM {$wpdb->options}
2928 + WHERE option_name LIKE 'mxchat_channel_%'
4373 2929 AND option_value = %s",
4374 2930 $channel_id
4375 2931 )
4376 2932 );
4377 -
2933 +
4378 2934 if ($session_option) {
4379 2935 $session_id = str_replace('mxchat_channel_', '', $session_option);
4380 -
2936 +
4381 2937 // Create a unique key for this specific message
4382 2938 $message_key = md5($session_id . $message_ts . $message_text);
4383 2939 $processed_messages = get_transient('mxchat_processed_messages_' . $session_id) ?: [];
4384 -
2940 +
4385 2941 // Check if we've already processed this exact message
4386 2942 if (in_array($message_key, $processed_messages)) {
4387 2943 //error_log("Duplicate message detected for session $session_id");
4388 2944 return new WP_REST_Response(['ok' => true]);
4389 2945 }
4390 -
2946 +
4391 2947 // Add to processed messages
4392 2948 $processed_messages[] = $message_key;
4393 2949 // Keep only last 50 messages per session
4394 2950 if (count($processed_messages) > 50) {
@@ -4394,46 +2950,14 @@
4394 2950 if (count($processed_messages) > 50) {
4395 2951 $processed_messages = array_slice($processed_messages, -50);
4396 2952 }
4397 2953 set_transient('mxchat_processed_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
4398 -
4399 - $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4400 -
4401 - // Handle agent ending the chat — transfer back to AI
4402 - // Format: "!endchat" or "!endchat <custom message to user>"
4403 - if (preg_match('/^!endchat\b/i', trim($message_text))) {
4404 - update_option("mxchat_mode_{$session_id}", 'ai');
4405 -
4406 - // Extract custom message after !endchat, or use empty string
4407 - $custom_message = trim(preg_replace('/^!endchat\s*/i', '', trim($message_text)));
4408 -
4409 - // Send the agent's custom farewell message if provided
4410 - if (!empty($custom_message)) {
4411 - $this->mxchat_save_chat_message($session_id, 'agent', $custom_message);
4412 - }
4413 -
4414 - // Confirm in Slack channel
4415 - if (!empty($slack_bot_token)) {
4416 - wp_remote_post('https://slack.com/api/chat.postMessage', [
4417 - 'headers' => [
4418 - 'Content-Type' => 'application/json',
4419 - 'Authorization' => 'Bearer ' . $slack_bot_token
4420 - ],
4421 - 'body' => json_encode([
4422 - 'channel' => $channel_id,
4423 - 'text' => "✅ *Chat ended.* User has been transferred back to AI mode.",
4424 - 'mrkdwn' => true
4425 - ])
4426 - ]);
4427 - }
4428 -
4429 - return new WP_REST_Response(['ok' => true]);
4430 - }
4431 -
2954 +
4432 2955 // Save the agent message
4433 2956 $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
4434 -
2957 +
4435 2958 // Send confirmation back to Slack (only once)
2959 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4436 2960 if (!empty($slack_bot_token)) {
4437 2961 // Use a transient to prevent duplicate confirmations
4438 2962 $confirm_key = 'mxchat_confirm_' . $message_key;
4439 2963 if (!get_transient($confirm_key)) {
@@ -4443,9 +2967,9 @@
4443 2967 'Authorization' => 'Bearer ' . $slack_bot_token
4444 2968 ],
4445 2969 'body' => json_encode([
4446 2970 'channel' => $channel_id,
4447 - 'text' => "✅ _Message sent to user_",
2971 + 'text' => "✅ _Message sent to user_",
4448 2972 'thread_ts' => $event['ts'] // Reply in thread
4449 2973 ])
4450 2974 ]);
4451 2975 // Set transient to prevent duplicate confirmations
@@ -4682,599 +3206,254 @@
4682 3206 'error_code' => 'embedding_exception'
4683 3207 ];
4684 3208 }
4685 3209 }
3210 +private function mxchat_find_relevant_content($user_embedding) {
3211 + //error_log('MXChat Vector Search: Starting content search...');
4686 3212
3213 + // Retrieve the add-on settings from the database.
3214 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
4687 3215
4688 -private function mxchat_find_relevant_content($user_embedding, $bot_id = 'default', $user_query = '') {
4689 - //error_log("MXCHAT DEBUG: find_relevant_content called with bot_id: " . $bot_id);
3216 + // Determine whether Pinecone is enabled.
3217 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
4690 3218
4691 - // Check for OpenAI Vector Store first (takes priority when enabled)
4692 - $bot_vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
3219 + //error_log('Pinecone enabled flag: ' . $use_pinecone);
4693 3220
4694 - if ($bot_vectorstore_config['use_vectorstore']) {
4695 - // Get current model to verify it's an OpenAI model
4696 - $bot_options = $this->get_bot_options($bot_id);
4697 - $mxchat_options = get_option('mxchat_options', array());
4698 - $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
4699 - $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
4700 -
4701 - if ($this->is_openai_chat_model($selected_model)) {
4702 - //error_log("MXCHAT DEBUG: Using OpenAI Vector Store for knowledge retrieval");
4703 - return $this->find_relevant_content_openai_vectorstore($user_query, $bot_id, $bot_vectorstore_config);
4704 - } else {
4705 - //error_log("MXCHAT DEBUG: Vector Store enabled but model is not OpenAI (" . $selected_model . "), skipping Vector Store");
4706 - }
4707 - }
4708 -
4709 - // Get bot-specific Pinecone configuration
4710 - $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
4711 -
4712 - // Debug: Log the Pinecone configuration
4713 - //error_log("MXCHAT DEBUG: Pinecone config for bot '$bot_id':");
4714 - //error_log(" - use_pinecone: " . ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false'));
4715 - //error_log(" - api_key: " . (empty($bot_pinecone_config['api_key']) ? 'EMPTY' : 'SET (hidden)'));
4716 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'NOT SET'));
4717 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'NOT SET'));
4718 -
4719 - // Determine whether to use Pinecone based on bot configuration
4720 - $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
4721 -
4722 - //error_log("MXCHAT DEBUG: Using " . ($use_pinecone ? "Pinecone" : "WordPress Database") . " for knowledge retrieval");
4723 -
4724 - if ($use_pinecone) {
4725 - return $this->find_relevant_content_pinecone($user_embedding, $bot_id, $bot_pinecone_config);
3221 + if ($use_pinecone === 1) {
3222 + //error_log('MXChat Vector Search: Using Pinecone database');
3223 + return $this->find_relevant_content_pinecone($user_embedding);
4726 3224 } else {
4727 - return $this->find_relevant_content_wordpress($user_embedding, $bot_id);
3225 + //error_log('MXChat Vector Search: Using WordPress database');
3226 + return $this->find_relevant_content_wordpress($user_embedding);
4728 3227 }
4729 3228 }
4730 3229
4731 -private function find_relevant_content_wordpress($user_embedding, $bot_id = 'default') {
3230 +private function find_relevant_content_wordpress($user_embedding) {
4732 3231 global $wpdb;
4733 3232 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3233 + $cache_key = 'mxchat_system_prompt_embeddings';
3234 + $batch_size = 500;
3235 +
4734 3236 // Initialize similarity analysis storage
4735 3237 $this->last_similarity_analysis = [
4736 3238 'knowledge_base_type' => 'WordPress Database',
4737 - 'bot_id' => $bot_id,
4738 3239 'top_matches' => [],
4739 3240 'threshold_used' => 0,
4740 3241 'total_checked' => 0
4741 3242 ];
4742 3243
4743 - // NEW: Initialize valid URLs array
4744 - $valid_urls = [];
3244 + // Retrieve embeddings from cache or database
3245 + $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3246 + if ($embeddings === false) {
3247 + // Cache miss - load embeddings from database WITH CONTENT for testing
3248 + $embeddings = [];
3249 + $offset = 0;
4745 3250
4746 - // Get bot-specific options for similarity threshold
4747 - $bot_options = $this->get_bot_options($bot_id);
4748 - $current_options = !empty($bot_options) ? $bot_options : $this->options;
3251 + do {
3252 + $query = $wpdb->prepare(
3253 + "SELECT id, embedding_vector, article_content, source_url
3254 + FROM {$system_prompt_table}
3255 + LIMIT %d OFFSET %d",
3256 + $batch_size,
3257 + $offset
3258 + );
4749 3259
4750 - // Get knowledge manager instance for role checking
4751 - $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
3260 + $batch = $wpdb->get_results($query);
3261 + if (empty($batch)) {
3262 + break;
3263 + }
4752 3264
4753 - // Get base similarity threshold from bot options or default options
4754 - $similarity_threshold = isset($current_options['similarity_threshold'])
4755 - ? ((int) $current_options['similarity_threshold']) / 100
4756 - : 0.35;
4757 - $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3265 + $embeddings = array_merge($embeddings, $batch);
3266 + $offset += $batch_size;
3267 + unset($batch);
3268 + } while (true);
4758 3269
4759 - // Precompute bot_filter once, outside the streaming loop
4760 - $bot_filter = '';
4761 - if ($bot_id !== 'default') {
4762 - $column_exists = $wpdb->get_var("SHOW COLUMNS FROM {$system_prompt_table} LIKE 'bot_metadata'");
4763 - if ($column_exists) {
4764 - $bot_filter = $wpdb->prepare(" AND (bot_metadata = %s OR bot_metadata IS NULL OR bot_metadata = '')", $bot_id);
3270 + if (empty($embeddings)) {
3271 + return '';
4765 3272 }
3273 +
3274 + // Cache embeddings for future use (but note: this now includes content)
3275 + wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
4766 3276 }
4767 3277
4768 - // ===== STREAMING TOP-K PASS =====
4769 - // Stream rows in small batches, compute cosine similarity per row, and keep only:
4770 - // - top 10 by raw similarity (for the testing/debug display panel)
4771 - // - candidates above threshold with access (capped) for context assembly
4772 - // This bounds peak memory regardless of knowledge base size and avoids loading
4773 - // article_content for every row. article_content is fetched in Phase 2 for winners only.
4774 - $batch_size = 250;
4775 - $max_candidates = 200; // safety cap, well above rag_sources_limit * max_chunks_per_source
4776 - $top_display = [];
4777 - $candidates = [];
4778 - $total_checked = 0;
4779 - $offset = 0;
4780 -
4781 - do {
4782 - $batch = $wpdb->get_results($wpdb->prepare(
4783 - "SELECT id, embedding_vector, source_url, role_restriction
4784 - FROM {$system_prompt_table}
4785 - WHERE 1=1 {$bot_filter}
4786 - LIMIT %d OFFSET %d",
4787 - $batch_size,
4788 - $offset
4789 - ));
4790 -
4791 - if (empty($batch)) {
4792 - break;
4793 - }
4794 -
4795 - foreach ($batch as $row) {
4796 - $database_embedding = $row->embedding_vector
4797 - ? unserialize($row->embedding_vector, ['allowed_classes' => false])
4798 - : null;
4799 -
4800 - if (!is_array($database_embedding) || !is_array($user_embedding)) {
4801 - unset($database_embedding);
4802 - continue;
4803 - }
4804 -
3278 + // Get configuration options
3279 + $main_options = get_option('mxchat_options', []);
3280 +
3281 + // Get base similarity threshold (default 75%)
3282 + $similarity_threshold = isset($main_options['similarity_threshold'])
3283 + ? ((int) $main_options['similarity_threshold']) / 100
3284 + : 0.75;
3285 +
3286 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3287 +
3288 + // Calculate similarities and build results array
3289 + $all_similarities = [];
3290 + $relevant_results = [];
3291 +
3292 + foreach ($embeddings as $embedding) {
3293 + $database_embedding = $embedding->embedding_vector
3294 + ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3295 + : null;
3296 +
3297 + if (is_array($database_embedding) && is_array($user_embedding)) {
4805 3298 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
4806 - unset($database_embedding);
4807 -
4808 - $role_restriction = $row->role_restriction ?? 'public';
4809 - $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
4810 - $source_url = $row->source_url ?? '';
4811 -
4812 - // Maintain top 10 display buffer (insert-if-beats-worst)
4813 - if (count($top_display) < 10) {
4814 - $top_display[] = [
4815 - 'id' => $row->id,
4816 - 'similarity' => $similarity,
4817 - 'source_url' => $source_url,
4818 - 'role_restriction' => $role_restriction,
4819 - 'has_access' => $has_access,
4820 - ];
4821 - usort($top_display, function ($a, $b) {
4822 - return $b['similarity'] <=> $a['similarity'];
4823 - });
4824 - } elseif ($similarity > $top_display[9]['similarity']) {
4825 - $top_display[9] = [
4826 - 'id' => $row->id,
4827 - 'similarity' => $similarity,
4828 - 'source_url' => $source_url,
4829 - 'role_restriction' => $role_restriction,
4830 - 'has_access' => $has_access,
4831 - ];
4832 - usort($top_display, function ($a, $b) {
4833 - return $b['similarity'] <=> $a['similarity'];
4834 - });
3299 +
3300 + // Store ALL similarities for testing (top 10)
3301 + $source_display = '';
3302 + if (!empty($embedding->source_url) && $embedding->source_url !== '#') {
3303 + $source_display = $embedding->source_url;
3304 + } else {
3305 + $content_preview = strip_tags($embedding->article_content ?? '');
3306 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3307 + $source_display = substr(trim($content_preview), 0, 50) . '...';
4835 3308 }
4836 -
4837 - // Track candidates for context assembly (above threshold + has access)
4838 - if ($similarity >= $similarity_threshold && $has_access) {
4839 - $candidates[] = [
4840 - 'id' => $row->id,
4841 - 'similarity' => $similarity,
4842 - 'source_url' => $source_url,
3309 +
3310 + $all_similarities[] = [
3311 + 'document_id' => $embedding->id,
3312 + 'similarity' => $similarity,
3313 + 'similarity_percentage' => round($similarity * 100, 2),
3314 + 'above_threshold' => $similarity >= $similarity_threshold,
3315 + 'source_display' => $source_display,
3316 + 'content_preview' => substr(strip_tags($embedding->article_content ?? ''), 0, 100) . '...',
3317 + 'used_for_context' => false // Initialize as false, we'll update this later
3318 + ];
3319 +
3320 + // Only consider results above threshold for actual content retrieval
3321 + if ($similarity >= $similarity_threshold) {
3322 + $relevant_results[] = [
3323 + 'id' => $embedding->id,
3324 + 'similarity' => $similarity
4843 3325 ];
4844 3326 }
4845 -
4846 - $total_checked++;
4847 3327 }
4848 -
4849 - unset($batch);
4850 -
4851 - // Trim candidates periodically to cap memory during long scans
4852 - if (count($candidates) > $max_candidates) {
4853 - usort($candidates, function ($a, $b) {
4854 - return $b['similarity'] <=> $a['similarity'];
4855 - });
4856 - $candidates = array_slice($candidates, 0, $max_candidates);
4857 - }
4858 -
4859 - $offset += $batch_size;
4860 - } while (true);
4861 -
4862 - if ($total_checked === 0) {
4863 - $this->current_valid_urls = [];
4864 - return '';
3328 +
3329 + unset($database_embedding);
4865 3330 }
4866 3331
4867 - // Final candidates sort (best first)
4868 - if (count($candidates) > 1) {
4869 - usort($candidates, function ($a, $b) {
4870 - return $b['similarity'] <=> $a['similarity'];
4871 - });
4872 - }
4873 -
4874 - // ===== PHASE 2: FETCH ARTICLE CONTENT ONLY FOR WINNERS =====
4875 - // Gather unique IDs we actually need (top_display + candidates) and pull
4876 - // article_content in bounded IN() batches. This avoids loading content for
4877 - // every row during the similarity scan.
4878 - $needed_ids = [];
4879 - foreach ($top_display as $item) {
4880 - $needed_ids[$item['id']] = true;
4881 - }
4882 - foreach ($candidates as $item) {
4883 - $needed_ids[$item['id']] = true;
4884 - }
4885 - $needed_ids = array_keys($needed_ids);
4886 -
4887 - $content_map = [];
4888 - if (!empty($needed_ids)) {
4889 - foreach (array_chunk($needed_ids, 250) as $chunk_ids) {
4890 - $placeholders = implode(',', array_fill(0, count($chunk_ids), '%d'));
4891 - $rows = $wpdb->get_results($wpdb->prepare(
4892 - "SELECT id, article_content FROM {$system_prompt_table} WHERE id IN ($placeholders)",
4893 - ...$chunk_ids
4894 - ));
4895 - foreach ($rows as $r) {
4896 - $content_map[$r->id] = $r->article_content;
4897 - }
4898 - unset($rows);
4899 - }
4900 - }
4901 -
4902 - // Build the all_similarities display array from the top 10
4903 - $all_similarities = [];
4904 - foreach ($top_display as $item) {
4905 - $article_content_for_parse = $content_map[$item['id']] ?? '';
4906 - $parsed_for_display = MxChat_Chunker::parse_stored_chunk($article_content_for_parse);
4907 - $is_chunk = $parsed_for_display['is_chunked'];
4908 - $chunk_meta = $parsed_for_display['metadata'];
4909 -
4910 - if (!empty($item['source_url']) && $item['source_url'] !== '#') {
4911 - $source_display = $item['source_url'];
4912 - } else {
4913 - $content_preview = strip_tags($article_content_for_parse);
4914 - $content_preview = preg_replace('/\s+/', ' ', $content_preview);
4915 - $source_display = substr(trim($content_preview), 0, 50) . '...';
4916 - }
4917 -
4918 - $all_similarities[] = [
4919 - 'document_id' => $item['id'],
4920 - 'similarity' => $item['similarity'],
4921 - 'similarity_percentage' => round($item['similarity'] * 100, 2),
4922 - 'above_threshold' => $item['similarity'] >= $similarity_threshold,
4923 - 'source_display' => $source_display,
4924 - 'content_preview' => substr(strip_tags($parsed_for_display['text'] ?? ''), 0, 100) . '...',
4925 - 'used_for_context' => false,
4926 - 'role_restriction' => $item['role_restriction'],
4927 - 'has_access' => $item['has_access'],
4928 - 'filtered_out' => !$item['has_access'],
4929 - 'is_chunk' => $is_chunk,
4930 - 'chunk_index' => $is_chunk ? ($chunk_meta['chunk_index'] ?? 0) : null,
4931 - 'total_chunks' => $is_chunk ? ($chunk_meta['total_chunks'] ?? 1) : null
4932 - ];
4933 - }
4934 -
4935 - // Build url_groups from candidates for chunk reassembly
4936 - $url_groups = array();
4937 - foreach ($candidates as $cand) {
4938 - $article_content = $content_map[$cand['id']] ?? '';
4939 - $parsed = MxChat_Chunker::parse_stored_chunk($article_content);
4940 - $is_chunked = $parsed['is_chunked'];
4941 - $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
4942 - $text_content = $parsed['text'];
4943 -
4944 - $source_url = $cand['source_url'];
4945 - $group_key = !empty($source_url) ? $source_url : '_manual_' . $cand['id'];
4946 -
4947 - if (!isset($url_groups[$group_key])) {
4948 - $url_groups[$group_key] = array(
4949 - 'source_url' => $source_url,
4950 - 'best_score' => 0,
4951 - 'is_chunked' => $is_chunked,
4952 - 'chunks' => array(),
4953 - 'single_text' => '',
4954 - 'single_id' => null
4955 - );
4956 - }
4957 -
4958 - if ($cand['similarity'] > $url_groups[$group_key]['best_score']) {
4959 - $url_groups[$group_key]['best_score'] = $cand['similarity'];
4960 - }
4961 -
4962 - if ($is_chunked) {
4963 - $url_groups[$group_key]['is_chunked'] = true;
4964 - $url_groups[$group_key]['chunks'][] = array(
4965 - 'id' => $cand['id'],
4966 - 'score' => $cand['similarity'],
4967 - 'chunk_index' => $chunk_index,
4968 - 'text' => $text_content
4969 - );
4970 - } else {
4971 - $url_groups[$group_key]['single_text'] = $text_content;
4972 - $url_groups[$group_key]['single_id'] = $cand['id'];
4973 - }
4974 - }
4975 -
4976 3332 // Sort ALL similarities for testing display (highest first)
4977 3333 usort($all_similarities, function ($a, $b) {
4978 3334 return $b['similarity'] <=> $a['similarity'];
4979 3335 });
4980 -
4981 - // Sort URL groups by best score (highest first)
4982 - uasort($url_groups, function($a, $b) {
4983 - return $b['best_score'] <=> $a['best_score'];
3336 +
3337 + // Sort relevant results by similarity (highest first)
3338 + usort($relevant_results, function ($a, $b) {
3339 + return $b['similarity'] <=> $a['similarity'];
4984 3340 });
4985 -
4986 - // Get RAG sources limit from options (default 6, min 3, max 10)
4987 - $rag_sources_limit = isset($current_options['rag_sources_limit']) ? intval($current_options['rag_sources_limit']) : 3;
4988 - if ($rag_sources_limit < 3) $rag_sources_limit = 3;
4989 - if ($rag_sources_limit > 10) $rag_sources_limit = 10;
4990 -
4991 - // Take top N unique URLs based on user setting
4992 - $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
4993 -
4994 - // Track which document IDs are used for context
3341 +
3342 + // Get top 5 results for actual content (standard approach)
3343 + $top_results = array_slice($relevant_results, 0, 5);
3344 +
3345 + // NOW mark which documents are actually used for context
4995 3346 $used_document_ids = [];
4996 - foreach ($top_urls as $group) {
4997 - if ($group['is_chunked']) {
4998 - foreach ($group['chunks'] as $chunk) {
4999 - $used_document_ids[] = $chunk['id'];
5000 - }
5001 - } elseif ($group['single_id']) {
5002 - $used_document_ids[] = $group['single_id'];
5003 - }
3347 + foreach ($top_results as $result) {
3348 + $used_document_ids[] = $result['id'];
5004 3349 }
5005 -
3350 +
5006 3351 // Update the all_similarities array to mark which were actually used
5007 3352 foreach ($all_similarities as &$similarity_item) {
5008 3353 $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
5009 3354 }
5010 -
5011 - // Store top 10 for testing panel
3355 +
3356 + // Store top 10 for testing panel (now with correct used_for_context flags)
5012 3357 $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
5013 - $this->last_similarity_analysis['total_checked'] = $total_checked;
5014 -
3358 + $this->last_similarity_analysis['total_checked'] = count($embeddings);
3359 +
3360 + //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " top matches for testing");
3361 +
5015 3362 // Initialize final content
5016 3363 $content = '';
5017 - $matches_used = 0;
5018 - $total_chunks_used = 0;
5019 - $max_total_chunks = isset($current_options['rag_chunks_limit']) ? intval($current_options['rag_chunks_limit']) : 15;
5020 - if ($max_total_chunks < 8) $max_total_chunks = 8;
5021 - if ($max_total_chunks > 20) $max_total_chunks = 20;
5022 - $max_chunks_per_source = 5; // Cap per individual source to limit token usage
5023 -
5024 - // Check if citation links are enabled (default to 'on' for backwards compatibility)
5025 - // Use fresh options to ensure we get the latest setting value
5026 - $fresh_options = get_option('mxchat_options', []);
5027 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
5028 -
5029 - // Build content from top sources
5030 - foreach ($top_urls as $group_key => $group) {
5031 - $source_url = $group['source_url']; // Use actual source_url, not the group key
5032 -
5033 - // Stop if we've hit the total chunk limit
5034 - if ($total_chunks_used >= $max_total_chunks) {
5035 - break;
3364 +
3365 + // Track document IDs to avoid duplicates
3366 + $added_document_ids = [];
3367 +
3368 + // Fetch and format content for each selected result
3369 + foreach ($top_results as $index => $result) {
3370 + if (in_array($result['id'], $added_document_ids)) {
3371 + continue;
5036 3372 }
5037 -
5038 - $full_text = '';
5039 - $chunks_in_this_source = 1; // Default for non-chunked content
5040 -
5041 - if ($group['is_chunked']) {
5042 - // Calculate how many chunks we can still use (respect both total and per-source caps)
5043 - $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
5044 -
5045 - // Fetch chunks for this URL with limit
5046 - $full_text = $this->reassemble_chunks_from_wordpress($source_url, $chunks_remaining, $chunks_in_this_source);
5047 -
5048 - // If fetching all chunks fails, fall back to matched chunks
5049 - if (empty($full_text)) {
5050 - // Sort matched chunks by index and concatenate
5051 - usort($group['chunks'], function($a, $b) {
5052 - return $a['chunk_index'] <=> $b['chunk_index'];
5053 - });
5054 -
5055 - $chunk_texts = array();
5056 - $chunks_in_this_source = 0;
5057 - foreach ($group['chunks'] as $chunk) {
5058 - if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
5059 - break;
5060 - }
5061 - $chunk_texts[] = $chunk['text'];
5062 - $chunks_in_this_source++;
5063 - }
5064 - $full_text = implode("\n\n", $chunk_texts);
3373 +
3374 + $chunk_content = $this->fetch_content_with_product_links($result['id']);
3375 + $added_document_ids[] = $result['id'];
3376 +
3377 + $content .= "## Reference " . ($index + 1) . " ##\n";
3378 + $content .= $chunk_content . "\n\n";
3379 +
3380 + // PDF surrounding pages logic (unchanged)
3381 + if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
3382 + $surrounding_content = $wpdb->get_results($wpdb->prepare(
3383 + "SELECT id, article_content FROM {$system_prompt_table}
3384 + WHERE id IN (
3385 + (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
3386 + (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
3387 + )",
3388 + $result['id'],
3389 + $result['id']
3390 + ));
3391 +
3392 + if (!empty($surrounding_content[0])) {
3393 + $content .= "## Related Content ##\n";
3394 + $content .= $surrounding_content[0]->article_content . "\n\n";
3395 + $added_document_ids[] = $surrounding_content[0]->id;
5065 3396 }
5066 - } else {
5067 - $full_text = $group['single_text'];
5068 - $chunks_in_this_source = 1;
5069 - }
5070 -
5071 - if (!empty($full_text)) {
5072 - // Strip URLs from content if citation links are disabled
5073 - if (!$citation_links_enabled) {
5074 - $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
5075 - $full_text = preg_replace('/\s+/', ' ', trim($full_text)); // Clean up extra spaces
3397 +
3398 + if (!empty($surrounding_content[1])) {
3399 + $content .= "## Related Content ##\n";
3400 + $content .= $surrounding_content[1]->article_content . "\n\n";
3401 + $added_document_ids[] = $surrounding_content[1]->id;
5076 3402 }
5077 -
5078 - // Use numbered reference for URL-based entries, plain info label for manual entries
5079 - if (!empty($source_url) && $source_url !== '#') {
5080 - $matches_used++;
5081 - $content .= "## Reference " . $matches_used . " ##\n";
5082 - $content .= $full_text . "\n\n";
5083 -
5084 - // Only include citation URLs if citation links are enabled
5085 - if ($citation_links_enabled) {
5086 - $valid_urls[] = $source_url;
5087 - $content .= "URL: " . $source_url . "\n\n";
5088 - }
5089 - } else {
5090 - // Manual entry — no reference number, no citation
5091 - $content .= "## Information ##\n";
5092 - $content .= $full_text . "\n\n";
5093 - }
5094 -
5095 - // Extract any URLs from the text content itself (only if citation links enabled)
5096 - if ($citation_links_enabled) {
5097 - preg_match_all(
5098 - '#\bhttps?://[^\s<>"\']+#i',
5099 - $full_text,
5100 - $content_urls
5101 - );
5102 - if (!empty($content_urls[0])) {
5103 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5104 - }
5105 - }
5106 -
5107 - $total_chunks_used += $chunks_in_this_source;
5108 3403 }
5109 3404 }
5110 -
5111 - // NEW: Store unique valid URLs for validation
5112 - $this->current_valid_urls = array_unique($valid_urls);
5113 -
5114 - // Store sources and chunks counts for testing/transcript display
5115 - $this->last_similarity_analysis['sources_used'] = $matches_used;
5116 - $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
5117 -
5118 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
5119 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
5120 -
5121 - // Add response guidelines
5122 - if (empty($top_urls)) {
5123 - $content = "No reference information was found for this query.\n\n";
5124 - } else {
5125 - // Build response guidelines based on citation links setting
5126 - $content .= "\n## Response Guidelines ##\n" .
5127 - "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
5128 - "Be conversational and friendly, but never mention your knowledge base or training data. " .
5129 - "If you don't have specific information or are uncertain about any details, it's always " .
5130 - "better to honestly say you don't know rather than making up or guessing at answers. " .
5131 - "When information is incomplete, let them know you are unsure.\n\n";
5132 -
5133 - // Only add hyperlink instructions if citation links are enabled
5134 - if ($citation_links_enabled) {
5135 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
5136 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
5137 - "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
3405 +
3406 + // Add response guidelines
3407 + if (empty($top_results)) {
3408 + $content = "No reference information was found for this query.\n\n";
5138 3409 } else {
5139 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
5140 - "Simply provide helpful answers based on the reference information without citing sources.";
3410 + $content .= "\n## Response Guidelines ##\n" .
3411 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3412 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
3413 + "If you don't have specific information or are uncertain about any details, it's always " .
3414 + "better to honestly say you don't know rather than making up or guessing at answers. " .
3415 + "When information is incomplete, let them know you are unsure.";
5141 3416 }
5142 - }
5143 -
3417 +
5144 3418 return trim($content);
5145 3419 }
5146 3420
5147 -/**
5148 - * Fetch and reassemble chunks for a URL from WordPress database
5149 - *
5150 - * @param string $source_url The source URL to fetch chunks for
5151 - * @param int $max_chunks Maximum number of chunks to return (0 = unlimited)
5152 - * @param int &$chunk_count Reference to store the actual number of chunks returned
5153 - * @return string Reassembled content from chunks
5154 - */
5155 -private function reassemble_chunks_from_wordpress($source_url, $max_chunks = 0, &$chunk_count = 0) {
5156 - global $wpdb;
5157 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
5158 -
5159 - // Fetch all rows with this source_url
5160 - $rows = $wpdb->get_results($wpdb->prepare(
5161 - "SELECT article_content FROM {$table}
5162 - WHERE source_url = %s
5163 - ORDER BY id ASC",
5164 - $source_url
5165 - ));
5166 -
5167 - if (empty($rows)) {
5168 - $chunk_count = 0;
5169 - return '';
5170 - }
5171 -
5172 - // Parse and sort chunks by index
5173 - $chunks = array();
5174 - foreach ($rows as $row) {
5175 - $parsed = MxChat_Chunker::parse_stored_chunk($row->article_content);
5176 -
5177 - if ($parsed['is_chunked']) {
5178 - $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
5179 - $chunks[$chunk_index] = $parsed['text'];
5180 - } else {
5181 - // Non-chunked content - just return it
5182 - $chunks[] = $parsed['text'];
5183 - }
5184 - }
5185 -
5186 - // Sort by chunk index
5187 - ksort($chunks);
5188 -
5189 - // Apply chunk limit if specified
5190 - if ($max_chunks > 0 && count($chunks) > $max_chunks) {
5191 - $chunks = array_slice($chunks, 0, $max_chunks, true);
5192 - }
5193 -
5194 - // Store actual chunk count
5195 - $chunk_count = count($chunks);
5196 -
5197 - // Reassemble content
5198 - return implode("\n\n", $chunks);
5199 -}
5200 -
5201 -private function find_relevant_content_pinecone($user_embedding, $bot_id = 'default', $bot_config = null) {
5202 - global $wpdb;
3421 +private function find_relevant_content_pinecone($user_embedding) {
3422 + $options = get_option('mxchat_pinecone_addon_options', array());
3423 + $api_key = $options['mxchat_pinecone_api_key'] ?? '';
3424 + $host = $options['mxchat_pinecone_host'] ?? '';
5203 3425
5204 - //error_log("MXCHAT DEBUG: find_relevant_content_pinecone called");
5205 - //error_log(" - bot_id: " . $bot_id);
5206 - //error_log(" - user_embedding is array: " . (is_array($user_embedding) ? 'yes' : 'no'));
5207 - //error_log(" - user_embedding count: " . (is_array($user_embedding) ? count($user_embedding) : 'N/A'));
5208 -
5209 - // Use bot-specific config or fall back to default
5210 - if ($bot_config === null) {
5211 - $bot_config = $this->get_bot_pinecone_config($bot_id);
5212 - }
5213 -
5214 - $api_key = $bot_config['api_key'] ?? '';
5215 - $host = $bot_config['host'] ?? '';
5216 - $namespace = $bot_config['namespace'] ?? '';
5217 -
5218 - //error_log("MXCHAT DEBUG: Pinecone query parameters:");
5219 - //error_log(" - API Key: " . (empty($api_key) ? 'EMPTY - ERROR!' : 'Present (length: ' . strlen($api_key) . ')'));
5220 - //error_log(" - Host: " . (empty($host) ? 'EMPTY - ERROR!' : $host));
5221 - //error_log(" - Namespace: " . (empty($namespace) ? 'EMPTY (will use default)' : $namespace));
5222 -
5223 3426 // Initialize similarity analysis storage
5224 3427 $this->last_similarity_analysis = [
5225 3428 'knowledge_base_type' => 'Pinecone',
5226 - 'bot_id' => $bot_id,
5227 - 'namespace' => $namespace,
5228 3429 'top_matches' => [],
5229 3430 'threshold_used' => 0,
5230 3431 'total_checked' => 0
5231 3432 ];
5232 3433
5233 - // NEW: Initialize valid URLs array
5234 - $valid_urls = [];
5235 -
5236 3434 if (empty($host) || empty($api_key)) {
5237 - //error_log("MXCHAT DEBUG ERROR: Missing Pinecone host or API key!");
5238 - //error_log(" - Host empty: " . (empty($host) ? 'YES' : 'NO'));
5239 - //error_log(" - API key empty: " . (empty($api_key) ? 'YES' : 'NO'));
5240 - // Store empty array for valid URLs since we can't proceed
5241 - $this->current_valid_urls = [];
5242 3435 return '';
5243 3436 }
5244 3437
5245 - // Get knowledge manager instance for role checking
5246 - $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
3438 + // Get the similarity threshold from the main options
3439 + $main_options = get_option('mxchat_options', []);
3440 + $similarity_threshold = isset($main_options['similarity_threshold'])
3441 + ? ((int) $main_options['similarity_threshold']) / 100
3442 + : 0.75;
5247 3443
5248 - // Get the similarity threshold from the bot options or main options
5249 - $bot_options = $this->get_bot_options($bot_id);
5250 - $current_options = !empty($bot_options) ? $bot_options : get_option('mxchat_options', []);
5251 -
5252 - $similarity_threshold = isset($current_options['similarity_threshold'])
5253 - ? ((int) $current_options['similarity_threshold']) / 100
5254 - : 0.35;
5255 -
5256 3444 $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
5257 3445
5258 - // Prepare the query request for Pinecone
3446 + // Prepare the query request for Pinecone (request more for testing)
5259 3447 $api_endpoint = "https://{$host}/query";
5260 3448
5261 3449 $request_body = array(
5262 3450 'vector' => $user_embedding,
5263 - 'topK' => 50, // Increased for chunked content grouping - need more candidates to find top N unique URLs
3451 + 'topK' => 20, // Request more to get good testing data
5264 3452 'includeMetadata' => true,
5265 3453 'includeValues' => true
5266 3454 );
5267 3455
5268 - // Add namespace if specified for this bot
5269 - if (!empty($namespace)) {
5270 - $request_body['namespace'] = $namespace;
5271 - }
5272 -
5273 - //error_log("MXCHAT DEBUG: About to call Pinecone API");
5274 - //error_log(" - Endpoint: " . $api_endpoint);
5275 - //error_log(" - Namespace in request: " . (!empty($namespace) ? $namespace : 'NOT SET'));
5276 -
5277 3456 $response = wp_remote_post($api_endpoint, array(
5278 3457 'headers' => array(
5279 3458 'Api-Key' => $api_key,
5280 3459 'accept' => 'application/json',
@@ -5284,252 +3463,47 @@
5284 3463 'timeout' => 30
5285 3464 ));
5286 3465
5287 3466 if (is_wp_error($response)) {
5288 - //error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
5289 - // Store empty array for valid URLs
5290 - $this->current_valid_urls = [];
5291 3467 return '';
5292 3468 }
5293 3469
5294 3470 $response_code = wp_remote_retrieve_response_code($response);
5295 - //error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
5296 -
5297 3471 if ($response_code !== 200) {
5298 - $response_body = wp_remote_retrieve_body($response);
5299 - //error_log("MXCHAT DEBUG ERROR: Pinecone API error response: " . substr($response_body, 0, 500));
5300 - // Store empty array for valid URLs
5301 - $this->current_valid_urls = [];
5302 3472 return '';
5303 3473 }
5304 3474
5305 - // ADD DETAILED DEBUG SECTION HERE
5306 - $response_body = wp_remote_retrieve_body($response);
5307 - //error_log("MXCHAT DEBUG: Raw Pinecone response length: " . strlen($response_body));
5308 -
5309 - $results = json_decode($response_body, true);
5310 -
5311 - if (json_last_error() !== JSON_ERROR_NONE) {
5312 - //error_log("MXCHAT DEBUG ERROR: JSON decode error: " . json_last_error_msg());
5313 - //error_log("MXCHAT DEBUG: First 500 chars of response: " . substr($response_body, 0, 500));
5314 - // Store empty array for valid URLs
5315 - $this->current_valid_urls = [];
5316 - return '';
5317 - }
5318 -
5319 - //error_log("MXCHAT DEBUG: Pinecone response structure:");
5320 - //error_log(" - Has 'matches' key: " . (isset($results['matches']) ? 'yes' : 'no'));
5321 - //error_log(" - Has 'namespace' key: " . (isset($results['namespace']) ? 'yes (' . $results['namespace'] . ')' : 'no'));
5322 -
3475 + $results = json_decode(wp_remote_retrieve_body($response), true);
5323 3476 if (empty($results['matches'])) {
5324 - //error_log("MXCHAT DEBUG: No matches found in Pinecone response");
5325 - //error_log("MXCHAT DEBUG: Response keys: " . implode(', ', array_keys($results)));
5326 - // Store empty array for valid URLs
5327 - $this->current_valid_urls = [];
5328 3477 return '';
5329 3478 }
5330 3479
5331 - //error_log("MXCHAT DEBUG: Found " . count($results['matches']) . " matches in Pinecone");
3480 + // First, determine which matches will actually be used for content
3481 + $matches_used_for_context = [];
3482 + $matches_used = 0;
5332 3483
5333 - // Log first match details for debugging
5334 - if (!empty($results['matches'][0])) {
5335 - $first_match = $results['matches'][0];
5336 - //error_log("MXCHAT DEBUG: First match details:");
5337 - //error_log(" - Score: " . ($first_match['score'] ?? 'no score'));
5338 - //error_log(" - Has metadata: " . (isset($first_match['metadata']) ? 'yes' : 'no'));
5339 - if (isset($first_match['metadata'])) {
5340 - //error_log(" - Metadata keys: " . implode(', ', array_keys($first_match['metadata'])));
5341 - }
5342 - }
5343 -
5344 - // Initialize the final content
5345 - $content = '';
5346 - $matches_used = 0;
5347 - $matches_used_for_context = [];
5348 - $total_chunks_used = 0;
5349 - $max_total_chunks = isset($current_options['rag_chunks_limit']) ? intval($current_options['rag_chunks_limit']) : 15;
5350 - if ($max_total_chunks < 8) $max_total_chunks = 8;
5351 - if ($max_total_chunks > 20) $max_total_chunks = 20;
5352 - $max_chunks_per_source = 5; // Cap per individual source to limit token usage
5353 -
5354 - // Check if citation links are enabled (default to 'on' for backwards compatibility)
5355 - // Use fresh options to ensure we get the latest setting value
5356 - $fresh_options = get_option('mxchat_options', []);
5357 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
5358 -
5359 - // NEW CHUNKING LOGIC: Group results by source_url for chunk reassembly
5360 - $url_groups = array();
5361 -
5362 3484 foreach ($results['matches'] as $index => $match) {
5363 3485 // Skip if similarity is below threshold
5364 3486 if ($match['score'] < $similarity_threshold) {
5365 3487 continue;
5366 3488 }
5367 -
5368 - $metadata = $match['metadata'] ?? array();
5369 - $source_url = $metadata['source_url'] ?? '';
5370 - $match_id = $match['id'] ?? '';
5371 -
5372 - // LAZY ROLE CHECK: Only check role for content we're actually considering
5373 - $role_restriction = $this->get_single_vector_role($match_id, $metadata);
5374 - $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
5375 -
5376 - // Skip if user doesn't have access
5377 - if (!$has_access) {
5378 - continue;
5379 - }
5380 -
5381 - // Use a unique key for manual entries without a source URL
5382 - $group_key = !empty($source_url) ? $source_url : '_manual_' . $match_id;
5383 -
5384 - // Group by source URL (or unique key for manual entries)
5385 - if (!isset($url_groups[$group_key])) {
5386 - $url_groups[$group_key] = array(
5387 - 'source_url' => $source_url,
5388 - 'best_score' => 0,
5389 - 'is_chunked' => isset($metadata['is_chunked']) && $metadata['is_chunked'],
5390 - 'chunks' => array(),
5391 - 'single_text' => ''
5392 - );
5393 - }
5394 -
5395 - // Track best score for this group
5396 - if ($match['score'] > $url_groups[$group_key]['best_score']) {
5397 - $url_groups[$group_key]['best_score'] = $match['score'];
5398 - }
5399 -
5400 - // Store chunk info or single text
5401 - if ($url_groups[$group_key]['is_chunked']) {
5402 - $url_groups[$group_key]['chunks'][] = array(
5403 - 'id' => $match_id,
5404 - 'score' => $match['score'],
5405 - 'chunk_index' => $metadata['chunk_index'] ?? 0,
5406 - 'text' => $metadata['text'] ?? ''
5407 - );
5408 - } else {
5409 - // Non-chunked content - just store the text
5410 - $url_groups[$group_key]['single_text'] = $metadata['text'] ?? '';
5411 - $url_groups[$group_key]['single_id'] = $match_id;
5412 - }
5413 - }
5414 -
5415 - // Sort URL groups by best score (highest first)
5416 - uasort($url_groups, function($a, $b) {
5417 - return $b['best_score'] <=> $a['best_score'];
5418 - });
5419 -
5420 - // Get RAG sources limit from options (default 6, min 3, max 10)
5421 - $rag_sources_limit = isset($current_options['rag_sources_limit']) ? intval($current_options['rag_sources_limit']) : 3;
5422 - if ($rag_sources_limit < 3) $rag_sources_limit = 3;
5423 - if ($rag_sources_limit > 10) $rag_sources_limit = 10;
5424 -
5425 - // Take top N unique URLs based on user setting
5426 - $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
5427 -
5428 - // Track which match IDs are actually used for context
5429 - foreach ($top_urls as $group) {
5430 - if ($group['is_chunked']) {
5431 - foreach ($group['chunks'] as $chunk) {
5432 - $matches_used_for_context[] = $chunk['id'];
5433 - }
5434 - } elseif (!empty($group['single_id'])) {
5435 - $matches_used_for_context[] = $group['single_id'];
5436 - }
5437 - }
5438 -
5439 - // Build content from top sources
5440 - foreach ($top_urls as $group_key => $group) {
5441 - $source_url = $group['source_url']; // Use actual source_url, not the group key
5442 -
5443 - // Stop if we've hit the total chunk limit
5444 - if ($total_chunks_used >= $max_total_chunks) {
3489 +
3490 + // Limit to top 5 matches above threshold
3491 + if ($matches_used >= 5) {
5445 3492 break;
5446 3493 }
5447 -
5448 - $full_text = '';
5449 - $chunks_in_this_source = 1; // Default for non-chunked content
5450 -
5451 - if ($group['is_chunked']) {
5452 - // Calculate how many chunks we can still use (respect both total and per-source caps)
5453 - $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
5454 -
5455 - // Fetch chunks for this URL with limit
5456 - $full_text = $this->reassemble_chunks_from_pinecone($source_url, $bot_config, $chunks_remaining, $chunks_in_this_source);
5457 -
5458 - // If fetching all chunks fails, fall back to matched chunks
5459 - if (empty($full_text)) {
5460 - // Sort matched chunks by index and concatenate
5461 - usort($group['chunks'], function($a, $b) {
5462 - return $a['chunk_index'] <=> $b['chunk_index'];
5463 - });
5464 -
5465 - $chunk_texts = array();
5466 - $chunks_in_this_source = 0;
5467 - foreach ($group['chunks'] as $chunk) {
5468 - if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
5469 - break;
5470 - }
5471 - $chunk_texts[] = $chunk['text'];
5472 - $chunks_in_this_source++;
5473 - }
5474 - $full_text = implode("\n\n", $chunk_texts);
5475 - }
5476 - } else {
5477 - $full_text = $group['single_text'];
5478 - $chunks_in_this_source = 1;
3494 +
3495 + if (!empty($match['metadata']['text'])) {
3496 + $matches_used_for_context[] = $match['id'] ?? $index;
3497 + $matches_used++;
5479 3498 }
5480 -
5481 - if (!empty($full_text)) {
5482 - // Strip URLs from content if citation links are disabled
5483 - if (!$citation_links_enabled) {
5484 - $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
5485 - $full_text = preg_replace('/\s+/', ' ', trim($full_text)); // Clean up extra spaces
5486 - }
5487 -
5488 - // Use numbered reference for URL-based entries, plain info label for manual entries
5489 - if (!empty($source_url) && $source_url !== '#') {
5490 - $matches_used++;
5491 - $content .= "## Reference " . $matches_used . " ##\n";
5492 - $content .= $full_text . "\n\n";
5493 -
5494 - // Only include citation URLs if citation links are enabled
5495 - if ($citation_links_enabled) {
5496 - $valid_urls[] = $source_url;
5497 - $content .= "URL: " . $source_url . "\n\n";
5498 - }
5499 - } else {
5500 - // Manual entry — no reference number, no citation
5501 - $content .= "## Information ##\n";
5502 - $content .= $full_text . "\n\n";
5503 - }
5504 -
5505 - // Extract any URLs from the text content itself (only if citation links enabled)
5506 - if ($citation_links_enabled) {
5507 - preg_match_all(
5508 - '#\bhttps?://[^\s<>"\']+#i',
5509 - $full_text,
5510 - $content_urls
5511 - );
5512 - if (!empty($content_urls[0])) {
5513 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5514 - }
5515 - }
5516 -
5517 - $total_chunks_used += $chunks_in_this_source;
5518 - }
5519 3499 }
5520 -
5521 - // Process ALL matches for testing data (top 10) - with role checking for testing display
3500 +
3501 + // Process ALL matches for testing data (top 10)
5522 3502 $all_matches = [];
5523 3503 foreach ($results['matches'] as $index => $match) {
5524 3504 if ($index >= 10) break; // Limit to top 10 for testing
5525 3505
5526 - $match_id = $match['id'] ?? '';
5527 -
5528 - // Check role access for testing display (use cache if available)
5529 - $role_restriction = $this->get_single_vector_role($match_id, $match['metadata']);
5530 - $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
5531 -
5532 3506 $source_display = '';
5533 3507 if (!empty($match['metadata']['source_url'])) {
5534 3508 $source_display = $match['metadata']['source_url'];
5535 3509 } else {
@@ -5537,34 +3511,18 @@
5537 3511 $content_preview = preg_replace('/\s+/', ' ', $content_preview);
5538 3512 $source_display = substr(trim($content_preview), 0, 50) . '...';
5539 3513 }
5540 3514
5541 - $match_id_for_display = $match['id'] ?? $index;
5542 -
5543 - // Check for chunk metadata in Pinecone
5544 - $is_chunk = isset($match['metadata']['is_chunked']) && $match['metadata']['is_chunked'];
5545 - $chunk_index = isset($match['metadata']['chunk_index']) ? intval($match['metadata']['chunk_index']) : null;
5546 - $total_chunks = isset($match['metadata']['total_chunks']) ? intval($match['metadata']['total_chunks']) : null;
5547 -
5548 - // Also detect chunk from vector ID pattern: {hash}_chunk_{index}
5549 - if (!$is_chunk && MxChat_Chunker::is_chunk_vector_id($match_id_for_display)) {
5550 - $is_chunk = true;
5551 - }
5552 -
3515 + $match_id = $match['id'] ?? $index;
3516 +
5553 3517 $all_matches[] = [
5554 - 'document_id' => $match_id_for_display,
3518 + 'document_id' => $match_id,
5555 3519 'similarity' => $match['score'],
5556 3520 'similarity_percentage' => round($match['score'] * 100, 2),
5557 3521 'above_threshold' => $match['score'] >= $similarity_threshold,
5558 3522 'source_display' => $source_display,
5559 3523 'content_preview' => substr(strip_tags($match['metadata']['text'] ?? ''), 0, 100) . '...',
5560 - 'used_for_context' => in_array($match_id_for_display, $matches_used_for_context),
5561 - 'role_restriction' => $role_restriction,
5562 - 'has_access' => $has_access,
5563 - 'filtered_out' => !$has_access,
5564 - 'is_chunk' => $is_chunk,
5565 - 'chunk_index' => $chunk_index,
5566 - 'total_chunks' => $total_chunks
3524 + 'used_for_context' => in_array($match_id, $matches_used_for_context) // Correct usage flag
5567 3525 ];
5568 3526 }
5569 3527
5570 3528 // Store for testing panel
@@ -5569,591 +3527,54 @@
5569 3527
5570 3528 // Store for testing panel
5571 3529 $this->last_similarity_analysis['top_matches'] = $all_matches;
5572 3530 $this->last_similarity_analysis['total_checked'] = count($results['matches']);
5573 - $this->last_similarity_analysis['sources_used'] = $matches_used;
5574 - $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
5575 -
5576 - // NEW: Store unique valid URLs for validation
5577 - $this->current_valid_urls = array_unique($valid_urls);
5578 -
5579 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
5580 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
5581 -
5582 - // Add response guidelines
5583 - if ($matches_used === 0) {
5584 - $content = "No reference information was found for this query.\n\n";
5585 - } else {
5586 - // Build response guidelines based on citation links setting
5587 - $content .= "\n## Response Guidelines ##\n" .
5588 - "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
5589 - "Be conversational and friendly, but never mention your knowledge base or training data. " .
5590 - "If you don't have specific information or are uncertain about any details, it's always " .
5591 - "better to honestly say you don't know rather than making up or guessing at answers. " .
5592 - "When information is incomplete, let them know you are unsure.\n\n";
5593 -
5594 - // Only add hyperlink instructions if citation links are enabled
5595 - if ($citation_links_enabled) {
5596 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
5597 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
5598 - "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
5599 - } else {
5600 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
5601 - "Simply provide helpful answers based on the reference information without citing sources.";
5602 - }
5603 - }
5604 -
5605 - return trim($content);
5606 -}
5607 -
5608 -/**
5609 - * Get role restriction for a single vector (with caching)
5610 - */
5611 -private function get_single_vector_role($vector_id, $metadata = array()) {
5612 - global $wpdb;
5613 3531
5614 - if (empty($vector_id)) {
5615 - return 'public';
5616 - }
3532 + //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " Pinecone matches for testing");
5617 3533
5618 - // Check cache first
5619 - $cache_key = 'mxchat_vector_role_' . $vector_id;
5620 - $cached_role = wp_cache_get($cache_key, 'mxchat_vector_roles');
3534 + // Initialize the final content
3535 + $content = '';
3536 + $matches_used = 0;
5621 3537
5622 - if ($cached_role !== false) {
5623 - return $cached_role;
5624 - }
5625 -
5626 - $role_restriction = 'public';
5627 -
5628 - // First try Pinecone metadata
5629 - if (!empty($metadata['role_restriction'])) {
5630 - $role_restriction = $metadata['role_restriction'];
5631 - } else {
5632 - // Check WordPress table for user-modified roles
5633 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
5634 - $stored_role = $wpdb->get_var($wpdb->prepare(
5635 - "SELECT role_restriction FROM {$roles_table} WHERE vector_id = %s",
5636 - $vector_id
5637 - ));
3538 + // Process each match for actual content (this is the real content generation)
3539 + foreach ($results['matches'] as $index => $match) {
3540 + // Skip if similarity is below threshold
3541 + if ($match['score'] < $similarity_threshold) {
3542 + continue;
3543 + }
5638 3544
5639 - if ($stored_role) {
5640 - $role_restriction = $stored_role;
3545 + // Limit to top 5 matches above threshold
3546 + if ($matches_used >= 5) {
3547 + break;
5641 3548 }
5642 - }
5643 -
5644 - // Cache individual role for 1 hour
5645 - wp_cache_set($cache_key, $role_restriction, 'mxchat_vector_roles', 3600);
5646 -
5647 - return $role_restriction;
5648 -}
5649 -
5650 -/**
5651 - * Fetch and reassemble all chunks for a URL from Pinecone
5652 - *
5653 - * @param string $source_url The source URL to fetch chunks for
5654 - * @param array $bot_config Bot-specific Pinecone configuration
5655 - * @return string Reassembled content from all chunks
5656 - */
5657 -private function reassemble_chunks_from_pinecone($source_url, $bot_config, $max_chunks = 0, &$chunk_count = 0) {
5658 - $api_key = $bot_config['api_key'] ?? '';
5659 - $host = $bot_config['host'] ?? '';
5660 - $namespace = $bot_config['namespace'] ?? '';
5661 -
5662 - if (empty($host) || empty($api_key)) {
5663 - $chunk_count = 0;
5664 - return '';
5665 - }
5666 -
5667 - $base_hash = md5($source_url);
5668 -
5669 - // Use Pinecone list API to find all chunk vectors with this prefix
5670 - $list_url = "https://{$host}/vectors/list";
5671 -
5672 - // Limit to max_chunks if specified, otherwise fetch up to 100
5673 - $fetch_limit = ($max_chunks > 0 && $max_chunks < 100) ? $max_chunks : 100;
5674 -
5675 - $list_body = array(
5676 - 'prefix' => $base_hash . '_chunk_',
5677 - 'limit' => $fetch_limit
5678 - );
5679 -
5680 - if (!empty($namespace)) {
5681 - $list_body['namespace'] = $namespace;
5682 - }
5683 -
5684 - $list_response = wp_remote_post($list_url, array(
5685 - 'headers' => array(
5686 - 'Api-Key' => $api_key,
5687 - 'accept' => 'application/json',
5688 - 'content-type' => 'application/json'
5689 - ),
5690 - 'body' => wp_json_encode($list_body),
5691 - 'timeout' => 30
5692 - ));
5693 -
5694 - if (is_wp_error($list_response)) {
5695 - //error_log('[MXCHAT-CHUNK] List API error: ' . $list_response->get_error_message());
5696 - return '';
5697 - }
5698 -
5699 - $list_data = json_decode(wp_remote_retrieve_body($list_response), true);
5700 -
5701 - if (empty($list_data['vectors'])) {
5702 - //error_log('[MXCHAT-CHUNK] No chunk vectors found for URL: ' . $source_url);
5703 - return '';
5704 - }
5705 -
5706 - // Extract vector IDs
5707 - $vector_ids = array();
5708 - foreach ($list_data['vectors'] as $vector) {
5709 - if (isset($vector['id'])) {
5710 - $vector_ids[] = $vector['id'];
5711 - }
5712 - }
5713 -
5714 - if (empty($vector_ids)) {
5715 - return '';
5716 - }
5717 -
5718 - // Fetch all chunk content
5719 - $fetch_url = "https://{$host}/vectors/fetch";
5720 -
5721 - $fetch_body = array(
5722 - 'ids' => $vector_ids
5723 - );
5724 -
5725 - if (!empty($namespace)) {
5726 - $fetch_body['namespace'] = $namespace;
5727 - }
5728 -
5729 - $fetch_response = wp_remote_post($fetch_url, array(
5730 - 'headers' => array(
5731 - 'Api-Key' => $api_key,
5732 - 'accept' => 'application/json',
5733 - 'content-type' => 'application/json'
5734 - ),
5735 - 'body' => wp_json_encode($fetch_body),
5736 - 'timeout' => 30
5737 - ));
5738 -
5739 - if (is_wp_error($fetch_response)) {
5740 - //error_log('[MXCHAT-CHUNK] Fetch API error: ' . $fetch_response->get_error_message());
5741 - return '';
5742 - }
5743 -
5744 - $fetch_data = json_decode(wp_remote_retrieve_body($fetch_response), true);
5745 -
5746 - if (empty($fetch_data['vectors'])) {
5747 - return '';
5748 - }
5749 -
5750 - // Sort chunks by index and reassemble
5751 - $chunks = array();
5752 - foreach ($fetch_data['vectors'] as $id => $vector) {
5753 - $metadata = $vector['metadata'] ?? array();
5754 - $chunk_index = $metadata['chunk_index'] ?? 0;
5755 - $text = $metadata['text'] ?? '';
5756 -
5757 - // Store chunk with its index
5758 - $chunks[$chunk_index] = $text;
5759 - }
5760 -
5761 - // Sort by chunk index
5762 - ksort($chunks);
5763 -
5764 - // Apply chunk limit if specified
5765 - if ($max_chunks > 0 && count($chunks) > $max_chunks) {
5766 - $chunks = array_slice($chunks, 0, $max_chunks, true);
5767 - }
5768 -
5769 - // Store actual chunk count
5770 - $chunk_count = count($chunks);
5771 -
5772 - // Reassemble content
5773 - return implode("\n\n", $chunks);
5774 -}
5775 -
5776 -/**
5777 - * Search for relevant content using OpenAI Vector Store (File Search)
5778 - *
5779 - * @param string $user_query The user's query text
5780 - * @param string $bot_id The bot ID
5781 - * @param array $vectorstore_config Vector Store configuration
5782 - * @return string Formatted context string with references
5783 - */
5784 -private function find_relevant_content_openai_vectorstore($user_query, $bot_id = 'default', $vectorstore_config = array()) {
5785 - //error_log("MXCHAT DEBUG: find_relevant_content_openai_vectorstore called");
5786 - //error_log(" - bot_id: " . $bot_id);
5787 - //error_log(" - user_query length: " . strlen($user_query));
5788 -
5789 - // Get OpenAI API key
5790 - $mxchat_options = get_option('mxchat_options', array());
5791 - $api_key = $mxchat_options['api_key'] ?? '';
5792 -
5793 - // Reset vectorstore error tracking
5794 - $this->last_vectorstore_error = null;
5795 -
5796 - if (empty($api_key)) {
5797 - //error_log("MXCHAT DEBUG ERROR: OpenAI API key not configured");
5798 - $this->last_vectorstore_error = 'Vector Store search failed: OpenAI API key is not configured.';
5799 - $this->current_valid_urls = [];
5800 - return '';
5801 - }
5802 -
5803 - // Get Vector Store configuration
5804 - if (empty($vectorstore_config)) {
5805 - $vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
5806 - }
5807 -
5808 - $vectorstore_ids_string = $vectorstore_config['vectorstore_ids'] ?? '';
5809 - $max_results = $vectorstore_config['max_results'] ?? 5;
5810 -
5811 - if (empty($vectorstore_ids_string)) {
5812 - //error_log("MXCHAT DEBUG ERROR: No Vector Store IDs configured");
5813 - $this->last_vectorstore_error = 'Vector Store search failed: No Vector Store IDs are configured for this bot.';
5814 - $this->current_valid_urls = [];
5815 - return '';
5816 - }
5817 -
5818 - // Parse Vector Store IDs
5819 - $vectorstore_ids = array_map('trim', explode(',', $vectorstore_ids_string));
5820 - $vectorstore_ids = array_filter($vectorstore_ids); // Remove empty values
5821 -
5822 - //error_log("MXCHAT DEBUG: Vector Store IDs: " . implode(', ', $vectorstore_ids));
5823 - //error_log("MXCHAT DEBUG: Max results: " . $max_results);
5824 -
5825 - // Initialize similarity analysis storage
5826 - $this->last_similarity_analysis = [
5827 - 'knowledge_base_type' => 'OpenAI Vector Store',
5828 - 'bot_id' => $bot_id,
5829 - 'vectorstore_ids' => $vectorstore_ids,
5830 - 'top_matches' => [],
5831 - 'threshold_used' => 0,
5832 - 'total_checked' => 0
5833 - ];
5834 -
5835 - $valid_urls = [];
5836 -
5837 - // Get the selected model
5838 - $bot_options = $this->get_bot_options($bot_id);
5839 - $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
5840 - $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
5841 -
5842 - // Verify it's an OpenAI model
5843 - if (!$this->is_openai_chat_model($selected_model)) {
5844 - //error_log("MXCHAT DEBUG ERROR: Vector Store search requires OpenAI model. Current: " . $selected_model);
5845 - $this->last_vectorstore_error = 'Vector Store search requires an OpenAI model. Current model: ' . $selected_model;
5846 - $this->current_valid_urls = [];
5847 - return '';
5848 - }
5849 -
5850 - // Use OpenAI Responses API with file_search tool
5851 - $request_body = array(
5852 - 'model' => $selected_model,
5853 - 'input' => $user_query,
5854 - 'tools' => array(
5855 - array(
5856 - 'type' => 'file_search',
5857 - 'vector_store_ids' => $vectorstore_ids,
5858 - 'max_num_results' => intval($max_results)
5859 - )
5860 - ),
5861 - 'include' => array('output[*].file_search_call.search_results')
5862 - );
5863 -
5864 - //error_log("MXCHAT VECTORSTORE: ========== REQUEST START ==========");
5865 - //error_log("MXCHAT VECTORSTORE: Model: " . $selected_model);
5866 - //error_log("MXCHAT VECTORSTORE: Query: " . substr($user_query, 0, 200));
5867 - //error_log("MXCHAT VECTORSTORE: Vector Store IDs: " . implode(', ', $vectorstore_ids));
5868 - //error_log("MXCHAT VECTORSTORE: Max Results: " . $max_results);
5869 - //error_log("MXCHAT VECTORSTORE: Request body: " . wp_json_encode($request_body));
5870 -
5871 - $response = wp_remote_post('https://api.openai.com/v1/responses', array(
5872 - 'headers' => array(
5873 - 'Authorization' => 'Bearer ' . $api_key,
5874 - 'Content-Type' => 'application/json'
5875 - ),
5876 - 'body' => wp_json_encode($request_body),
5877 - 'timeout' => 60
5878 - ));
5879 -
5880 - if (is_wp_error($response)) {
5881 - //error_log("MXCHAT VECTORSTORE ERROR: WP Error: " . $response->get_error_message());
5882 - $this->last_vectorstore_error = 'Vector Store API request failed: ' . $response->get_error_message();
5883 - $this->current_valid_urls = [];
5884 - return '';
5885 - }
5886 -
5887 - $response_code = wp_remote_retrieve_response_code($response);
5888 - //error_log("MXCHAT VECTORSTORE: Response code: " . $response_code);
5889 -
5890 - $response_body = wp_remote_retrieve_body($response);
5891 - //error_log("MXCHAT VECTORSTORE: Raw response (first 2000 chars): " . substr($response_body, 0, 2000));
5892 -
5893 - if ($response_code !== 200) {
5894 - //error_log("MXCHAT VECTORSTORE ERROR: API error response: " . $response_body);
5895 - $api_error_detail = '';
5896 - $decoded_error = json_decode($response_body, true);
5897 - if (isset($decoded_error['error']['message'])) {
5898 - $api_error_detail = $decoded_error['error']['message'];
5899 - }
5900 - $this->last_vectorstore_error = 'Vector Store API returned HTTP ' . $response_code . ($api_error_detail ? ': ' . $api_error_detail : '');
5901 - $this->current_valid_urls = [];
5902 - return '';
5903 - }
5904 - $result = json_decode($response_body, true);
5905 -
5906 - if (json_last_error() !== JSON_ERROR_NONE) {
5907 - //error_log("MXCHAT VECTORSTORE ERROR: JSON decode error: " . json_last_error_msg());
5908 - $this->last_vectorstore_error = 'Vector Store response could not be parsed: ' . json_last_error_msg();
5909 - $this->current_valid_urls = [];
5910 - return '';
5911 - }
5912 -
5913 - // Debug: Log the structure of the result
5914 - //error_log("MXCHAT VECTORSTORE: Result keys: " . implode(', ', array_keys($result)));
5915 - if (isset($result['output'])) {
5916 - //error_log("MXCHAT VECTORSTORE: Output count: " . count($result['output']));
5917 - foreach ($result['output'] as $idx => $out) {
5918 - //error_log("MXCHAT VECTORSTORE: Output[$idx] type: " . ($out['type'] ?? 'unknown'));
5919 - //error_log("MXCHAT VECTORSTORE: Output[$idx] keys: " . implode(', ', array_keys($out)));
5920 - }
5921 - } else {
5922 - //error_log("MXCHAT VECTORSTORE: No 'output' key in result!");
5923 - }
5924 -
5925 - // Extract file search results from the response
5926 - $content = '';
5927 - $matches_used = 0;
5928 - $all_matches = [];
5929 -
5930 - // The Responses API returns output array with tool results
5931 - if (isset($result['output']) && is_array($result['output'])) {
5932 - foreach ($result['output'] as $output_item) {
5933 - // Look for file_search_call results
5934 - if (isset($output_item['type']) && $output_item['type'] === 'file_search_call') {
5935 - //error_log("MXCHAT VECTORSTORE: Found file_search_call output item");
5936 - //error_log("MXCHAT VECTORSTORE: file_search_call keys: " . implode(', ', array_keys($output_item)));
5937 -
5938 - // Check for search_results in the output item directly
5939 - $search_results = $output_item['search_results'] ?? $output_item['results'] ?? [];
5940 - //error_log("MXCHAT VECTORSTORE: Search results count: " . count($search_results));
5941 -
5942 - if (empty($search_results)) {
5943 - //error_log("MXCHAT VECTORSTORE: No search results found in file_search_call");
5944 - //error_log("MXCHAT VECTORSTORE: file_search_call content: " . wp_json_encode($output_item));
5945 - }
5946 -
5947 - foreach ($search_results as $index => $search_result) {
5948 - $filename = $search_result['filename'] ?? '';
5949 - $score = $search_result['score'] ?? 0;
5950 - $text_content = '';
5951 -
5952 - // Extract text content from the result
5953 - // The text can be directly on the result OR nested under content array
5954 - if (isset($search_result['text']) && !empty($search_result['text'])) {
5955 - // Direct text field (OpenAI's actual format)
5956 - $text_content = $search_result['text'];
5957 - //error_log("MXCHAT VECTORSTORE: Found text directly on result[$index], length: " . strlen($text_content));
5958 - } elseif (isset($search_result['content']) && is_array($search_result['content'])) {
5959 - // Nested content array format
5960 - foreach ($search_result['content'] as $content_item) {
5961 - if (isset($content_item['text'])) {
5962 - $text_content .= $content_item['text'] . "\n";
5963 - }
5964 - }
5965 - //error_log("MXCHAT VECTORSTORE: Found text in content array for result[$index], length: " . strlen($text_content));
5966 - } else {
5967 - //error_log("MXCHAT VECTORSTORE: No text found for result[$index]. Keys: " . implode(', ', array_keys($search_result)));
5968 - }
5969 -
5970 - if (!empty($text_content)) {
5971 - $content .= "## Reference " . ($matches_used + 1) . " ##\n";
5972 - $content .= trim($text_content) . "\n\n";
5973 -
5974 - if (!empty($filename)) {
5975 - $content .= "Source: " . $filename . "\n\n";
5976 - }
5977 -
5978 - // Extract URLs from content
5979 - preg_match_all(
5980 - '#\bhttps?://[^\s<>"\']+#i',
5981 - $text_content,
5982 - $content_urls
5983 - );
5984 - if (!empty($content_urls[0])) {
5985 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5986 - }
5987 -
5988 - $matches_used++;
5989 - }
5990 -
5991 - // Store for similarity analysis
5992 - $all_matches[] = [
5993 - 'document_id' => $filename ?: ('result_' . $index),
5994 - 'similarity' => $score,
5995 - 'similarity_percentage' => round($score * 100, 2),
5996 - 'above_threshold' => true,
5997 - 'source_display' => $filename,
5998 - 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
5999 - 'used_for_context' => true,
6000 - 'role_restriction' => 'public',
6001 - 'has_access' => true,
6002 - 'filtered_out' => false
6003 - ];
6004 - }
3549 +
3550 + if (!empty($match['metadata']['text'])) {
3551 + $content .= "## Reference " . ($matches_used + 1) . " ##\n";
3552 + $content .= $match['metadata']['text'] . "\n\n";
3553 +
3554 + if (!empty($match['metadata']['source_url'])) {
3555 + $content .= "URL: " . $match['metadata']['source_url'] . "\n\n";
6005 3556 }
6006 -
6007 - // Also check for message content with annotations (citations)
6008 - if (isset($output_item['type']) && $output_item['type'] === 'message') {
6009 - if (isset($output_item['content']) && is_array($output_item['content'])) {
6010 - foreach ($output_item['content'] as $content_block) {
6011 - if (isset($content_block['annotations']) && is_array($content_block['annotations'])) {
6012 - foreach ($content_block['annotations'] as $annotation) {
6013 - if (isset($annotation['filename'])) {
6014 - $filename = $annotation['filename'];
6015 - $score = $annotation['score'] ?? 0;
6016 - $text_content = '';
6017 -
6018 - if (isset($annotation['content']) && is_array($annotation['content'])) {
6019 - foreach ($annotation['content'] as $ann_content) {
6020 - if (isset($ann_content['text'])) {
6021 - $text_content .= $ann_content['text'] . "\n";
6022 - }
6023 - }
6024 - }
6025 -
6026 - if (!empty($text_content) && $matches_used < $max_results) {
6027 - $content .= "## Reference " . ($matches_used + 1) . " ##\n";
6028 - $content .= trim($text_content) . "\n\n";
6029 - $content .= "Source: " . $filename . "\n\n";
6030 -
6031 - preg_match_all(
6032 - '#\bhttps?://[^\s<>"\']+#i',
6033 - $text_content,
6034 - $content_urls
6035 - );
6036 - if (!empty($content_urls[0])) {
6037 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
6038 - }
6039 -
6040 - $matches_used++;
6041 -
6042 - $all_matches[] = [
6043 - 'document_id' => $filename,
6044 - 'similarity' => $score,
6045 - 'similarity_percentage' => round($score * 100, 2),
6046 - 'above_threshold' => true,
6047 - 'source_display' => $filename,
6048 - 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
6049 - 'used_for_context' => true,
6050 - 'role_restriction' => 'public',
6051 - 'has_access' => true,
6052 - 'filtered_out' => false
6053 - ];
6054 - }
6055 - }
6056 - }
6057 - }
6058 - }
6059 - }
6060 - }
3557 +
3558 + $matches_used++;
6061 3559 }
6062 3560 }
6063 -
6064 - // Store for testing panel
6065 - $this->last_similarity_analysis['top_matches'] = $all_matches;
6066 - $this->last_similarity_analysis['total_checked'] = count($all_matches);
6067 -
6068 - // Store unique valid URLs for validation
6069 - $this->current_valid_urls = array_unique($valid_urls);
6070 -
6071 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
6072 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
6073 -
6074 - //error_log("MXCHAT VECTORSTORE: ========== SEARCH COMPLETE ==========");
6075 - //error_log("MXCHAT VECTORSTORE: Matches used: " . $matches_used);
6076 - //error_log("MXCHAT VECTORSTORE: All matches count: " . count($all_matches));
6077 - //error_log("MXCHAT VECTORSTORE: Content length: " . strlen($content));
6078 - if ($matches_used > 0) {
6079 - //error_log("MXCHAT VECTORSTORE: Content preview: " . substr($content, 0, 500));
6080 - }
6081 -
6082 - // Check if citation links are enabled
6083 - $citation_links_enabled = ($mxchat_options['citation_links_toggle'] ?? 'on') === 'on';
6084 -
3561 +
6085 3562 // Add response guidelines
6086 3563 if ($matches_used === 0) {
6087 - //error_log("MXCHAT VECTORSTORE: No matches found - returning empty reference message");
6088 3564 $content = "No reference information was found for this query.\n\n";
6089 3565 } else {
6090 - // Build response guidelines based on citation links setting
6091 - $content .= "\n## Response Guidelines ##\n" .
6092 - "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
6093 - "Be conversational and friendly, but never mention your knowledge base or training data. " .
6094 - "If you don't have specific information or are uncertain about any details, it's always " .
6095 - "better to honestly say you don't know rather than making up or guessing at answers. " .
6096 - "When information is incomplete, let them know you are unsure.\n\n";
6097 -
6098 - // Only add hyperlink instructions if citation links are enabled
6099 - if ($citation_links_enabled) {
6100 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
6101 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about.";
6102 - } else {
6103 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
6104 - "Simply provide helpful answers based on the reference information without citing sources.";
6105 - }
3566 + $content .= "\n## Response Guidelines ##\n" .
3567 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3568 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
3569 + "If you don't have specific information or are uncertain about any details, it's always " .
3570 + "better to honestly say you don't know rather than making up or guessing at answers. " .
3571 + "When information is incomplete, let them know you are unsure.";
6106 3572 }
6107 -
6108 - //error_log("MXCHAT DEBUG: Vector Store search complete. Matches used: " . $matches_used);
6109 -
3573 +
6110 3574 return trim($content);
6111 3575 }
6112 3576
6113 -/**
6114 - * Check if the given model is an OpenAI chat model
6115 - *
6116 - * @param string $model The model ID
6117 - * @return bool True if it's an OpenAI model
6118 - */
6119 -private function is_openai_chat_model($model) {
6120 - $openai_prefixes = array('gpt-', 'o1-', 'o3-');
6121 - foreach ($openai_prefixes as $prefix) {
6122 - if (strpos($model, $prefix) === 0) {
6123 - return true;
6124 - }
6125 - }
6126 - return false;
6127 -}
6128 -
6129 -/**
6130 - * Get bot-specific Vector Store configuration
6131 - *
6132 - * @param string $bot_id The bot ID
6133 - * @return array Configuration array
6134 - */
6135 -private function get_bot_vectorstore_config($bot_id = 'default') {
6136 - $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
6137 -
6138 - // Default global settings
6139 - $default_config = array(
6140 - 'use_vectorstore' => ($vectorstore_options['mxchat_use_openai_vectorstore'] ?? '0') === '1',
6141 - 'vectorstore_ids' => $vectorstore_options['mxchat_vectorstore_ids'] ?? '',
6142 - 'max_results' => $vectorstore_options['mxchat_vectorstore_max_results'] ?? 5
6143 - );
6144 -
6145 - // Allow multi-bot plugin to override with bot-specific settings
6146 - $bot_config = apply_filters('mxchat_get_bot_vectorstore_config', $default_config, $bot_id);
6147 -
6148 - // Preserve max_results from global settings if not set in bot config
6149 - if (!isset($bot_config['max_results'])) {
6150 - $bot_config['max_results'] = $default_config['max_results'];
6151 - }
6152 -
6153 - return $bot_config;
6154 -}
6155 -
6156 3577 private function mxchat_find_relevant_products($user_embedding) {
6157 3578 //error_log('MXChat Vector Search: Starting product search...');
6158 3579
6159 3580 // Retrieve the add-on settings from the database
@@ -6174,75 +3595,73 @@
6174 3595 }
6175 3596 private function find_relevant_products_wordpress($user_embedding) {
6176 3597 global $wpdb;
6177 3598 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3599 + $cache_key = 'mxchat_system_prompt_embeddings';
3600 + $batch_size = 500;
6178 3601
6179 - if (!is_array($user_embedding)) {
6180 - return '';
6181 - }
3602 + // Original WordPress database search logic
3603 + // [Previous implementation remains the same]
3604 + $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3605 + if ($embeddings === false) {
3606 + $embeddings = [];
3607 + $offset = 0;
6182 3608
6183 - // Streaming top-K pass: scan rows in small batches, keep only the top 3
6184 - // results above the similarity threshold. Peak memory is bounded by
6185 - // $batch_size embedding rows plus a 3-element top list.
6186 - $batch_size = 250;
6187 - $similarity_threshold = 0.85;
6188 - $top_k = 3;
6189 - $top_results = [];
6190 - $offset = 0;
3609 + do {
3610 + $query = $wpdb->prepare(
3611 + "SELECT id, embedding_vector
3612 + FROM {$system_prompt_table}
3613 + LIMIT %d OFFSET %d",
3614 + $batch_size,
3615 + $offset
3616 + );
6191 3617
6192 - do {
6193 - $batch = $wpdb->get_results($wpdb->prepare(
6194 - "SELECT id, embedding_vector
6195 - FROM {$system_prompt_table}
6196 - LIMIT %d OFFSET %d",
6197 - $batch_size,
6198 - $offset
6199 - ));
3618 + $batch = $wpdb->get_results($query);
3619 + if (empty($batch)) {
3620 + break;
3621 + }
6200 3622
6201 - if (empty($batch)) {
6202 - break;
6203 - }
3623 + $embeddings = array_merge($embeddings, $batch);
3624 + $offset += $batch_size;
6204 3625
6205 - foreach ($batch as $row) {
6206 - $database_embedding = $row->embedding_vector
6207 - ? unserialize($row->embedding_vector, ['allowed_classes' => false])
6208 - : null;
3626 + unset($batch);
6209 3627
6210 - if (!is_array($database_embedding)) {
6211 - unset($database_embedding);
6212 - continue;
6213 - }
3628 + } while (true);
6214 3629
3630 + if (empty($embeddings)) {
3631 + return '';
3632 + }
3633 + wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3634 + }
3635 +
3636 + $relevant_results = [];
3637 + foreach ($embeddings as $embedding) {
3638 + $database_embedding = $embedding->embedding_vector
3639 + ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3640 + : null;
3641 + if (is_array($database_embedding) && is_array($user_embedding)) {
6215 3642 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
6216 - unset($database_embedding);
6217 -
6218 - if ($similarity < $similarity_threshold) {
6219 - continue;
6220 - }
6221 -
6222 - // Insert into bounded top-K (kept sorted descending)
6223 - if (count($top_results) < $top_k) {
6224 - $top_results[] = ['id' => $row->id, 'similarity' => $similarity];
6225 - usort($top_results, function ($a, $b) {
6226 - return $b['similarity'] <=> $a['similarity'];
6227 - });
6228 - } elseif ($similarity > $top_results[$top_k - 1]['similarity']) {
6229 - $top_results[$top_k - 1] = ['id' => $row->id, 'similarity' => $similarity];
6230 - usort($top_results, function ($a, $b) {
6231 - return $b['similarity'] <=> $a['similarity'];
6232 - });
6233 - }
3643 + $relevant_results[] = [
3644 + 'id' => $embedding->id,
3645 + 'similarity' => $similarity
3646 + ];
6234 3647 }
3648 + unset($database_embedding);
3649 + }
6235 3650
6236 - unset($batch);
6237 - $offset += $batch_size;
6238 - } while (true);
3651 + // Use fixed threshold for products
3652 + $similarity_threshold = 0.85;
6239 3653
6240 - if (empty($top_results)) {
6241 - return '';
6242 - }
3654 + $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
3655 + return $result['similarity'] >= $similarity_threshold;
3656 + });
3657 + usort($relevant_results, function ($a, $b) {
3658 + return $b['similarity'] <=> $a['similarity'];
3659 + });
6243 3660
3661 + $top_results = array_slice($relevant_results, 0, 5);
6244 3662 $content = '';
3663 +
6245 3664 foreach ($top_results as $result) {
6246 3665 $chunk_content = $this->fetch_content_with_product_links($result['id']);
6247 3666 $content .= $chunk_content . "\n\n";
6248 3667 }
@@ -6248,10 +3667,8 @@
6248 3667 }
6249 3668
6250 3669 return trim($content);
6251 3670 }
6252 -
6253 -
6254 3671 private function find_relevant_products_pinecone($user_embedding) {
6255 3672 //error_log('Starting Pinecone product search...');
6256 3673
6257 3674 $options = get_option('mxchat_pinecone_addon_options', array());
@@ -6326,10 +3743,8 @@
6326 3743 }
6327 3744
6328 3745 return trim($content);
6329 3746 }
6330 -
6331 -
6332 3747 private function fetch_content_with_product_links($most_relevant_id) {
6333 3748 global $wpdb;
6334 3749 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
6335 3750
@@ -6349,85 +3764,13 @@
6349 3764 return null;
6350 3765 }
6351 3766
6352 3767 /**
6353 - * Get system instructions for a specific bot or default
6354 - * Checks for multi-bot add-on and uses bot-specific instructions if available
6355 - * Automatically strips URLs if citation links are disabled
6356 - * Replaces {visitor_name} placeholder with actual visitor name if available
6357 - *
6358 - * @param string $bot_id The bot ID to get instructions for
6359 - * @param string $session_id Optional session ID to lookup visitor name
3768 + * Modified streaming functions to include testing data
6360 3769 */
6361 -private function get_system_instructions($bot_id = 'default', $session_id = '') {
6362 - $instructions = '';
6363 3770
6364 - // Check if multi-bot add-on is active
6365 - if (class_exists('MxChat_Multi_Bot_Core_Manager') && $bot_id !== 'default') {
6366 - // Get bot-specific options from multi-bot add-on
6367 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
6368 -
6369 - // If bot has custom system instructions, use those
6370 - if (!empty($bot_options['system_prompt_instructions'])) {
6371 - $instructions = $bot_options['system_prompt_instructions'];
6372 - }
6373 - }
6374 -
6375 - // Fall back to default system instructions
6376 - if (empty($instructions)) {
6377 - $instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
6378 - }
6379 -
6380 - // Check if citation links are disabled - if so, strip URLs from instructions
6381 - $fresh_options = get_option('mxchat_options', []);
6382 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
6383 -
6384 - if (!$citation_links_enabled && !empty($instructions)) {
6385 - $instructions = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $instructions);
6386 - $instructions = preg_replace('/\s+/', ' ', trim($instructions)); // Clean up extra spaces
6387 - }
6388 -
6389 - // Replace {visitor_name} placeholder with actual visitor name if available
6390 - if (!empty($instructions) && !empty($session_id) && stripos($instructions, '{visitor_name}') !== false) {
6391 - $name_option_key = "mxchat_name_{$session_id}";
6392 - $visitor_name = get_option($name_option_key, '');
6393 -
6394 - if (!empty($visitor_name)) {
6395 - $instructions = str_ireplace('{visitor_name}', sanitize_text_field($visitor_name), $instructions);
6396 - } else {
6397 - // Remove placeholder if no name is available
6398 - $instructions = str_ireplace('{visitor_name}', '', $instructions);
6399 - $instructions = preg_replace('/\s{2,}/', ' ', trim($instructions)); // Clean up extra spaces
6400 - }
6401 - }
6402 -
6403 - // Allow developers to filter system instructions and process shortcodes
6404 - $instructions = apply_filters('mxchat_system_instructions', $instructions, $bot_id, $session_id);
6405 - $instructions = do_shortcode($instructions);
6406 -
6407 - return $instructions;
6408 -}
6409 -/**
6410 - * Get the current bot ID from session or request context
6411 - */
6412 -private function get_current_bot_id($session_id = '') {
6413 - // First, check if bot_id is passed in the current request
6414 - if (isset($_POST['bot_id']) && !empty($_POST['bot_id'])) {
6415 - return sanitize_key($_POST['bot_id']);
6416 - }
6417 -
6418 - // If not in POST, try to get it from session data
6419 - if (!empty($session_id)) {
6420 - $bot_id = get_option("mxchat_session_bot_{$session_id}", '');
6421 - if (!empty($bot_id)) {
6422 - return $bot_id;
6423 - }
6424 - }
6425 -
6426 - // Fall back to default
6427 - return 'default';
6428 -}
6429 -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') {
3771 +// 1. Update the main handler to pass testing data to streaming functions
3772 +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) {
6430 3773 try {
6431 3774 if (!$relevant_content) {
6432 3775 $error_response = [
6433 3776 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
@@ -6433,74 +3776,25 @@
6433 3776 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
6434 3777 'error_code' => 'no_relevant_content'
6435 3778 ];
6436 3779
3780 + // Add testing data to error response if available
6437 3781 if ($testing_data !== null) {
6438 3782 $error_response['testing_data'] = $testing_data;
3783 + //error_log("MxChat Testing: Added testing data to no_relevant_content error");
6439 3784 }
6440 3785
6441 3786 return $error_response;
6442 3787 }
6443 3788
3789 + // Ensure conversation_history is an array
6444 3790 if (!is_array($conversation_history)) {
6445 3791 $conversation_history = array();
6446 3792 }
6447 3793
6448 - // Check if this is an OpenRouter model
6449 - if ($selected_model === 'openrouter') {
6450 - // Get the actual OpenRouter model from options
6451 - $openrouter_selected_model = $this->options['openrouter_selected_model'] ?? '';
6452 -
6453 - if (empty($openrouter_selected_model)) {
6454 - $error_response = [
6455 - 'error' => esc_html__('No OpenRouter model selected. Please select a model in settings.', 'mxchat'),
6456 - 'error_code' => 'no_openrouter_model_selected'
6457 - ];
6458 - if ($testing_data !== null) {
6459 - $error_response['testing_data'] = $testing_data;
6460 - }
6461 - return $error_response;
6462 - }
6463 -
6464 - if (empty($openrouter_api_key)) {
6465 - $error_response = [
6466 - 'error' => esc_html__('OpenRouter API key is not configured', 'mxchat'),
6467 - 'error_code' => 'missing_openrouter_api_key'
6468 - ];
6469 - if ($testing_data !== null) {
6470 - $error_response['testing_data'] = $testing_data;
6471 - }
6472 - return $error_response;
6473 - }
6474 -
6475 - if ($streaming) {
6476 - return $this->mxchat_generate_response_openrouter_stream(
6477 - $openrouter_selected_model,
6478 - $openrouter_api_key,
6479 - $conversation_history,
6480 - $relevant_content,
6481 - $session_id,
6482 - $testing_data
6483 - );
6484 - } else {
6485 - $response = $this->mxchat_generate_response_openrouter(
6486 - $openrouter_selected_model,
6487 - $openrouter_api_key,
6488 - $conversation_history,
6489 - $relevant_content
6490 - );
6491 - }
6492 -
6493 - if (is_array($response) && isset($response['error'])) {
6494 - if ($testing_data !== null) {
6495 - $response['testing_data'] = $testing_data;
6496 - }
6497 - return $response;
6498 - }
6499 -
6500 - return $response;
6501 - }
6502 -
3794 + // Get selected model with default fallback
3795 + $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
3796 +
6503 3797 // Extract model prefix to determine the provider
6504 3798 $model_parts = explode('-', $selected_model);
6505 3799 $provider = strtolower($model_parts[0]);
6506 3800
@@ -6542,9 +3836,9 @@
6542 3836 $claude_api_key,
6543 3837 $conversation_history,
6544 3838 $relevant_content,
6545 3839 $session_id,
6546 - $testing_data
3840 + $testing_data // Pass testing data
6547 3841 );
6548 3842 } else {
6549 3843 $response = $this->mxchat_generate_response_claude(
6550 3844 $selected_model,
@@ -6572,9 +3866,9 @@
6572 3866 $xai_api_key,
6573 3867 $conversation_history,
6574 3868 $relevant_content,
6575 3869 $session_id,
6576 - $testing_data
3870 + $testing_data // Pass testing data
6577 3871 );
6578 3872 } else {
6579 3873 $response = $this->mxchat_generate_response_xai(
6580 3874 $selected_model,
@@ -6595,25 +3889,14 @@
6595 3889 $error_response['testing_data'] = $testing_data;
6596 3890 }
6597 3891 return $error_response;
6598 3892 }
6599 - if ($streaming) {
6600 - return $this->mxchat_generate_response_deepseek_stream(
6601 - $selected_model,
6602 - $deepseek_api_key,
6603 - $conversation_history,
6604 - $relevant_content,
6605 - $session_id,
6606 - $testing_data
6607 - );
6608 - } else {
6609 - $response = $this->mxchat_generate_response_deepseek(
6610 - $selected_model,
6611 - $deepseek_api_key,
6612 - $conversation_history,
6613 - $relevant_content
6614 - );
6615 - }
3893 + $response = $this->mxchat_generate_response_deepseek(
3894 + $selected_model,
3895 + $deepseek_api_key,
3896 + $conversation_history,
3897 + $relevant_content
3898 + );
6616 3899 break;
6617 3900
6618 3901 case 'gpt':
6619 3902 case 'o1':
@@ -6626,27 +3909,9 @@
6626 3909 $error_response['testing_data'] = $testing_data;
6627 3910 }
6628 3911 return $error_response;
6629 3912 }
6630 -
6631 - // Check if web search is enabled for this OpenAI model
6632 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
6633 - // Models that don't support web search
6634 - $unsupported_web_search_models = array('gpt-4.1-nano');
6635 - $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
6636 -
6637 - if ($web_search_enabled && $model_supports_web_search) {
6638 - // Use Responses API (required for some models, or when web search is enabled)
6639 - return $this->mxchat_generate_response_openai_web_search(
6640 - $selected_model,
6641 - $api_key,
6642 - $conversation_history,
6643 - $relevant_content,
6644 - $session_id,
6645 - $testing_data,
6646 - $streaming
6647 - );
6648 - } elseif ($streaming) {
3913 + if ($streaming) {
6649 3914 return $this->mxchat_generate_response_openai_stream(
6650 3915 $selected_model,
6651 3916 $api_key,
6652 3917 $conversation_history,
@@ -6651,9 +3916,9 @@
6651 3916 $api_key,
6652 3917 $conversation_history,
6653 3918 $relevant_content,
6654 3919 $session_id,
6655 - $testing_data
3920 + $testing_data // Pass testing data
6656 3921 );
6657 3922 } else {
6658 3923 $response = $this->mxchat_generate_response_openai(
6659 3924 $selected_model,
@@ -6664,8 +3929,9 @@
6664 3929 }
6665 3930 break;
6666 3931
6667 3932 default:
3933 + // Default to OpenAI for custom models or unrecognized prefixes
6668 3934 if (empty($api_key)) {
6669 3935 $error_response = [
6670 3936 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
6671 3937 'error_code' => 'missing_openai_api_key'
@@ -6674,25 +3940,9 @@
6674 3940 $error_response['testing_data'] = $testing_data;
6675 3941 }
6676 3942 return $error_response;
6677 3943 }
6678 -
6679 - // Check if web search is enabled (default case also handles OpenAI models)
6680 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
6681 - $unsupported_web_search_models = array('gpt-4.1-nano');
6682 - $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
6683 -
6684 - if ($web_search_enabled && $model_supports_web_search) {
6685 - return $this->mxchat_generate_response_openai_web_search(
6686 - $selected_model,
6687 - $api_key,
6688 - $conversation_history,
6689 - $relevant_content,
6690 - $session_id,
6691 - $testing_data,
6692 - $streaming
6693 - );
6694 - } elseif ($streaming) {
3944 + if ($streaming) {
6695 3945 return $this->mxchat_generate_response_openai_stream(
6696 3946 $selected_model,
6697 3947 $api_key,
6698 3948 $conversation_history,
@@ -6697,9 +3947,9 @@
6697 3947 $api_key,
6698 3948 $conversation_history,
6699 3949 $relevant_content,
6700 3950 $session_id,
6701 - $testing_data
3951 + $testing_data // Pass testing data
6702 3952 );
6703 3953 } else {
6704 3954 $response = $this->mxchat_generate_response_openai(
6705 3955 $selected_model,
@@ -6710,18 +3960,24 @@
6710 3960 }
6711 3961 break;
6712 3962 }
6713 3963
3964 + // Check if the response is an error array from the provider-specific function
6714 3965 if (is_array($response) && isset($response['error'])) {
3966 + // Add testing data to error response if available
6715 3967 if ($testing_data !== null) {
6716 3968 $response['testing_data'] = $testing_data;
3969 + //error_log("MxChat Testing: Added testing data to provider error response");
6717 3970 }
6718 - return $response;
3971 + return $response; // Pass through the error with testing data
6719 3972 }
6720 3973
3974 + // For successful non-streaming responses, we don't add testing data here
3975 + // because it will be added in the main handler
6721 3976 return $response;
6722 3977
6723 3978 } catch (Exception $e) {
3979 + //error_log('MXChat Error: ' . $e->getMessage());
6724 3980 $error_response = [
6725 3981 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
6726 3982 'error_code' => 'system_exception',
6727 3983 'exception_details' => $e->getMessage()
@@ -6726,871 +3982,24 @@
6726 3982 'error_code' => 'system_exception',
6727 3983 'exception_details' => $e->getMessage()
6728 3984 ];
6729 3985
3986 + // Add testing data to exception response if available
6730 3987 if ($testing_data !== null) {
6731 3988 $error_response['testing_data'] = $testing_data;
3989 + //error_log("MxChat Testing: Added testing data to exception response");
6732 3990 }
6733 3991
6734 3992 return $error_response;
6735 3993 }
6736 3994 }
6737 -private function mxchat_generate_response_openrouter_stream($selected_model, $openrouter_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
6738 - try {
6739 - $bot_id = $this->get_current_bot_id($session_id);
6740 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
6741 -
6742 - if (!is_array($conversation_history)) {
6743 - $conversation_history = array();
6744 - }
6745 3995
6746 - $formatted_conversation = array();
6747 -
6748 - $formatted_conversation[] = array(
6749 - 'role' => 'system',
6750 - 'content' => $system_prompt_instructions . " " . $relevant_content
6751 - );
6752 -
6753 - foreach ($conversation_history as $message) {
6754 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
6755 - $role = $message['role'];
6756 - if ($role === 'bot' || $role === 'agent') {
6757 - $role = 'assistant';
6758 - }
6759 - if (!in_array($role, ['system', 'assistant', 'user'])) {
6760 - $role = 'user';
6761 - }
6762 - $formatted_conversation[] = array(
6763 - 'role' => $role,
6764 - 'content' => $message['content']
6765 - );
6766 - }
6767 - }
6768 -
6769 - if (headers_sent() || !function_exists('curl_init')) {
6770 - $regular_response = $this->mxchat_generate_response_openrouter(
6771 - $selected_model,
6772 - $openrouter_api_key,
6773 - $conversation_history,
6774 - $relevant_content
6775 - );
6776 -
6777 - // Save bot response to transcript
6778 - if (!empty($regular_response) && !empty($session_id)) {
6779 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
6780 - }
6781 -
6782 - $response_data = [
6783 - 'text' => $regular_response,
6784 - 'html' => '',
6785 - 'session_id' => $session_id
6786 - ];
6787 -
6788 - if ($testing_data !== null) {
6789 - $response_data['testing_data'] = $testing_data;
6790 - }
6791 -
6792 - header('Content-Type: application/json');
6793 - echo json_encode($response_data);
6794 - return true;
6795 - }
6796 -
6797 - $body = json_encode([
6798 - 'model' => $selected_model,
6799 - 'messages' => $formatted_conversation,
6800 - 'temperature' => 1,
6801 - 'stream' => true
6802 - ]);
6803 -
6804 - // Setup streaming headers now that we know we're actually streaming
6805 - $this->setup_streaming_headers();
6806 -
6807 - $ch = curl_init();
6808 - curl_setopt($ch, CURLOPT_URL, 'https://openrouter.ai/api/v1/chat/completions');
6809 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
6810 - curl_setopt($ch, CURLOPT_POST, true);
6811 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
6812 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
6813 - 'Content-Type: application/json',
6814 - 'Authorization: Bearer ' . $openrouter_api_key,
6815 - 'HTTP-Referer: ' . home_url(),
6816 - 'X-Title: ' . get_bloginfo('name')
6817 - ));
6818 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
6819 - curl_setopt($ch, CURLOPT_TIMEOUT, 60);
6820 -
6821 - $full_response = '';
6822 - $stream_started = false;
6823 - $buffer = '';
6824 -
6825 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
6826 - if (!$stream_started && $testing_data !== null) {
6827 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
6828 - flush();
6829 - $stream_started = true;
6830 - }
6831 -
6832 - $buffer .= $data;
6833 - $lines = explode("\n", $buffer);
6834 - $buffer = array_pop($lines);
6835 -
6836 - foreach ($lines as $line) {
6837 - if (trim($line) === '') {
6838 - continue;
6839 - }
6840 -
6841 - if (strpos($line, 'data: ') !== 0) {
6842 - continue;
6843 - }
6844 -
6845 - $json_str = substr($line, 6);
6846 -
6847 - if (trim($json_str) === '[DONE]') {
6848 - echo "data: [DONE]\n\n";
6849 - flush();
6850 - continue;
6851 - }
6852 -
6853 - $json = json_decode(trim($json_str), true);
6854 - if ($json && isset($json['choices'][0]['delta']['content'])) {
6855 - $content = $json['choices'][0]['delta']['content'];
6856 - $full_response .= $content;
6857 -
6858 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
6859 - flush();
6860 - }
6861 - }
6862 -
6863 - return strlen($data);
6864 - });
6865 -
6866 - $response = curl_exec($ch);
6867 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
6868 -
6869 - if (curl_errno($ch) || $http_code !== 200) {
6870 - curl_close($ch);
6871 -
6872 - $regular_response = $this->mxchat_generate_response_openrouter(
6873 - $selected_model,
6874 - $openrouter_api_key,
6875 - $conversation_history,
6876 - $relevant_content
6877 - );
6878 -
6879 - $response_data = [
6880 - 'text' => $regular_response,
6881 - 'html' => '',
6882 - 'session_id' => $session_id
6883 - ];
6884 -
6885 - if ($testing_data !== null) {
6886 - $response_data['testing_data'] = $testing_data;
6887 - }
6888 -
6889 - header('Content-Type: application/json');
6890 - echo json_encode($response_data);
6891 - return true;
6892 - }
6893 -
6894 - curl_close($ch);
6895 -
6896 - if (!empty($full_response) && !empty($session_id)) {
6897 - // Prepare RAG context for streaming response
6898 - $rag_context_for_storage = null;
6899 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
6900 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
6901 -
6902 - if ($has_rag_data || $has_action_data) {
6903 - $rag_context_for_storage = [];
6904 -
6905 - if ($has_rag_data) {
6906 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
6907 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
6908 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
6909 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
6910 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
6911 - }
6912 -
6913 - if ($has_action_data) {
6914 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
6915 - }
6916 - }
6917 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
6918 - }
6919 -
6920 - return true;
6921 -
6922 - } catch (Exception $e) {
6923 - $regular_response = $this->mxchat_generate_response_openrouter(
6924 - $selected_model,
6925 - $openrouter_api_key,
6926 - $conversation_history,
6927 - $relevant_content
6928 - );
6929 -
6930 - $response_data = [
6931 - 'text' => $regular_response,
6932 - 'html' => '',
6933 - 'session_id' => $session_id
6934 - ];
6935 -
6936 - if ($testing_data !== null) {
6937 - $response_data['testing_data'] = $testing_data;
6938 - }
6939 -
6940 - header('Content-Type: application/json');
6941 - echo json_encode($response_data);
6942 - return true;
6943 - }
6944 -}
6945 -private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
3996 +// 2. streaming function
3997 +private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
6946 3998 try {
6947 - $bot_id = $this->get_current_bot_id($session_id);
6948 -
6949 - // Get system prompt instructions using centralized function
6950 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
6951 -
6952 - // Ensure conversation_history is an array
6953 - if (!is_array($conversation_history)) {
6954 - $conversation_history = array();
6955 - }
3999 + // Get system prompt instructions from options
4000 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
6956 4001
6957 - // Format conversation history for OpenAI
6958 - $formatted_conversation = array();
6959 -
6960 - $formatted_conversation[] = array(
6961 - 'role' => 'system',
6962 - 'content' => $system_prompt_instructions . " " . $relevant_content
6963 - );
6964 -
6965 - foreach ($conversation_history as $message) {
6966 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
6967 - $role = $message['role'];
6968 - if ($role === 'bot' || $role === 'agent') {
6969 - $role = 'assistant';
6970 - }
6971 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
6972 - $role = 'user';
6973 - }
6974 - $formatted_conversation[] = array(
6975 - 'role' => $role,
6976 - 'content' => $message['content']
6977 - );
6978 - }
6979 - }
6980 -
6981 - // Check if we can actually stream
6982 - if (headers_sent() || !function_exists('curl_init')) {
6983 - // Fallback to regular response with testing data
6984 - $regular_response = $this->mxchat_generate_response_openai(
6985 - $selected_model,
6986 - $api_key,
6987 - $conversation_history,
6988 - $relevant_content
6989 - );
6990 -
6991 - // Save bot response to transcript
6992 - if (!empty($regular_response) && !empty($session_id)) {
6993 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
6994 - }
6995 -
6996 - $response_data = [
6997 - 'text' => $regular_response,
6998 - 'html' => '',
6999 - 'session_id' => $session_id
7000 - ];
7001 -
7002 - if ($testing_data !== null) {
7003 - $response_data['testing_data'] = $testing_data;
7004 - }
7005 -
7006 - header('Content-Type: application/json');
7007 - echo json_encode($response_data);
7008 - return true;
7009 - }
7010 -
7011 - // Check if this is a GPT-5 model (supports reasoning_effort parameter)
7012 - $is_gpt5_model = (
7013 - strpos($selected_model, 'gpt-5') === 0 ||
7014 - $selected_model === 'gpt-5.2' ||
7015 - $selected_model === 'gpt-5.1-2025-11-13' ||
7016 - $selected_model === 'gpt-5' ||
7017 - $selected_model === 'gpt-5-mini' ||
7018 - $selected_model === 'gpt-5-nano'
7019 - );
7020 -
7021 - // Build request body with optimal settings for fast streaming
7022 - $request_body = [
7023 - 'model' => $selected_model,
7024 - 'messages' => $formatted_conversation,
7025 - 'temperature' => 1,
7026 - 'stream' => true
7027 - ];
7028 -
7029 - // Add reasoning_effort only for GPT-5 models that support it
7030 - // These chat models don't support reasoning_effort parameter
7031 - $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');
7032 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
7033 - // GPT-5.1 uses 'low' instead of 'minimal'
7034 - if ($selected_model === 'gpt-5.1-2025-11-13') {
7035 - $request_body['reasoning_effort'] = 'low';
7036 - } elseif ($selected_model === 'gpt-5.4') {
7037 - $request_body['reasoning_effort'] = 'none';
7038 - } else {
7039 - $request_body['reasoning_effort'] = 'minimal';
7040 - }
7041 - }
7042 -
7043 - $body = json_encode($request_body);
7044 -
7045 - // Setup streaming headers now that we know we're actually streaming
7046 - $this->setup_streaming_headers();
7047 -
7048 - // Use cURL for streaming support
7049 - $ch = curl_init();
7050 - curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
7051 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
7052 - curl_setopt($ch, CURLOPT_POST, true);
7053 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
7054 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
7055 - 'Content-Type: application/json',
7056 - 'Authorization: Bearer ' . $api_key
7057 - ));
7058 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7059 - curl_setopt($ch, CURLOPT_TIMEOUT, 60);
7060 -
7061 - $full_response = ''; // Accumulate full response for saving
7062 - $stream_started = false;
7063 - $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
7064 -
7065 - // Buffer control for real-time streaming
7066 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
7067 - // Send testing data as the first event if available
7068 - if (!$stream_started && $testing_data !== null) {
7069 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
7070 - flush();
7071 - $stream_started = true;
7072 - }
7073 -
7074 - // CRITICAL FIX: Append new data to buffer
7075 - $buffer .= $data;
7076 -
7077 - // Process complete lines only
7078 - $lines = explode("\n", $buffer);
7079 -
7080 - // CRITICAL FIX: Keep the last incomplete line in the buffer
7081 - // The last element might be incomplete, so keep it in buffer
7082 - $buffer = array_pop($lines);
7083 -
7084 - foreach ($lines as $line) {
7085 - // Skip empty lines
7086 - if (trim($line) === '') {
7087 - continue;
7088 - }
7089 -
7090 - // Only process lines that start with "data: "
7091 - if (strpos($line, 'data: ') !== 0) {
7092 - continue;
7093 - }
7094 -
7095 - $json_str = substr($line, 6); // Remove 'data: ' prefix
7096 -
7097 - if (trim($json_str) === '[DONE]') {
7098 - echo "data: [DONE]\n\n";
7099 - flush();
7100 - continue;
7101 - }
7102 -
7103 - // Try to decode JSON
7104 - $json = json_decode(trim($json_str), true);
7105 - if ($json && isset($json['choices'][0]['delta']['content'])) {
7106 - $content = $json['choices'][0]['delta']['content'];
7107 - $full_response .= $content; // Accumulate the full response
7108 -
7109 - // Send as SSE format
7110 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
7111 - flush();
7112 - }
7113 - }
7114 -
7115 - return strlen($data);
7116 - });
7117 -
7118 - $response = curl_exec($ch);
7119 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7120 -
7121 - if (curl_errno($ch) || $http_code !== 200) {
7122 - $curl_error = curl_error($ch);
7123 - curl_close($ch);
7124 -
7125 - // Fallback to regular response
7126 - $regular_response = $this->mxchat_generate_response_openai(
7127 - $selected_model,
7128 - $api_key,
7129 - $conversation_history,
7130 - $relevant_content
7131 - );
7132 -
7133 - // FIXED: Check if regular response returned an error
7134 - if (is_array($regular_response) && isset($regular_response['error'])) {
7135 - // Send error in SSE format since we're in streaming mode
7136 - echo "data: " . json_encode([
7137 - 'error' => true,
7138 - 'error_message' => $regular_response['error'],
7139 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7140 - 'text' => $regular_response['error'],
7141 - 'message' => $regular_response['error']
7142 - ]) . "\n\n";
7143 - echo "data: [DONE]\n\n";
7144 - flush();
7145 - return true;
7146 - }
7147 -
7148 - $response_data = [
7149 - 'text' => $regular_response,
7150 - 'html' => '',
7151 - 'session_id' => $session_id
7152 - ];
7153 -
7154 - if ($testing_data !== null) {
7155 - $response_data['testing_data'] = $testing_data;
7156 - }
7157 -
7158 - header('Content-Type: application/json');
7159 - echo json_encode($response_data);
7160 - return true;
7161 - }
7162 -
7163 - curl_close($ch);
7164 -
7165 - // Save the complete response to maintain chat persistence
7166 - if (!empty($full_response) && !empty($session_id)) {
7167 - // Prepare RAG context for streaming response
7168 - $rag_context_for_storage = null;
7169 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7170 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7171 -
7172 - if ($has_rag_data || $has_action_data) {
7173 - $rag_context_for_storage = [];
7174 -
7175 - if ($has_rag_data) {
7176 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7177 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7178 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7179 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7180 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7181 - }
7182 -
7183 - if ($has_action_data) {
7184 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7185 - }
7186 - }
7187 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
7188 - }
7189 -
7190 - return true; // Indicate streaming completed successfully
7191 -
7192 - } catch (Exception $e) {
7193 - // Fallback to regular response
7194 - $regular_response = $this->mxchat_generate_response_openai(
7195 - $selected_model,
7196 - $api_key,
7197 - $conversation_history,
7198 - $relevant_content
7199 - );
7200 -
7201 - // FIXED: Check if regular response returned an error
7202 - if (is_array($regular_response) && isset($regular_response['error'])) {
7203 - // Send error in SSE format since we're in streaming mode
7204 - echo "data: " . json_encode([
7205 - 'error' => true,
7206 - 'error_message' => $regular_response['error'],
7207 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7208 - 'text' => $regular_response['error'],
7209 - 'message' => $regular_response['error']
7210 - ]) . "\n\n";
7211 - echo "data: [DONE]\n\n";
7212 - flush();
7213 - return true;
7214 - }
7215 -
7216 - $response_data = [
7217 - 'text' => $regular_response,
7218 - 'html' => '',
7219 - 'session_id' => $session_id
7220 - ];
7221 -
7222 - if ($testing_data !== null) {
7223 - $response_data['testing_data'] = $testing_data;
7224 - }
7225 -
7226 - header('Content-Type: application/json');
7227 - echo json_encode($response_data);
7228 - return true;
7229 - }
7230 -}
7231 -
7232 -/**
7233 - * Generate response using OpenAI Responses API with web search tool
7234 - * This uses the newer Responses API which supports web search functionality
7235 - */
7236 -private function mxchat_generate_response_openai_web_search($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null, $streaming = false) {
7237 - try {
7238 - $bot_id = $this->get_current_bot_id($session_id);
7239 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
7240 -
7241 - if (!is_array($conversation_history)) {
7242 - $conversation_history = array();
7243 - }
7244 -
7245 - // Build the input for Responses API
7246 - // The Responses API uses a different format - we need to construct the input properly
7247 - $input_parts = [];
7248 -
7249 - // Add system instructions as context
7250 - $system_context = $system_prompt_instructions . "\n\n" . $relevant_content;
7251 -
7252 - // Build conversation as input items for Responses API
7253 - foreach ($conversation_history as $message) {
7254 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
7255 - $role = $message['role'];
7256 - if ($role === 'bot' || $role === 'agent') {
7257 - $role = 'assistant';
7258 - }
7259 - if (!in_array($role, ['assistant', 'user'])) {
7260 - $role = 'user';
7261 - }
7262 - $input_parts[] = [
7263 - 'type' => 'message',
7264 - 'role' => $role,
7265 - 'content' => $message['content']
7266 - ];
7267 - }
7268 - }
7269 -
7270 - // Build request body for Responses API
7271 - $request_body = [
7272 - 'model' => $selected_model,
7273 - 'input' => $input_parts,
7274 - 'instructions' => $system_context,
7275 - 'stream' => $streaming
7276 - ];
7277 -
7278 - // Only add web search tool if web search is enabled in settings
7279 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
7280 - if ($web_search_enabled) {
7281 - $request_body['tools'] = [
7282 - ['type' => 'web_search']
7283 - ];
7284 - }
7285 -
7286 - // Add reasoning effort for supported models
7287 - $is_gpt5_model = strpos($selected_model, 'gpt-5') === 0;
7288 - $no_reasoning_web = array('gpt-5.2', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
7289 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_web, true)) {
7290 - if ($selected_model === 'gpt-5.1-2025-11-13') {
7291 - $request_body['reasoning'] = ['effort' => 'low'];
7292 - } elseif ($selected_model === 'gpt-5.4') {
7293 - $request_body['reasoning'] = ['effort' => 'low'];
7294 - }
7295 - }
7296 -
7297 - //error_log("MXCHAT WEB SEARCH: Request body: " . json_encode($request_body));
7298 -
7299 - if ($streaming) {
7300 - return $this->mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data);
7301 - } else {
7302 - return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7303 - }
7304 -
7305 - } catch (Exception $e) {
7306 - //error_log("MXCHAT WEB SEARCH ERROR: " . $e->getMessage());
7307 - return [
7308 - 'error' => sprintf(esc_html__('Web search error: %s', 'mxchat'), esc_html($e->getMessage())),
7309 - 'error_code' => 'web_search_exception'
7310 - ];
7311 - }
7312 -}
7313 -
7314 -/**
7315 - * Handle non-streaming web search response
7316 - */
7317 -private function mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data) {
7318 - $request_body['stream'] = false;
7319 -
7320 - $response = wp_remote_post('https://api.openai.com/v1/responses', array(
7321 - 'headers' => array(
7322 - 'Authorization' => 'Bearer ' . $api_key,
7323 - 'Content-Type' => 'application/json'
7324 - ),
7325 - 'body' => json_encode($request_body),
7326 - 'timeout' => 90
7327 - ));
7328 -
7329 - if (is_wp_error($response)) {
7330 - //error_log("MXCHAT WEB SEARCH ERROR: WP Error: " . $response->get_error_message());
7331 - return [
7332 - 'error' => esc_html__('Failed to connect to OpenAI web search API', 'mxchat'),
7333 - 'error_code' => 'web_search_connection_error'
7334 - ];
7335 - }
7336 -
7337 - $response_code = wp_remote_retrieve_response_code($response);
7338 - $response_body = wp_remote_retrieve_body($response);
7339 -
7340 - //error_log("MXCHAT WEB SEARCH: Response code: " . $response_code);
7341 - //error_log("MXCHAT WEB SEARCH: Response body (first 2000): " . substr($response_body, 0, 2000));
7342 -
7343 - if ($response_code !== 200) {
7344 - $error_data = json_decode($response_body, true);
7345 - $error_message = $error_data['error']['message'] ?? 'Unknown API error';
7346 - return [
7347 - 'error' => sprintf(esc_html__('OpenAI API error: %s', 'mxchat'), esc_html($error_message)),
7348 - 'error_code' => 'web_search_api_error'
7349 - ];
7350 - }
7351 -
7352 - $result = json_decode($response_body, true);
7353 -
7354 - if (json_last_error() !== JSON_ERROR_NONE) {
7355 - return [
7356 - 'error' => esc_html__('Invalid response from OpenAI', 'mxchat'),
7357 - 'error_code' => 'web_search_json_error'
7358 - ];
7359 - }
7360 -
7361 - // Extract the response text and citations from Responses API format
7362 - $output_text = '';
7363 - $citations = [];
7364 -
7365 - if (isset($result['output'])) {
7366 - foreach ($result['output'] as $output_item) {
7367 - if ($output_item['type'] === 'message' && isset($output_item['content'])) {
7368 - foreach ($output_item['content'] as $content_item) {
7369 - if ($content_item['type'] === 'output_text') {
7370 - $output_text .= $content_item['text'];
7371 -
7372 - // Extract citations/annotations
7373 - if (isset($content_item['annotations'])) {
7374 - foreach ($content_item['annotations'] as $annotation) {
7375 - if ($annotation['type'] === 'url_citation') {
7376 - $citations[] = [
7377 - 'url' => $annotation['url'],
7378 - 'title' => $annotation['title'] ?? ''
7379 - ];
7380 - }
7381 - }
7382 - }
7383 - }
7384 - }
7385 - }
7386 - }
7387 - }
7388 -
7389 - // If we have citations, append them to the response
7390 - if (!empty($citations)) {
7391 - $output_text .= "\n\n**Sources:**\n";
7392 - $seen_urls = [];
7393 - foreach ($citations as $citation) {
7394 - if (!in_array($citation['url'], $seen_urls)) {
7395 - $seen_urls[] = $citation['url'];
7396 - $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
7397 - $output_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
7398 - }
7399 - }
7400 - }
7401 -
7402 - // Transcript save is handled by the main handler (mxchat_handle_chat_request)
7403 - // which includes rag_context for the "sources" link in transcripts.
7404 -
7405 - return $output_text;
7406 -}
7407 -
7408 -/**
7409 - * Handle streaming web search response using Responses API
7410 - */
7411 -private function mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data) {
7412 - $request_body['stream'] = true;
7413 -
7414 - // Check if we can stream
7415 - if (headers_sent() || !function_exists('curl_init')) {
7416 - // Fallback to non-streaming
7417 - return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7418 - }
7419 -
7420 - // Setup streaming headers
7421 - $this->setup_streaming_headers();
7422 -
7423 - $ch = curl_init();
7424 - curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/responses');
7425 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
7426 - curl_setopt($ch, CURLOPT_POST, true);
7427 - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($request_body));
7428 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
7429 - 'Content-Type: application/json',
7430 - 'Authorization: Bearer ' . $api_key
7431 - ));
7432 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7433 - curl_setopt($ch, CURLOPT_TIMEOUT, 120);
7434 -
7435 - $full_response = '';
7436 - $stream_started = false;
7437 - $buffer = '';
7438 - $citations = [];
7439 -
7440 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$citations, $testing_data) {
7441 - // Send testing data as first event if available
7442 - if (!$stream_started && $testing_data !== null) {
7443 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
7444 - flush();
7445 - $stream_started = true;
7446 - }
7447 -
7448 - $buffer .= $data;
7449 - $lines = explode("\n", $buffer);
7450 - $buffer = array_pop($lines);
7451 -
7452 - foreach ($lines as $line) {
7453 - if (trim($line) === '') continue;
7454 - if (strpos($line, 'data: ') !== 0) continue;
7455 -
7456 - $json_str = substr($line, 6);
7457 -
7458 - if (trim($json_str) === '[DONE]') {
7459 - // Append citations if we have any
7460 - if (!empty($citations)) {
7461 - $citation_text = "\n\n**Sources:**\n";
7462 - $seen_urls = [];
7463 - foreach ($citations as $citation) {
7464 - if (!in_array($citation['url'], $seen_urls)) {
7465 - $seen_urls[] = $citation['url'];
7466 - $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
7467 - $citation_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
7468 - }
7469 - }
7470 - echo "data: " . json_encode(['content' => $citation_text]) . "\n\n";
7471 - $full_response .= $citation_text;
7472 - flush();
7473 - }
7474 - echo "data: [DONE]\n\n";
7475 - flush();
7476 - continue;
7477 - }
7478 -
7479 - $json = json_decode(trim($json_str), true);
7480 - if (!$json) continue;
7481 -
7482 - // Handle Responses API streaming events
7483 - // The format is different from Chat Completions
7484 - if (isset($json['type'])) {
7485 - switch ($json['type']) {
7486 - case 'response.output_text.delta':
7487 - // Text content delta
7488 - if (isset($json['delta'])) {
7489 - $content = $json['delta'];
7490 - $full_response .= $content;
7491 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
7492 - flush();
7493 - }
7494 - break;
7495 -
7496 - case 'response.output_item.done':
7497 - // Check for citations in completed items
7498 - if (isset($json['item']['content'])) {
7499 - foreach ($json['item']['content'] as $content_item) {
7500 - if (isset($content_item['annotations'])) {
7501 - foreach ($content_item['annotations'] as $annotation) {
7502 - if ($annotation['type'] === 'url_citation') {
7503 - $citations[] = [
7504 - 'url' => $annotation['url'],
7505 - 'title' => $annotation['title'] ?? ''
7506 - ];
7507 - }
7508 - }
7509 - }
7510 - }
7511 - }
7512 - break;
7513 - }
7514 - }
7515 - }
7516 -
7517 - return strlen($data);
7518 - });
7519 -
7520 - $response = curl_exec($ch);
7521 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7522 -
7523 - if (curl_errno($ch) || $http_code !== 200) {
7524 - $curl_error = curl_error($ch);
7525 - curl_close($ch);
7526 -
7527 - //error_log("MXCHAT WEB SEARCH STREAM ERROR: HTTP $http_code, cURL error: $curl_error");
7528 -
7529 - // Fallback to non-streaming
7530 - $fallback_response = $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7531 -
7532 - if (is_array($fallback_response) && isset($fallback_response['error'])) {
7533 - echo "data: " . json_encode([
7534 - 'error' => true,
7535 - 'error_message' => $fallback_response['error'],
7536 - 'error_code' => $fallback_response['error_code'] ?? 'web_search_error'
7537 - ]) . "\n\n";
7538 - echo "data: [DONE]\n\n";
7539 - flush();
7540 - return true;
7541 - }
7542 -
7543 - $response_data = [
7544 - 'text' => $fallback_response,
7545 - 'html' => '',
7546 - 'session_id' => $session_id
7547 - ];
7548 - if ($testing_data !== null) {
7549 - $response_data['testing_data'] = $testing_data;
7550 - }
7551 - header('Content-Type: application/json');
7552 - echo json_encode($response_data);
7553 - return true;
7554 - }
7555 -
7556 - curl_close($ch);
7557 -
7558 - // Save the complete response with RAG context so the "sources" link
7559 - // appears in transcripts — mirrors the pattern used by Claude/OpenAI streaming.
7560 - if (!empty($full_response) && !empty($session_id)) {
7561 - $rag_context_for_storage = null;
7562 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7563 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7564 -
7565 - if ($has_rag_data || $has_action_data) {
7566 - $rag_context_for_storage = [];
7567 -
7568 - if ($has_rag_data) {
7569 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7570 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7571 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7572 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7573 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7574 - }
7575 -
7576 - if ($has_action_data) {
7577 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7578 - }
7579 - }
7580 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
7581 - }
7582 -
7583 - return true;
7584 -}
7585 -
7586 -private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
7587 - try {
7588 - // Get bot ID from session or request
7589 - $bot_id = $this->get_current_bot_id($session_id);
7590 -
7591 - // Get system prompt instructions using centralized function
7592 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
7593 4002 // Ensure conversation_history is an array
7594 4003 if (!is_array($conversation_history)) {
7595 4004 $conversation_history = array();
7596 4005 }
@@ -7642,13 +4051,8 @@
7642 4051 array_slice($conversation_history, 0, -1), // Remove the added content
7643 4052 $relevant_content
7644 4053 );
7645 4054
7646 - // Save bot response to transcript
7647 - if (!empty($regular_response) && !empty($session_id)) {
7648 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
7649 - }
7650 -
7651 4055 // Return as JSON with testing data
7652 4056 $response_data = [
7653 4057 'text' => $regular_response,
7654 4058 'html' => '',
@@ -7667,11 +4071,8 @@
7667 4071 echo json_encode($response_data);
7668 4072 return true; // Indicate we handled the response
7669 4073 }
7670 4074
7671 - // Setup streaming headers now that we know we're actually streaming
7672 - $this->setup_streaming_headers();
7673 -
7674 4075 // Use cURL for streaming support
7675 4076 $ch = curl_init();
7676 4077 curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
7677 4078 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
@@ -7686,12 +4087,11 @@
7686 4087 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
7687 4088
7688 4089 $full_response = ''; // Accumulate full response for saving
7689 4090 $stream_started = false;
7690 - $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
7691 4091
7692 4092 // Buffer control for real-time streaming
7693 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
4093 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
7694 4094 // Send testing data as the first event if available
7695 4095 if (!$stream_started && $testing_data !== null) {
7696 4096 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
7697 4097 flush();
@@ -7698,17 +4098,10 @@
7698 4098 $stream_started = true;
7699 4099 //error_log("MxChat Testing: Sent testing data in Claude stream");
7700 4100 }
7701 4101
7702 - // CRITICAL FIX: Append new data to buffer
7703 - $buffer .= $data;
7704 -
7705 - // Process complete lines only
7706 - $lines = explode("\n", $buffer);
7707 -
7708 - // CRITICAL FIX: Keep the last incomplete line in the buffer
7709 - // The last element might be incomplete, so keep it in buffer
7710 - $buffer = array_pop($lines);
4102 + // Process each chunk of data
4103 + $lines = explode("\n", $data);
7711 4104
7712 4105 foreach ($lines as $line) {
7713 4106 if (trim($line) === '') {
7714 4107 continue;
@@ -7722,9 +4115,9 @@
7722 4115
7723 4116 if (strpos($line, 'data: ') === 0) {
7724 4117 $json_str = substr($line, 6); // Remove 'data: ' prefix
7725 4118
7726 - $json = json_decode(trim($json_str), true);
4119 + $json = json_decode($json_str, true);
7727 4120 if (json_last_error() !== JSON_ERROR_NONE) {
7728 4121 continue;
7729 4122 }
7730 4123
@@ -7776,35 +4169,20 @@
7776 4169 $claude_api_key,
7777 4170 array_slice($conversation_history, 0, -1), // Remove the added content
7778 4171 $relevant_content
7779 4172 );
7780 -
7781 - // FIXED: Check if regular response returned an error
7782 - if (is_array($regular_response) && isset($regular_response['error'])) {
7783 - // Send error in SSE format since we're in streaming mode
7784 - echo "data: " . json_encode([
7785 - 'error' => true,
7786 - 'error_message' => $regular_response['error'],
7787 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7788 - 'text' => $regular_response['error'],
7789 - 'message' => $regular_response['error']
7790 - ]) . "\n\n";
7791 - echo "data: [DONE]\n\n";
7792 - flush();
7793 - return true;
7794 - }
7795 -
4173 +
7796 4174 $response_data = [
7797 4175 'text' => $regular_response,
7798 4176 'html' => '',
7799 4177 'session_id' => $session_id
7800 4178 ];
7801 -
4179 +
7802 4180 if ($testing_data !== null) {
7803 4181 $response_data['testing_data'] = $testing_data;
7804 4182 //error_log("MxChat Testing: Added testing data to Claude error fallback");
7805 4183 }
7806 -
4184 +
7807 4185 header('Content-Type: application/json');
7808 4186 echo json_encode($response_data);
7809 4187 return true;
7810 4188 }
@@ -7810,29 +4188,9 @@
7810 4188 }
7811 4189
7812 4190 // Save the complete response to maintain chat persistence
7813 4191 if (!empty($full_response) && !empty($session_id)) {
7814 - // Prepare RAG context for streaming response
7815 - $rag_context_for_storage = null;
7816 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7817 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7818 -
7819 - if ($has_rag_data || $has_action_data) {
7820 - $rag_context_for_storage = [];
7821 -
7822 - if ($has_rag_data) {
7823 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7824 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7825 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7826 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7827 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7828 - }
7829 -
7830 - if ($has_action_data) {
7831 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7832 - }
7833 - }
7834 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
4192 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
7835 4193 }
7836 4194
7837 4195 return true; // Indicate streaming completed successfully
7838 4196
@@ -7837,9 +4195,9 @@
7837 4195 return true; // Indicate streaming completed successfully
7838 4196
7839 4197 } catch (Exception $e) {
7840 4198 //error_log("MxChat Claude streaming exception: " . $e->getMessage());
7841 -
4199 +
7842 4200 // Fallback to regular response on exception
7843 4201 $regular_response = $this->mxchat_generate_response_claude(
7844 4202 $selected_model,
7845 4203 $claude_api_key,
@@ -7845,54 +4203,36 @@
7845 4203 $claude_api_key,
7846 4204 $conversation_history,
7847 4205 $relevant_content
7848 4206 );
7849 -
7850 - // FIXED: Check if regular response returned an error
7851 - if (is_array($regular_response) && isset($regular_response['error'])) {
7852 - // Send error in SSE format since we're in streaming mode
7853 - echo "data: " . json_encode([
7854 - 'error' => true,
7855 - 'error_message' => $regular_response['error'],
7856 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7857 - 'text' => $regular_response['error'],
7858 - 'message' => $regular_response['error']
7859 - ]) . "\n\n";
7860 - echo "data: [DONE]\n\n";
7861 - flush();
7862 - return true;
7863 - }
7864 -
4207 +
7865 4208 $response_data = [
7866 4209 'text' => $regular_response,
7867 4210 'html' => '',
7868 4211 'session_id' => $session_id
7869 4212 ];
7870 -
4213 +
7871 4214 if ($testing_data !== null) {
7872 4215 $response_data['testing_data'] = $testing_data;
7873 4216 //error_log("MxChat Testing: Added testing data to Claude exception fallback");
7874 4217 }
7875 -
4218 +
7876 4219 header('Content-Type: application/json');
7877 4220 echo json_encode($response_data);
7878 4221 return true;
7879 4222 }
7880 4223 }
7881 -private function mxchat_generate_response_xai_stream($selected_model, $xai_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4224 +private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
7882 4225 try {
7883 - // Get bot ID from session or request
7884 - $bot_id = $this->get_current_bot_id($session_id);
4226 + // Get system prompt instructions from options
4227 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
7885 4228
7886 - // Get system prompt instructions using centralized function
7887 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
7888 -
7889 4229 // Ensure conversation_history is an array
7890 4230 if (!is_array($conversation_history)) {
7891 4231 $conversation_history = array();
7892 4232 }
7893 4233
7894 - // Format conversation history for X.AI (same as OpenAI format)
4234 + // Format conversation history for OpenAI
7895 4235 $formatted_conversation = array();
7896 4236
7897 4237 $formatted_conversation[] = array(
7898 4238 'role' => 'system',
@@ -7917,21 +4257,16 @@
7917 4257
7918 4258 // Check if we can actually stream
7919 4259 if (headers_sent() || !function_exists('curl_init')) {
7920 4260 // Fallback to regular response with testing data
7921 - //error_log("MxChat: X.AI streaming not possible, falling back to regular response");
7922 - $regular_response = $this->mxchat_generate_response_xai(
4261 + //error_log("MxChat: OpenAI streaming not possible, falling back to regular response");
4262 + $regular_response = $this->mxchat_generate_response_openai(
7923 4263 $selected_model,
7924 - $xai_api_key,
4264 + $api_key,
7925 4265 $conversation_history,
7926 4266 $relevant_content
7927 4267 );
7928 4268
7929 - // Save bot response to transcript
7930 - if (!empty($regular_response) && !empty($session_id)) {
7931 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
7932 - }
7933 -
7934 4269 $response_data = [
7935 4270 'text' => $regular_response,
7936 4271 'html' => '',
7937 4272 'session_id' => $session_id
@@ -7938,9 +4273,9 @@
7938 4273 ];
7939 4274
7940 4275 if ($testing_data !== null) {
7941 4276 $response_data['testing_data'] = $testing_data;
7942 - //error_log("MxChat Testing: Added testing data to X.AI fallback response");
4277 + //error_log("MxChat Testing: Added testing data to OpenAI fallback response");
7943 4278 }
7944 4279
7945 4280 header('Content-Type: application/json');
7946 4281 echo json_encode($response_data);
@@ -7954,20 +4289,17 @@
7954 4289 'temperature' => 0.8,
7955 4290 'stream' => true
7956 4291 ]);
7957 4292
7958 - // Setup streaming headers now that we know we're actually streaming
7959 - $this->setup_streaming_headers();
7960 -
7961 4293 // Use cURL for streaming support
7962 4294 $ch = curl_init();
7963 - curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
4295 + curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
7964 4296 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
7965 4297 curl_setopt($ch, CURLOPT_POST, true);
7966 4298 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
7967 4299 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
7968 4300 'Content-Type: application/json',
7969 - 'Authorization: Bearer ' . $xai_api_key
4301 + 'Authorization: Bearer ' . $api_key
7970 4302 ));
7971 4303 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7972 4304 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
7973 4305
@@ -7972,52 +4304,37 @@
7972 4304 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
7973 4305
7974 4306 $full_response = ''; // Accumulate full response for saving
7975 4307 $stream_started = false;
7976 - $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
7977 4308
7978 4309 // Buffer control for real-time streaming
7979 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
4310 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
7980 4311 // Send testing data as the first event if available
7981 4312 if (!$stream_started && $testing_data !== null) {
7982 4313 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
7983 4314 flush();
7984 4315 $stream_started = true;
7985 - //error_log("MxChat Testing: Sent testing data in X.AI stream");
4316 + //error_log("MxChat Testing: Sent testing data in OpenAI stream");
7986 4317 }
7987 4318
7988 - // CRITICAL FIX: Append new data to buffer
7989 - $buffer .= $data;
4319 + // Process each chunk of data
4320 + $lines = explode("\n", $data);
7990 4321
7991 - // Process complete lines only
7992 - $lines = explode("\n", $buffer);
7993 -
7994 - // CRITICAL FIX: Keep the last incomplete line in the buffer
7995 - // The last element might be incomplete, so keep it in buffer
7996 - $buffer = array_pop($lines);
7997 -
7998 4322 foreach ($lines as $line) {
7999 - // Skip empty lines
8000 - if (trim($line) === '') {
4323 + if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
8001 4324 continue;
8002 4325 }
8003 4326
8004 - // Only process lines that start with "data: "
8005 - if (strpos($line, 'data: ') !== 0) {
8006 - continue;
8007 - }
8008 -
8009 4327 $json_str = substr($line, 6); // Remove 'data: ' prefix
8010 4328
8011 - if (trim($json_str) === '[DONE]') {
4329 + if ($json_str === '[DONE]') {
8012 4330 echo "data: [DONE]\n\n";
8013 4331 flush();
8014 4332 continue;
8015 4333 }
8016 4334
8017 - // Try to decode JSON
8018 - $json = json_decode(trim($json_str), true);
8019 - if ($json && isset($json['choices'][0]['delta']['content'])) {
4335 + $json = json_decode($json_str, true);
4336 + if (isset($json['choices'][0]['delta']['content'])) {
8020 4337 $content = $json['choices'][0]['delta']['content'];
8021 4338 $full_response .= $content; // Accumulate
8022 4339 // Send as SSE format
8023 4340 echo "data: " . json_encode(['content' => $content]) . "\n\n";
@@ -8034,12 +4351,12 @@
8034 4351 if (curl_errno($ch) || $http_code !== 200) {
8035 4352 curl_close($ch);
8036 4353
8037 4354 // Fallback to regular response
8038 - //error_log("MxChat: X.AI streaming failed, falling back");
8039 - $regular_response = $this->mxchat_generate_response_xai(
4355 + //error_log("MxChat: OpenAI streaming failed, falling back");
4356 + $regular_response = $this->mxchat_generate_response_openai(
8040 4357 $selected_model,
8041 - $xai_api_key,
4358 + $api_key,
8042 4359 $conversation_history,
8043 4360 $relevant_content
8044 4361 );
8045 4362
@@ -8050,9 +4367,9 @@
8050 4367 ];
8051 4368
8052 4369 if ($testing_data !== null) {
8053 4370 $response_data['testing_data'] = $testing_data;
8054 - //error_log("MxChat Testing: Added testing data to X.AI error fallback");
4371 + //error_log("MxChat Testing: Added testing data to OpenAI error fallback");
8055 4372 }
8056 4373
8057 4374 header('Content-Type: application/json');
8058 4375 echo json_encode($response_data);
@@ -8059,43 +4376,23 @@
8059 4376 return true;
8060 4377 }
8061 4378
8062 4379 curl_close($ch);
8063 -
4380 +
8064 4381 // Save the complete response to maintain chat persistence
8065 4382 if (!empty($full_response) && !empty($session_id)) {
8066 - // Prepare RAG context for streaming response
8067 - $rag_context_for_storage = null;
8068 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8069 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8070 -
8071 - if ($has_rag_data || $has_action_data) {
8072 - $rag_context_for_storage = [];
8073 -
8074 - if ($has_rag_data) {
8075 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8076 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8077 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8078 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8079 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8080 - }
8081 -
8082 - if ($has_action_data) {
8083 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8084 - }
8085 - }
8086 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
4383 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
8087 4384 }
8088 -
4385 +
8089 4386 return true; // Indicate streaming completed successfully
8090 -
4387 +
8091 4388 } catch (Exception $e) {
8092 - //error_log("MxChat X.AI streaming exception: " . $e->getMessage());
8093 -
4389 + //error_log("MxChat OpenAI streaming exception: " . $e->getMessage());
4390 +
8094 4391 // Fallback to regular response
8095 - $regular_response = $this->mxchat_generate_response_xai(
4392 + $regular_response = $this->mxchat_generate_response_openai(
8096 4393 $selected_model,
8097 - $xai_api_key,
4394 + $api_key,
8098 4395 $conversation_history,
8099 4396 $relevant_content
8100 4397 );
8101 4398
@@ -8106,9 +4403,9 @@
8106 4403 ];
8107 4404
8108 4405 if ($testing_data !== null) {
8109 4406 $response_data['testing_data'] = $testing_data;
8110 - //error_log("MxChat Testing: Added testing data to X.AI exception fallback");
4407 + //error_log("MxChat Testing: Added testing data to OpenAI exception fallback");
8111 4408 }
8112 4409
8113 4410 header('Content-Type: application/json');
8114 4411 echo json_encode($response_data);
@@ -8114,22 +4411,19 @@
8114 4411 echo json_encode($response_data);
8115 4412 return true;
8116 4413 }
8117 4414 }
8118 -private function mxchat_generate_response_deepseek_stream($selected_model, $deepseek_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4415 +private function mxchat_generate_response_xai_stream($selected_model, $xai_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
8119 4416 try {
8120 - // Get bot ID from session or request
8121 - $bot_id = $this->get_current_bot_id($session_id);
4417 + // Get system prompt instructions from options
4418 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
8122 4419
8123 - // Get system prompt instructions using centralized function
8124 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8125 -
8126 4420 // Ensure conversation_history is an array
8127 4421 if (!is_array($conversation_history)) {
8128 4422 $conversation_history = array();
8129 4423 }
8130 4424
8131 - // Format conversation history for DeepSeek
4425 + // Format conversation history for X.AI (same as OpenAI format)
8132 4426 $formatted_conversation = array();
8133 4427
8134 4428 $formatted_conversation[] = array(
8135 4429 'role' => 'system',
@@ -8154,21 +4448,16 @@
8154 4448
8155 4449 // Check if we can actually stream
8156 4450 if (headers_sent() || !function_exists('curl_init')) {
8157 4451 // Fallback to regular response with testing data
8158 - //error_log("MxChat: DeepSeek streaming not possible, falling back to regular response");
8159 - $regular_response = $this->mxchat_generate_response_deepseek(
4452 + //error_log("MxChat: X.AI streaming not possible, falling back to regular response");
4453 + $regular_response = $this->mxchat_generate_response_xai(
8160 4454 $selected_model,
8161 - $deepseek_api_key,
4455 + $xai_api_key,
8162 4456 $conversation_history,
8163 4457 $relevant_content
8164 4458 );
8165 4459
8166 - // Save bot response to transcript
8167 - if (!empty($regular_response) && !empty($session_id)) {
8168 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
8169 - }
8170 -
8171 4460 $response_data = [
8172 4461 'text' => $regular_response,
8173 4462 'html' => '',
8174 4463 'session_id' => $session_id
@@ -8175,9 +4464,9 @@
8175 4464 ];
8176 4465
8177 4466 if ($testing_data !== null) {
8178 4467 $response_data['testing_data'] = $testing_data;
8179 - //error_log("MxChat Testing: Added testing data to DeepSeek fallback response");
4468 + //error_log("MxChat Testing: Added testing data to X.AI fallback response");
8180 4469 }
8181 4470
8182 4471 header('Content-Type: application/json');
8183 4472 echo json_encode($response_data);
@@ -8191,20 +4480,17 @@
8191 4480 'temperature' => 0.8,
8192 4481 'stream' => true
8193 4482 ]);
8194 4483
8195 - // Setup streaming headers now that we know we're actually streaming
8196 - $this->setup_streaming_headers();
8197 -
8198 4484 // Use cURL for streaming support
8199 4485 $ch = curl_init();
8200 - curl_setopt($ch, CURLOPT_URL, 'https://api.deepseek.com/v1/chat/completions');
4486 + curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
8201 4487 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
8202 4488 curl_setopt($ch, CURLOPT_POST, true);
8203 4489 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
8204 4490 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
8205 4491 'Content-Type: application/json',
8206 - 'Authorization: Bearer ' . $deepseek_api_key
4492 + 'Authorization: Bearer ' . $xai_api_key
8207 4493 ));
8208 4494 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
8209 4495 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
8210 4496
@@ -8209,55 +4495,39 @@
8209 4495 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
8210 4496
8211 4497 $full_response = ''; // Accumulate full response for saving
8212 4498 $stream_started = false;
8213 - $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
8214 4499
8215 4500 // Buffer control for real-time streaming
8216 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
4501 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
8217 4502 // Send testing data as the first event if available
8218 4503 if (!$stream_started && $testing_data !== null) {
8219 4504 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
8220 4505 flush();
8221 4506 $stream_started = true;
8222 - //error_log("MxChat Testing: Sent testing data in DeepSeek stream");
4507 + //error_log("MxChat Testing: Sent testing data in X.AI stream");
8223 4508 }
8224 4509
8225 - // CRITICAL FIX: Append new data to buffer
8226 - $buffer .= $data;
4510 + // Process each chunk of data
4511 + $lines = explode("\n", $data);
8227 4512
8228 - // Process complete lines only
8229 - $lines = explode("\n", $buffer);
8230 -
8231 - // CRITICAL FIX: Keep the last incomplete line in the buffer
8232 - // The last element might be incomplete, so keep it in buffer
8233 - $buffer = array_pop($lines);
8234 -
8235 4513 foreach ($lines as $line) {
8236 - // Skip empty lines
8237 - if (trim($line) === '') {
4514 + if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
8238 4515 continue;
8239 4516 }
8240 4517
8241 - // Only process lines that start with "data: "
8242 - if (strpos($line, 'data: ') !== 0) {
8243 - continue;
8244 - }
8245 -
8246 4518 $json_str = substr($line, 6); // Remove 'data: ' prefix
8247 4519
8248 - if (trim($json_str) === '[DONE]') {
4520 + if ($json_str === '[DONE]') {
8249 4521 echo "data: [DONE]\n\n";
8250 4522 flush();
8251 4523 continue;
8252 4524 }
8253 4525
8254 - // Try to decode JSON
8255 - $json = json_decode(trim($json_str), true);
8256 - if ($json && isset($json['choices'][0]['delta']['content'])) {
4526 + $json = json_decode($json_str, true);
4527 + if (isset($json['choices'][0]['delta']['content'])) {
8257 4528 $content = $json['choices'][0]['delta']['content'];
8258 - $full_response .= $content; // Accumulate the full response
8259 -
4529 + $full_response .= $content; // Accumulate
8260 4530 // Send as SSE format
8261 4531 echo "data: " . json_encode(['content' => $content]) . "\n\n";
8262 4532 flush();
8263 4533 }
@@ -8269,32 +4539,19 @@
8269 4539 $response = curl_exec($ch);
8270 4540 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
8271 4541
8272 4542 if (curl_errno($ch) || $http_code !== 200) {
8273 - $curl_error = curl_error($ch);
8274 4543 curl_close($ch);
8275 4544
8276 - // Log the specific error for debugging
8277 - //error_log("MxChat: DeepSeek streaming failed - HTTP: $http_code, cURL: $curl_error");
8278 -
8279 4545 // Fallback to regular response
8280 - $regular_response = $this->mxchat_generate_response_deepseek(
4546 + //error_log("MxChat: X.AI streaming failed, falling back");
4547 + $regular_response = $this->mxchat_generate_response_xai(
8281 4548 $selected_model,
8282 - $deepseek_api_key,
4549 + $xai_api_key,
8283 4550 $conversation_history,
8284 4551 $relevant_content
8285 4552 );
8286 4553
8287 - // Handle error response from regular function
8288 - if (is_array($regular_response) && isset($regular_response['error'])) {
8289 - if ($testing_data !== null) {
8290 - $regular_response['testing_data'] = $testing_data;
8291 - }
8292 - header('Content-Type: application/json');
8293 - echo json_encode($regular_response);
8294 - return true;
8295 - }
8296 -
8297 4554 $response_data = [
8298 4555 'text' => $regular_response,
8299 4556 'html' => '',
8300 4557 'session_id' => $session_id
@@ -8301,9 +4558,9 @@
8301 4558 ];
8302 4559
8303 4560 if ($testing_data !== null) {
8304 4561 $response_data['testing_data'] = $testing_data;
8305 - //error_log("MxChat Testing: Added testing data to DeepSeek error fallback");
4562 + //error_log("MxChat Testing: Added testing data to X.AI error fallback");
8306 4563 }
8307 4564
8308 4565 header('Content-Type: application/json');
8309 4566 echo json_encode($response_data);
@@ -8310,57 +4567,27 @@
8310 4567 return true;
8311 4568 }
8312 4569
8313 4570 curl_close($ch);
8314 -
4571 +
8315 4572 // Save the complete response to maintain chat persistence
8316 4573 if (!empty($full_response) && !empty($session_id)) {
8317 - // Prepare RAG context for streaming response
8318 - $rag_context_for_storage = null;
8319 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8320 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8321 -
8322 - if ($has_rag_data || $has_action_data) {
8323 - $rag_context_for_storage = [];
8324 -
8325 - if ($has_rag_data) {
8326 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8327 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8328 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8329 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8330 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8331 - }
8332 -
8333 - if ($has_action_data) {
8334 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8335 - }
8336 - }
8337 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
4574 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
8338 4575 }
8339 -
4576 +
8340 4577 return true; // Indicate streaming completed successfully
8341 -
4578 +
8342 4579 } catch (Exception $e) {
8343 - //error_log("MxChat DeepSeek streaming exception: " . $e->getMessage());
8344 -
4580 + //error_log("MxChat X.AI streaming exception: " . $e->getMessage());
4581 +
8345 4582 // Fallback to regular response
8346 - $regular_response = $this->mxchat_generate_response_deepseek(
4583 + $regular_response = $this->mxchat_generate_response_xai(
8347 4584 $selected_model,
8348 - $deepseek_api_key,
4585 + $xai_api_key,
8349 4586 $conversation_history,
8350 4587 $relevant_content
8351 4588 );
8352 4589
8353 - // Handle error response from regular function
8354 - if (is_array($regular_response) && isset($regular_response['error'])) {
8355 - if ($testing_data !== null) {
8356 - $regular_response['testing_data'] = $testing_data;
8357 - }
8358 - header('Content-Type: application/json');
8359 - echo json_encode($regular_response);
8360 - return true;
8361 - }
8362 -
8363 4590 $response_data = [
8364 4591 'text' => $regular_response,
8365 4592 'html' => '',
8366 4593 'session_id' => $session_id
@@ -8367,9 +4594,9 @@
8367 4594 ];
8368 4595
8369 4596 if ($testing_data !== null) {
8370 4597 $response_data['testing_data'] = $testing_data;
8371 - //error_log("MxChat Testing: Added testing data to DeepSeek exception fallback");
4598 + //error_log("MxChat Testing: Added testing data to X.AI exception fallback");
8372 4599 }
8373 4600
8374 4601 header('Content-Type: application/json');
8375 4602 echo json_encode($response_data);
@@ -8377,119 +4604,127 @@
8377 4604 }
8378 4605 }
8379 4606
8380 4607
8381 -private function mxchat_generate_response_openrouter($selected_model, $openrouter_api_key, $conversation_history, $relevant_content) {
8382 - try {
8383 - if (!is_array($conversation_history)) {
8384 - $conversation_history = array();
8385 - }
4608 +public function test_streaming_request() {
4609 + $options = get_option('mxchat_options', []);
4610 + $model = $options['model'] ?? 'gpt-4o';
8386 4611
8387 - $bot_id = $this->get_current_bot_id('');
8388 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8389 -
8390 - $formatted_conversation = array();
4612 + // Detect provider from model prefix
4613 + $provider = strtolower(explode('-', $model)[0]);
8391 4614
8392 - $formatted_conversation[] = array(
8393 - 'role' => 'system',
8394 - 'content' => $system_prompt_instructions . " " . $relevant_content
8395 - );
4615 + $sample_prompt = 'Hello! Can you stream this response back to me?';
4616 + $messages = [['role' => 'user', 'content' => $sample_prompt]];
4617 + $headers = [];
4618 + $body = [];
4619 + $url = '';
4620 + $api_key = '';
8396 4621
8397 - foreach ($conversation_history as $message) {
8398 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
8399 - $role = $message['role'];
4622 + switch ($provider) {
4623 + case 'gpt':
4624 + case 'o1':
4625 + $api_key = $options['api_key'] ?? '';
4626 + if (empty($api_key)) return '❌ Missing API key for OpenAI';
4627 + $url = 'https://api.openai.com/v1/chat/completions';
4628 + $headers = [
4629 + 'Content-Type: application/json',
4630 + 'Authorization: Bearer ' . $api_key
4631 + ];
4632 + $body = [
4633 + 'model' => $model,
4634 + 'messages' => $messages,
4635 + 'stream' => true
4636 + ];
4637 + break;
8400 4638
8401 - if ($role === 'bot' || $role === 'agent') {
8402 - $role = 'assistant';
8403 - }
8404 - if (!in_array($role, ['system', 'assistant', 'user'])) {
8405 - $role = 'user';
8406 - }
4639 + case 'claude':
4640 + $api_key = $options['claude_api_key'] ?? '';
4641 + if (empty($api_key)) return '❌ Missing API key for Claude';
4642 + $url = 'https://api.anthropic.com/v1/messages';
4643 + $headers = [
4644 + 'Content-Type: application/json',
4645 + 'x-api-key: ' . $api_key,
4646 + 'anthropic-version: 2023-06-01'
4647 + ];
4648 + $body = [
4649 + 'model' => $model,
4650 + 'messages' => $messages,
4651 + 'max_tokens' => 100,
4652 + 'stream' => true
4653 + ];
4654 + break;
8407 4655
8408 - $formatted_conversation[] = array(
8409 - 'role' => $role,
8410 - 'content' => $message['content']
8411 - );
8412 - }
8413 - }
4656 + case 'grok':
4657 + $api_key = $options['xai_api_key'] ?? '';
4658 + if (empty($api_key)) return '❌ Missing API key for X.AI';
4659 + $url = 'https://api.x.ai/v1/chat/completions';
4660 + $headers = [
4661 + 'Content-Type: application/json',
4662 + 'Authorization: Bearer ' . $api_key
4663 + ];
4664 + $body = [
4665 + 'model' => $model,
4666 + 'messages' => $messages,
4667 + 'stream' => true
4668 + ];
4669 + break;
8414 4670
8415 - $body = json_encode([
8416 - 'model' => $selected_model,
8417 - 'messages' => $formatted_conversation,
8418 - 'temperature' => 1,
8419 - ]);
4671 + case 'deepseek':
4672 + $api_key = $options['deepseek_api_key'] ?? '';
4673 + if (empty($api_key)) return '❌ Missing API key for DeepSeek';
4674 + $url = 'https://api.deepseek.com/v1/chat/completions';
4675 + $headers = [
4676 + 'Content-Type: application/json',
4677 + 'Authorization: Bearer ' . $api_key
4678 + ];
4679 + $body = [
4680 + 'model' => $model,
4681 + 'messages' => $messages,
4682 + 'stream' => true
4683 + ];
4684 + break;
8420 4685
8421 - $args = [
8422 - 'body' => $body,
8423 - 'headers' => [
8424 - 'Content-Type' => 'application/json',
8425 - 'Authorization' => 'Bearer ' . $openrouter_api_key,
8426 - 'HTTP-Referer' => home_url(),
8427 - 'X-Title' => get_bloginfo('name'),
8428 - ],
8429 - 'timeout' => 60,
8430 - 'redirection' => 5,
8431 - 'blocking' => true,
8432 - 'httpversion' => '1.0',
8433 - 'sslverify' => true,
8434 - ];
4686 + case 'gemini':
4687 + $api_key = $options['gemini_api_key'] ?? '';
4688 + if (empty($api_key)) return '❌ Missing API key for Gemini';
4689 + $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':streamGenerateContent?key=' . $api_key;
4690 + $headers = ['Content-Type: application/json'];
4691 + $body = [
4692 + 'contents' => [['role' => 'user', 'parts' => [['text' => $sample_prompt]]]],
4693 + 'generationConfig' => ['temperature' => 0.7]
4694 + ];
4695 + break;
8435 4696
8436 - $response = wp_remote_post('https://openrouter.ai/api/v1/chat/completions', $args);
4697 + default:
4698 + return '❌ Unsupported provider: ' . $provider;
4699 + }
8437 4700
8438 - if (is_wp_error($response)) {
8439 - $error_message = $response->get_error_message();
8440 - return [
8441 - 'error' => esc_html__('Connection error when contacting OpenRouter: ', 'mxchat') . esc_html($error_message),
8442 - 'error_code' => 'openrouter_connection_error',
8443 - 'provider' => 'openrouter'
8444 - ];
8445 - }
4701 + // Do the actual streaming test
4702 + $ch = curl_init($url);
4703 + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
4704 + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
4705 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
4706 + curl_setopt($ch, CURLOPT_TIMEOUT, 15);
4707 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
8446 4708
8447 - $status_code = wp_remote_retrieve_response_code($response);
8448 - if ($status_code !== 200) {
8449 - $response_body = wp_remote_retrieve_body($response);
8450 - $decoded_response = json_decode($response_body, true);
8451 -
8452 - $error_message = isset($decoded_response['error']['message'])
8453 - ? $decoded_response['error']['message']
8454 - : 'HTTP Error ' . $status_code;
8455 -
8456 - return [
8457 - 'error' => esc_html__('OpenRouter API error: ', 'mxchat') . esc_html($error_message),
8458 - 'error_code' => 'openrouter_api_error',
8459 - 'provider' => 'openrouter',
8460 - 'status_code' => $status_code
8461 - ];
8462 - }
4709 + $response = curl_exec($ch);
4710 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4711 + $error = curl_error($ch);
4712 + curl_close($ch);
8463 4713
8464 - $response_body = wp_remote_retrieve_body($response);
8465 - $decoded_response = json_decode($response_body, true);
4714 + if ($error) return "❌ cURL error: $error";
4715 + if ($http_code !== 200) {
4716 + $error_message = json_decode($response, true)['error']['message'] ?? 'Unknown';
4717 + return "❌ HTTP $http_code: $error_message";
4718 + }
8466 4719
8467 - if (isset($decoded_response['choices'][0]['message']['content'])) {
8468 - return trim($decoded_response['choices'][0]['message']['content']);
8469 - } else {
8470 - return [
8471 - 'error' => esc_html__('Unexpected response format from OpenRouter.', 'mxchat'),
8472 - 'error_code' => 'openrouter_response_format_error',
8473 - 'provider' => 'openrouter'
8474 - ];
8475 - }
8476 - } catch (Exception $e) {
8477 - return [
8478 - 'error' => esc_html__('System error when processing OpenRouter request: ', 'mxchat') . esc_html($e->getMessage()),
8479 - 'error_code' => 'openrouter_exception',
8480 - 'provider' => 'openrouter'
8481 - ];
8482 - }
4720 + return true;
8483 4721 }
4722 +
8484 4723 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
8485 -
8486 - // Get bot ID from session or request
8487 - $bot_id = $this->get_current_bot_id($session_id);
8488 -
8489 - // Get system prompt instructions using centralized function
8490 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8491 -
4724 + // Get system prompt instructions from options
4725 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4726 +
8492 4727 // Clean and validate conversation history
8493 4728 foreach ($conversation_history as &$message) {
8494 4729 // Convert bot and agent roles to assistant
8495 4730 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
@@ -8591,14 +4826,11 @@
8591 4826 if (!is_array($conversation_history)) {
8592 4827 $conversation_history = array();
8593 4828 }
8594 4829
8595 - // Get bot ID from session or request
8596 - $bot_id = $this->get_current_bot_id('');
8597 -
8598 - // Get system prompt instructions using centralized function
8599 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8600 -
4830 + // Get system prompt instructions from options
4831 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4832 +
8601 4833 // Create a new array for the formatted conversation
8602 4834 $formatted_conversation = array();
8603 4835
8604 4836 // Add system message first
@@ -8626,42 +4858,15 @@
8626 4858 );
8627 4859 }
8628 4860 }
8629 4861
8630 - // Check if this is a GPT-5 model (supports reasoning_effort parameter)
8631 - $is_gpt5_model = (
8632 - strpos($selected_model, 'gpt-5') === 0 ||
8633 - $selected_model === 'gpt-5.2' ||
8634 - $selected_model === 'gpt-5.1-2025-11-13' ||
8635 - $selected_model === 'gpt-5' ||
8636 - $selected_model === 'gpt-5-mini' ||
8637 - $selected_model === 'gpt-5-nano'
8638 - );
8639 -
8640 - // Build request body with optimal settings for fast responses
8641 - $request_body = [
4862 + $body = json_encode([
8642 4863 'model' => $selected_model,
8643 4864 'messages' => $formatted_conversation,
8644 - 'temperature' => 1,
4865 + 'temperature' => 0.8,
8645 4866 'stream' => false
8646 - ];
4867 + ]);
8647 4868
8648 - // Add reasoning_effort only for GPT-5 models that support it
8649 - // These chat models don't support reasoning_effort parameter
8650 - $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');
8651 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
8652 - // GPT-5.1 uses 'low' instead of 'minimal'
8653 - if ($selected_model === 'gpt-5.1-2025-11-13') {
8654 - $request_body['reasoning_effort'] = 'low';
8655 - } elseif ($selected_model === 'gpt-5.4') {
8656 - $request_body['reasoning_effort'] = 'none';
8657 - } else {
8658 - $request_body['reasoning_effort'] = 'minimal';
8659 - }
8660 - }
8661 -
8662 - $body = json_encode($request_body);
8663 -
8664 4869 $args = [
8665 4870 'body' => $body,
8666 4871 'headers' => [
8667 4872 'Content-Type' => 'application/json',
@@ -8677,8 +4882,9 @@
8677 4882 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
8678 4883
8679 4884 if (is_wp_error($response)) {
8680 4885 $error_message = $response->get_error_message();
4886 + //error_log('OpenAI API Error: ' . $error_message);
8681 4887 return [
8682 4888 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
8683 4889 'error_code' => 'openai_connection_error',
8684 4890 'provider' => 'openai'
@@ -8697,8 +4903,10 @@
8697 4903 $error_type = isset($decoded_response['error']['type'])
8698 4904 ? $decoded_response['error']['type']
8699 4905 : 'unknown';
8700 4906
4907 + //error_log('OpenAI API HTTP Error: ' . $status_code . ' - ' . $error_message);
4908 +
8701 4909 // Handle specific error types
8702 4910 switch ($error_type) {
8703 4911 case 'invalid_request_error':
8704 4912 if (strpos($error_message, 'API key') !== false) {
@@ -8746,8 +4954,9 @@
8746 4954
8747 4955 if (isset($decoded_response['choices'][0]['message']['content'])) {
8748 4956 return trim($decoded_response['choices'][0]['message']['content']);
8749 4957 } else {
4958 + //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
8750 4959 return [
8751 4960 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
8752 4961 'error_code' => 'openai_response_format_error',
8753 4962 'provider' => 'openai'
@@ -8753,8 +4962,9 @@
8753 4962 'provider' => 'openai'
8754 4963 ];
8755 4964 }
8756 4965 } catch (Exception $e) {
4966 + //error_log('OpenAI Exception: ' . $e->getMessage());
8757 4967 return [
8758 4968 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
8759 4969 'error_code' => 'openai_exception',
8760 4970 'provider' => 'openai'
@@ -8760,17 +4970,13 @@
8760 4970 'provider' => 'openai'
8761 4971 ];
8762 4972 }
8763 4973 }
8764 -
8765 4974 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
8766 4975 try {
8767 - // Get bot ID from session or request
8768 - $bot_id = $this->get_current_bot_id($session_id);
8769 -
8770 - // Get system prompt instructions using centralized function
8771 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8772 -
4976 + // Get system prompt instructions from options
4977 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4978 +
8773 4979 // Add system prompt to relevant content
8774 4980 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
8775 4981
8776 4982 // Prepend system instructions to the conversation history
@@ -8955,8 +5161,9 @@
8955 5161 }
8956 5162
8957 5163
8958 5164 }
5165 +
8959 5166 private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
8960 5167 try {
8961 5168 // Ensure conversation_history is an array
8962 5169 if (!is_array($conversation_history)) {
@@ -8962,14 +5169,11 @@
8962 5169 if (!is_array($conversation_history)) {
8963 5170 $conversation_history = array();
8964 5171 }
8965 5172
8966 - // Get bot ID from session or request
8967 - $bot_id = $this->get_current_bot_id($session_id);
8968 -
8969 - // Get system prompt instructions using centralized function
8970 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8971 -
5173 + // Get system prompt instructions from options
5174 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5175 +
8972 5176 // Create a new array for the formatted conversation
8973 5177 $formatted_conversation = array();
8974 5178
8975 5179 // Add system message first
@@ -9120,15 +5324,13 @@
9120 5324 'provider' => 'deepseek'
9121 5325 ];
9122 5326 }
9123 5327 }
5328 +
9124 5329 private function mxchat_generate_response_gemini($selected_model, $gemini_api_key, $conversation_history, $relevant_content) {
9125 - // Get bot ID from session or request
9126 - $bot_id = $this->get_current_bot_id($session_id);
9127 -
9128 - // Get system prompt instructions using centralized function
9129 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
9130 -
5330 + // Get system prompt instructions from options
5331 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5332 +
9131 5333 // Add system prompt to relevant content
9132 5334 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
9133 5335
9134 5336 // Format messages for Gemini API
@@ -9223,11 +5425,9 @@
9223 5425 ]
9224 5426 ]);
9225 5427
9226 5428 // Prepare the API endpoint
9227 - // Use v1beta for preview models (Gemini 3, experimental), v1 for stable models
9228 - $api_version = (strpos($selected_model, 'preview') !== false || strpos($selected_model, 'exp') !== false) ? 'v1beta' : 'v1';
9229 - $api_endpoint = 'https://generativelanguage.googleapis.com/' . $api_version . '/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
5429 + $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
9230 5430
9231 5431 // Set up the API request
9232 5432 $args = [
9233 5433 'body' => $body,
@@ -9267,138 +5467,9 @@
9267 5467 }
9268 5468 }
9269 5469
9270 5470
9271 -public function test_streaming_request() {
9272 - $options = get_option('mxchat_options', []);
9273 - $model = $options['model'] ?? 'gpt-5.1-chat-latest';
9274 5471
9275 - // Detect provider from model prefix
9276 - $provider = strtolower(explode('-', $model)[0]);
9277 -
9278 - $sample_prompt = 'Hello! Can you stream this response back to me?';
9279 - $messages = [['role' => 'user', 'content' => $sample_prompt]];
9280 - $headers = [];
9281 - $body = [];
9282 - $url = '';
9283 - $api_key = '';
9284 -
9285 - switch ($provider) {
9286 - case 'gpt':
9287 - case 'o1':
9288 - $api_key = $options['api_key'] ?? '';
9289 - if (empty($api_key)) return '❌ Missing API key for OpenAI';
9290 - $url = 'https://api.openai.com/v1/chat/completions';
9291 - $headers = [
9292 - 'Content-Type: application/json',
9293 - 'Authorization: Bearer ' . $api_key
9294 - ];
9295 - $body = [
9296 - 'model' => $model,
9297 - 'messages' => $messages,
9298 - 'stream' => true
9299 - ];
9300 - break;
9301 -
9302 - case 'claude':
9303 - $api_key = $options['claude_api_key'] ?? '';
9304 - if (empty($api_key)) return '❌ Missing API key for Claude';
9305 - $url = 'https://api.anthropic.com/v1/messages';
9306 - $headers = [
9307 - 'Content-Type: application/json',
9308 - 'x-api-key: ' . $api_key,
9309 - 'anthropic-version: 2023-06-01'
9310 - ];
9311 - $body = [
9312 - 'model' => $model,
9313 - 'messages' => $messages,
9314 - 'max_tokens' => 100,
9315 - 'stream' => true
9316 - ];
9317 - break;
9318 -
9319 - case 'grok':
9320 - $api_key = $options['xai_api_key'] ?? '';
9321 - if (empty($api_key)) return '❌ Missing API key for X.AI';
9322 - $url = 'https://api.x.ai/v1/chat/completions';
9323 - $headers = [
9324 - 'Content-Type: application/json',
9325 - 'Authorization: Bearer ' . $api_key
9326 - ];
9327 - $body = [
9328 - 'model' => $model,
9329 - 'messages' => $messages,
9330 - 'stream' => true
9331 - ];
9332 - break;
9333 -
9334 - case 'deepseek':
9335 - if (empty($deepseek_api_key)) {
9336 - $error_response = [
9337 - 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
9338 - 'error_code' => 'missing_deepseek_api_key'
9339 - ];
9340 - if ($testing_data !== null) {
9341 - $error_response['testing_data'] = $testing_data;
9342 - }
9343 - return $error_response;
9344 - }
9345 - if ($streaming) {
9346 - return $this->mxchat_generate_response_deepseek_stream(
9347 - $selected_model,
9348 - $deepseek_api_key,
9349 - $conversation_history,
9350 - $relevant_content,
9351 - $session_id,
9352 - $testing_data // Pass testing data
9353 - );
9354 - } else {
9355 - $response = $this->mxchat_generate_response_deepseek(
9356 - $selected_model,
9357 - $deepseek_api_key,
9358 - $conversation_history,
9359 - $relevant_content
9360 - );
9361 - }
9362 - break;
9363 -
9364 - case 'gemini':
9365 - $api_key = $options['gemini_api_key'] ?? '';
9366 - if (empty($api_key)) return '❌ Missing API key for Gemini';
9367 - $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':streamGenerateContent?key=' . $api_key;
9368 - $headers = ['Content-Type: application/json'];
9369 - $body = [
9370 - 'contents' => [['role' => 'user', 'parts' => [['text' => $sample_prompt]]]],
9371 - 'generationConfig' => ['temperature' => 0.7]
9372 - ];
9373 - break;
9374 -
9375 - default:
9376 - return '❌ Unsupported provider: ' . $provider;
9377 - }
9378 -
9379 - // Do the actual streaming test
9380 - $ch = curl_init($url);
9381 - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
9382 - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
9383 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
9384 - curl_setopt($ch, CURLOPT_TIMEOUT, 15);
9385 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
9386 -
9387 - $response = curl_exec($ch);
9388 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
9389 - $error = curl_error($ch);
9390 - curl_close($ch);
9391 -
9392 - if ($error) return "❌ cURL error: $error";
9393 - if ($http_code !== 200) {
9394 - $error_message = json_decode($response, true)['error']['message'] ?? 'Unknown';
9395 - return "❌ HTTP $http_code: $error_message";
9396 - }
9397 -
9398 - return true;
9399 -}
9400 -
9401 5472 public function mxchat_dismiss_pre_chat_message() {
9402 5473 // Get and sanitize the user identifier
9403 5474 $user_id = $this->mxchat_get_user_identifier();
9404 5475 $user_id = sanitize_key($user_id);
@@ -9452,58 +5523,38 @@
9452 5523
9453 5524 return $dotProduct / ($normA * $normB);
9454 5525 }
9455 5526
9456 -
9457 5527 public function mxchat_enqueue_scripts_styles() {
9458 - // Fetch options from the database first to check loading strategy
9459 - $this->options = get_option('mxchat_options');
9460 - $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
9461 -
9462 - // Always enqueue CSS immediately
5528 + // Define version numbers for the styles and scripts
5529 + $chat_style_version = '2.3.6';
5530 + $chat_script_version = '2.3.6';
5531 + // Enqueue the script
5532 + wp_enqueue_script(
5533 + 'mxchat-chat-js',
5534 + plugin_dir_url(__FILE__) . '../js/chat-script.js',
5535 + array('jquery'),
5536 + $chat_script_version,
5537 + true
5538 + );
5539 + // Enqueue the CSS
9463 5540 wp_enqueue_style(
9464 5541 'mxchat-chat-css',
9465 5542 plugin_dir_url(__FILE__) . '../css/chat-style.css',
9466 5543 array(),
9467 - MXCHAT_VERSION
5544 + $chat_style_version
9468 5545 );
9469 -
9470 - // Handle script loading based on strategy
9471 - if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
9472 - // Enqueue the script normally
9473 - wp_enqueue_script(
9474 - 'mxchat-chat-js',
9475 - plugin_dir_url(__FILE__) . '../js/chat-script.js',
9476 - array('jquery'),
9477 - MXCHAT_VERSION,
9478 - true
9479 - );
9480 -
9481 - // Add defer attribute if strategy is 'defer'
9482 - if ($loading_strategy === 'defer') {
9483 - wp_script_add_data('mxchat-chat-js', 'strategy', 'defer');
9484 - }
9485 - } else {
9486 - // For delay or interaction-based loading, we'll use a custom loader
9487 - // Don't enqueue the main script - we'll load it dynamically
9488 - add_action('wp_footer', array($this, 'mxchat_output_delayed_script_loader'), 99);
9489 - }
9490 -
5546 + // Fetch options from the database
5547 + $this->options = get_option('mxchat_options');
9491 5548 $prompts_options = get_option('mxchat_prompts_options', array());
9492 -
9493 - // Check if AI theme is active - if so, skip inline colors in JavaScript
9494 - $theme_options = get_option('mxchat_theme_options', array());
9495 - $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
9496 - $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
9497 - $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
9498 -
5549 +
9499 5550 // Prepare settings for JavaScript
9500 5551 $style_settings = array(
9501 5552 'ajax_url' => admin_url('admin-ajax.php'),
9502 5553 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
9503 - 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest',
5554 + 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-4o',
9504 5555 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
9505 - 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
5556 + 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off', // ADD THIS LINE
9506 5557 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
9507 5558 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
9508 5559 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
9509 5560 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
@@ -9525,137 +5576,15 @@
9525 5576 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
9526 5577 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
9527 5578 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
9528 5579 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
9529 - 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off', // FIXED
9530 - 'initial_email_state' => null, // Also fixed this undefined variable
9531 - 'skip_email_check' => true,
9532 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
9533 - 'skip_inline_colors' => $skip_inline_colors,
9534 - 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array()
5580 + 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
9535 5581 );
9536 -
9537 - // For normal/defer loading, use wp_localize_script
9538 - // For delayed loading, we store settings in a transient to be output inline
9539 - if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
9540 - wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
9541 - } else {
9542 - // Store settings for the delayed loader to use
9543 - set_transient('mxchat_delayed_settings_' . get_current_user_id(), $style_settings, 60);
9544 - }
5582 + // Pass the settings to the script
5583 + wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
9545 5584 }
9546 5585
9547 -/**
9548 - * Output the delayed script loader for performance optimization
9549 - */
9550 -public function mxchat_output_delayed_script_loader() {
9551 - $this->options = get_option('mxchat_options');
9552 - $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
9553 - $script_url = plugin_dir_url(__FILE__) . '../js/chat-script.js?ver=' . MXCHAT_VERSION;
9554 5586
9555 - // Get the stored settings
9556 - $prompts_options = get_option('mxchat_prompts_options', array());
9557 - $theme_options = get_option('mxchat_theme_options', array());
9558 - $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
9559 - $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
9560 - $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
9561 -
9562 - $style_settings = array(
9563 - 'ajax_url' => admin_url('admin-ajax.php'),
9564 - 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
9565 - 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest',
9566 - 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
9567 - 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
9568 - 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
9569 - 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
9570 - 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
9571 - 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
9572 - 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
9573 - 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
9574 - 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
9575 - 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
9576 - 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
9577 - 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
9578 - 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
9579 - 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
9580 - 'icon_color' => $this->options['icon_color'] ?? '#fff',
9581 - 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
9582 - 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
9583 - 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
9584 - 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
9585 - 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
9586 - 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
9587 - 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
9588 - 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
9589 - 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
9590 - 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
9591 - 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off',
9592 - 'initial_email_state' => null,
9593 - 'skip_email_check' => true,
9594 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
9595 - 'skip_inline_colors' => $skip_inline_colors,
9596 - 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array()
9597 - );
9598 -
9599 - // Determine delay time based on strategy
9600 - $delay_ms = 0;
9601 - switch ($loading_strategy) {
9602 - case 'delay_1s':
9603 - $delay_ms = 1000;
9604 - break;
9605 - case 'delay_3s':
9606 - $delay_ms = 3000;
9607 - break;
9608 - case 'delay_5s':
9609 - $delay_ms = 5000;
9610 - break;
9611 - }
9612 -
9613 - ?>
9614 - <script type="text/javascript">
9615 - (function() {
9616 - var mxchatLoaded = false;
9617 - var mxchatChat = <?php echo wp_json_encode($style_settings); ?>;
9618 - window.mxchatChat = mxchatChat;
9619 -
9620 - function loadMxChatScript() {
9621 - if (mxchatLoaded) return;
9622 - mxchatLoaded = true;
9623 -
9624 - function appendChatScript() {
9625 - var script = document.createElement('script');
9626 - script.src = <?php echo wp_json_encode($script_url); ?>;
9627 - script.type = 'text/javascript';
9628 - document.body.appendChild(script);
9629 - }
9630 -
9631 - if (typeof jQuery !== 'undefined') {
9632 - appendChatScript();
9633 - } else {
9634 - var jq = document.createElement('script');
9635 - jq.src = <?php echo wp_json_encode(includes_url('js/jquery/jquery.min.js')); ?>;
9636 - jq.onload = appendChatScript;
9637 - document.body.appendChild(jq);
9638 - }
9639 - }
9640 -
9641 - <?php if ($loading_strategy === 'on_interaction'): ?>
9642 - // Load on user interaction
9643 - var events = ['scroll', 'mousemove', 'touchstart', 'keydown', 'click'];
9644 - events.forEach(function(evt) {
9645 - window.addEventListener(evt, loadMxChatScript, {once: true, passive: true});
9646 - });
9647 - // Fallback: load after 8 seconds if no interaction
9648 - setTimeout(loadMxChatScript, 8000);
9649 - <?php else: ?>
9650 - // Load after specified delay
9651 - setTimeout(loadMxChatScript, <?php echo intval($delay_ms); ?>);
9652 - <?php endif; ?>
9653 - })();
9654 - </script>
9655 - <?php
9656 -}
9657 -
9658 5587 /**
9659 5588 * Setup the cron jobs for rate limits with guard against multiple calls
9660 5589 */
9661 5590 public function setup_rate_limit_cron_jobs() {
@@ -9671,9 +5600,9 @@
9671 5600
9672 5601 try {
9673 5602 // First, check if WordPress cron is disabled
9674 5603 if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
9675 - //error_log('MxChat: WordPress cron is disabled (DISABLE_WP_CRON = true), using fallback system');
5604 + error_log('MxChat: WordPress cron is disabled (DISABLE_WP_CRON = true), using fallback system');
9676 5605 $this->setup_fallback_rate_limit_system();
9677 5606 return;
9678 5607 }
9679 5608
@@ -9678,9 +5607,9 @@
9678 5607 }
9679 5608
9680 5609 // Check if cron is already scheduled - if so, don't mess with it
9681 5610 if (wp_next_scheduled('mxchat_reset_rate_limits')) {
9682 - //error_log('MxChat: Rate limit cron already scheduled, skipping setup');
5611 + error_log('MxChat: Rate limit cron already scheduled, skipping setup');
9683 5612 return;
9684 5613 }
9685 5614
9686 5615 // Clear any orphaned hooks (but don't loop indefinitely)
@@ -9708,16 +5637,16 @@
9708 5637 $initial_time = time() + 300; // Start in 5 minutes
9709 5638 $result = wp_schedule_event($initial_time, 'hourly', 'mxchat_reset_rate_limits');
9710 5639
9711 5640 if ($result === false) {
9712 - //error_log('MxChat: Failed to schedule cron, using fallback system');
5641 + error_log('MxChat: Failed to schedule cron, using fallback system');
9713 5642 $this->setup_fallback_rate_limit_system();
9714 5643 } else {
9715 - //error_log('MxChat: Successfully scheduled rate limit reset cron');
5644 + error_log('MxChat: Successfully scheduled rate limit reset cron');
9716 5645 }
9717 5646
9718 5647 } catch (Exception $e) {
9719 - //error_log('MxChat: Cron setup exception: ' . $e->getMessage());
5648 + error_log('MxChat: Cron setup exception: ' . $e->getMessage());
9720 5649 $this->setup_fallback_rate_limit_system();
9721 5650 }
9722 5651 }
9723 5652
@@ -9728,9 +5657,9 @@
9728 5657 try {
9729 5658 // Method 1: Try with current time instead of future time
9730 5659 $result1 = wp_schedule_event(time(), 'hourly', 'mxchat_reset_rate_limits');
9731 5660 if ($result1 !== false) {
9732 - //error_log('MxChat: Alternative method 1 (current time) succeeded');
5661 + error_log('MxChat: Alternative method 1 (current time) succeeded');
9733 5662 return true;
9734 5663 }
9735 5664
9736 5665 // Method 2: Try with a different interval
@@ -9735,9 +5664,9 @@
9735 5664
9736 5665 // Method 2: Try with a different interval
9737 5666 $result2 = wp_schedule_event($initial_time, 'daily', 'mxchat_reset_rate_limits');
9738 5667 if ($result2 !== false) {
9739 - //error_log('MxChat: Alternative method 2 (daily interval) succeeded');
5668 + error_log('MxChat: Alternative method 2 (daily interval) succeeded');
9740 5669 return true;
9741 5670 }
9742 5671
9743 5672 // Method 3: Try wp_schedule_single_event first, then recurring
@@ -9742,9 +5671,9 @@
9742 5671
9743 5672 // Method 3: Try wp_schedule_single_event first, then recurring
9744 5673 $result3 = wp_schedule_single_event($initial_time, 'mxchat_reset_rate_limits');
9745 5674 if ($result3 !== false) {
9746 - //error_log('MxChat: Alternative method 3 (single event) succeeded');
5675 + error_log('MxChat: Alternative method 3 (single event) succeeded');
9747 5676 // Schedule the next one manually in the handler
9748 5677 return true;
9749 5678 }
9750 5679
@@ -9750,9 +5679,9 @@
9750 5679
9751 5680 return false;
9752 5681
9753 5682 } catch (Exception $e) {
9754 - //error_log('MxChat: Alternative cron scheduling exception: ' . $e->getMessage());
5683 + error_log('MxChat: Alternative cron scheduling exception: ' . $e->getMessage());
9755 5684 return false;
9756 5685 }
9757 5686 }
9758 5687
@@ -9768,9 +5697,9 @@
9768 5697
9769 5698 // Also set up a more frequent fallback check (every 4 hours)
9770 5699 update_option('mxchat_fallback_check_interval', 4 * 3600);
9771 5700
9772 - //error_log('MxChat: Fallback rate limit system activated');
5701 + error_log('MxChat: Fallback rate limit system activated');
9773 5702 }
9774 5703
9775 5704 /**
9776 5705 * Enhanced fallback check method
@@ -9785,9 +5714,9 @@
9785 5714 $next_check = get_option('mxchat_next_rate_limit_check', 0);
9786 5715 $check_interval = get_option('mxchat_fallback_check_interval', 3600);
9787 5716
9788 5717 if (time() >= $next_check) {
9789 - //error_log('MxChat: Running fallback rate limit cleanup');
5718 + error_log('MxChat: Running fallback rate limit cleanup');
9790 5719 $this->mxchat_reset_rate_limits();
9791 5720
9792 5721 // Schedule next check
9793 5722 update_option('mxchat_next_rate_limit_check', time() + $check_interval);
@@ -9793,9 +5722,9 @@
9793 5722 update_option('mxchat_next_rate_limit_check', time() + $check_interval);
9794 5723 }
9795 5724 }
9796 5725 /**
9797 - * Enhanced rate limit check that includes fallback cleanup and bot-specific rate limits
5726 + * Enhanced rate limit check that includes fallback cleanup
9798 5727 */
9799 5728 public function check_rate_limit() {
9800 5729 // Check if we need to run fallback cleanup
9801 5730 $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
@@ -9805,18 +5734,11 @@
9805 5734 $this->mxchat_reset_rate_limits();
9806 5735 update_option('mxchat_next_rate_limit_check', time() + 3600); // Next hour
9807 5736 }
9808 5737
9809 - // Get bot ID from current request context
9810 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
5738 + // Continue with your existing rate limit logic...
5739 + $all_options = get_option('mxchat_options', []);
9811 5740
9812 - // Get bot-specific options (includes rate limits if overridden)
9813 - $bot_options = $this->get_bot_options($bot_id);
9814 - $current_options = !empty($bot_options) ? $bot_options : $this->options;
9815 -
9816 - // Use bot-specific rate limits if available, otherwise fall back to default
9817 - $rate_limits_source = isset($current_options['rate_limits']) ? $current_options['rate_limits'] : get_option('mxchat_options', [])['rate_limits'] ?? [];
9818 -
9819 5741 // Determine user role or if logged out
9820 5742 if (is_user_logged_in()) {
9821 5743 $user = wp_get_current_user();
9822 5744 $user_id = $user->ID;
@@ -9836,13 +5758,13 @@
9836 5758 $user_id = $this->get_client_ip();
9837 5759 }
9838 5760
9839 5761 // Check if rate limits are configured for this role
9840 - if (!isset($rate_limits_source[$role])) {
5762 + if (!isset($all_options['rate_limits'][$role])) {
9841 5763 return true; // No limit set for this role
9842 5764 }
9843 5765
9844 - $limit = $rate_limits_source[$role]['limit'];
5766 + $limit = $all_options['rate_limits'][$role]['limit'];
9845 5767
9846 5768 // If unlimited, return true immediately
9847 5769 if ($limit === 'unlimited') {
9848 5770 return true;
@@ -9847,16 +5769,13 @@
9847 5769 if ($limit === 'unlimited') {
9848 5770 return true;
9849 5771 }
9850 5772
9851 - // Get the option name for this user/role with safer naming (include bot_id for bot-specific limits)
5773 + // Get the option name for this user/role with safer naming
9852 5774 $safe_role = preg_replace('/[^a-zA-Z0-9_]/', '_', $role);
9853 5775 $safe_user_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $user_id);
9854 - $safe_bot_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $bot_id);
5776 + $option_name = 'mxchat_chat_limit_' . $safe_role . '_' . $safe_user_id;
9855 5777
9856 - // Include bot_id in option name so each bot has separate rate limits
9857 - $option_name = 'mxchat_chat_limit_' . $safe_bot_id . '_' . $safe_role . '_' . $safe_user_id;
9858 -
9859 5778 // Get the counter data
9860 5779 $limit_data = get_option($option_name, ['count' => 0, 'timestamp' => time()]);
9861 5780
9862 5781 // If first request or counter reset needed, set the initial timestamp
@@ -9865,10 +5784,10 @@
9865 5784 update_option($option_name, $limit_data);
9866 5785 }
9867 5786
9868 5787 // Get the timeframe
9869 - $timeframe = isset($rate_limits_source[$role]['timeframe']) ?
9870 - $rate_limits_source[$role]['timeframe'] : 'daily';
5788 + $timeframe = isset($all_options['rate_limits'][$role]['timeframe']) ?
5789 + $all_options['rate_limits'][$role]['timeframe'] : 'daily';
9871 5790
9872 5791 // Check if the counter needs to be reset based on timeframe
9873 5792 $current_time = time();
9874 5793 $timestamp = $limit_data['timestamp'];
@@ -9897,10 +5816,10 @@
9897 5816
9898 5817 // Check if user has exceeded their limit
9899 5818 if ($limit_data['count'] >= intval($limit)) {
9900 5819 // Get the custom message for this role
9901 - $message = !empty($rate_limits_source[$role]['message'])
9902 - ? $rate_limits_source[$role]['message']
5820 + $message = !empty($all_options['rate_limits'][$role]['message'])
5821 + ? $all_options['rate_limits'][$role]['message']
9903 5822 : __('Rate limit exceeded. Please try again later.', 'mxchat');
9904 5823
9905 5824 // Add timeframe information to the message if placeholders exist
9906 5825 $timeframe_label = '';
@@ -9972,9 +5891,9 @@
9972 5891
9973 5892 foreach ($option_names as $option_name) {
9974 5893 // Check processing time limit
9975 5894 if ((time() - $start_time) > $max_processing_time) {
9976 - //error_log('MxChat: Rate limit reset timeout after processing ' . $processed_count . ' entries');
5895 + error_log('MxChat: Rate limit reset timeout after processing ' . $processed_count . ' entries');
9977 5896 break;
9978 5897 }
9979 5898
9980 5899 // Parse the option name more safely
@@ -10038,12 +5957,12 @@
10038 5957
10039 5958 // Clean up any orphaned cache entries
10040 5959 wp_cache_delete('mxchat_all_chat_limits', 'options');
10041 5960
10042 - //error_log("MxChat: Rate limit reset completed. Processed {$processed_count} entries.");
5961 + error_log("MxChat: Rate limit reset completed. Processed {$processed_count} entries.");
10043 5962
10044 5963 } catch (Exception $e) {
10045 - //error_log('MxChat: Rate limit reset error: ' . $e->getMessage());
5964 + error_log('MxChat: Rate limit reset error: ' . $e->getMessage());
10046 5965 }
10047 5966 }
10048 5967
10049 5968
@@ -10190,11 +6109,8 @@
10190 6109
10191 6110 /**
10192 6111 * AJAX handler to get system information for testing panel
10193 6112 */
10194 -/**
10195 - * AJAX handler to get system information for testing panel
10196 - */
10197 6113 public function mxchat_get_system_info() {
10198 6114 // Verify nonce for security
10199 6115 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
10200 6116 wp_send_json_error(['message' => 'Invalid nonce']);
@@ -10212,24 +6128,10 @@
10212 6128 ? $this->options['system_prompt_instructions']
10213 6129 : 'No system prompt configured';
10214 6130
10215 6131 // Get selected model
10216 - $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest';
6132 + $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
10217 6133
10218 - // Check if OpenRouter is being used
10219 - $is_openrouter = ($selected_model === 'openrouter');
10220 - $openrouter_model = '';
10221 -
10222 - if ($is_openrouter) {
10223 - // Get the actual OpenRouter model that's selected
10224 - $openrouter_model = isset($this->options['openrouter_selected_model'])
10225 - ? $this->options['openrouter_selected_model']
10226 - : 'No OpenRouter model selected';
10227 -
10228 - // Update selected_model display to show both
10229 - $selected_model = 'OpenRouter: ' . $openrouter_model;
10230 - }
10231 -
10232 6134 // Get API key status (just check if they exist, don't expose the keys)
10233 6135 $api_status = [];
10234 6136 $api_status['openai'] = !empty($this->options['api_key']);
10235 6137 $api_status['claude'] = !empty($this->options['claude_api_key']);
@@ -10235,15 +6137,12 @@
10235 6137 $api_status['claude'] = !empty($this->options['claude_api_key']);
10236 6138 $api_status['gemini'] = !empty($this->options['gemini_api_key']);
10237 6139 $api_status['xai'] = !empty($this->options['xai_api_key']);
10238 6140 $api_status['deepseek'] = !empty($this->options['deepseek_api_key']);
10239 - $api_status['openrouter'] = !empty($this->options['openrouter_api_key']);
10240 6141
10241 6142 wp_send_json_success([
10242 6143 'system_prompt' => $system_prompt,
10243 6144 'selected_model' => $selected_model,
10244 - 'is_openrouter' => $is_openrouter,
10245 - 'openrouter_model' => $openrouter_model,
10246 6145 'api_status' => $api_status
10247 6146 ]);
10248 6147 }
10249 6148
@@ -10262,12 +6161,12 @@
10262 6161 wp_send_json_error(['message' => 'Unauthorized']);
10263 6162 return;
10264 6163 }
10265 6164
10266 - // Get similarity threshold from main options (default 35%)
6165 + // Get similarity threshold from main options (default 75%)
10267 6166 $similarity_threshold = isset($this->options['similarity_threshold'])
10268 6167 ? ((int) $this->options['similarity_threshold']) / 100
10269 - : 0.35;
6168 + : 0.75;
10270 6169
10271 6170 wp_send_json_success([
10272 6171 'threshold' => $similarity_threshold,
10273 6172 'threshold_percentage' => ($similarity_threshold * 100) . '%'
@@ -10282,42 +6181,24 @@
10282 6181 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
10283 6182 wp_send_json_error(['message' => 'Invalid nonce']);
10284 6183 return;
10285 6184 }
10286 -
6185 +
10287 6186 // Only allow admin users
10288 6187 if (!current_user_can('administrator')) {
10289 6188 wp_send_json_error(['message' => 'Unauthorized']);
10290 6189 return;
10291 6190 }
10292 -
10293 - // Check OpenAI Vector Store first (takes priority)
10294 - $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
10295 - $use_vectorstore = (isset($vectorstore_options['mxchat_use_openai_vectorstore']) && $vectorstore_options['mxchat_use_openai_vectorstore'] === '1');
10296 -
10297 - if ($use_vectorstore) {
10298 - $vectorstore_ids = $vectorstore_options['mxchat_openai_vectorstore_ids'] ?? '';
10299 - $id_count = !empty($vectorstore_ids) ? count(array_filter(array_map('trim', explode(',', $vectorstore_ids)))) : 0;
10300 -
10301 - $kb_info = [
10302 - 'type' => 'OpenAI Vector Store',
10303 - 'status' => 'Active',
10304 - 'documents' => $id_count > 0 ? $id_count . ' vector store' . ($id_count > 1 ? 's' : '') . ' configured' : 'No vector stores configured'
10305 - ];
10306 -
10307 - wp_send_json_success($kb_info);
10308 - return;
10309 - }
10310 -
6191 +
10311 6192 // Check Pinecone vs WordPress
10312 6193 $addon_options = get_option('mxchat_pinecone_addon_options', array());
10313 6194 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
10314 -
6195 +
10315 6196 $kb_info = [
10316 6197 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
10317 6198 'status' => 'Active'
10318 6199 ];
10319 -
6200 +
10320 6201 // Get document count
10321 6202 if ($use_pinecone) {
10322 6203 $kb_info['documents'] = 'Connected to Pinecone';
10323 6204 $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
@@ -10327,9 +6208,9 @@
10327 6208 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
10328 6209 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
10329 6210 $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
10330 6211 }
10331 -
6212 +
10332 6213 wp_send_json_success($kb_info);
10333 6214 }
10334 6215
10335 6216 /**
@@ -10411,13 +6292,9 @@
10411 6292 // Clear any other session-specific transients
10412 6293 delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
10413 6294 delete_transient("mxchat_include_pdf_in_context_{$session_id}");
10414 6295 delete_transient("mxchat_include_word_in_context_{$session_id}");
10415 -
10416 - // Clear form addon state (pending forms and submitted forms)
10417 - delete_option("mxchat_pending_form_{$session_id}");
10418 - delete_option("mxchat_submitted_forms_{$session_id}");
10419 -
6296 +
10420 6297 //error_log("MxChat: Cleared all data for session: {$session_id}");
10421 6298 }
10422 6299
10423 6300 /**
@@ -10452,15 +6329,15 @@
10452 6329 $testing_data = [
10453 6330 'query' => $message,
10454 6331 'timestamp' => time(),
10455 6332 'top_matches' => [],
10456 - 'action_matches' => [] // Add action matches
6333 + 'action_matches' => [] // NEW: Add action matches
10457 6334 ];
10458 6335
10459 6336 // Get similarity threshold
10460 6337 $similarity_threshold = isset($this->options['similarity_threshold'])
10461 6338 ? ((int) $this->options['similarity_threshold']) / 100
10462 - : 0.35;
6339 + : 0.75;
10463 6340
10464 6341 $testing_data['similarity_threshold'] = $similarity_threshold;
10465 6342
10466 6343 // Use the real similarity analysis if available
@@ -10475,9 +6352,9 @@
10475 6352
10476 6353 $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
10477 6354 }
10478 6355
10479 - // Include action analysis if available
6356 + // NEW: Include action analysis if available
10480 6357 if (isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
10481 6358 $testing_data['action_matches'] = $this->last_action_analysis;
10482 6359
10483 6360 // Clear it after capturing to avoid stale data
@@ -10488,9 +6365,9 @@
10488 6365 }
10489 6366
10490 6367
10491 6368 /**
10492 - * Track URL clicks from chatbot responses
6369 + * NEW: Track URL clicks from chatbot responses
10493 6370 */
10494 6371 public function mxchat_track_url_click() {
10495 6372 // Verify nonce for security
10496 6373 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
@@ -10527,9 +6404,9 @@
10527 6404 wp_die();
10528 6405 }
10529 6406
10530 6407 /**
10531 - * Get URL click analytics for a session
6408 + * NEW: Get URL click analytics for a session
10532 6409 */
10533 6410 public function mxchat_get_url_clicks($session_id) {
10534 6411 global $wpdb;
10535 6412 $table_name = $wpdb->prefix . 'mxchat_url_clicks';
@@ -10541,9 +6418,9 @@
10541 6418
10542 6419 return $clicks;
10543 6420 }
10544 6421 /**
10545 - * Track the originating page where chat was started
6422 + * NEW: Track the originating page where chat was started
10546 6423 */
10547 6424 public function mxchat_track_originating_page() {
10548 6425 // Verify nonce
10549 6426 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
@@ -10592,195 +6469,8 @@
10592 6469 wp_send_json_success(['message' => 'Originating page tracked']);
10593 6470 wp_die();
10594 6471 }
10595 6472
10596 -/**
10597 - * Validate and clean URLs from AI response
10598 - * Removes any URLs that aren't in the knowledge base
10599 - *
10600 - * @param string $response_text The AI-generated response
10601 - * @param array $valid_urls Array of URLs from the knowledge base
10602 - * @return string Cleaned response with invalid URLs removed/flagged
10603 - */
10604 -private function validate_and_clean_urls($response_text, $valid_urls) {
10605 - // DEBUG: Log what we're working with
10606 - //error_log("=== MxChat URL Validation Debug ===");
10607 - //error_log("Valid URLs count: " . count($valid_urls));
10608 - //error_log("Valid URLs: " . print_r($valid_urls, true));
10609 - //error_log("Response text length: " . strlen($response_text));
10610 - //error_log("Response text preview: " . substr($response_text, 0, 500));
10611 -
10612 - // If no valid URLs provided or empty response, return as-is
10613 - if (empty($valid_urls) || empty($response_text)) {
10614 - //error_log("Validation skipped - empty valid_urls or response");
10615 - return $response_text;
10616 - }
10617 -
10618 - // Extract all URLs from the AI response
10619 - // This regex matches http:// and https:// URLs
10620 - preg_match_all(
10621 - '#\bhttps?://[^\s<>"\')\]]+#i',
10622 - $response_text,
10623 - $matches
10624 - );
10625 -
10626 - // If no URLs found in response, return as-is
10627 - if (empty($matches[0])) {
10628 - //error_log("No URLs found in response");
10629 - return $response_text;
10630 - }
10631 -
10632 - $found_urls = $matches[0];
10633 - $cleaned_response = $response_text;
10634 - $removed_count = 0;
10635 -
10636 - // Normalize valid URLs for comparison (remove trailing slashes, fragments, etc.)
10637 - $normalized_valid_urls = array_map(function($url) {
10638 - // Remove trailing slash
10639 - $url = rtrim($url, '/');
10640 - // Remove URL fragments (#section)
10641 - $url = preg_replace('/#.*$/', '', $url);
10642 - // Remove trailing punctuation that might have been captured
10643 - $url = rtrim($url, '.,;:!?');
10644 - return $url;
10645 - }, $valid_urls);
10646 -
10647 - //error_log("Normalized valid URLs: " . print_r($normalized_valid_urls, true));
10648 -
10649 - foreach ($found_urls as $found_url) {
10650 - // Clean up the found URL (remove trailing punctuation that might have been captured)
10651 - $clean_found_url = rtrim($found_url, '.,;:!?)');
10652 -
10653 - // DEBUG: Log each URL being checked
10654 - //error_log("Checking found URL: " . $found_url);
10655 -
10656 - // Normalize for comparison
10657 - $normalized_found = rtrim($clean_found_url, '/');
10658 - $normalized_found = preg_replace('/#.*$/', '', $normalized_found);
10659 -
10660 - //error_log("Normalized found URL: " . $normalized_found);
10661 -
10662 - // Check if this URL exists in our valid URLs list
10663 - $is_valid = false;
10664 -
10665 - //error_log("Starting validation checks for: " . $normalized_found);
10666 -
10667 - // First, try exact match
10668 - if (in_array($normalized_found, $normalized_valid_urls)) {
10669 - $is_valid = true;
10670 - //error_log("EXACT MATCH FOUND");
10671 - } else {
10672 - //error_log("No exact match, checking variations...");
10673 - // If no exact match, check if it's a variation (with query params, etc.)
10674 - foreach ($normalized_valid_urls as $valid_url) {
10675 - //error_log(" Comparing against valid URL: " . $valid_url);
10676 -
10677 - // Check if the found URL starts with a valid URL (handles query params)
10678 - if (strpos($normalized_found, $valid_url) === 0) {
10679 - // Check what comes after the valid URL
10680 - $remainder = substr($normalized_found, strlen($valid_url));
10681 -
10682 - // Only valid if:
10683 - // 1. Exact match (remainder is empty)
10684 - // 2. Query params (starts with ?)
10685 - // 3. Fragment (starts with #)
10686 - if (empty($remainder) || $remainder[0] === '?' || $remainder[0] === '#') {
10687 - $is_valid = true;
10688 - //error_log(" MATCH: Found URL is valid variation of base URL");
10689 - break;
10690 - } else {
10691 - //error_log(" NOT A MATCH: Found URL extends path beyond valid URL (remainder: " . $remainder . ")");
10692 - }
10693 - }
10694 - // Also check the reverse (in case valid URL has query params)
10695 - if (strpos($valid_url, $normalized_found) === 0) {
10696 - $is_valid = true;
10697 - //error_log(" MATCH: Valid URL starts with found URL");
10698 - break;
10699 - }
10700 - }
10701 -
10702 - if (!$is_valid) {
10703 - //error_log("NO MATCH FOUND - URL should be removed");
10704 - }
10705 - }
10706 -
10707 - // If URL is not valid, remove it from the response
10708 - if (!$is_valid) {
10709 - // Log the removal for debugging
10710 - //error_log("MxChat: Removed hallucinated URL: " . $found_url);
10711 - //error_log("MxChat: Valid URLs were: " . implode(', ', array_slice($normalized_valid_urls, 0, 5)));
10712 -
10713 - $removed_count++;
10714 -
10715 - // Check if URL is part of a markdown link: [text](url)
10716 - $markdown_pattern = '/\[([^\]]+)\]\(' . preg_quote($found_url, '/') . '\)/';
10717 - if (preg_match($markdown_pattern, $cleaned_response)) {
10718 - //error_log("Found markdown link, removing but keeping text");
10719 - // Remove the markdown link but keep the text
10720 - $cleaned_response = preg_replace($markdown_pattern, '$1', $cleaned_response);
10721 - }
10722 - // Check if URL is part of an HTML link: <a href="url">text</a>
10723 - else if (preg_match('/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>(.*?)<\/a>/i', $cleaned_response, $link_match)) {
10724 - //error_log("Found HTML link, removing but keeping text");
10725 - // Remove the HTML link but keep the text
10726 - $link_text = $link_match[1];
10727 - $cleaned_response = preg_replace(
10728 - '/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>.*?<\/a>/i',
10729 - $link_text,
10730 - $cleaned_response
10731 - );
10732 - }
10733 - // Otherwise just remove the bare URL
10734 - else {
10735 - //error_log("Removing bare URL");
10736 - $cleaned_response = str_replace($found_url, '', $cleaned_response);
10737 - }
10738 - }
10739 - }
10740 -
10741 - // Log summary if any URLs were removed
10742 - if ($removed_count > 0) {
10743 - //error_log("MxChat: URL Validation Summary - Removed {$removed_count} hallucinated URL(s)");
10744 - } else {
10745 - //error_log("MxChat: URL Validation Summary - No URLs removed, all were valid");
10746 - }
10747 -
10748 - // Clean up any double spaces or awkward punctuation left behind
10749 - // IMPORTANT: Only collapse horizontal whitespace (spaces/tabs), preserve newlines for markdown formatting
10750 - $cleaned_response = preg_replace('/[^\S\n]+/', ' ', $cleaned_response); // Collapse spaces/tabs but NOT newlines
10751 - $cleaned_response = preg_replace('/[^\S\n]+([.,;:!?])/', '$1', $cleaned_response); // Same for punctuation cleanup
10752 -
10753 - //error_log("Final cleaned response: " . $cleaned_response);
10754 -
10755 - return trim($cleaned_response);
10756 -}
10757 -
10758 -/**
10759 - * AJAX handler to get current chat mode for a session
10760 - */
10761 -public function mxchat_get_current_chat_mode() {
10762 - // Verify nonce for security
10763 - if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
10764 - wp_send_json_error(['message' => 'Invalid nonce']);
10765 - wp_die();
10766 - }
10767 -
10768 - $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
10769 -
10770 - if (empty($session_id)) {
10771 - wp_send_json_error(['message' => 'Session ID missing']);
10772 - wp_die();
10773 - }
10774 -
10775 - // Get the current chat mode for this session
10776 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
10777 -
10778 - wp_send_json_success([
10779 - 'chat_mode' => $chat_mode
10780 - ]);
10781 - wp_die();
10782 -}
10783 6473
10784 6474
10785 6475
10786 6476 }