PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.7
MxChat – AI Chatbot & Content Generation for WordPress v3.1.7
3.2.22 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 All 153 releases
← All changes | includes/class-mxchat-integrator.php +8591 -2018 2.0.33.1.7 View file →
@@ -9,50 +9,80 @@
9 9 private $chat_count;
10 10 private $fallbackResponse;
11 11 private $productCardHtml;
12 12 private $word_handler;
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
13 18
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 +
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 +/**
47 + * Class constructor
48 + */
14 49 public function __construct() {
15 50 $this->options = get_option('mxchat_options');
16 51 $this->prompts_options = get_option('mxchat_prompts_options', array());
17 -
18 52 $this->chat_count = get_option('mxchat_chat_count', 0);
19 53 $this->word_handler = new MXChat_Word_Handler($this->options);
20 -
54 +
55 + // Add all action hooks
21 56 add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
22 57 add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
23 58 add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
24 -
25 59 add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
26 60 add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
61 +
27 62 // Add the AJAX actions for checking if the pre-chat message was dismissed
28 63 add_action('wp_ajax_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
29 64 add_action('wp_ajax_nopriv_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
30 -
31 65 add_action('wp_ajax_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
32 66 add_action('wp_ajax_nopriv_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
33 -
34 67 add_action('wp_ajax_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
35 68 add_action('wp_ajax_nopriv_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
36 -
37 - if (!wp_next_scheduled('mxchat_reset_rate_limits')) {
38 - wp_schedule_event(time(), 'daily', 'mxchat_reset_rate_limits');
39 - }
40 -
69 +
41 70 // Add REST API routes registration
42 71 add_action('rest_api_init', array($this, 'register_routes'));
43 -
44 72 add_action('wp_ajax_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
45 73 add_action('wp_ajax_nopriv_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
46 -
74 +
75 + // Rate limit action - notice we removed the old schedule setup
47 76 add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
48 -
77 +
78 + // File upload and handling actions
49 79 add_action('wp_ajax_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
50 80 add_action('wp_ajax_nopriv_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
51 81 add_action('wp_ajax_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
52 82 add_action('wp_ajax_nopriv_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
53 -
54 - // Add these with your other add_action hooks
83 +
84 + // Word document handling actions
55 85 add_action('wp_ajax_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
56 86 add_action('wp_ajax_nopriv_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
57 87 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
58 88 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
@@ -57,16 +87,63 @@
57 87 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
58 88 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
59 89 add_action('wp_ajax_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
60 90 add_action('wp_ajax_nopriv_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
61 -
91 +
92 + // Email handling actions
62 93 add_action('wp_ajax_nopriv_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
63 94 add_action('wp_ajax_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
64 95 add_action('wp_ajax_nopriv_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
65 96 add_action('wp_ajax_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
97 +
98 + add_action('wp_ajax_mxchat_stream_chat', array($this, 'mxchat_handle_chat_request'));
99 + add_action('wp_ajax_nopriv_mxchat_stream_chat', array($this, 'mxchat_handle_chat_request'));
100 +
101 + // Testing panel AJAX actions
102 + add_action('wp_ajax_mxchat_get_system_info', array($this, 'mxchat_get_system_info'));
103 + add_action('wp_ajax_mxchat_get_similarity_threshold', array($this, 'mxchat_get_similarity_threshold'));
104 + add_action('wp_ajax_mxchat_get_kb_status', array($this, 'mxchat_get_kb_status'));
105 + add_action('wp_ajax_mxchat_start_fresh_session', array($this, 'mxchat_start_fresh_session'));
106 + // Add to your existing constructor, in the section with other AJAX actions:
107 + add_action('wp_ajax_mxchat_track_url_click', array($this, 'mxchat_track_url_click'));
108 + add_action('wp_ajax_nopriv_mxchat_track_url_click', array($this, 'mxchat_track_url_click'));
109 + 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'));
114 +
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 + add_filter('mxchat_check_actions_only', array($this, 'check_actions_for_addons'), 10, 4);
123 +
124 +
66 125 }
67 126
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 +}
68 134
135 +// In your core plugin's check_actions_for_addons method:
136 +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);
138 +
139 + $result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
140 +
141 + //error_log('MxChat Core: Intent check result = ' . ($result === false ? 'false' : 'true'));
142 +
143 + return $result;
144 +}
145 +
69 146 private function mxchat_increment_chat_count() {
70 147 $chat_count = get_option('mxchat_chat_count', 0);
71 148 $chat_count++;
72 149 update_option('mxchat_chat_count', $chat_count);
@@ -78,8 +155,31 @@
78 155 wp_die();
79 156 }
80 157
81 158 $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 + // If session has an owner and it doesn't match current user, trigger session reset
168 + if ($session_owner && $session_owner !== $current_user_identifier) {
169 + wp_send_json_error([
170 + 'message' => esc_html__('Your session has expired. Starting a new conversation.', 'mxchat'),
171 + 'code' => 'session_expired',
172 + 'action' => 'reset_session'
173 + ]);
174 + wp_die();
175 + }
176 +
177 + // If no owner is set yet, claim ownership (for legacy sessions)
178 + if (!$session_owner) {
179 + update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
180 + }
181 +
82 182 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history
83 183 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai'); // Get current chat mode
84 184
85 185 if (empty($history)) {
@@ -96,26 +196,25 @@
96 196 'chat_mode' => $chat_mode
97 197 ]);
98 198 wp_die();
99 199 }
100 -private function mxchat_fetch_conversation_history_for_ajax($session_id) {
101 - $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history based on session ID
102 - $formatted_history = [];
200 +private function mxchat_fetch_conversation_history_for_ai($session_id, $session_start_timestamp = 0) {
201 + $history = get_option("mxchat_history_{$session_id}", []);
103 202
104 - // Format the history to align with the expected structure for OpenAI
105 - foreach ($history as $entry) {
106 - $formatted_history[] = [
107 - 'role' => $entry['role'], // Ensure this matches 'user' or 'assistant'
108 - 'content' => $entry['content']
109 - ];
203 + // Check persistence setting - when OFF, only include messages from current page load
204 + $options = get_option('mxchat_options', []);
205 + $persistence_enabled = isset($options['chat_persistence_toggle']) && $options['chat_persistence_toggle'] === 'on';
206 +
207 + // Filter history when persistence is OFF to match what the user sees
208 + if (!$persistence_enabled && $session_start_timestamp > 0) {
209 + $history = array_filter($history, function($entry) use ($session_start_timestamp) {
210 + // Include messages from this page load onwards
211 + return isset($entry['timestamp']) && $entry['timestamp'] >= $session_start_timestamp;
212 + });
213 + // Re-index array after filtering
214 + $history = array_values($history);
110 215 }
111 216
112 - return $formatted_history;
113 -}
114 -
115 -
116 -private function mxchat_fetch_conversation_history_for_ai($session_id) {
117 - $history = get_option("mxchat_history_{$session_id}", []);
118 217 $formatted_history = [];
119 218
120 219 // Adjusted for code-heavy conversations
121 220 $max_tokens = 120000; // Context window size
@@ -150,9 +249,9 @@
150 249 if (!$has_code && $clean_content !== strip_tags($entry['content'])) {
151 250 continue;
152 251 }
153 252
154 - // More accurate token estimation (1 token ≈ 4 characters)
253 + // More accurate token estimation (1 token ≈ 4 characters)
155 254 $token_estimate = ceil(mb_strlen($clean_content, 'UTF-8') / 4);
156 255
157 256 // Check token budget with the new estimate
158 257 if (($current_token_count + $token_estimate + $reserved_tokens) > $max_tokens) {
@@ -206,9 +305,22 @@
206 305 'methods' => 'POST',
207 306 'callback' => [$this, 'handle_slack_interaction'],
208 307 'permission_callback' => [$this, 'verify_slack_request'],
209 308 ]);
309 +
310 + register_rest_route('mxchat/v1', '/slack-messages', [
311 + 'methods' => 'POST',
312 + 'callback' => [$this, 'handle_slack_messages'],
313 + 'permission_callback' => [$this, 'verify_slack_request'],
314 + ]);
210 315
316 + // Telegram webhook endpoint
317 + register_rest_route('mxchat/v1', '/telegram-webhook', [
318 + 'methods' => 'POST',
319 + 'callback' => [$this, 'handle_telegram_webhook'],
320 + 'permission_callback' => [$this, 'verify_telegram_request'],
321 + ]);
322 +
211 323 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
212 324 }
213 325
214 326 /**
@@ -248,10 +360,11 @@
248 360 //error_log(esc_html__('Slack request timestamp too old', 'mxchat'));
249 361 return false;
250 362 }
251 363
252 - // Get raw request body
253 - $request_body = file_get_contents('php://input');
364 + // Get raw request body from the WP_REST_Request object
365 + // (php://input may already be consumed by WordPress at this point)
366 + $request_body = $request->get_body();
254 367
255 368 // Create the signature base string
256 369 $sig_basestring = "v0:{$timestamp}:{$request_body}";
257 370
@@ -260,8 +373,43 @@
260 373
261 374 // Compare signatures
262 375 return hash_equals($my_signature, $slack_signature);
263 376 }
377 +
378 +/**
379 + * Verify request is coming from Telegram.
380 + *
381 + * @param WP_REST_Request $request
382 + * @return bool True if valid, false otherwise.
383 + */
384 +public function verify_telegram_request($request) {
385 + $secret_token = $this->options['telegram_webhook_secret'] ?? '';
386 +
387 + //error_log('[MxChat Telegram DEBUG] verify_telegram_request called');
388 + //error_log('[MxChat Telegram DEBUG] Stored secret: ' . (empty($secret_token) ? 'EMPTY' : substr($secret_token, 0, 10) . '...'));
389 +
390 + if (empty($secret_token)) {
391 + // If no secret is configured, allow the request (for initial setup)
392 + //error_log('[MxChat Telegram DEBUG] No secret configured, allowing request');
393 + return true;
394 + }
395 +
396 + // Telegram sends the secret token in the X-Telegram-Bot-Api-Secret-Token header
397 + $request_token = $request->get_header('X-Telegram-Bot-Api-Secret-Token');
398 +
399 + //error_log('[MxChat Telegram DEBUG] Request token: ' . (empty($request_token) ? 'EMPTY' : substr($request_token, 0, 10) . '...'));
400 +
401 + if (empty($request_token)) {
402 + //error_log('[MxChat Telegram DEBUG] Request rejected: No token in header');
403 + return false;
404 + }
405 +
406 + // Timing-safe comparison
407 + $result = hash_equals($secret_token, $request_token);
408 + //error_log('[MxChat Telegram DEBUG] Token comparison result: ' . ($result ? 'MATCH' : 'MISMATCH'));
409 + return $result;
410 +}
411 +
264 412 public function mxchat_stream_events(WP_REST_Request $request) {
265 413 header('Content-Type: text/event-stream');
266 414 header('Cache-Control: no-cache');
267 415 header('Connection: keep-alive');
@@ -295,20 +443,45 @@
295 443
296 444
297 445
298 446
299 -private function mxchat_save_chat_message($session_id, $role, $message) {
447 +private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null, $rag_context = null) {
300 448 global $wpdb;
301 -
302 449 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
303 450 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
304 -
451 +
452 + // Check if this is the first message in a new session (before any other database operations)
453 + $is_new_session = false;
454 + if ($role === 'user') { // Only check for user messages, not bot responses
455 + $existing_messages = $wpdb->get_var($wpdb->prepare(
456 + "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
457 + $session_id
458 + ));
459 + $is_new_session = ($existing_messages == 0);
460 +
461 + // Log for debugging
462 + if ($is_new_session) {
463 + //error_log("[DEBUG] This is a NEW session - first message");
464 + }
465 + }
466 +
467 + // SECURITY FIX: Set session ownership for new sessions
468 + if ($is_new_session && $role === 'user') {
469 + $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
470 + $session_owner_key = "mxchat_session_owner_{$session_id}";
471 +
472 + // Only set ownership if not already set
473 + if (!get_option($session_owner_key)) {
474 + update_option($session_owner_key, $current_user_identifier, 'no');
475 + //error_log("[DEBUG] Set session ownership for {$session_id} to {$current_user_identifier}");
476 + }
477 + }
478 +
305 479 // 1) Extract agent name if present
306 480 $agent_name = '';
307 481 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
308 482 $agent_name = $matches[1];
309 483 $message = str_replace("Agent: $agent_name - ", '', $message);
310 -
311 484 $session_meta_key = "mxchat_agent_name_{$session_id}";
312 485 if (empty(get_option($session_meta_key))) {
313 486 update_option($session_meta_key, $agent_name);
314 487 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
@@ -313,42 +486,57 @@
313 486 update_option($session_meta_key, $agent_name);
314 487 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
315 488 }
316 489 }
317 -
490 +
318 491 // 2) Generate unique message_id
319 492 $message_id = uniqid();
320 493 //error_log("[DEBUG] mxchat_save_chat_message -> Generated message_id: {$message_id}");
321 -
494 +
322 495 // 3) Determine user_id
323 496 $user_id = is_user_logged_in() ? get_current_user_id() : 0;
324 -
497 +
325 498 // 4) Determine user_identifier
326 499 $user_identifier = $agent_name
327 500 ? $agent_name
328 501 : MxChat_User::mxchat_get_user_identifier();
329 -
502 +
330 503 // 5) Determine displayed_name
331 504 $user_email = MxChat_User::mxchat_get_user_email();
332 505 $displayed_name = $agent_name ? $agent_name : ($user_email ?: $user_identifier);
333 -
506 +
334 507 // 6) Check for a saved email in wp_options
335 508 $email_option_key = "mxchat_email_{$session_id}";
336 509 $saved_email = get_option($email_option_key);
337 510 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for email_option_key: {$email_option_key}, found: {$saved_email}");
338 -
339 - // If found, update DB user_email
340 - if ($saved_email) {
341 - $update_res = $wpdb->update(
342 - $table_name,
343 - ['user_email' => $saved_email],
344 - ['session_id' => $session_id],
345 - ['%s'],
346 - ['%s']
347 - );
348 - //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email update for session_id {$session_id}. update_res: {$update_res}");
511 +
512 + // Check for a saved name in wp_options
513 + $name_option_key = "mxchat_name_{$session_id}";
514 + $saved_name = get_option($name_option_key);
515 + //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for name_option_key: {$name_option_key}, found: {$saved_name}");
516 +
517 + // If found, update DB user_email and user_name
518 + if ($saved_email || $saved_name) {
519 + $update_data = [];
520 + if ($saved_email) {
521 + $update_data['user_email'] = $saved_email;
522 + }
523 + if ($saved_name) {
524 + $update_data['user_name'] = $saved_name;
525 + }
526 +
527 + if (!empty($update_data)) {
528 + $update_res = $wpdb->update(
529 + $table_name,
530 + $update_data,
531 + ['session_id' => $session_id],
532 + array_fill(0, count($update_data), '%s'),
533 + ['%s']
534 + );
535 + //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email/user_name update for session_id {$session_id}. update_res: {$update_res}");
536 + }
349 537 }
350 -
538 +
351 539 // 7) Save to session history in wp_options
352 540 $history_key = "mxchat_history_{$session_id}";
353 541 $history = get_option($history_key, []);
354 542 $history[] = [
@@ -357,34 +545,351 @@
357 545 'content' => $message,
358 546 'timestamp' => round(microtime(true) * 1000),
359 547 'agent_name' => $displayed_name,
360 548 ];
361 - update_option($history_key, $history);
549 + update_option($history_key, $history, 'no');
362 550 //error_log("[DEBUG] mxchat_save_chat_message -> Updated session history in option: {$history_key}");
363 -
551 +
364 552 // 8) Save the message to DB (INSERT)
365 553 $insert_data = [
366 554 'user_id' => $user_id,
367 555 'user_identifier'=> $user_identifier,
368 556 'user_email' => $saved_email ?: $user_email,
557 + 'user_name' => $saved_name ?: '', // Add name to insert data
369 558 'session_id' => $session_id,
370 559 'role' => $role,
371 560 'message' => $message,
372 561 'timestamp' => current_time('mysql', 1),
373 562 ];
563 +
564 + // IMPROVED: Handle originating page data
565 + $columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
566 +
567 + if ($columns_exist) {
568 + if ($is_new_session && $role === 'user') {
569 + // For the first user message, set originating page data
570 +
571 + // First check if we have it from the parameter
572 + if ($originating_page && !empty($originating_page['url'])) {
573 + $insert_data['originating_page_url'] = $originating_page['url'];
574 + $insert_data['originating_page_title'] = $originating_page['title'] ?? '';
575 +
576 + //error_log("[DEBUG] Setting originating page from parameter: " . $originating_page['url']);
577 + }
578 + // Otherwise check if it's stored in the instance property
579 + else if (isset($this->pending_originating_page) && !empty($this->pending_originating_page['url'])) {
580 + $insert_data['originating_page_url'] = $this->pending_originating_page['url'];
581 + $insert_data['originating_page_title'] = $this->pending_originating_page['title'] ?? '';
582 +
583 + //error_log("[DEBUG] Setting originating page from pending_originating_page: " . $this->pending_originating_page['url']);
584 +
585 + // Clear after using
586 + unset($this->pending_originating_page);
587 + }
588 + // Fallback to HTTP_REFERER if nothing else is available
589 + else if (isset($_SERVER['HTTP_REFERER'])) {
590 + $referer_url = esc_url_raw($_SERVER['HTTP_REFERER']);
591 + $insert_data['originating_page_url'] = $referer_url;
592 +
593 + // Generate title from URL
594 + $parsed_url = parse_url($referer_url);
595 + $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
596 +
597 + if (empty($path) || $path === 'index.php' || $path === 'index.html') {
598 + $insert_data['originating_page_title'] = 'Homepage';
599 + } else {
600 + $title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
601 + $insert_data['originating_page_title'] = ucwords(trim($title));
602 + }
603 +
604 + //error_log("[DEBUG] Setting originating page from HTTP_REFERER: " . $referer_url);
605 + }
606 +
607 + // Store for this session so all messages have the same originating page
608 + if (!empty($insert_data['originating_page_url'])) {
609 + update_option("mxchat_originating_page_{$session_id}", [
610 + 'url' => $insert_data['originating_page_url'],
611 + 'title' => $insert_data['originating_page_title']
612 + ], 'no');
613 + }
614 + } else {
615 + // For subsequent messages in the session, use the stored originating page
616 + $stored_originating = get_option("mxchat_originating_page_{$session_id}");
617 + if ($stored_originating && !empty($stored_originating['url'])) {
618 + $insert_data['originating_page_url'] = $stored_originating['url'];
619 + $insert_data['originating_page_title'] = $stored_originating['title'] ?? '';
620 + }
621 + }
622 + }
623 +
624 + // Add RAG context if provided (for bot messages)
625 + if ($rag_context !== null && $role === 'bot') {
626 + $rag_context_column_exists = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'rag_context'");
627 + if ($rag_context_column_exists) {
628 + $insert_data['rag_context'] = is_array($rag_context) ? wp_json_encode($rag_context) : $rag_context;
629 + }
630 + }
631 +
374 632 $wpdb->insert($table_name, $insert_data);
375 633 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
376 -
634 +
635 + // 9) Send notification email if this is the first user message in a new session
636 + if ($wpdb->insert_id && $is_new_session && $role === 'user') {
637 + $this->send_new_chat_notification($session_id, array(
638 + 'identifier' => $user_identifier,
639 + 'email' => $saved_email ?: $user_email,
640 + 'ip' => $_SERVER['REMOTE_ADDR']
641 + ));
642 + }
643 +
644 + // 10) Schedule delayed transcript email if enabled and message is from user
645 + if ($wpdb->insert_id && $role === 'user') {
646 + $this->schedule_delayed_transcript_email($session_id);
647 + }
648 +
377 649 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
378 650 return $message_id;
379 651 }
380 652
653 +private function send_new_chat_notification($session_id, $user_info = array()) {
654 + $options = get_option('mxchat_transcripts_options');
655 +
656 + // Check if notifications are enabled
657 + if (empty($options['mxchat_enable_notifications'])) {
658 + return false;
659 + }
660 +
661 + // Get notification email
662 + $to = !empty($options['mxchat_notification_email']) ?
663 + $options['mxchat_notification_email'] :
664 + get_option('admin_email');
665 +
666 + if (!is_email($to)) {
667 + return false;
668 + }
669 +
670 + // Prepare email content
671 + $subject = sprintf('[%s] New Chat Session Started', get_bloginfo('name'));
672 +
673 + $user_identifier = isset($user_info['identifier']) ? $user_info['identifier'] : 'Guest';
674 + $user_email = isset($user_info['email']) ? $user_info['email'] : 'Not provided';
675 + $user_ip = isset($user_info['ip']) ? $user_info['ip'] : $_SERVER['REMOTE_ADDR'];
676 +
677 + $message = sprintf(
678 + "A new chat session has started on your website.\n\n" .
679 + "Session ID: %s\n" .
680 + "User: %s\n" .
681 + "Email: %s\n" .
682 + "IP Address: %s\n" .
683 + "Time: %s\n\n" .
684 + "View transcripts: %s",
685 + $session_id,
686 + $user_identifier,
687 + $user_email,
688 + $user_ip,
689 + current_time('mysql'),
690 + admin_url('admin.php?page=mxchat-transcripts')
691 + );
692 +
693 + // Send email
694 + return wp_mail($to, $subject, $message);
695 +}
696 +
697 +/**
698 + * Schedule delayed transcript email for a session
699 + * Reschedules if a new user message is received
700 + */
701 +private function schedule_delayed_transcript_email($session_id) {
702 + $options = get_option('mxchat_transcripts_options');
703 +
704 + // Check if auto-email is enabled
705 + if (empty($options['mxchat_auto_email_transcript_enabled'])) {
706 + return;
707 + }
708 +
709 + // Get notification email
710 + $email = !empty($options['mxchat_notification_email']) ?
711 + $options['mxchat_notification_email'] :
712 + get_option('admin_email');
713 +
714 + if (!is_email($email)) {
715 + return;
716 + }
717 +
718 + // Get delay in minutes (default 30)
719 + $delay_minutes = isset($options['mxchat_auto_email_transcript_delay']) ?
720 + intval($options['mxchat_auto_email_transcript_delay']) : 30;
721 +
722 + // Clear any existing scheduled event for this session
723 + $hook = 'mxchat_send_delayed_transcript';
724 + $args = array($session_id);
725 + $timestamp = wp_next_scheduled($hook, $args);
726 +
727 + if ($timestamp) {
728 + wp_unschedule_event($timestamp, $hook, $args);
729 + }
730 +
731 + // Schedule new event
732 + $schedule_time = time() + ($delay_minutes * 60);
733 + wp_schedule_single_event($schedule_time, $hook, $args);
734 +}
735 +
736 +/**
737 + * Check if chat messages contain contact information (email or phone number)
738 + *
739 + * @param array $messages Array of message objects with 'message' property
740 + * @param object|null $session_data Session data object with user_email property
741 + * @return bool True if contact info found, false otherwise
742 + */
743 +private function chat_contains_contact_info($messages, $session_data = null) {
744 + // Check if session already has a stored email
745 + if ($session_data && !empty($session_data->user_email)) {
746 + return true;
747 + }
748 +
749 + // Email regex pattern
750 + $email_pattern = '/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/';
751 +
752 + // Phone number patterns (covers various formats including international, WhatsApp style)
753 + // Matches: +1234567890, (123) 456-7890, 123-456-7890, 123.456.7890, 1234567890, +1 234 567 8900, etc.
754 + $phone_pattern = '/(?:\+?\d{1,3}[-.\s]?)?\(?\d{2,4}\)?[-.\s]?\d{2,4}[-.\s]?\d{2,4}(?:[-.\s]?\d{1,4})?/';
755 +
756 + // Only check user messages (not assistant responses)
757 + foreach ($messages as $msg) {
758 + if ($msg->role !== 'user') {
759 + continue;
760 + }
761 +
762 + $message_text = $msg->message;
763 +
764 + // Check for email
765 + if (preg_match($email_pattern, $message_text)) {
766 + return true;
767 + }
768 +
769 + // Check for phone number (must be at least 7 digits total to avoid false positives)
770 + if (preg_match($phone_pattern, $message_text, $matches)) {
771 + // Count actual digits to avoid matching short numbers
772 + $digits_only = preg_replace('/\D/', '', $matches[0]);
773 + if (strlen($digits_only) >= 7) {
774 + return true;
775 + }
776 + }
777 + }
778 +
779 + return false;
780 +}
781 +
782 +/**
783 + * Send the delayed transcript email with .txt attachment
784 + */
785 +public function mxchat_send_delayed_transcript($session_id) {
786 + global $wpdb;
787 +
788 + $options = get_option('mxchat_transcripts_options');
789 +
790 + // Get notification email
791 + $to = !empty($options['mxchat_notification_email']) ?
792 + $options['mxchat_notification_email'] :
793 + get_option('admin_email');
794 +
795 + if (!is_email($to)) {
796 + return false;
797 + }
798 +
799 + // Get all messages for this session
800 + $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
801 + $messages = $wpdb->get_results($wpdb->prepare(
802 + "SELECT role, message, timestamp FROM {$table_name}
803 + WHERE session_id = %s
804 + ORDER BY timestamp ASC",
805 + $session_id
806 + ));
807 +
808 + if (empty($messages)) {
809 + return false;
810 + }
811 +
812 + // Get session metadata
813 + $sessions_table = $wpdb->prefix . 'mxchat_sessions';
814 + $session_data = $wpdb->get_row($wpdb->prepare(
815 + "SELECT * FROM {$sessions_table} WHERE session_id = %s",
816 + $session_id
817 + ));
818 +
819 + // Check if contact info is required and if it's present
820 + $require_contact = !empty($options['mxchat_auto_email_transcript_require_contact']);
821 + if ($require_contact && !$this->chat_contains_contact_info($messages, $session_data)) {
822 + // Contact info required but not found - skip sending
823 + return false;
824 + }
825 +
826 + // Build transcript content
827 + $transcript_content = "Chat Transcript\n";
828 + $transcript_content .= "================\n\n";
829 + $transcript_content .= "Session ID: " . $session_id . "\n";
830 +
831 + if ($session_data) {
832 + $transcript_content .= "User: " . ($session_data->user_identifier ?: 'Guest') . "\n";
833 + $transcript_content .= "Email: " . ($session_data->user_email ?: 'Not provided') . "\n";
834 + $transcript_content .= "Started: " . $session_data->created_at . "\n";
835 + }
836 +
837 + $transcript_content .= "\n" . str_repeat("=", 50) . "\n\n";
838 +
839 + // Add messages
840 + foreach ($messages as $msg) {
841 + $role_label = ($msg->role === 'user') ? 'User' : 'Assistant';
842 + $transcript_content .= "[{$msg->timestamp}] {$role_label}:\n";
843 + $transcript_content .= $msg->message . "\n\n";
844 + }
845 +
846 + // Create temporary file for attachment using WP_Filesystem
847 + $upload_dir = wp_upload_dir();
848 + $temp_file = $upload_dir['basedir'] . '/mxchat-transcript-' . $session_id . '.txt';
849 + global $wp_filesystem;
850 + if (empty($wp_filesystem)) {
851 + require_once ABSPATH . 'wp-admin/includes/file.php';
852 + WP_Filesystem();
853 + }
854 + $wp_filesystem->put_contents($temp_file, $transcript_content, FS_CHMOD_FILE);
855 +
856 + // Prepare email
857 + $subject = sprintf('[%s] Chat Transcript - Session %s', get_bloginfo('name'), substr($session_id, 0, 8));
858 +
859 + $message = "Please find attached the full chat transcript.\n\n";
860 + $message .= "Session ID: {$session_id}\n";
861 +
862 + if ($session_data) {
863 + $message .= "User: " . ($session_data->user_identifier ?: 'Guest') . "\n";
864 + $message .= "Email: " . ($session_data->user_email ?: 'Not provided') . "\n";
865 + }
866 +
867 + $message .= "\nView online: " . admin_url('admin.php?page=mxchat-transcripts');
868 +
869 + // Send email with attachment
870 + $attachments = array($temp_file);
871 + $result = wp_mail($to, $subject, $message, '', $attachments);
872 +
873 + // Clean up temporary file
874 + if (file_exists($temp_file)) {
875 + unlink($temp_file);
876 + }
877 +
878 + return $result;
879 +}
880 +
881 +
882 +
381 883 public function mxchat_handle_save_email_and_response() {
382 884 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
885 + //error_log('DEBUG: POST data: ' . print_r($_POST, true));
383 886
887 + nocache_headers();
888 +
384 889 // Validate nonce
385 890 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
386 - error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
891 + //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
387 892 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
388 893 wp_die();
389 894 }
390 895
@@ -389,22 +894,41 @@
389 894 }
390 895
391 896 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
392 897 $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
898 + $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
393 899
394 - //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}");
900 + //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}, name: {$name}");
395 901
396 - if (empty($session_id) || empty($email)) {
902 + if (empty($session_id) || $session_id === 'null' || empty($email)) {
397 903 //error_log("[ERROR] Missing session_id or email: session_id={$session_id}, email={$email}");
398 904 wp_send_json_error(['message' => esc_html__('Session ID or email is missing.', 'mxchat')]);
399 905 wp_die();
400 906 }
401 907
402 - // 1) Always store in wp_options
403 - $option_key = "mxchat_email_{$session_id}";
404 - update_option($option_key, $email);
405 - //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$option_key} => {$email}");
908 + // Validate name if provided (check if name field is enabled and name is required)
909 + $options = get_option('mxchat_options', []);
910 + $name_field_enabled = isset($options['enable_name_field']) &&
911 + ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
912 +
913 + if ($name_field_enabled && (empty($name) || strlen(trim($name)) < 2 || strlen(trim($name)) > 100)) {
914 + //error_log("[ERROR] Invalid name: {$name} (enabled: {$name_field_enabled})");
915 + wp_send_json_error(['message' => esc_html__('Name must be between 2 and 100 characters.', 'mxchat')]);
916 + wp_die();
917 + }
406 918
919 + // 1) Always store email in wp_options
920 + $email_option_key = "mxchat_email_{$session_id}";
921 + update_option($email_option_key, $email, 'no');
922 + //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$email_option_key} => {$email}");
923 +
924 + // Store name in wp_options if provided
925 + if (!empty($name)) {
926 + $name_option_key = "mxchat_name_{$session_id}";
927 + update_option($name_option_key, $name, 'no');
928 + //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$name_option_key} => {$name}");
929 + }
930 +
407 931 // 2) (Optional) Also store in DB if a row already exists
408 932 global $wpdb;
409 933 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
410 934
@@ -414,21 +938,30 @@
414 938
415 939 //error_log("[DEBUG] handle_save_email_and_response -> session_count for {$session_id}: {$session_count} (SQL: {$sql})");
416 940
417 941 if ($session_count) {
418 - // Update user_email if row(s) exist
419 - $update_sql = $wpdb->prepare(
420 - "UPDATE {$table_name} SET user_email = %s WHERE session_id = %s",
421 - $email,
422 - $session_id
423 - );
942 + // Update both user_email and user_name if row(s) exist
943 + if (!empty($name)) {
944 + $update_sql = $wpdb->prepare(
945 + "UPDATE {$table_name} SET user_email = %s, user_name = %s WHERE session_id = %s",
946 + $email,
947 + $name,
948 + $session_id
949 + );
950 + } else {
951 + $update_sql = $wpdb->prepare(
952 + "UPDATE {$table_name} SET user_email = %s WHERE session_id = %s",
953 + $email,
954 + $session_id
955 + );
956 + }
424 957 $wpdb->query($update_sql);
425 958 //error_log("[DEBUG] handle_save_email_and_response -> DB updated: {$update_sql}");
426 959 } else {
427 - //error_log("[INFO] handle_save_email_and_response -> No DB entry for {$session_id}, so email is only in wp_options.");
960 + //error_log("[INFO] handle_save_email_and_response -> No DB entry for {$session_id}, so email/name is only in wp_options.");
428 961 }
429 962
430 - // Provide success response
963 + // Provide success response (same as original)
431 964 $bot_message = __('Thanks for providing your email! You can continue chatting now.', 'mxchat');
432 965 //error_log("[DEBUG] handle_save_email_and_response -> success, returning bot_message: {$bot_message}");
433 966 wp_send_json_success(['message' => $bot_message]);
434 967 wp_die();
@@ -436,8 +969,10 @@
436 969
437 970 public function mxchat_check_email_provided() {
438 971 //error_log('[DEBUG] ---------- mxchat_check_email_provided START ----------');
439 972
973 + nocache_headers();
974 +
440 975 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
441 976 //error_log('[ERROR] Invalid nonce in mxchat_check_email_provided');
442 977 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
443 978 }
@@ -442,9 +977,9 @@
442 977 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
443 978 }
444 979
445 980 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
446 - if (empty($session_id)) {
981 + if (empty($session_id) || $session_id === 'null') {
447 982 //error_log('[ERROR] No session ID provided in mxchat_check_email_provided');
448 983 wp_send_json_error(['message' => esc_html__('No session ID provided', 'mxchat')]);
449 984 }
450 985
@@ -451,105 +986,82 @@
451 986 // Check if the user is logged in
452 987 if (is_user_logged_in()) {
453 988 $current_user = wp_get_current_user();
454 989 //error_log("[DEBUG] User is logged in as {$current_user->user_email}");
455 - wp_send_json_success(['logged_in' => true, 'email' => $current_user->user_email]);
990 +
991 + // Get user's display name for logged in users
992 + $user_name = !empty($current_user->display_name) ? $current_user->display_name :
993 + (!empty($current_user->first_name) ? $current_user->first_name : '');
994 +
995 + $response_data = ['logged_in' => true, 'email' => $current_user->user_email];
996 + if (!empty($user_name)) {
997 + $response_data['name'] = $user_name;
998 + }
999 +
1000 + wp_send_json_success($response_data);
456 1001 }
457 1002
458 - $option_key = "mxchat_email_{$session_id}";
459 - $stored_email = get_option($option_key, '');
1003 + // Check if name field is required
1004 + $options = get_option('mxchat_options', []);
1005 + $name_field_enabled = isset($options['enable_name_field']) &&
1006 + ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
460 1007
461 - //error_log("[DEBUG] mxchat_check_email_provided -> Checking option: {$option_key}, found: {$stored_email}");
1008 + $email_option_key = "mxchat_email_{$session_id}";
1009 + $stored_email = get_option($email_option_key, '');
1010 +
1011 + // Check for stored name
1012 + $name_option_key = "mxchat_name_{$session_id}";
1013 + $stored_name = get_option($name_option_key, '');
462 1014
463 - if (!empty($stored_email)) {
464 - //error_log("[DEBUG] mxchat_check_email_provided -> Email found, returning success");
465 - wp_send_json_success(['email' => $stored_email]);
466 - } else {
467 - //error_log("[DEBUG] mxchat_check_email_provided -> No email found, returning error");
468 - wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
469 - }
470 -}
1015 + //error_log("[DEBUG] mxchat_check_email_provided -> Checking email option: {$email_option_key}, found: {$stored_email}");
1016 + //error_log("[DEBUG] mxchat_check_email_provided -> Checking name option: {$name_option_key}, found: {$stored_name}, required: " . ($name_field_enabled ? 'yes' : 'no'));
471 1017
472 -
473 -// First, add this helper function to get the highest rate limit for a user's roles
474 -private function get_user_role_rate_limit($user_id) {
475 - //error_log(esc_html__("Checking rate limit for user ID: ", 'mxchat') . $user_id);
476 -
477 - if (!$user_id) {
478 - //error_log(esc_html__("No user ID provided, using logged-out limit: ", 'mxchat') . ($this->options['rate_limit_logged_out'] ?? '10'));
479 - return $this->options['rate_limit_logged_out'] ?? '10';
1018 + // Check if we have email and name (if name is required)
1019 + $has_required_info = !empty($stored_email);
1020 +
1021 + if ($name_field_enabled) {
1022 + $has_required_info = $has_required_info && !empty($stored_name);
480 1023 }
481 1024
482 - $user = get_userdata($user_id);
483 - if (!$user || !$user->roles) {
484 - //error_log(esc_html__("No user data or roles found, using logged-out limit: ", 'mxchat') . ($this->options['rate_limit_logged_out'] ?? '10'));
485 - return $this->options['rate_limit_logged_out'] ?? '10';
486 - }
487 -
488 - //error_log(esc_html__("User roles: ", 'mxchat') . print_r($user->roles, true));
489 - //error_log(esc_html__("Available role rate limits: ", 'mxchat') . print_r($this->options['role_rate_limits'] ?? [], true));
490 -
491 - $max_limit = 0;
492 - foreach ($user->roles as $role) {
493 - //error_log(esc_html__("Checking limit for role: ", 'mxchat') . $role);
494 - if (isset($this->options['role_rate_limits'][$role])) {
495 - $role_limit = $this->options['role_rate_limits'][$role];
496 - //error_log(esc_html__("Found limit for role ", 'mxchat') . $role . esc_html__(": ", 'mxchat') . $role_limit);
497 -
498 - if ($role_limit === 'unlimited') {
499 - //error_log(esc_html__("Returning unlimited for role: ", 'mxchat') . $role);
500 - return 'unlimited';
501 - }
502 -
503 - $max_limit = max($max_limit, (int)$role_limit);
504 - //error_log(esc_html__("Current max limit: ", 'mxchat') . $max_limit);
505 - } else {
506 - //error_log(esc_html__("No limit found for role: ", 'mxchat') . $role);
1025 + if ($has_required_info) {
1026 + //error_log("[DEBUG] mxchat_check_email_provided -> Required info found, returning success");
1027 +
1028 + $response_data = ['email' => $stored_email];
1029 + if (!empty($stored_name)) {
1030 + $response_data['name'] = $stored_name;
507 1031 }
1032 +
1033 + wp_send_json_success($response_data);
1034 + } else {
1035 + //error_log("[DEBUG] mxchat_check_email_provided -> Required info missing, returning error");
1036 + wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
508 1037 }
509 -
510 - $final_limit = $max_limit > 0 ? (string)$max_limit : '100';
511 - //error_log(esc_html__("Final rate limit: ", 'mxchat') . $final_limit);
512 - return $final_limit;
513 1038 }
514 1039
515 -
516 -// Add this to your plugin's main PHP file
517 -public function mxchat_check_new_messages() {
518 - if (!isset($_POST['session_id']) || !isset($_POST['last_seen_id'])) {
519 - wp_send_json_error(['message' => 'Missing required parameters']);
520 - wp_die();
521 - }
522 -
523 - $session_id = sanitize_text_field($_POST['session_id']);
524 - $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
525 -
526 - // Get chat history
527 - $history = get_option("mxchat_history_{$session_id}", []);
528 -
529 - if (empty($history)) {
530 - wp_send_json_success([
531 - 'hasNewMessages' => false,
532 - 'new_messages' => []
1040 +/**
1041 + * Send error response in appropriate format based on streaming mode
1042 + * ADDED: Helper method to consistently handle errors in both streaming and non-streaming modes
1043 + *
1044 + * @param string $error_message The error message to display
1045 + * @param string $error_code Optional error code for debugging
1046 + */
1047 +private function send_error_response($error_message, $error_code = 'api_error') {
1048 + if ($this->is_streaming) {
1049 + echo "data: " . json_encode([
1050 + 'error' => true,
1051 + 'error_message' => $error_message,
1052 + 'error_code' => $error_code,
1053 + 'text' => $error_message,
1054 + 'message' => $error_message
1055 + ]) . "\n\n";
1056 + echo "data: [DONE]\n\n";
1057 + flush();
1058 + } else {
1059 + wp_send_json_error([
1060 + 'error_message' => $error_message,
1061 + 'error_code' => $error_code
533 1062 ]);
534 - wp_die();
535 1063 }
536 -
537 - // Filter new messages
538 - $new_messages = array_filter($history, function($message) use ($last_seen_id) {
539 - return isset($message['id']) && $message['id'] > $last_seen_id;
540 - });
541 -
542 - // Sort by ID to ensure proper order
543 - usort($new_messages, function($a, $b) {
544 - return $a['id'] <=> $b['id'];
545 - });
546 -
547 - wp_send_json_success([
548 - 'hasNewMessages' => !empty($new_messages),
549 - 'new_messages' => array_values($new_messages),
550 - 'latestMessageId' => end($new_messages)['id'] ?? $last_seen_id
551 - ]);
552 1064 wp_die();
553 1065 }
554 1066
555 1067 public function mxchat_handle_chat_request() {
@@ -554,10 +1066,30 @@
554 1066
555 1067 public function mxchat_handle_chat_request() {
556 1068 global $wpdb;
557 1069
1070 + // Debug: Log incoming bot_id
1071 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
1072 + //error_log("=== MXCHAT DEBUG: Starting chat request ===");
1073 + //error_log("MXCHAT DEBUG: Bot ID received: " . $bot_id);
1074 +
1075 + // Get bot-specific options
1076 + $bot_options = $this->get_bot_options($bot_id);
1077 + $current_options = !empty($bot_options) ? $bot_options : $this->options;
558 1078
559 - // Check if MX Chat Moderation is active
1079 + // Check if this is a streaming request
1080 + // Allow force_streaming_test parameter to bypass the setting check (for admin compatibility testing)
1081 + $force_streaming_test = isset($_POST['force_streaming_test']) && $_POST['force_streaming_test'] === '1' && current_user_can('administrator');
1082 + $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat' &&
1083 + ($force_streaming_test || (isset($current_options['enable_streaming_toggle']) && $current_options['enable_streaming_toggle'] === 'on'));
1084 +
1085 + // ADDED: Store streaming state in class property for use in private methods
1086 + $this->is_streaming = $is_streaming;
1087 +
1088 + // NOTE: Streaming headers are now set later via setup_streaming_headers()
1089 + // This allows actions/forms to return JSON responses without header conflicts
1090 +
1091 + // Check if MX Chat Moderation is active
560 1092 if (class_exists('MX_Chat_Moderation')) {
561 1093 // Get user email and IP
562 1094 $user_email = '';
563 1095 $user_ip = $_SERVER['REMOTE_ADDR'];
@@ -591,14 +1123,12 @@
591 1123 wp_die();
592 1124 }
593 1125 }
594 1126
595 -
596 - // Reset fallback response at the start of each request
597 1127 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
598 1128 $this->productCardHtml = '';
599 1129
600 - // Get the actual WordPress user ID if logged in
1130 + // Get the actual WordPress user ID if logged in
601 1131 $is_logged_in = is_user_logged_in();
602 1132 if ($is_logged_in) {
603 1133 $user_id = get_current_user_id(); // This will get the actual WordPress user ID
604 1134 } else {
@@ -608,179 +1138,333 @@
608 1138
609 1139 // Get and sanitize the user identifier
610 1140 $user_id = sanitize_key($user_id);
611 1141
612 - // Determine if user is logged in
613 - $is_logged_in = is_user_logged_in();
614 - //error_log("User logged in status: " . ($is_logged_in ? 'true' : 'false'));
1142 + // Check rate limit using new settings structure
1143 + $rate_limit_result = $this->check_rate_limit();
615 1144
616 - // Get rate limit based on user status
617 - // Get rate limit based on user status
618 - $rate_limit = $is_logged_in
619 - ? $this->get_user_role_rate_limit($user_id)
620 - : ($this->options['rate_limit_logged_out'] ?? '10');
621 -
622 - //error_log("Selected rate limit: " . $rate_limit);
623 -
624 - // Rest of your code remains the same
625 - // If rate limit is 'unlimited', skip rate limiting checks
626 - if ($rate_limit !== 'unlimited') {
627 - // Convert rate limit to integer
628 - $rate_limit = intval($rate_limit);
629 - // Setup rate limiting
630 - $rate_limit_transient_key = 'mxchat_chat_limit_' . $user_id;
631 - $chat_count = get_transient($rate_limit_transient_key);
632 - if ($chat_count === false) {
633 - // Initialize new counter if none exists
634 - $chat_count = 0;
635 - }
636 - // Check if user has exceeded their rate limit
637 - if ($chat_count >= $rate_limit) {
638 - // Get custom rate limit message or use default
639 - $rate_limit_message = isset($this->options['rate_limit_message'])
640 - ? $this->options['rate_limit_message']
641 - : esc_html__('Rate limit exceeded. Please try again later.', 'mxchat');
642 - // Replace placeholder if it exists in the message
643 - $rate_limit_message = str_replace(
644 - array('{limit}', '{count}', '{remaining}'),
645 - array($rate_limit, $chat_count, max(0, $rate_limit - $chat_count)),
646 - $rate_limit_message
647 - );
648 - wp_send_json([
649 - 'success' => false,
650 - 'message' => $rate_limit_message,
651 - 'status' => 'rate_limit_exceeded',
652 - 'limit' => $rate_limit,
653 - 'count' => $chat_count
654 - ]);
655 - wp_die();
656 - }
657 - // Increment the counter
658 - $chat_count++;
659 - // Store the updated count with 24-hour expiration
660 - set_transient($rate_limit_transient_key, $chat_count, DAY_IN_SECONDS);
1145 + if ($rate_limit_result !== true) {
1146 + wp_send_json([
1147 + 'success' => false,
1148 + 'message' => $rate_limit_result['message'],
1149 + 'status' => 'rate_limit_exceeded'
1150 + ]);
1151 + wp_die();
661 1152 }
662 1153
663 1154 // Rest of your existing code...
664 1155 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
665 - //error_log("Session ID: $session_id");
666 1156
667 1157 if (empty($session_id)) {
668 - //error_log("Error: Session ID is missing.");
669 1158 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
670 1159 wp_die();
671 1160 }
672 1161
1162 + // SECURITY FIX: Verify session ownership before processing chat request
1163 + // If IP/user changed, signal frontend to reset session instead of blocking
1164 + $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
1165 + $session_owner = get_option("mxchat_session_owner_{$session_id}");
1166 +
1167 + if ($session_owner && $session_owner !== $current_user_identifier) {
1168 + // Instead of blocking, tell frontend to start a fresh session
1169 + wp_send_json_error([
1170 + 'message' => esc_html__('Your session has expired. Starting a new conversation.', 'mxchat'),
1171 + 'code' => 'session_expired',
1172 + 'action' => 'reset_session'
1173 + ]);
1174 + wp_die();
1175 + }
1176 +
673 1177 // Validate and sanitize the incoming message
674 1178 if (empty($_POST['message'])) {
675 - //error_log("Error: No message received.");
676 1179 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
677 1180 wp_die();
678 1181 }
1182 +
1183 +
1184 + // Track originating page for first message in session
1185 + $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
679 1186
1187 + // Check if originating page columns exist
1188 + $columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
680 1189
681 -// Modify the message sanitization to preserve PHP tags in code blocks
682 -$allowed_tags = [
683 - 'pre' => [],
684 - 'code' => ['class' => true],
685 - 'span' => ['class' => true],
686 - 'div' => ['class' => true],
687 -];
1190 + if ($columns_exist) {
1191 + // Check if this session already has messages
1192 + $message_count = $wpdb->get_var($wpdb->prepare(
1193 + "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
1194 + $session_id
1195 + ));
1196 +
1197 + // If this is the first message in the session
1198 + if ($message_count == 0) {
1199 + // Get originating page from JavaScript (preferred) or HTTP_REFERER (fallback)
1200 + $originating_url = '';
1201 + $originating_title = '';
1202 +
1203 + // Try to get from POST data first (sent by JavaScript)
1204 + if (isset($_POST['current_page_url'])) {
1205 + $originating_url = esc_url_raw($_POST['current_page_url']);
1206 + $originating_title = isset($_POST['current_page_title'])
1207 + ? sanitize_text_field($_POST['current_page_title'])
1208 + : '';
1209 + }
1210 + // Fallback to HTTP_REFERER if not provided by JavaScript
1211 + else if (isset($_SERVER['HTTP_REFERER'])) {
1212 + $originating_url = esc_url_raw($_SERVER['HTTP_REFERER']);
1213 + }
1214 +
1215 + // Generate title if we have URL but no title
1216 + if ($originating_url && empty($originating_title)) {
1217 + $parsed_url = parse_url($originating_url);
1218 + $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
1219 +
1220 + if (empty($path) || $path === 'index.php' || $path === 'index.html') {
1221 + $originating_title = 'Homepage';
1222 + } else {
1223 + // Clean up the path to make a readable title
1224 + $originating_title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
1225 + $originating_title = ucwords(trim($originating_title));
1226 + }
1227 + }
1228 +
1229 + // Store for later use when saving the message
1230 + $this->pending_originating_page = [
1231 + 'url' => $originating_url,
1232 + 'title' => $originating_title
1233 + ];
1234 + }
1235 + }
1236 +
1237 +
688 1238
689 -// First preserve code blocks
690 -$message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
691 - return htmlspecialchars_decode($matches[0]);
692 -}, $_POST['message']);
1239 + // Get page context if provided
1240 + $page_context = null;
1241 + if (isset($_POST['page_context']) && !empty($_POST['page_context'])) {
1242 + $page_context_raw = stripslashes($_POST['page_context']);
1243 + $page_context = json_decode($page_context_raw, true);
1244 +
1245 + // Validate page context structure
1246 + if (is_array($page_context) &&
1247 + isset($page_context['url']) &&
1248 + isset($page_context['title']) &&
1249 + isset($page_context['content'])) {
1250 +
1251 + // Sanitize page context
1252 + $page_context['url'] = esc_url_raw($page_context['url']);
1253 + $page_context['title'] = sanitize_text_field($page_context['title']);
1254 + $page_context['content'] = wp_kses_post($page_context['content']);
1255 + } else {
1256 + $page_context = null;
1257 + }
1258 + }
693 1259
694 -// Then apply sanitization
695 -$message = wp_kses($message, $allowed_tags);
1260 + // Modify the message sanitization to preserve PHP tags in code blocks
1261 + $allowed_tags = [
1262 + 'pre' => [],
1263 + 'code' => ['class' => true],
1264 + 'span' => ['class' => true],
1265 + 'div' => ['class' => true],
1266 + ];
696 1267
697 -// Decode code blocks
698 -$message = preg_replace_callback('/(&lt;pre&gt;&lt;code.*?&gt;.*?&lt;\/code&gt;&lt;\/pre&gt;)/s', function($matches) {
699 - return htmlspecialchars_decode($matches[1]);
700 -}, $message);
1268 + // First preserve code blocks
1269 + $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
1270 + return htmlspecialchars_decode($matches[0]);
1271 + }, $_POST['message']);
701 1272
702 -$message = trim($message);
1273 + // Then apply sanitization
1274 + $message = wp_kses($message, $allowed_tags);
703 1275
704 -// Preserve code blocks from markdown conversion
705 -$message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
1276 + // Preserve code blocks from markdown conversion
1277 + $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
1278 + $message = apply_filters('mxchat_filter_message', $message, 'prompt', $session_id);
706 1279
707 - // Save the user's message
708 - $this->mxchat_save_chat_message($session_id, 'user', $message);
1280 + // ===== SIMPLIFIED TESTING PANEL INITIALIZATION =====
1281 + // Always initialize testing data for admins (no toggle needed)
1282 + $testing_data = null;
1283 + if (current_user_can('administrator')) {
1284 + // For vision messages, use the original user message for the query display
1285 + $query_for_testing = $message;
1286 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
1287 + $query_for_testing = sanitize_textarea_field($_POST['original_user_message']);
1288 + }
1289 +
1290 + $testing_data = [
1291 + 'query' => $query_for_testing,
1292 + 'timestamp' => time(),
1293 + 'top_matches' => [],
1294 + 'action_matches' => [], // Initialize action matches array
1295 + 'page_context' => $page_context, // Include page context in testing data
1296 + 'is_vision' => isset($_POST['vision_processed']) && $_POST['vision_processed'],
1297 + 'bot_id' => $bot_id // Include bot ID in testing data
1298 + ];
1299 +
1300 + // Get similarity threshold from bot options or default options
1301 + $similarity_threshold = isset($current_options['similarity_threshold'])
1302 + ? ((int) $current_options['similarity_threshold']) / 100
1303 + : 0.35;
1304 +
1305 + $testing_data['similarity_threshold'] = $similarity_threshold;
1306 +
1307 + // Determine knowledge base type using bot-specific config
1308 + $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
1309 + $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
1310 + $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
1311 + }
1312 + // ===== END SIMPLIFIED TESTING INITIALIZATION =====
709 1313
710 - // Check if the message is an email address
711 - if (is_email($message)) {
712 - // Add the email to Loops
713 - $this->add_email_to_loops($message);
1314 + // Add debug before and after:
1315 + //error_log('MxChat Core: About to call mxchat_pre_process_message filter with message: ' . $message);
1316 + $pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
1317 + //error_log('MxChat Core: Filter returned: ' . (is_array($pre_processed_result) ? 'array' : $pre_processed_result));
714 1318
715 - // Send success response
716 - $response_message = $this->options['email_capture_response'] ??
717 - esc_html__('Thank you! Your coupon is on the way!', 'mxchat');
718 1319
719 - wp_send_json([
720 - 'success' => true,
721 - 'status' => 'email_captured',
722 - 'message' => $response_message
723 - ]);
724 - wp_die();
725 - }
1320 + // If the pre-processing returned a result (not the original message), use it directly
1321 + if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
1322 + // Save the AI response
1323 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
1324 +
1325 + // Save HTML content if provided
1326 + if (!empty($pre_processed_result['html'])) {
1327 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
1328 + }
1329 +
1330 + // Add testing data if admin
1331 + $response_data = [
1332 + 'text' => $pre_processed_result['text'],
1333 + 'html' => $pre_processed_result['html'] ?? '',
1334 + 'session_id' => $session_id
1335 + ];
1336 +
1337 + if ($testing_data !== null) {
1338 + $response_data['testing_data'] = $testing_data;
1339 + }
1340 +
1341 + wp_send_json($response_data);
1342 + wp_die();
1343 + }
726 1344
727 - $intent_info = '';
1345 + // Save the user's message - handle vision processed messages differently
1346 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
1347 + // For vision messages, save the original user message with image indicator
1348 + $original_message = sanitize_textarea_field($_POST['original_user_message']);
1349 + if (isset($_POST['vision_images_count']) && $_POST['vision_images_count'] > 0) {
1350 + $image_count = intval($_POST['vision_images_count']);
1351 + $original_message .= " [{$image_count} image(s)]";
1352 + }
1353 + $this->mxchat_save_chat_message($session_id, 'user', $original_message);
1354 + } else {
1355 + // Regular message - save as normal
1356 + $this->mxchat_save_chat_message($session_id, 'user', $message);
1357 + }
728 1358
729 - // Check chat mode
730 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
731 - //error_log("Chat Mode: $chat_mode");
1359 +
1360 + if (is_email($message)) {
1361 + // Add the email to Loops
1362 + $this->add_email_to_loops($message);
1363 +
1364 + // Get the user's success message instruction using current_options
1365 + $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
1366 +
1367 + // Set instruction for AI using the user's success message
1368 + $this->current_action_instruction = $user_success_message;
1369 +
1370 + // Clear the email capture transient since we got the email
1371 + delete_transient('mxchat_email_capture_' . $user_id);
1372 + }
1373 +
1374 + // Check if we're in an email capture flow but user hasn't provided email yet
1375 + elseif (get_transient('mxchat_email_capture_' . $user_id)) {
1376 + // Check if the message contains an email (not the whole message being an email)
1377 + if (preg_match('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', $message, $matches)) {
1378 + $extracted_email = $matches[0];
1379 +
1380 + // Add the extracted email to Loops
1381 + $this->add_email_to_loops($extracted_email);
1382 +
1383 + // Get the user's success message instruction using current_options
1384 + $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
1385 +
1386 + // Set instruction for AI using the user's success message
1387 + $this->current_action_instruction = $user_success_message;
1388 +
1389 + // Clear the email capture transient since we got the email
1390 + delete_transient('mxchat_email_capture_' . $user_id);
1391 + }
1392 + // If no email found but we're in capture mode, remind them
1393 + else {
1394 + // Get the original instruction to remind them using current_options
1395 + $original_instruction = $current_options['triggered_phrase_response'] ?? __("Please provide your email address.", 'mxchat');
1396 + $this->current_action_instruction = $original_instruction;
1397 + }
1398 + }
732 1399
733 - // Handle agent mode
734 - if ($chat_mode === 'agent') {
735 - // First, check for switch intent before doing anything else
736 - $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1400 + $intent_info = '';
737 1401
738 - // If we matched an intent and it's the switch intent, handle it
739 - if ($intent_matched && !empty($this->fallbackResponse['text'])) {
740 - //error_log("Switch to chatbot intent detected");
1402 + // Check chat mode
1403 + $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
741 1404
742 - // Update chat mode first
743 - update_option("mxchat_mode_{$session_id}", 'ai');
1405 + // Handle agent mode
1406 + // Handle agent mode
1407 + if ($chat_mode === 'agent') {
1408 + // First, check for switch intent before doing anything else
1409 + $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
744 1410
745 - // Clear any existing PDF context to start fresh
746 - $this->clear_pdf_transients($session_id);
1411 + // Capture action analysis for testing panel after intent check
1412 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1413 + $testing_data['action_matches'] = $this->last_action_analysis;
1414 + }
1415 +
1416 + // Around line 506, in the agent mode handling section:
1417 + if ($intent_matched && !empty($this->fallbackResponse['text'])) {
1418 + // Update chat mode first
1419 + update_option("mxchat_mode_{$session_id}", 'ai');
1420 +
1421 + // Clear any existing PDF context to start fresh
1422 + $this->clear_pdf_transients($session_id);
1423 +
1424 + // Prepare clean switch response with explicit chat_mode
1425 + $response_data = [
1426 + 'text' => $this->fallbackResponse['text'],
1427 + 'html' => $this->fallbackResponse['html'] ?? '',
1428 + 'session_id' => $session_id,
1429 + 'chat_mode' => 'ai' // EXPLICITLY SET THIS
1430 + ];
1431 +
1432 + if ($testing_data !== null) {
1433 + $response_data['testing_data'] = $testing_data;
1434 + }
1435 +
1436 + // Save the mode switch message
1437 + $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
1438 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1439 +
1440 + // Send response and exit
1441 + wp_send_json($response_data);
1442 + wp_die();
1443 + } elseif (!$intent_matched) {
1444 + // No intent matched, handle live agent message
1445 + try {
1446 + $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
747 1447
748 - // Prepare clean switch response
749 - $response_data = [
750 - 'text' => $this->fallbackResponse['text'],
751 - 'html' => '',
752 - 'session_id' => $session_id,
753 - 'chat_mode' => 'ai'
754 - ];
1448 + $agent_response = [
1449 + 'status' => 'waiting_for_agent',
1450 + 'message' => esc_html__('Message sent to live agent.', 'mxchat')
1451 + ];
1452 +
1453 + if ($testing_data !== null) {
1454 + $agent_response['testing_data'] = $testing_data;
1455 + }
755 1456
756 - // Save the mode switch message
757 - $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
758 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
759 -
760 - // Send response and exit
761 - wp_send_json($response_data);
762 - wp_die();
763 - } elseif (!$intent_matched) {
764 - // No intent matched, handle live agent message
765 - try {
766 - $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
767 - //error_log("Message sent to agent.");
768 -
769 - wp_send_json_success([
770 - 'status' => 'waiting_for_agent',
771 - 'message' => esc_html__('Message sent to live agent.', 'mxchat')
772 - ]);
773 - } catch (\Exception $e) {
774 - //error_log("Error sending message to agent: " . $e->getMessage());
775 - wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
1457 + wp_send_json_success($agent_response);
1458 + } catch (\Exception $e) {
1459 + wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
1460 + }
1461 + wp_die();
776 1462 }
777 - wp_die();
778 1463 }
779 - }
780 1464
781 1465 // Step 1: Check for new PDF URL in the message
782 - if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1466 + if (!isset($_POST['vision_processed']) && preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
783 1467 $new_pdf_url = $matches[0];
784 1468
785 1469 // Check if this is likely a PDF-related request
786 1470 $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
@@ -802,15 +1486,15 @@
802 1486
803 1487 // Clear previous PDF transients
804 1488 $this->clear_pdf_transients($session_id);
805 1489
806 - // Process new PDF
807 - $max_pages = $this->options['pdf_max_pages'] ?? 69;
1490 + // Process new PDF using current_options
1491 + $max_pages = $current_options['pdf_max_pages'] ?? 69;
808 1492 $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
809 1493
810 1494 if ($embeddings === 'too_many_pages') {
811 1495 $error_text = sprintf(
812 - $this->options['pdf_intent_error_text'] ??
1496 + $current_options['pdf_intent_error_text'] ??
813 1497 esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
814 1498 $max_pages
815 1499 );
816 1500 $this->fallbackResponse['text'] = $error_text;
@@ -815,15 +1499,13 @@
815 1499 );
816 1500 $this->fallbackResponse['text'] = $error_text;
817 1501 } elseif ($embeddings) {
818 1502 // Store new PDF information
819 - // Create a more meaningful filename from URL
820 1503 $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
821 1504
822 - // If the filename is generic (like results_download.php), create a more descriptive one
1505 + // If the filename is generic, create a more descriptive one
823 1506 if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
824 1507 strpos($pdf_filename, '.php') !== false) {
825 - // Create a timestamp-based name
826 1508 $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
827 1509 }
828 1510
829 1511 set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
@@ -830,77 +1512,257 @@
830 1512 set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
831 1513 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
832 1514 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
833 1515
834 - $success_text = $this->options['pdf_intent_success_text'] ??
1516 + $success_text = $current_options['pdf_intent_success_text'] ??
835 1517 esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
836 1518
837 - // Return success with filename for UI update
838 - wp_send_json([
1519 + $pdf_response = [
839 1520 'success' => true,
840 1521 'message' => $success_text,
841 1522 'data' => [
842 1523 'filename' => $pdf_filename
843 1524 ]
844 - ]);
1525 + ];
1526 +
1527 + if ($testing_data !== null) {
1528 + $pdf_response['testing_data'] = $testing_data;
1529 + }
1530 +
1531 + wp_send_json($pdf_response);
845 1532 wp_die();
846 1533 } else {
847 - $error_text = $this->options['pdf_intent_error_text'] ??
1534 + $error_text = $current_options['pdf_intent_error_text'] ??
848 1535 esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
849 1536 $this->fallbackResponse['text'] = $error_text;
850 1537 }
851 1538
852 - wp_send_json([
1539 + $pdf_error_response = [
853 1540 'success' => false,
854 1541 'message' => $this->fallbackResponse['text']
855 - ]);
1542 + ];
1543 +
1544 + if ($testing_data !== null) {
1545 + $pdf_error_response['testing_data'] = $testing_data;
1546 + }
1547 +
1548 + wp_send_json($pdf_error_response);
856 1549 wp_die();
857 1550 }
858 1551 }
859 1552 }
860 1553
861 - // Step 2: Detect intent and handle intent-based responses
862 - $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
863 - //error_log("Intent Matched: " . ($intent_matched ? "Yes" : "No"));
864 1554
865 - // Step 3: If intent is matched and handled, respond immediately
866 - if ($intent_matched && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
867 - //error_log("Intent response triggered.");
868 - $response_data = [
869 - 'text' => $this->fallbackResponse['text'],
870 - 'html' => $this->fallbackResponse['html'],
871 - 'session_id' => $session_id
872 - ];
873 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text'] . $this->fallbackResponse['html']);
874 - wp_send_json($response_data);
875 - wp_die();
876 - }
1555 + // Step 2: Detect intent and handle intent-based responses
1556 + $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
877 1557
878 - // If no intent matched or product not found, proceed with AI response
879 - //error_log("No matching intent or fallback. Generating AI response.");
1558 + // Capture action analysis for testing panel after intent check
1559 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1560 + $testing_data['action_matches'] = $this->last_action_analysis;
1561 + }
880 1562
881 - // Step 4: Generate AI response
882 - $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
883 - $this->mxchat_increment_chat_count();
1563 + // Step 3: Handle the intent result appropriately
1564 + if ($intent_result !== false) {
1565 + // Intent was matched - ALWAYS send as JSON response, never streaming
1566 +
1567 + if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
1568 + // Intent returned a direct response array
1569 + $response_data = [
1570 + 'text' => $intent_result['text'] ?? '',
1571 + 'html' => $intent_result['html'] ?? '',
1572 + 'session_id' => $session_id
1573 + ];
884 1574
885 - // Generate embedding for the user's query
886 - $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
887 - if (!is_array($user_message_embedding)) {
888 - //error_log("Failed to generate message embedding for session $session_id");
889 - wp_send_json_error(esc_html__('Error processing your message.', 'mxchat'));
890 - wp_die();
891 - }
1575 + // IMPORTANT: Include chat_mode if present (for WhatsApp, Slack, etc.)
1576 + if (isset($intent_result['chat_mode'])) {
1577 + $response_data['chat_mode'] = $intent_result['chat_mode'];
1578 + }
892 1579
893 - // Build context with both knowledge base and PDF content if available
894 - $context_content = "User asked: '{$message}'\n\n";
1580 + if ($testing_data !== null) {
1581 + $response_data['testing_data'] = $testing_data;
1582 + }
895 1583
896 - // Get relevant content from knowledge base
897 - $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
898 - if (!empty($relevant_content)) {
899 - $context_content .= "Relevant content from knowledge database:\n" . $relevant_content . "\n\n";
900 - }
1584 + wp_send_json($response_data);
1585 + wp_die();
1586 + } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1587 + // Intent returned true and set fallbackResponse
901 1588
1589 + // SAVE TO TRANSCRIPT
1590 + if (!empty($this->fallbackResponse['text'])) {
1591 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1592 + }
1593 + // Save action HTML (product cards, featured products, etc.) so it renders in transcripts
1594 + if (!empty($this->fallbackResponse['html'])) {
1595 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1596 + }
902 1597
1598 + $response_data = [
1599 + 'text' => $this->fallbackResponse['text'] ?? '',
1600 + 'html' => $this->fallbackResponse['html'] ?? '',
1601 + 'session_id' => $session_id
1602 + ];
1603 +
1604 + if (isset($this->fallbackResponse['chat_mode'])) {
1605 + $response_data['chat_mode'] = $this->fallbackResponse['chat_mode'];
1606 + }
1607 +
1608 + if ($testing_data !== null) {
1609 + $response_data['testing_data'] = $testing_data;
1610 + }
1611 +
1612 + wp_send_json($response_data);
1613 + wp_die();
1614 + }
1615 + }
1616 +
1617 + // If we get here, no intent matched OR the intent didn't provide a usable response
1618 +
1619 + // Step 4: Generate AI response
1620 + // Get session start timestamp - when persistence is OFF, only include messages from this page load
1621 + $session_start_timestamp = isset($_POST['session_start_timestamp']) ? intval($_POST['session_start_timestamp']) : 0;
1622 + $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id, $session_start_timestamp);
1623 + $this->mxchat_increment_chat_count();
1624 +
1625 + // Generate embedding for the user's query - USE BOT-SPECIFIC API KEY
1626 + $api_key = $current_options['api_key'] ?? $this->options['api_key'];
1627 + $user_message_embedding = $this->mxchat_generate_embedding($message, $api_key);
1628 +
1629 + // Check if the embedding generation returned an error
1630 + if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1631 + $error_message = $user_message_embedding['error'];
1632 + $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
1633 +
1634 + // FIXED: Send error in appropriate format based on streaming mode
1635 + if ($is_streaming) {
1636 + echo "data: " . json_encode([
1637 + 'error' => true,
1638 + 'error_message' => $error_message,
1639 + 'error_code' => $error_code,
1640 + 'text' => $error_message,
1641 + 'message' => $error_message
1642 + ]) . "\n\n";
1643 + echo "data: [DONE]\n\n";
1644 + flush();
1645 + } else {
1646 + wp_send_json_error([
1647 + 'error_message' => $error_message,
1648 + 'error_code' => $error_code
1649 + ]);
1650 + }
1651 + wp_die();
1652 + }
1653 +
1654 + // Check if the embedding is valid
1655 + if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1656 + $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
1657 +
1658 + // FIXED: Send error in appropriate format based on streaming mode
1659 + if ($is_streaming) {
1660 + echo "data: " . json_encode([
1661 + 'error' => true,
1662 + 'error_message' => $error_message,
1663 + 'error_code' => 'invalid_embedding',
1664 + 'text' => $error_message,
1665 + 'message' => $error_message
1666 + ]) . "\n\n";
1667 + echo "data: [DONE]\n\n";
1668 + flush();
1669 + } else {
1670 + wp_send_json_error([
1671 + 'error_message' => $error_message,
1672 + 'error_code' => 'invalid_embedding'
1673 + ]);
1674 + }
1675 + wp_die();
1676 + }
1677 +
1678 + // Build context with both knowledge base and PDF content if available
1679 + $context_content = "User asked: '{$message}'\n\n";
1680 +
1681 + // Add action instruction if present (add this right after the above line)
1682 + if (!empty($this->current_action_instruction)) {
1683 + $context_content .= "===== SPECIAL INSTRUCTION =====\n";
1684 + $context_content .= "IMPORTANT: " . $this->current_action_instruction . "\n";
1685 + $context_content .= "Respond naturally and conversationally while following this instruction.\n";
1686 + $context_content .= "===== END SPECIAL INSTRUCTION =====\n\n";
1687 +
1688 + // Clear the instruction after using it
1689 + $this->current_action_instruction = null;
1690 + }
1691 +
1692 +
1693 + // Add page context if available and contextual awareness is enabled using current_options
1694 + if ($page_context && isset($current_options['contextual_awareness_toggle']) && $current_options['contextual_awareness_toggle'] === 'on') {
1695 + $context_content .= "===== CURRENT PAGE CONTEXT =====\n";
1696 + $context_content .= "Page URL: " . $page_context['url'] . "\n";
1697 + $context_content .= "Page Title: " . $page_context['title'] . "\n";
1698 + $context_content .= "Page Content: " . $page_context['content'] . "\n";
1699 + $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1700 + }
1701 +
1702 + // Get relevant content from knowledge base - PASS BOT_ID and MESSAGE for Vector Store
1703 + $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding, $bot_id, $message);
1704 +
1705 + // NEW: Also extract URLs from system instructions (only if citation links enabled)
1706 + // Use fresh options to ensure we get the latest setting value
1707 + $fresh_options = get_option('mxchat_options', []);
1708 + $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
1709 +
1710 + $system_instructions = $this->get_system_instructions($bot_id, $session_id);
1711 + if ($citation_links_enabled && !empty($system_instructions)) {
1712 + preg_match_all(
1713 + '#\bhttps?://[^\s<>"\']+#i',
1714 + $system_instructions,
1715 + $system_instruction_urls
1716 + );
1717 +
1718 + if (!empty($system_instruction_urls[0])) {
1719 + // Merge with existing valid URLs
1720 + $this->current_valid_urls = array_merge(
1721 + $this->current_valid_urls,
1722 + $system_instruction_urls[0]
1723 + );
1724 + // Remove duplicates
1725 + $this->current_valid_urls = array_unique($this->current_valid_urls);
1726 +
1727 + //error_log("Added " . count($system_instruction_urls[0]) . " URLs from system instructions");
1728 + }
1729 + }
1730 +
1731 +// ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1732 +if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1733 + // Update testing data with the REAL similarity analysis
1734 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1735 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1736 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1737 + $testing_data['sources_used'] = $this->last_similarity_analysis['sources_used'] ?? 0;
1738 + $testing_data['total_chunks_used'] = $this->last_similarity_analysis['total_chunks_used'] ?? 0;
1739 +}
1740 +// ===== END SIMILARITY DATA CAPTURE =====
1741 +
1742 +// NEW: Add valid URLs to testing data for admin panel display (AFTER similarity data)
1743 +if ($testing_data !== null && !empty($this->current_valid_urls)) {
1744 + $testing_data['approved_urls'] = array_values($this->current_valid_urls);
1745 + //error_log("Added " . count($this->current_valid_urls) . " approved URLs to testing data");
1746 +}
1747 +
1748 + if (!empty($relevant_content)) {
1749 + $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1750 + } else {
1751 + $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
1752 + }
1753 +
1754 + // NEW: Add approved URLs list to context for AI (only if citation links enabled)
1755 + if ($citation_links_enabled && !empty($this->current_valid_urls)) {
1756 + $context_content .= "===== APPROVED URLS FOR CITATIONS =====\n";
1757 + $context_content .= "You may ONLY use these exact URLs in your response:\n";
1758 + foreach ($this->current_valid_urls as $url) {
1759 + $context_content .= "- " . $url . "\n";
1760 + }
1761 + $context_content .= "\nCRITICAL: Do NOT create, modify, extend, or invent any other URLs. ";
1762 + $context_content .= "===== END APPROVED URLS =====\n\n";
1763 + }
1764 +
903 1765 // Check for and include PDF content
904 1766 $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
905 1767 $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
906 1768 $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
@@ -928,87 +1790,284 @@
928 1790 }
929 1791 $context_content .= "\n";
930 1792 }
931 1793 }
932 - // Generate the response using the full context
933 - $response = $this->mxchat_generate_response(
934 - $context_content,
935 - $this->options['api_key'],
936 - $this->options['xai_api_key'],
937 - $this->options['claude_api_key'],
938 - $this->options['deepseek_api_key'],
939 - $conversation_history
940 - );
1794 +
1795 + $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
941 1796
942 - $this->mxchat_save_chat_message($session_id, 'bot', $response);
1797 + // Extract model from current options for bot-specific model support
1798 + $selected_model = isset($current_options['model']) ? $current_options['model'] : 'gpt-5.1-chat-latest';
1799 +
1800 + $response = $this->mxchat_generate_response(
1801 + $context_content,
1802 + $current_options['api_key'] ?? $this->options['api_key'],
1803 + $current_options['xai_api_key'] ?? $this->options['xai_api_key'],
1804 + $current_options['claude_api_key'] ?? $this->options['claude_api_key'],
1805 + $current_options['deepseek_api_key'] ?? $this->options['deepseek_api_key'],
1806 + $current_options['gemini_api_key'] ?? $this->options['gemini_api_key'],
1807 + $current_options['openrouter_api_key'] ?? $this->options['openrouter_api_key'],
1808 + $conversation_history,
1809 + $is_streaming,
1810 + $session_id,
1811 + $testing_data,
1812 + $selected_model
1813 + );
1814 +
1815 + // Handle streaming vs non-streaming responses
1816 + if ($is_streaming) {
1817 + // Check if streaming actually happened or if it fell back to regular response
1818 + if ($response === true) {
1819 + wp_die();
1820 + }
1821 + // If we get here, streaming fell back to regular response, continue
1822 + // But if there's an error, we need to send it as SSE format since headers are already set
1823 + if (is_array($response) && isset($response['error'])) {
1824 + $error_message = $response['error'];
1825 + $error_code = $response['error_code'] ?? 'api_error';
1826 + // Send error in SSE format that the client JS can handle
1827 + echo "data: " . json_encode([
1828 + 'error' => true,
1829 + 'error_message' => $error_message,
1830 + 'error_code' => $error_code,
1831 + 'text' => $error_message, // Also include as text for fallback handling
1832 + 'message' => $error_message
1833 + ]) . "\n\n";
1834 + echo "data: [DONE]\n\n";
1835 + flush();
1836 + wp_die();
1837 + }
1838 + }
943 1839
944 - // Step 5: Save additional content if available
945 - if (!empty($this->productCardHtml)) {
946 - $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
947 - }
1840 + // Check if the response is an error array (non-streaming mode)
1841 + if (is_array($response) && isset($response['error'])) {
1842 + wp_send_json_error([
1843 + 'error_message' => $response['error'],
1844 + 'error_code' => $response['error_code'] ?? 'api_error'
1845 + ]);
1846 + wp_die();
1847 + }
1848 +
1849 + // DEBUG: Check what we have
1850 + //error_log("=== BEFORE URL VALIDATION ===");
1851 + //error_log("current_valid_urls is empty? " . (empty($this->current_valid_urls) ? 'YES' : 'NO'));
1852 + //error_log("current_valid_urls count: " . count($this->current_valid_urls));
1853 + //error_log("current_valid_urls content: " . print_r($this->current_valid_urls, true));
1854 +
1855 + // If we get here, the response is valid text - now validate URLs
1856 + if (!empty($this->current_valid_urls)) {
1857 + //error_log("CALLING validate_and_clean_urls");
1858 + $response = $this->validate_and_clean_urls($response, $this->current_valid_urls);
1859 + } else {
1860 + //error_log("SKIPPING validation - current_valid_urls is empty");
1861 + }
1862 + // ===== END URL VALIDATION =====
948 1863
949 - if (!empty($this->fallbackResponse['html'])) {
950 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
951 - }
1864 + // Prepare RAG context data for storage (only include documents used for context)
1865 + $rag_context_for_storage = null;
1866 + $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
1867 + $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
952 1868
953 - // Step 6: Return the response
954 - $response_data = [
955 - 'text' => $response,
956 - 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
957 - 'session_id' => $session_id
958 - ];
1869 + if ($has_rag_data || $has_action_data) {
1870 + $rag_context_for_storage = [];
959 1871
960 - wp_send_json($response_data);
961 - wp_die();
1872 + // Add RAG/source data if available
1873 + if ($has_rag_data) {
1874 + $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
1875 + $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
1876 + $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
1877 + $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
1878 + $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1879 + $rag_context_for_storage['sources_used'] = $this->last_similarity_analysis['sources_used'] ?? 0;
1880 + $rag_context_for_storage['total_chunks_used'] = $this->last_similarity_analysis['total_chunks_used'] ?? 0;
1881 + }
1882 +
1883 + // Add action analysis data if available
1884 + if ($has_action_data) {
1885 + $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
1886 + }
1887 + }
1888 +
1889 + // Save the cleaned response with RAG context
1890 + $this->mxchat_save_chat_message($session_id, 'bot', $response, null, $rag_context_for_storage);
1891 +
1892 + // Step 5: Save additional content if available
1893 + if (!empty($this->productCardHtml)) {
1894 + $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
1895 + }
1896 +
1897 + if (!empty($this->fallbackResponse['html'])) {
1898 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1899 + }
1900 +
1901 + // Step 6: Return the response
1902 + // DEBUG: Check if newlines exist in the response
1903 + //error_log("=== MXCHAT NON-STREAMING RESPONSE DEBUG ===");
1904 + //error_log("Response has newlines: " . (strpos($response, "\n") !== false ? 'YES' : 'NO'));
1905 + //error_log("Response first 500 chars: " . substr($response, 0, 500));
1906 +
1907 + $response_data = [
1908 + 'text' => $response,
1909 + 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1910 + 'session_id' => $session_id
1911 + ];
1912 +
1913 + // Include vectorstore error info for admin debugging (only visible to admins via testing_data)
1914 + if (!empty($this->last_vectorstore_error) && $testing_data !== null) {
1915 + $testing_data['vectorstore_error'] = $this->last_vectorstore_error;
1916 + }
1917 +
1918 + // Also pass it as a top-level field so JS can show a better error message to admins
1919 + if (!empty($this->last_vectorstore_error) && current_user_can('manage_options')) {
1920 + $response_data['vectorstore_error'] = $this->last_vectorstore_error;
1921 + }
1922 +
1923 + // Always add testing data for admins (no toggle needed)
1924 + if ($testing_data !== null) {
1925 + $response_data['testing_data'] = $testing_data;
1926 + }
1927 +
1928 + wp_send_json($response_data);
1929 + wp_die();
962 1930 }
963 1931
1932 +/**
1933 + * Get bot-specific options for multi-bot functionality
1934 + * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
1935 + */
1936 +// Also debug the bot options retrieval
1937 +private function get_bot_options($bot_id = 'default') {
1938 + //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
1939 +
1940 + if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1941 + //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
1942 + return array();
1943 + }
1944 +
1945 + $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
1946 +
1947 + if (!empty($bot_options)) {
1948 + //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
1949 + if (isset($bot_options['similarity_threshold'])) {
1950 + //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
1951 + }
1952 + }
1953 +
1954 + return is_array($bot_options) ? $bot_options : array();
1955 +}
964 1956
965 -// Helper function to clear PDF and Word document related transients
966 -private function clear_pdf_transients($session_id) {
967 - // PDF transients
968 - delete_transient('mxchat_pdf_url_' . $session_id);
969 - delete_transient('mxchat_pdf_embeddings_' . $session_id);
970 - delete_transient('mxchat_include_pdf_in_context_' . $session_id);
971 - delete_transient('mxchat_waiting_for_pdf_url_' . $session_id);
1957 +/**
1958 + * Get bot-specific Pinecone configuration
1959 + * Used in the knowledge retrieval functions
1960 + */
1961 +// Also add debugging to your get_bot_pinecone_config function
1962 +private function get_bot_pinecone_config($bot_id = 'default') {
1963 + //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
1964 +
1965 + // If default bot or multi-bot add-on not active, use default Pinecone config
1966 + if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1967 + //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
1968 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
1969 + $config = array(
1970 + 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
1971 + 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
1972 + 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
1973 + 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
1974 + );
1975 + //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
1976 + return $config;
1977 + }
1978 +
1979 + //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
1980 +
1981 + // Hook for multi-bot add-on to provide bot-specific Pinecone config
1982 + $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
1983 +
1984 + if (!empty($bot_pinecone_config)) {
1985 + //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
1986 + //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
1987 + //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
1988 + //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
1989 + } else {
1990 + //error_log("MXCHAT DEBUG: Filter returned empty config!");
1991 + }
1992 +
1993 + return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
1994 +}
972 1995
973 - // Word document transients
974 - delete_transient('mxchat_word_url_' . $session_id);
975 - delete_transient('mxchat_word_filename_' . $session_id);
976 - delete_transient('mxchat_word_embeddings_' . $session_id);
977 - delete_transient('mxchat_include_word_in_context_' . $session_id);
978 - delete_transient('mxchat_waiting_for_word_' . $session_id);
979 -}
980 1996
981 -// New function to check intents and invoke the callback function
1997 +// Updated function to check intents and invoke the callback function
982 1998 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
983 1999 global $wpdb;
984 2000 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
985 2001
986 - //error_log("[MxChat] Checking intents for message: " . $message);
2002 + // Get the current bot_id
2003 + $current_bot_id = $this->get_current_bot_id($session_id);
987 2004
988 2005 // Generate the user embedding
989 2006 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
990 - if (!is_array($user_embedding)) {
991 - //error_log("[MxChat] Failed to generate user embedding");
992 - return false;
2007 +
2008 + // Check if embedding generation returned an error
2009 + if (is_array($user_embedding) && isset($user_embedding['error'])) {
2010 + $error_message = $user_embedding['error'];
2011 + $error_code = $user_embedding['error_code'] ?? 'embedding_error';
2012 +
2013 + // FIXED: Send error in appropriate format based on streaming mode
2014 + if ($this->is_streaming) {
2015 + echo "data: " . json_encode([
2016 + 'error' => true,
2017 + 'error_message' => $error_message,
2018 + 'error_code' => $error_code,
2019 + 'text' => $error_message,
2020 + 'message' => $error_message
2021 + ]) . "\n\n";
2022 + echo "data: [DONE]\n\n";
2023 + flush();
2024 + } else {
2025 + wp_send_json_error([
2026 + 'error_message' => $error_message,
2027 + 'error_code' => $error_code
2028 + ]);
2029 + }
2030 + wp_die();
993 2031 }
994 -
2032 +
2033 + // Check if embedding is valid
2034 + if (!is_array($user_embedding) || empty($user_embedding)) {
2035 + $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
2036 +
2037 + // FIXED: Send error in appropriate format based on streaming mode
2038 + if ($this->is_streaming) {
2039 + echo "data: " . json_encode([
2040 + 'error' => true,
2041 + 'error_message' => $error_message,
2042 + 'error_code' => 'invalid_embedding',
2043 + 'text' => $error_message,
2044 + 'message' => $error_message
2045 + ]) . "\n\n";
2046 + echo "data: [DONE]\n\n";
2047 + flush();
2048 + } else {
2049 + wp_send_json_error([
2050 + 'error_message' => $error_message,
2051 + 'error_code' => 'invalid_embedding'
2052 + ]);
2053 + }
2054 + wp_die();
2055 + }
2056 +
995 2057 // Fetch intents from the database
996 2058 $table_name = $wpdb->prefix . 'mxchat_intents';
997 2059 if ($chat_mode === 'agent') {
998 2060 $query = $wpdb->prepare(
999 - "SELECT * FROM $table_name WHERE callback_function = %s",
2061 + "SELECT * FROM $table_name WHERE callback_function = %s AND (enabled = 1 OR enabled IS NULL)",
1000 2062 'mxchat_handle_switch_to_chatbot_intent'
1001 2063 );
1002 2064 $intents = $wpdb->get_results($query);
1003 2065 } else {
1004 - $intents = $wpdb->get_results("SELECT * FROM $table_name");
2066 + $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
1005 2067 }
1006 2068
1007 - //error_log("[MxChat] Found " . count($intents) . " intents to check");
1008 -
1009 2069 if (empty($intents)) {
1010 - //error_log("[MxChat] No intents found in database");
1011 2070 return false;
1012 2071 }
1013 2072
1014 2073 $highest_similarity = -INF;
@@ -1013,11 +2072,23 @@
1013 2072
1014 2073 $highest_similarity = -INF;
1015 2074 $matched_intent = null;
1016 2075
2076 + // Array to store action analysis for testing panel
2077 + $action_analysis = [];
2078 +
1017 2079 foreach ($intents as $intent) {
1018 - //error_log("[MxChat] Checking intent: " . $intent->intent_label . " (callback: " . $intent->callback_function . ")");
2080 + // Additional check for enabled state
2081 + $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
2082 + if (!$is_enabled) {
2083 + continue;
2084 + }
1019 2085
2086 + // Check if this action is enabled for the current bot
2087 + if (!$this->is_action_enabled_for_bot($intent, $current_bot_id)) {
2088 + continue;
2089 + }
2090 +
1020 2091 $intent_embedding_serialized = $intent->embedding_vector;
1021 2092 $intent_embedding = $intent_embedding_serialized
1022 2093 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
1023 2094 : null;
@@ -1022,9 +2093,8 @@
1022 2093 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
1023 2094 : null;
1024 2095
1025 2096 if (!is_array($intent_embedding)) {
1026 - //error_log("[MxChat] Invalid embedding for intent: " . $intent->intent_label);
1027 2097 continue;
1028 2098 }
1029 2099
1030 2100 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
@@ -1029,20 +2099,46 @@
1029 2099
1030 2100 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
1031 2101 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
1032 2102
1033 - //error_log("[MxChat] Similarity for {$intent->intent_label}: {$similarity} (threshold: {$intent_threshold})");
2103 + // Store action analysis data for testing panel
2104 + $action_analysis[] = [
2105 + 'intent_label' => $intent->intent_label,
2106 + 'callback_function' => $intent->callback_function,
2107 + 'similarity' => round($similarity, 4),
2108 + 'similarity_percentage' => round($similarity * 100, 2),
2109 + 'threshold' => $intent_threshold,
2110 + 'threshold_percentage' => round($intent_threshold * 100, 2),
2111 + 'above_threshold' => $similarity >= $intent_threshold,
2112 + 'triggered' => false // Will be updated below if this intent is triggered
2113 + ];
1034 2114
1035 2115 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
1036 2116 $highest_similarity = $similarity;
1037 2117 $matched_intent = $intent;
1038 - //error_log("[MxChat] New best match: {$intent->intent_label} with similarity {$similarity}");
1039 2118 }
1040 2119 }
1041 2120
2121 + // Mark the triggered action if any
1042 2122 if ($matched_intent) {
1043 - //error_log("[MxChat] Invoking callback: " . $matched_intent->callback_function);
1044 -
2123 + foreach ($action_analysis as &$action) {
2124 + if ($action['intent_label'] === $matched_intent->intent_label) {
2125 + $action['triggered'] = true;
2126 + break;
2127 + }
2128 + }
2129 + }
2130 +
2131 + // Sort actions by similarity (highest first) and store for testing panel
2132 + usort($action_analysis, function($a, $b) {
2133 + return $b['similarity'] <=> $a['similarity'];
2134 + });
2135 +
2136 + // Store action analysis for testing panel capture
2137 + $this->last_action_analysis = $action_analysis;
2138 +
2139 + // Around line 715 in your mxchat_check_intent_and_invoke_callback function
2140 + if ($matched_intent) {
1045 2141 // If the callback is a method on this instance (core callback), call it directly
1046 2142 if (method_exists($this, $matched_intent->callback_function)) {
1047 2143 $callback_result = call_user_func(
1048 2144 [$this, $matched_intent->callback_function],
@@ -1048,15 +2144,16 @@
1048 2144 [$this, $matched_intent->callback_function],
1049 2145 $message,
1050 2146 $user_id,
1051 2147 $session_id,
1052 - $matched_intent
2148 + $matched_intent,
2149 + $user_context ?? null
1053 2150 );
1054 2151 } else {
1055 2152 // Otherwise, use apply_filters for add-on callbacks
1056 2153 $callback_result = apply_filters(
1057 2154 $matched_intent->callback_function,
1058 - false, // default return value
2155 + false,
1059 2156 $message,
1060 2157 $user_id,
1061 2158 $session_id,
1062 2159 $matched_intent
@@ -1062,231 +2159,269 @@
1062 2159 $matched_intent
1063 2160 );
1064 2161 }
1065 2162
1066 - //error_log("[MxChat] Callback result: " . print_r($callback_result, true));
2163 + // Handle the callback result properly
1067 2164 if ($callback_result !== false) {
1068 - $this->fallbackResponse = $callback_result;
1069 - return true;
2165 + // If callback returned an array with chat_mode, use it directly
2166 + if (is_array($callback_result) && isset($callback_result['chat_mode'])) {
2167 + $this->fallbackResponse = $callback_result;
2168 + return $callback_result; // Return the full array
2169 + } else {
2170 + $this->fallbackResponse = $callback_result;
2171 + return true;
2172 + }
1070 2173 }
1071 2174 }
1072 2175
1073 - //error_log("[MxChat] No matching intent found");
1074 2176 return false;
1075 2177 }
1076 2178
1077 -
1078 -//verified good
1079 -public function mxchat_handle_order_history($message, $user_id, $session_id) {
1080 - if (!class_exists('WooCommerce')) {
1081 - $this->fallbackResponse['text'] = esc_html__("I can't access order information right now. The order system seems to be unavailable.", 'mxchat');
2179 +/**
2180 + * Check if an action is enabled for a specific bot
2181 + */
2182 +private function is_action_enabled_for_bot($intent, $bot_id) {
2183 + // If enabled_bots column doesn't exist or is null, assume it's enabled for all bots (backward compatibility)
2184 + if (!isset($intent->enabled_bots) || empty($intent->enabled_bots)) {
1082 2185 return true;
1083 2186 }
1084 -
1085 - $orderDetails = MxChat_WooCommerce::mxchat_fetch_user_orders_details('all');
1086 -
1087 - if (empty($orderDetails)) {
1088 - $this->fallbackResponse['text'] = esc_html__("I don't see any orders associated with your account. Are you logged in?", 'mxchat');
2187 +
2188 + $enabled_bots = json_decode($intent->enabled_bots, true);
2189 +
2190 + // If JSON decode fails or returns empty array, assume enabled for all (backward compatibility)
2191 + if (!is_array($enabled_bots) || empty($enabled_bots)) {
1089 2192 return true;
1090 2193 }
2194 +
2195 + // Check if the current bot is in the enabled bots list
2196 + return in_array($bot_id, $enabled_bots);
2197 +}
1091 2198
1092 - // Generate AI prompt with context
1093 - $prompt = __("User asked about their orders:", 'mxchat') . " '{$message}'\n\n";
1094 - $prompt .= __("Order information:", 'mxchat') . "\n";
1095 - foreach ($orderDetails as $order) {
1096 - $items_list = array_map(function($item) {
1097 - return "{$item['name']} ({$item['quantity']})";
1098 - }, $order['items']);
2199 +// Helper function to clear PDF and Word document related transients
2200 +private function clear_pdf_transients($session_id) {
2201 + // PDF transients
2202 + delete_transient('mxchat_pdf_url_' . $session_id);
2203 + delete_transient('mxchat_pdf_embeddings_' . $session_id);
2204 + delete_transient('mxchat_include_pdf_in_context_' . $session_id);
2205 + delete_transient('mxchat_waiting_for_pdf_url_' . $session_id);
1099 2206
1100 - $prompt .= __("Order #", 'mxchat') . "{$order['order_id']}: {$order['formatted_total']} " . __("on", 'mxchat') . " {$order['date']}\n";
1101 - $prompt .= __("Items:", 'mxchat') . " " . implode(', ', $items_list) . "\n";
1102 - }
2207 + // Word document transients
2208 + delete_transient('mxchat_word_url_' . $session_id);
2209 + delete_transient('mxchat_word_filename_' . $session_id);
2210 + delete_transient('mxchat_word_embeddings_' . $session_id);
2211 + delete_transient('mxchat_include_word_in_context_' . $session_id);
2212 + delete_transient('mxchat_waiting_for_word_' . $session_id);
2213 +}
1103 2214
1104 - $prompt .= __("\nProvide a natural, conversational response focusing on the specific information the user asked about. ", 'mxchat');
1105 - $prompt .= __("If they ask about a specific order or detail, provide just that information. ", 'mxchat');
1106 - $prompt .= __("If they ask about license keys or sensitive information, inform them to check their email or contact support.", 'mxchat');
1107 2215
1108 - // Get AI response
1109 - $ai_response = $this->mxchat_call_ai_api($prompt);
1110 - $this->fallbackResponse['text'] = $ai_response['text'];
1111 2216
1112 - return true;
2217 +//verified good
2218 +public function mxchat_handle_email_capture($message, $user_id, $session_id) {
2219 + // Get the user's original instruction/message
2220 + $user_instruction = esc_html($this->options['triggered_phrase_response'] ?? esc_html__("Please provide your email address.", 'mxchat'));
2221 +
2222 + // Set instruction for AI - just pass along what the user wanted to say
2223 + $this->current_action_instruction = $user_instruction;
2224 +
2225 + // Set the transient to track email capture flow
2226 + set_transient('mxchat_email_capture_' . $user_id, true, 5 * MINUTE_IN_SECONDS);
2227 +
2228 + // Return false to let the AI generate the response
2229 + return false;
1113 2230 }
1114 2231
2232 +public function mxchat_generate_image($message, $user_id, $session_id) {
2233 + //error_log("Starting image generation for message: " . $message);
2234 +
2235 + // Prepare a prompt for OpenAI image generation
2236 + $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
1115 2237
1116 -public function mxchat_handle_product_inquiry($message, $user_id, $session_id) {
1117 - //error_log("PRODUCT INQUIRY START - Message: $message, User ID: $user_id");
2238 + // Use the existing OpenAI API key
2239 + $openai_api_key = sanitize_text_field($this->options['api_key']);
1118 2240
1119 - // Sanitize user ID
1120 - $sanitized_user_id = sanitize_key($user_id);
1121 - //error_log("Sanitized User ID: $sanitized_user_id");
2241 + // Call OpenAI GPT Image to generate an image
2242 + $image_response = $this->mxchat_generate_openai_image($prompt, $openai_api_key);
2243 +
2244 + // Check if the response contains an image URL
2245 + if (isset($image_response['imageUrl'])) {
2246 + $image_url = esc_url_raw($image_response['imageUrl']);
2247 +
2248 + // Construct the HTML with a CSS class instead of inline styles
2249 + $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
2250 + $response_text = esc_html__('Here is the image I generated:', 'mxchat');
2251 +
2252 + // Save the bot message with both text and HTML
2253 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2254 + $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
2255 +
2256 + // Set the fallback response for the chat handler
2257 + $this->fallbackResponse = [
2258 + 'text' => $response_text,
2259 + 'html' => $response_html,
2260 + 'images' => [$image_url]
2261 + ];
2262 +
2263 + // For debugging/verification - Use json_encode to verify what's being set
2264 + //error_log("Image generation successful - fallbackResponse set: " . json_encode($this->fallbackResponse));
1122 2265
1123 - // Find product
1124 - $product_id = $this->find_product_in_message($message);
1125 - //error_log("Initial product ID from message: " . ($product_id ?? 'null'));
1126 -
1127 - // Check transient if no product found
1128 - if (!$product_id) {
1129 - $product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1130 - //error_log("Product ID from transient: " . ($product_id ?? 'null'));
2266 + // Return the response directly instead of relying on the property
2267 + return $this->fallbackResponse;
2268 + } else {
2269 + $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
2270 +
2271 + // Save the error message
2272 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2273 +
2274 + // Set the fallback response for the chat handler
2275 + $this->fallbackResponse = [
2276 + 'text' => $response_text,
2277 + 'html' => '',
2278 + 'images' => []
2279 + ];
2280 +
2281 + //error_log("DALL-E image generation error: " . esc_html($image_response['error'] ?? 'Unknown error.'));
2282 + //error_log("Error fallbackResponse set: " . json_encode($this->fallbackResponse));
2283 +
2284 + // Return the response directly instead of relying on the property
2285 + return $this->fallbackResponse;
1131 2286 }
2287 +}
1132 2288
1133 - // Handle product inquiries
1134 - if ($product_id && class_exists('WooCommerce')) {
1135 - $product = wc_get_product($product_id);
1136 - if ($product) {
1137 - //error_log("Found product: " . $product->get_name() . " (ID: $product_id)");
2289 +public function mxchat_generate_gemini_image($message, $user_id, $session_id) {
2290 + $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
1138 2291
1139 - // Prepare product details
1140 - $product_name = esc_html($product->get_name());
1141 - $product_price = $product->get_price_html();
1142 - $product_image_url = esc_url(wp_get_attachment_url($product->get_image_id()));
1143 - $product_url = esc_url(get_permalink($product_id));
1144 - $product_id_attr = esc_attr($product_id);
2292 + $gemini_api_key = sanitize_text_field($this->options['gemini_api_key'] ?? '');
2293 + if (empty($gemini_api_key)) {
2294 + $response_text = esc_html__("Gemini API key is not configured.", 'mxchat');
2295 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2296 + return ['text' => $response_text, 'html' => '', 'images' => []];
2297 + }
1145 2298
1146 - // Get product description
1147 - $product_description = $product->get_description() ?: $product->get_short_description();
2299 + $image_response = $this->mxchat_generate_imagen_image($prompt, $gemini_api_key);
1148 2300
1149 - // Log embeddings process
1150 - //error_log("Generating embedding for message: $message");
1151 - $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1152 - //error_log("Finding relevant content for product inquiry");
1153 - $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
2301 + if (isset($image_response['imageUrl'])) {
2302 + $image_url = esc_url_raw($image_response['imageUrl']);
1154 2303
1155 - // Build AI prompt
1156 - $ai_prompt = esc_html__("You are a knowledgeable product assistant. ", 'mxchat');
1157 - $ai_prompt .= esc_html__("Respond to this user query: '{$message}'\n\n", 'mxchat');
2304 + $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
2305 + $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1158 2306
1159 - if (!empty($relevant_content)) {
1160 - $ai_prompt .= esc_html__("Relevant information from our knowledge base:\n{$relevant_content}\n\n", 'mxchat');
1161 - }
2307 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2308 + $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
1162 2309
1163 - $ai_prompt .= esc_html__("Product details:\n", 'mxchat');
1164 - $ai_prompt .= esc_html__("Name: {$product_name}\n", 'mxchat');
1165 - $ai_prompt .= esc_html__("Price: ", 'mxchat') . strip_tags($product_price) . "\n";
1166 - if ($product_description) {
1167 - $ai_prompt .= esc_html__("Description: {$product_description}\n", 'mxchat');
1168 - }
2310 + $this->fallbackResponse = [
2311 + 'text' => $response_text,
2312 + 'html' => $response_html,
2313 + 'images' => [$image_url]
2314 + ];
1169 2315
1170 - $ai_prompt .= esc_html__("\nInstructions:\n", 'mxchat');
1171 - $ai_prompt .= esc_html__("1. Address the user's specific question or concern about the product\n", 'mxchat');
1172 - $ai_prompt .= esc_html__("2. Incorporate relevant information from our knowledge base if provided\n", 'mxchat');
1173 - $ai_prompt .= esc_html__("3. Highlight key product features that relate to their query\n", 'mxchat');
1174 - $ai_prompt .= esc_html__("4. Include a natural suggestion to check out the product\n", 'mxchat');
1175 - $ai_prompt .= esc_html__("5. Keep the response conversational and helpful\n", 'mxchat');
2316 + return $this->fallbackResponse;
2317 + } else {
2318 + $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
1176 2319
1177 - //error_log("Sending AI prompt: " . $ai_prompt);
2320 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1178 2321
1179 - // Build and log AI response
1180 - $ai_response = $this->mxchat_call_ai_api($ai_prompt);
1181 - //error_log("AI Response received for product inquiry: " . print_r($ai_response, true));
2322 + $this->fallbackResponse = [
2323 + 'text' => $response_text,
2324 + 'html' => '',
2325 + 'images' => []
2326 + ];
1182 2327
1183 - // Generate and log product card
1184 - $product_card_html = <<<HTML
1185 -<div class="mxchat-product-card">
1186 - <a href="{$product_url}" target="_blank">
1187 - <img src="{$product_image_url}" alt="{$product_name}" class="mxchat-product-image" />
1188 - <h3 class="mxchat-product-name">{$product_name}</h3>
1189 - </a>
1190 - <div class="mxchat-product-price">{$product_price}</div>
1191 - <button class="mxchat-add-to-cart-button" data-product-id="{$product_id_attr}">Add to Cart</button>
1192 -</div>
1193 -HTML;
2328 + return $this->fallbackResponse;
2329 + }
2330 +}
1194 2331
1195 - // Save response and set transient
1196 - $this->productCardHtml = $product_card_html;
1197 - $this->fallbackResponse = [
1198 - 'text' => $ai_response['text'],
1199 - 'html' => $this->productCardHtml,
1200 - ];
2332 +private function mxchat_save_generated_image($base64_data, $mime_type = 'image/png', $prefix = 'mxchat-generated') {
2333 + $extension = ($mime_type === 'image/jpeg') ? 'jpg' : 'png';
2334 + $filename = sanitize_file_name($prefix . '-' . wp_generate_uuid4() . '.' . $extension);
2335 + $decoded = base64_decode($base64_data);
1201 2336
1202 - //error_log("Setting transient for product: $product_id with key: mxchat_last_discussed_product_$sanitized_user_id");
1203 - set_transient('mxchat_last_discussed_product_' . $sanitized_user_id, $product_id, HOUR_IN_SECONDS);
2337 + if ($decoded === false) {
2338 + return new \WP_Error('decode_failed', esc_html__('Failed to decode image data.', 'mxchat'));
2339 + }
1204 2340
1205 - // Verify transient was set
1206 - $verify_transient = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1207 - //error_log("Verification - Retrieved transient value: " . ($verify_transient ?? 'null'));
2341 + $upload = wp_upload_bits($filename, null, $decoded);
1208 2342
1209 - return true;
1210 - }
2343 + if (!empty($upload['error'])) {
2344 + return new \WP_Error('upload_failed', $upload['error']);
1211 2345 }
1212 2346
1213 - //error_log("PRODUCT INQUIRY END - No product found or WooCommerce not available");
1214 - return false;
1215 -}
2347 + $attach_id = wp_insert_attachment([
2348 + 'post_mime_type' => $mime_type,
2349 + 'post_title' => $prefix,
2350 + 'post_content' => '',
2351 + 'post_status' => 'inherit',
2352 + ], $upload['file']);
1216 2353
1217 -//verified good
1218 -public function mxchat_handle_email_capture($message, $user_id, $session_id) {
1219 - // Log the message safely
1220 - //error_log("Triggered email capture intent for message: " . sanitize_text_field($message));
2354 + if (is_wp_error($attach_id)) {
2355 + return $attach_id;
2356 + }
1221 2357
1222 - // Initiate email capture flow
1223 - $response = esc_html($this->options['triggered_phrase_response'] ?? esc_html__("Would you like to join our mailing list? Please provide your email below.", 'mxchat'));
2358 + require_once ABSPATH . 'wp-admin/includes/image.php';
2359 + $metadata = wp_generate_attachment_metadata($attach_id, $upload['file']);
2360 + wp_update_attachment_metadata($attach_id, $metadata);
1224 2361
1225 - set_transient('mxchat_email_capture_' . $user_id, true, 5 * MINUTE_IN_SECONDS);
1226 - $this->mxchat_save_chat_message($session_id, 'bot', $response);
1227 -
1228 - // Respond to the user
1229 - wp_send_json(['message' => $response]);
1230 - wp_die();
2362 + return esc_url_raw(wp_get_attachment_url($attach_id));
1231 2363 }
1232 2364
1233 -//very good
1234 -public function mxchat_generate_image($message, $user_id, $session_id) {
1235 - // Prepare a prompt for DALL-E
1236 - $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2365 +private function mxchat_generate_openai_image($prompt, $api_key, $model = 'gpt-image-1', $timeout = 60) {
2366 + $api_url = 'https://api.openai.com/v1/images/generations';
2367 + $body = json_encode([
2368 + 'prompt' => sanitize_text_field($prompt),
2369 + 'n' => 1,
2370 + 'size' => '1024x1024',
2371 + 'quality' => 'medium',
2372 + 'output_format' => 'png',
2373 + 'model' => sanitize_text_field($model),
2374 + ]);
1237 2375
1238 - // Use the existing OpenAI API key
1239 - $openai_api_key = sanitize_text_field($this->options['api_key']);
2376 + $args = [
2377 + 'body' => $body,
2378 + 'headers' => [
2379 + 'Content-Type' => 'application/json',
2380 + 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
2381 + ],
2382 + 'method' => 'POST',
2383 + 'timeout' => absint($timeout),
2384 + ];
1240 2385
1241 - // Call DALL-E to generate an image
1242 - $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
2386 + $response = wp_remote_post($api_url, $args);
1243 2387
1244 - // Check if the response contains an image URL
1245 - if (isset($image_response['imageUrl'])) {
1246 - $image_url = esc_url_raw($image_response['imageUrl']);
2388 + if (is_wp_error($response)) {
2389 + return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2390 + }
1247 2391
1248 - // Construct the HTML with a CSS class instead of inline styles
1249 - $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
2392 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
1250 2393
1251 - $response_text = esc_html__('Here is the image I generated:', 'mxchat');
2394 + $b64 = $response_body['data'][0]['b64_json'] ?? $response_body['data'][0]['b64'] ?? null;
2395 + if ($b64) {
2396 + $saved_url = $this->mxchat_save_generated_image($b64, 'image/png', 'mxchat-openai');
2397 + if (is_wp_error($saved_url)) {
2398 + return ['error' => $saved_url->get_error_message()];
2399 + }
2400 + return ['imageUrl' => $saved_url];
1252 2401 } else {
1253 - $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
1254 - $response_html = '';
1255 - //error_log("DALL-E image generation error: " . esc_html($image_response['error'] ?? 'Unknown error.'));
2402 + return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
1256 2403 }
2404 +}
1257 2405
1258 - // Save both text and HTML responses
1259 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text . "\n" . $response_html);
2406 +private function mxchat_generate_imagen_image($prompt, $api_key, $timeout = 60) {
2407 + $api_url = 'https://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-generate-001:predict';
1260 2408
1261 - // Prepare the response data
1262 - $response_data = [
1263 - 'message' => $response_text,
1264 - 'html' => $response_html,
1265 - 'image_url' => $image_url ?? '',
1266 - ];
1267 -
1268 - // Send the JSON response
1269 - header('Content-Type: application/json; charset=' . get_option('blog_charset'));
1270 - echo json_encode($response_data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
1271 - wp_die();
1272 -}
1273 -private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
1274 - $api_url = 'https://api.openai.com/v1/images/generations';
1275 2409 $body = json_encode([
1276 - 'prompt' => sanitize_text_field($prompt),
1277 - 'n' => 1,
1278 - 'size' => '1024x1024',
1279 - 'model' => sanitize_text_field($model),
2410 + 'instances' => [['prompt' => sanitize_text_field($prompt)]],
2411 + 'parameters' => [
2412 + 'sampleCount' => 1,
2413 + 'aspectRatio' => '1:1',
2414 + ],
1280 2415 ]);
1281 2416
1282 2417 $args = [
1283 - 'body' => $body,
2418 + 'body' => $body,
1284 2419 'headers' => [
1285 - 'Content-Type' => 'application/json',
1286 - 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
2420 + 'Content-Type' => 'application/json',
2421 + 'x-goog-api-key' => sanitize_text_field($api_key),
1287 2422 ],
1288 - 'method' => 'POST',
2423 + 'method' => 'POST',
1289 2424 'timeout' => absint($timeout),
1290 2425 ];
1291 2426
1292 2427 $response = wp_remote_post($api_url, $args);
@@ -1291,18 +2426,22 @@
1291 2426
1292 2427 $response = wp_remote_post($api_url, $args);
1293 2428
1294 2429 if (is_wp_error($response)) {
1295 - //error_log("DALL-E request failed: " . $response->get_error_message());
1296 2430 return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
1297 2431 }
1298 2432
1299 2433 $response_body = json_decode(wp_remote_retrieve_body($response), true);
1300 2434
1301 - if (isset($response_body['data'][0]['url'])) {
1302 - return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])];
2435 + $b64 = $response_body['predictions'][0]['bytesBase64Encoded'] ?? $response_body['predictions'][0]['imageBytes'] ?? null;
2436 + if ($b64) {
2437 + $mime = $response_body['predictions'][0]['mimeType'] ?? 'image/png';
2438 + $saved_url = $this->mxchat_save_generated_image($b64, $mime, 'mxchat-gemini');
2439 + if (is_wp_error($saved_url)) {
2440 + return ['error' => $saved_url->get_error_message()];
2441 + }
2442 + return ['imageUrl' => $saved_url];
1303 2443 } else {
1304 - //error_log("DALL-E response error: " . wp_remote_retrieve_body($response));
1305 2444 return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
1306 2445 }
1307 2446 }
1308 2447
@@ -1308,44 +2447,43 @@
1308 2447
1309 2448 /**
1310 2449 * Handle web search requests.
1311 2450 *
1312 - * Sends the refined search query to the Brave Search API and displays neatly formatted,
1313 - * styled search results. Results are cached for performance.
2451 + * Sends the refined search query to the Brave Search API and uses the
2452 + * results to generate a conversational response with the AI model.
1314 2453 *
1315 2454 * @since 1.0.0
1316 2455 * @param string $message The user's search query.
1317 2456 * @param string $user_id The user identifier.
1318 2457 * @param string $session_id The current session ID.
1319 - * @return void
2458 + * @return array Response array containing text with embedded HTML links
1320 2459 */
1321 -public function mxchat_handle_search_request( $message, $user_id, $session_id ) {
2460 +public function mxchat_handle_search_request($message, $user_id, $session_id) {
1322 2461 // Step 1: Interpret and refine the search query
1323 - $refined_search_query = $this->mxchat_interpret_search_query( $message );
1324 -
1325 - if ( empty( $refined_search_query ) ) {
1326 - $this->fallbackResponse = array(
1327 - 'text' => esc_html__( 'I apologize, but could you please rephrase your search request?', 'mxchat' ),
2462 + $refined_search_query = $this->mxchat_interpret_search_query($message);
2463 + if (empty($refined_search_query)) {
2464 + return array(
2465 + 'text' => esc_html__('I apologize, but could you please rephrase your search request?', 'mxchat'),
2466 + 'html' => ''
1328 2467 );
1329 - return;
1330 2468 }
1331 -
2469 +
1332 2470 // Retrieve and validate API settings
1333 - $options = get_option( 'mxchat_options' );
1334 - $api_key = isset( $options['brave_api_key'] ) ? sanitize_text_field( $options['brave_api_key'] ) : '';
1335 - $results_count = isset( $options['brave_results_count'] ) ? absint( $options['brave_results_count'] ) : 5;
1336 -
1337 - if ( empty( $api_key ) ) {
1338 - $this->fallbackResponse = array(
1339 - 'text' => esc_html__( 'Search functionality is temporarily unavailable. Please try again later.', 'mxchat' ),
2471 + $options = get_option('mxchat_options');
2472 + $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
2473 + $results_count = isset($options['brave_results_count']) ? absint($options['brave_results_count']) : 5;
2474 +
2475 + if (empty($api_key)) {
2476 + return array(
2477 + 'text' => esc_html__('Search functionality is temporarily unavailable. Please try again later.', 'mxchat'),
2478 + 'html' => ''
1340 2479 );
1341 - return;
1342 2480 }
1343 -
2481 +
1344 2482 // Build the API request URL
1345 2483 $api_url = add_query_arg(
1346 2484 array(
1347 - 'q' => rawurlencode( $refined_search_query ),
2485 + 'q' => rawurlencode($refined_search_query),
1348 2486 'count' => $results_count,
1349 2487 'text_decorations' => 'true',
1350 2488 'rich_data' => 'true',
1351 2489 ),
@@ -1350,16 +2488,16 @@
1350 2488 'rich_data' => 'true',
1351 2489 ),
1352 2490 'https://api.search.brave.com/res/v1/web/search'
1353 2491 );
1354 -
2492 +
1355 2493 // Attempt to retrieve cached results first
1356 - $transient_key = 'mxchat_search_' . md5( $refined_search_query );
1357 - $results = get_transient( $transient_key );
1358 -
1359 - if ( false === $results ) {
1360 - // Fetch new results from the Brave Search API
1361 - $response = wp_remote_get(
2494 + $transient_key = 'mxchat_search_' . md5($refined_search_query);
2495 + $results = get_transient($transient_key);
2496 +
2497 + if (false === $results) {
2498 + // SECURITY FIX: Changed to wp_safe_remote_get
2499 + $response = wp_safe_remote_get(
1362 2500 $api_url,
1363 2501 array(
1364 2502 'headers' => array(
1365 2503 'Accept' => 'application/json',
@@ -1368,162 +2506,98 @@
1368 2506 ),
1369 2507 'timeout' => 10,
1370 2508 )
1371 2509 );
1372 -
1373 - if ( is_wp_error( $response ) ) {
1374 - $this->fallbackResponse = array(
1375 - 'text' => esc_html__( 'I encountered an error while searching. Please try again.', 'mxchat' ),
2510 +
2511 + if (is_wp_error($response)) {
2512 + return array(
2513 + 'text' => esc_html__('I encountered an error while searching. Please try again.', 'mxchat'),
2514 + 'html' => ''
1376 2515 );
1377 - return;
1378 2516 }
1379 -
1380 - $results = json_decode( wp_remote_retrieve_body( $response ), true );
1381 -
1382 - if ( json_last_error() !== JSON_ERROR_NONE ) {
1383 - $this->fallbackResponse = array(
1384 - 'text' => esc_html__( 'I received an invalid response from the search service.', 'mxchat' ),
2517 +
2518 + $results = json_decode(wp_remote_retrieve_body($response), true);
2519 +
2520 + if (json_last_error() !== JSON_ERROR_NONE) {
2521 + return array(
2522 + 'text' => esc_html__('I received an invalid response from the search service.', 'mxchat'),
2523 + 'html' => ''
1385 2524 );
1386 - return;
1387 2525 }
1388 -
2526 +
1389 2527 // Cache results for one hour
1390 - set_transient( $transient_key, $results, HOUR_IN_SECONDS );
2528 + set_transient($transient_key, $results, HOUR_IN_SECONDS);
1391 2529 }
1392 -
1393 - // Process and display results
1394 - if ( ! empty( $results['web']['results'] ) && is_array( $results['web']['results'] ) ) {
1395 - $html = $this->generate_search_results_html( $results['web']['results'], $refined_search_query );
1396 -
1397 - // Only return HTML (no large text summary)
1398 - $this->fallbackResponse = array(
1399 - 'html' => $html,
2530 +
2531 + // Process results
2532 + if (!empty($results['web']['results']) && is_array($results['web']['results'])) {
2533 + // Create a more straightforward summary with HTML links
2534 + $search_results_text = '';
2535 +
2536 + // Add a simple intro
2537 + $search_results_text .= sprintf(
2538 + esc_html__("Here's what I found about '%s':", 'mxchat'),
2539 + esc_html($refined_search_query)
1400 2540 );
1401 -
2541 +
2542 + // Add the top results with HTML links
2543 + foreach (array_slice($results['web']['results'], 0, 5) as $result) {
2544 + $title = isset($result['title']) ? wp_strip_all_tags($result['title']) : '';
2545 + $url = isset($result['url']) ? esc_url($result['url']) : '';
2546 + $description = isset($result['description']) ? wp_strip_all_tags($result['description']) : '';
2547 +
2548 + // Add a line break after the intro
2549 + $search_results_text .= '<br><br>';
2550 +
2551 + // Add title as a link
2552 + $search_results_text .= sprintf(
2553 + '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a><br>',
2554 + $url,
2555 + $title
2556 + );
2557 +
2558 + // Add a condensed description
2559 + $search_results_text .= sprintf("%s", $description);
2560 + }
2561 +
1402 2562 // Save to chat history
1403 - $this->mxchat_save_chat_message( $session_id, 'bot', $html );
2563 + $this->mxchat_save_chat_message($session_id, 'bot', $search_results_text);
2564 +
2565 + // Return the formatted text with embedded HTML links
2566 + return array(
2567 + 'text' => $search_results_text,
2568 + 'html' => ''
2569 + );
1404 2570 } else {
1405 - $this->fallbackResponse = array(
2571 + return array(
1406 2572 'text' => sprintf(
1407 - esc_html__( 'I couldn\'t find any relevant results for "%s". Would you like to try different search terms?', 'mxchat' ),
1408 - esc_html( $refined_search_query )
2573 + esc_html__('I searched for "%s" but couldn\'t find any relevant results. Would you like to try different search terms?', 'mxchat'),
2574 + esc_html($refined_search_query)
1409 2575 ),
2576 + 'html' => ''
1410 2577 );
1411 2578 }
1412 2579 }
1413 2580
1414 -
2581 +//very good
1415 2582 /**
1416 - * Format search results into a natural text summary.
2583 + * Handle image search requests from the chatbot
1417 2584 *
1418 - * @since 1.0.0
1419 - * @param array $results The search results from the API.
1420 - * @param string $query The original search query.
1421 - * @return string The text summary of the top results.
2585 + * @param string $message The user's search query
2586 + * @param int $user_id The user's ID
2587 + * @param string $session_id The chat session ID
2588 + * @return array Response array with text and HTML content
1422 2589 */
1423 -private function format_search_results( $results, $query ) {
1424 - $summary = sprintf(
1425 - esc_html__( 'Here are the most relevant results for "%s":', 'mxchat' ),
1426 - esc_html( $query )
1427 - ) . "\n\n";
1428 -
1429 - $max_results = min( count( $results ), 3 );
1430 - for ( $i = 0; $i < $max_results; $i++ ) {
1431 - $result = $results[ $i ];
1432 - $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1433 - $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1434 -
1435 - // Append title and description to the summary
1436 - $summary .= sprintf(
1437 - "%s\n%s\n\n",
1438 - esc_html( $title ),
1439 - esc_html( $description )
1440 - );
1441 - }
1442 -
1443 - return $summary;
1444 -}
1445 -
1446 -/**
1447 - * Generate HTML markup for search results.
1448 - *
1449 - * @since 1.0.0
1450 - * @param array $results The search results from the API.
1451 - * @param string $query The user-refined query.
1452 - * @return string The HTML markup for displaying the results.
1453 - */
1454 -private function generate_search_results_html( $results, $query ) {
1455 - ob_start();
1456 - ?>
1457 - <div class="mxchat-search-results">
1458 - <?php foreach ( $results as $result ) :
1459 - $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1460 - $url = isset( $result['url'] ) ? esc_url( $result['url'] ) : '#';
1461 - $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1462 - $favicon = isset( $result['meta_url']['favicon'] ) ? esc_url( $result['meta_url']['favicon'] ) : '';
1463 - $thumbnail = isset( $result['thumbnail']['src'] ) ? esc_url( $result['thumbnail']['src'] ) : '';
1464 - $domain = parse_url( $url, PHP_URL_HOST );
1465 - ?>
1466 - <div class="mxchat-search-item">
1467 - <div class="mxchat-search-header">
1468 - <?php if ( $favicon ) : ?>
1469 - <img
1470 - src="<?php echo esc_url( $favicon ); ?>"
1471 - class="mxchat-site-icon"
1472 - alt="<?php echo esc_attr__( 'Site icon', 'mxchat' ); ?>"
1473 - width="16"
1474 - height="16"
1475 - />
1476 - <?php endif; ?>
1477 - <div class="mxchat-site-url"><?php echo esc_html( $domain ); ?></div>
1478 - </div>
1479 -
1480 - <div class="mxchat-search-content">
1481 - <h3 class="mxchat-search-title">
1482 - <a href="<?php echo esc_url( $url ); ?>"
1483 - target="_blank"
1484 - rel="noopener noreferrer"
1485 - >
1486 - <?php echo esc_html( $title ); ?>
1487 - </a>
1488 - </h3>
1489 -
1490 - <?php if ( $thumbnail ) : ?>
1491 - <div class="mxchat-search-thumbnail">
1492 - <img
1493 - src="<?php echo esc_url( $thumbnail ); ?>"
1494 - alt="<?php echo esc_attr__( 'Thumbnail image', 'mxchat' ); ?>"
1495 - loading="lazy"
1496 - />
1497 - </div>
1498 - <?php endif; ?>
1499 -
1500 - <div class="mxchat-search-description">
1501 - <?php echo esc_html( $description ); ?>
1502 - </div>
1503 - </div>
1504 - </div>
1505 - <?php endforeach; ?>
1506 - </div>
1507 - <?php
1508 - return ob_get_clean();
1509 -}
1510 -
1511 -
1512 -//very good
1513 2590 public function mxchat_handle_image_search_request($message, $user_id, $session_id) {
1514 -
1515 - // Step 1: Interpret the search query for better results
2591 + // Step 1: Interpret the search query using the user's selected AI model
1516 2592 $refined_search_query = $this->mxchat_interpret_search_query($message);
1517 2593
1518 -
1519 2594 // If no query was interpreted, return a fallback message
1520 2595 if (empty($refined_search_query)) {
1521 - $this->fallbackResponse = [
2596 + return array(
1522 2597 'text' => __("I'm sorry, I couldn't interpret your search query. Please specify what you'd like to see images of.", 'mxchat'),
1523 2598 'html' => "",
1524 - ];
1525 - return;
2599 + );
1526 2600 }
1527 2601
1528 2602 // Brave API URL
1529 2603 $api_url = 'https://api.search.brave.com/res/v1/images/search';
@@ -1532,19 +2606,12 @@
1532 2606 $options = get_option('mxchat_options');
1533 2607 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1534 2608
1535 2609 if (empty($api_key)) {
1536 -/*
1537 - if (defined('WP_DEBUG') && WP_DEBUG) {
1538 - error_log("Brave API key is missing.");
1539 - }
1540 -*/
1541 -
1542 - $this->fallbackResponse = [
2610 + return array(
1543 2611 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'),
1544 2612 'html' => "",
1545 - ];
1546 - return;
2613 + );
1547 2614 }
1548 2615
1549 2616 $image_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1550 2617 $safe_search = isset($options['brave_safe_search']) ? sanitize_text_field($options['brave_safe_search']) : 'strict';
@@ -1555,16 +2622,8 @@
1555 2622 'count' => $image_count,
1556 2623 'safesearch' => $safe_search,
1557 2624 ], $api_url);
1558 2625
1559 -/*
1560 - // Log the final API URL for the search
1561 - if (defined('WP_DEBUG') && WP_DEBUG) {
1562 - error_log("Final API URL for Brave Image Search: " . esc_url_raw($api_url));
1563 - }
1564 -*/
1565 -
1566 -
1567 2626 // Implement caching
1568 2627 $transient_key = 'mxchat_image_search_' . md5($refined_search_query);
1569 2628 $body = get_transient($transient_key);
1570 2629
@@ -1577,22 +2636,16 @@
1577 2636 ],
1578 2637 'timeout' => 10,
1579 2638 ];
1580 2639
1581 - $response = wp_remote_get($api_url, $args);
2640 + // SECURITY FIX: Changed to wp_safe_remote_get
2641 + $response = wp_safe_remote_get($api_url, $args);
1582 2642
1583 2643 if (is_wp_error($response)) {
1584 -/*
1585 - if (defined('WP_DEBUG') && WP_DEBUG) {
1586 - error_log("Brave Image API request failed: " . $response->get_error_message());
1587 - }
1588 -*/
1589 -
1590 - $this->fallbackResponse = [
2644 + return array(
1591 2645 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1592 2646 'html' => "",
1593 - ];
1594 - return;
2647 + );
1595 2648 }
1596 2649
1597 2650 $body = json_decode(wp_remote_retrieve_body($response), true);
1598 2651 set_transient($transient_key, $body, HOUR_IN_SECONDS);
@@ -1600,10 +2653,16 @@
1600 2653
1601 2654 // Process the API response
1602 2655 if (isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) {
1603 2656 $html_output = '<div class="mxchat-image-gallery">';
1604 -
1605 - foreach ($body['results'] as $image) {
2657 +
2658 + // Get the configured image count (1-6)
2659 + $display_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
2660 + $display_count = min($display_count, count($body['results'])); // Make sure we don't exceed available images
2661 +
2662 + // Use only the requested number of images
2663 + for ($i = 0; $i < $display_count; $i++) {
2664 + $image = $body['results'][$i];
1606 2665 $image_url = isset($image['url']) ? esc_url($image['url']) : '';
1607 2666 $thumbnail_url = isset($image['thumbnail']['src']) ? esc_url($image['thumbnail']['src']) : '';
1608 2667 $title = isset($image['title']) ? esc_html($image['title']) : esc_html__('Image', 'mxchat');
1609 2668
@@ -1617,47 +2676,95 @@
1617 2676 }
1618 2677
1619 2678 $html_output .= '</div>';
1620 2679
1621 - $this->fallbackResponse = [
1622 - 'text' => "",
1623 - 'html' => $html_output,
1624 - ];
1625 -
1626 - // Save response in chat history
2680 + // Create response text
2681 + $response_text = sprintf(__("Here are some images of %s:", 'mxchat'), $refined_search_query);
2682 +
2683 + // Save both response text and HTML to chat history
2684 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1627 2685 $this->mxchat_save_chat_message($session_id, 'bot', $html_output);
1628 2686
2687 + // Return the combined response
2688 + return array(
2689 + 'text' => $response_text,
2690 + 'html' => $html_output,
2691 + );
1629 2692 } else {
1630 -/*
1631 - if (defined('WP_DEBUG') && WP_DEBUG) {
1632 - error_log("Brave Image API response did not contain expected data structure or was empty: " . print_r($body, true));
1633 - }
1634 -*/
1635 -
1636 - $this->fallbackResponse = [
1637 - 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
2693 + $response_text = __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat');
2694 +
2695 + // Save the error message to chat history
2696 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2697 +
2698 + return array(
2699 + 'text' => $response_text,
1638 2700 'html' => "",
1639 - ];
2701 + );
1640 2702 }
1641 2703 }
2704 +
2705 +/**
2706 + * Interpret the search query using the user's selected AI model
2707 + *
2708 + * @param string $user_query The original query from the user
2709 + * @return string The refined search query
2710 + */
1642 2711 public function mxchat_interpret_search_query($user_query) {
1643 2712 $system_prompt = esc_html__("Interpret the user's request to provide only the essential keywords or phrases for image searching. Remove conversational language, politeness, or extra context. Return a concise search query that doesn't lose any of the original meaning.", 'mxchat');
1644 -
1645 - // Retrieve OpenAI API key using 'api_key' as the option key
1646 - $api_key = isset($this->options['api_key']) ? sanitize_text_field($this->options['api_key']) : sanitize_text_field(get_option('mxchat_options')['api_key']);
1647 -
1648 - /*
1649 - // Log the API key check, without exposing the key
1650 - if (defined('WP_DEBUG') && WP_DEBUG) {
1651 - error_log("Retrieved OpenAI API Key: " . ($api_key ? "Present" : "Missing"));
2713 +
2714 + // Get options and determine the selected model
2715 + $options = $this->options ?? get_option('mxchat_options');
2716 + $selected_model = isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest';
2717 +
2718 + // Extract model prefix to determine the provider
2719 + $model_parts = explode('-', $selected_model);
2720 + $provider = strtolower($model_parts[0]);
2721 +
2722 + // Determine which API key to use based on the provider
2723 + switch ($provider) {
2724 + case 'gemini':
2725 + $api_key = isset($options['gemini_api_key']) ? sanitize_text_field($options['gemini_api_key']) : '';
2726 + if (empty($api_key)) {
2727 + return sanitize_text_field($user_query); // Default to original query if API key missing
2728 + }
2729 + return $this->interpret_query_with_gemini($user_query, $system_prompt, $api_key, $selected_model);
2730 +
2731 + case 'claude':
2732 + $api_key = isset($options['claude_api_key']) ? sanitize_text_field($options['claude_api_key']) : '';
2733 + if (empty($api_key)) {
2734 + return sanitize_text_field($user_query);
2735 + }
2736 + return $this->interpret_query_with_claude($user_query, $system_prompt, $api_key, $selected_model);
2737 +
2738 + case 'grok':
2739 + $api_key = isset($options['xai_api_key']) ? sanitize_text_field($options['xai_api_key']) : '';
2740 + if (empty($api_key)) {
2741 + return sanitize_text_field($user_query);
2742 + }
2743 + return $this->interpret_query_with_xai($user_query, $system_prompt, $api_key, $selected_model);
2744 +
2745 + case 'deepseek':
2746 + $api_key = isset($options['deepseek_api_key']) ? sanitize_text_field($options['deepseek_api_key']) : '';
2747 + if (empty($api_key)) {
2748 + return sanitize_text_field($user_query);
2749 + }
2750 + return $this->interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $selected_model);
2751 +
2752 + case 'gpt':
2753 + default:
2754 + // Default to OpenAI for custom models or unrecognized prefixes
2755 + $api_key = isset($options['api_key']) ? sanitize_text_field($options['api_key']) : '';
2756 + if (empty($api_key)) {
2757 + return sanitize_text_field($user_query);
2758 + }
2759 + return $this->interpret_query_with_openai($user_query, $system_prompt, $api_key, $selected_model);
1652 2760 }
1653 - */
2761 +}
1654 2762
1655 - if (empty($api_key)) {
1656 - //error_log("OpenAI API key is missing.");
1657 - return sanitize_text_field($user_query); // Default to the original query if API key is missing
1658 - }
1659 -
2763 +/**
2764 + * Interpret query using OpenAI models
2765 + */
2766 +private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-5.1-chat-latest') {
1660 2767 $url = 'https://api.openai.com/v1/chat/completions';
1661 2768 $args = [
1662 2769 'headers' => [
1663 2770 'Authorization' => 'Bearer ' . $api_key,
@@ -1663,9 +2770,9 @@
1663 2770 'Authorization' => 'Bearer ' . $api_key,
1664 2771 'Content-Type' => 'application/json',
1665 2772 ],
1666 2773 'body' => wp_json_encode([
1667 - 'model' => 'gpt-3.5-turbo',
2774 + 'model' => $model,
1668 2775 'messages' => [
1669 2776 ['role' => 'system', 'content' => $system_prompt],
1670 2777 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1671 2778 ],
@@ -1672,292 +2779,178 @@
1672 2779 'temperature' => 0.2,
1673 2780 'max_tokens' => 20,
1674 2781 ]),
1675 2782 'method' => 'POST',
2783 + 'timeout' => 15,
1676 2784 ];
1677 2785
1678 2786 $response = wp_remote_post($url, $args);
1679 -
1680 2787 if (is_wp_error($response)) {
1681 - //error_log("OpenAI request failed: " . $response->get_error_message());
1682 - return sanitize_text_field($user_query); // Fallback to the original query if there's an error
2788 + return sanitize_text_field($user_query);
1683 2789 }
1684 2790
1685 2791 $body = json_decode(wp_remote_retrieve_body($response), true);
2792 + return isset($body['choices'][0]['message']['content'])
2793 + ? sanitize_text_field(trim($body['choices'][0]['message']['content']))
2794 + : sanitize_text_field($user_query);
2795 +}
1686 2796
1687 - // Check for a valid response and sanitize output
1688 - if (isset($body['choices'][0]['message']['content'])) {
1689 - $interpreted_query = sanitize_text_field(trim($body['choices'][0]['message']['content']));
2797 +/**
2798 + * Interpret query using Claude models
2799 + */
2800 +private function interpret_query_with_claude($user_query, $system_prompt, $api_key, $model) {
2801 + $url = 'https://api.anthropic.com/v1/messages';
2802 +
2803 + $args = [
2804 + 'headers' => [
2805 + 'Content-Type' => 'application/json',
2806 + 'x-api-key' => $api_key,
2807 + 'anthropic-version' => '2023-06-01',
2808 + ],
2809 + 'body' => wp_json_encode([
2810 + 'model' => $model,
2811 + 'system' => $system_prompt,
2812 + 'messages' => [
2813 + ['role' => 'user', 'content' => sanitize_text_field($user_query)]
2814 + ],
2815 + 'max_tokens' => 20,
2816 + 'temperature' => 0.2,
2817 + ]),
2818 + 'method' => 'POST',
2819 + 'timeout' => 15,
2820 + ];
1690 2821
1691 - /*
1692 - // Log the interpreted query for debugging
1693 - if (defined('WP_DEBUG') && WP_DEBUG) {
1694 - error_log("Interpreted search query: " . $interpreted_query);
1695 - }
1696 - */
1697 -
1698 - return $interpreted_query;
1699 - } else {
1700 - //error_log("Unexpected API response format: " . print_r($body, true));
2822 + $response = wp_remote_post($url, $args);
2823 + if (is_wp_error($response)) {
1701 2824 return sanitize_text_field($user_query);
1702 2825 }
1703 -}
1704 2826
1705 -public function mxchat_handle_add_to_cart_intent($message, $user_id, $session_id) {
1706 - //error_log("ADD TO CART START - Message: $message, User ID: $user_id");
1707 -
1708 - if (!class_exists('WooCommerce')) {
1709 - //error_log("WooCommerce not available");
1710 - return $this->generate_intent_response([
1711 - 'intent' => 'add_to_cart',
1712 - 'status' => 'error',
1713 - 'reason' => esc_html__('woocommerce_not_available', 'mxchat')
1714 - ], $session_id);
1715 - }
1716 -
1717 - $sanitized_user_id = sanitize_key($user_id);
1718 - // error_log("Sanitized User ID: $sanitized_user_id");
1719 - $product_id = null;
1720 -
1721 - // If button click, use transient
1722 - if ($message === '!addtocart') {
1723 - //error_log("Button click detected - using transient");
1724 - $product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1725 - //error_log("Product ID from transient: " . ($product_id ?? 'null'));
1726 - } else {
1727 - // For text commands, search message first
1728 - //error_log("Text command detected - searching message first");
1729 - $product_id = $this->find_product_in_message($message);
1730 - //error_log("Product ID from message search: " . ($product_id ?? 'null'));
1731 -
1732 - // Only use transient as fallback if no product found in message
1733 - if (!$product_id) {
1734 - //error_log("No product found in message, checking transient as fallback");
1735 - $product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1736 - //error_log("Product ID from transient fallback: " . ($product_id ?? 'null'));
1737 - }
1738 - }
1739 -
1740 - // Handle no product found
1741 - if (!$product_id) {
1742 - //error_log("No product ID found through any method");
1743 - return $this->generate_intent_response([
1744 - 'intent' => 'add_to_cart',
1745 - 'status' => 'error',
1746 - 'reason' => esc_html__('no_product_context', 'mxchat'),
1747 - 'action_needed' => esc_html__('request_product_name', 'mxchat'),
1748 - 'searched_message' => $message
1749 - ], $session_id);
1750 - }
1751 -
1752 - // Get and verify product
1753 - $product = wc_get_product($product_id);
1754 - if (!$product) {
1755 - //error_log("Product not found with ID: $product_id");
1756 - return $this->generate_intent_response([
1757 - 'intent' => 'add_to_cart',
1758 - 'status' => 'error',
1759 - 'reason' => esc_html__('product_not_found', 'mxchat'),
1760 - 'product_id' => $product_id
1761 - ], $session_id);
1762 - }
1763 -
1764 - //error_log("Found product: " . $product->get_name() . " (ID: $product_id)");
1765 -
1766 - // Add to cart
1767 - $added = WC()->cart->add_to_cart($product_id);
1768 - if ($added) {
1769 - //error_log("Successfully added to cart: " . $product->get_name());
1770 - return $this->generate_intent_response([
1771 - 'intent' => 'add_to_cart',
1772 - 'status' => 'success',
1773 - 'product' => [
1774 - 'name' => $product->get_name(),
1775 - 'id' => $product_id
1776 - ],
1777 - 'cart_url' => wc_get_cart_url(),
1778 - 'available_actions' => [
1779 - esc_html__('view_cart', 'mxchat'),
1780 - esc_html__('checkout', 'mxchat'),
1781 - esc_html__('continue_shopping', 'mxchat')
1782 - ]
1783 - ], $session_id);
1784 - } else {
1785 - //error_log("Failed to add to cart: " . $product->get_name());
1786 - return $this->generate_intent_response([
1787 - 'intent' => 'add_to_cart',
1788 - 'status' => 'error',
1789 - 'reason' => esc_html__('add_to_cart_failed', 'mxchat'),
1790 - 'product' => [
1791 - 'name' => $product->get_name(),
1792 - 'id' => $product_id
1793 - ]
1794 - ], $session_id);
1795 - }
2827 + $body = json_decode(wp_remote_retrieve_body($response), true);
2828 + if (!empty($body['content'][0]['text'])) {
2829 + return sanitize_text_field(trim($body['content'][0]['text']));
2830 + }
2831 +
2832 + return sanitize_text_field($user_query);
1796 2833 }
1797 -private function find_product_in_message($message) {
1798 - global $wpdb;
1799 2834
1800 - // Get embedding for the search query
1801 - $query_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1802 - if (!is_array($query_embedding)) {
1803 - return null;
1804 - }
2835 +/**
2836 + * Interpret query using Gemini models
2837 + */
2838 +private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
2839 + // Use v1beta for preview models, v1 for stable models
2840 + $api_version = (strpos($model, 'preview') !== false || strpos($model, 'exp') !== false) ? 'v1beta' : 'v1';
1805 2841
1806 - // Get relevant content as string
1807 - $relevant_content = $this->mxchat_find_relevant_products($query_embedding);
1808 - if (empty($relevant_content)) {
1809 - // Return null to indicate no results and set fallback response
1810 - $this->fallbackResponse['text'] = esc_html__("I couldn't find any relevant products based on your query. Could you please be more specific about the product you're looking for?", 'mxchat');
1811 - return null;
2842 + $url = "https://generativelanguage.googleapis.com/{$api_version}/models/{$model}:generateContent?key=" . urlencode($api_key);
2843 +
2844 + $args = [
2845 + 'headers' => [
2846 + 'Content-Type' => 'application/json',
2847 + ],
2848 + 'body' => wp_json_encode([
2849 + 'contents' => [
2850 + [
2851 + 'role' => 'user',
2852 + 'parts' => [
2853 + ['text' => $system_prompt . "\n\nQuery: " . sanitize_text_field($user_query)]
2854 + ]
2855 + ]
2856 + ],
2857 + 'generationConfig' => [
2858 + 'temperature' => 0.2,
2859 + 'maxOutputTokens' => 20,
2860 + ],
2861 + ]),
2862 + 'method' => 'POST',
2863 + 'timeout' => 15,
2864 + ];
2865 +
2866 + $response = wp_remote_post($url, $args);
2867 + if (is_wp_error($response)) {
2868 + return sanitize_text_field($user_query);
1812 2869 }
1813 -
1814 - // Extract product URLs from the content
1815 - preg_match_all('/https?:\/\/[^\s<>"\']+?\/product\/[^\s<>"\']+/', $relevant_content, $matches);
1816 -
1817 - if (!empty($matches[0])) {
1818 - // Try each URL found
1819 - foreach ($matches[0] as $url) {
1820 - // Clean the URL
1821 - $url = rtrim($url, '/."\']');
1822 -
1823 - // Get the product slug
1824 - $path = parse_url($url, PHP_URL_PATH);
1825 - $slug = basename(rtrim($path, '/'));
1826 -
1827 - // Find product by slug
1828 - $args = array(
1829 - 'post_type' => 'product',
1830 - 'post_status' => 'publish',
1831 - 'name' => $slug,
1832 - 'posts_per_page' => 1
1833 - );
1834 -
1835 - $products = get_posts($args);
1836 -
1837 - if (!empty($products)) {
1838 - $product_id = $products[0]->ID;
1839 - $product = wc_get_product($product_id);
1840 -
1841 - if ($product && $product->is_purchasable()) {
1842 - return $product_id;
1843 - }
1844 - }
1845 - }
2870 +
2871 + $body = json_decode(wp_remote_retrieve_body($response), true);
2872 + if (!empty($body['candidates'][0]['content']['parts'][0]['text'])) {
2873 + return sanitize_text_field(trim($body['candidates'][0]['content']['parts'][0]['text']));
1846 2874 }
2875 +
2876 + return sanitize_text_field($user_query);
2877 +}
1847 2878
1848 - // Fallback: Look for product names in the content
1849 - $products = wc_get_products([
1850 - 'status' => 'publish',
1851 - 'limit' => -1,
1852 - 'return' => 'all'
1853 - ]);
1854 -
1855 - foreach ($products as $product) {
1856 - $name = $product->get_name();
1857 - if (stripos($relevant_content, $name) !== false) {
1858 - if ($product->is_purchasable()) {
1859 - return $product->get_id();
1860 - }
1861 - }
2879 +/**
2880 + * Interpret query using X.AI (Grok) models
2881 + */
2882 +private function interpret_query_with_xai($user_query, $system_prompt, $api_key, $model) {
2883 + $url = 'https://api.xai.com/v1/chat/completions';
2884 +
2885 + $args = [
2886 + 'headers' => [
2887 + 'Content-Type' => 'application/json',
2888 + 'Authorization' => 'Bearer ' . $api_key,
2889 + ],
2890 + 'body' => wp_json_encode([
2891 + 'model' => $model,
2892 + 'messages' => [
2893 + ['role' => 'system', 'content' => $system_prompt],
2894 + ['role' => 'user', 'content' => sanitize_text_field($user_query)],
2895 + ],
2896 + 'temperature' => 0.2,
2897 + 'max_tokens' => 20,
2898 + ]),
2899 + 'method' => 'POST',
2900 + 'timeout' => 15,
2901 + ];
2902 +
2903 + $response = wp_remote_post($url, $args);
2904 + if (is_wp_error($response)) {
2905 + return sanitize_text_field($user_query);
1862 2906 }
1863 -
1864 - // If no product is found after all checks, set the fallback response
1865 - $this->fallbackResponse['text'] = esc_html__("I couldn't find any relevant products based on your query. Try to be more specific", 'mxchat');
1866 - return null;
1867 -}
1868 -
1869 -// New method to handle intent responses
1870 -private function generate_intent_response($context_content, $session_id) {
1871 - // Convert the context array to a structured string for the AI
1872 - $context_string = $this->format_intent_context($context_content);
1873 -
1874 - // Generate AI response using the context
1875 - $response = $this->mxchat_generate_response(
1876 - $context_string,
1877 - $this->options['api_key'],
1878 - $this->options['xai_api_key'],
1879 - $this->options['claude_api_key'],
1880 - $this->options['deepseek_api_key'],
1881 - $this->mxchat_fetch_conversation_history_for_ai($session_id)
1882 - );
1883 -
1884 - $this->fallbackResponse['text'] = $response;
1885 - return true;
1886 -}
1887 -// Helper method to format intent context
1888 -private function format_intent_context($context) {
1889 - $context_string = esc_html__("INTENT CONTEXT:\n", 'mxchat');
1890 -
1891 - switch ($context['intent']) {
1892 - case 'add_to_cart':
1893 - if ($context['status'] === 'success') {
1894 - $context_string .= esc_html__("Action: Successfully added product to cart\n", 'mxchat');
1895 - $context_string .= sprintf(esc_html__("Product: %s\n", 'mxchat'), $context['product']['name']);
1896 - $context_string .= esc_html__("Available actions: ", 'mxchat') . implode(', ', $context['available_actions']) . "\n";
1897 - $context_string .= sprintf(esc_html__("Cart URL: %s\n", 'mxchat'), $context['cart_url']);
1898 - $context_string .= esc_html__("\nPlease inform the user of the successful addition and their available options.", 'mxchat');
1899 - } else {
1900 - $context_string .= esc_html__("Action: Failed to add product to cart\n", 'mxchat');
1901 - $context_string .= sprintf(esc_html__("Reason: %s\n", 'mxchat'), $context['reason']);
1902 - switch ($context['reason']) {
1903 - case 'woocommerce_not_available':
1904 - $context_string .= esc_html__("\nPlease inform the user that shopping features are not available.", 'mxchat');
1905 - break;
1906 - case 'no_product_context':
1907 - $context_string .= esc_html__("\nPlease ask the user to specify which product they want to add.", 'mxchat');
1908 - break;
1909 - case 'product_not_found':
1910 - $context_string .= esc_html__("\nPlease inform the user that the product couldn't be found and ask them to try again.", 'mxchat');
1911 - break;
1912 - case 'add_to_cart_failed':
1913 - $context_string .= esc_html__("\nPlease apologize to the user and suggest they try again or ask for assistance.", 'mxchat');
1914 - break;
1915 - }
1916 - }
1917 - break;
2907 +
2908 + $body = json_decode(wp_remote_retrieve_body($response), true);
2909 + if (isset($body['choices'][0]['message']['content'])) {
2910 + return sanitize_text_field(trim($body['choices'][0]['message']['content']));
1918 2911 }
1919 -
1920 - return $context_string;
2912 +
2913 + return sanitize_text_field($user_query);
1921 2914 }
1922 2915
1923 -
1924 -public function mxchat_handle_checkout_intent($message, $user_id, $session_id) {
1925 - if (!class_exists('WooCommerce')) {
1926 - $this->fallbackResponse['text'] = esc_html__("I apologize, but the checkout feature isn't available at the moment.", 'mxchat');
1927 - return true;
2916 +/**
2917 + * Interpret query using DeepSeek models
2918 + */
2919 +private function interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $model) {
2920 + $url = 'https://api.deepseek.com/v1/chat/completions';
2921 +
2922 + $args = [
2923 + 'headers' => [
2924 + 'Content-Type' => 'application/json',
2925 + 'Authorization' => 'Bearer ' . $api_key,
2926 + ],
2927 + 'body' => wp_json_encode([
2928 + 'model' => $model,
2929 + 'messages' => [
2930 + ['role' => 'system', 'content' => $system_prompt],
2931 + ['role' => 'user', 'content' => sanitize_text_field($user_query)],
2932 + ],
2933 + 'temperature' => 0.2,
2934 + 'max_tokens' => 20,
2935 + ]),
2936 + 'method' => 'POST',
2937 + 'timeout' => 15,
2938 + ];
2939 +
2940 + $response = wp_remote_post($url, $args);
2941 + if (is_wp_error($response)) {
2942 + return sanitize_text_field($user_query);
1928 2943 }
1929 -
1930 - // Check if cart has items
1931 - if (WC()->cart->is_empty()) {
1932 - $this->fallbackResponse['text'] = esc_html__("Your cart is empty at the moment. Would you like to see our products?", 'mxchat');
1933 - return true;
2944 +
2945 + $body = json_decode(wp_remote_retrieve_body($response), true);
2946 + if (isset($body['choices'][0]['message']['content'])) {
2947 + return sanitize_text_field(trim($body['choices'][0]['message']['content']));
1934 2948 }
1935 -
1936 - // Get cart summary
1937 - $cart_count = WC()->cart->get_cart_contents_count();
1938 - $cart_total = WC()->cart->get_total();
1939 -
1940 - // Get and validate checkout URL
1941 - $checkout_url = wc_get_checkout_url();
1942 - if (!$checkout_url) {
1943 - $this->fallbackResponse['text'] = esc_html__("I'm having trouble accessing the checkout page. Please try again in a moment.", 'mxchat');
1944 - return true;
1945 - }
1946 -
1947 - wp_send_json([
1948 - 'text' => sprintf(
1949 - esc_html__("You have %d item%s in your cart totaling %s. I'll redirect you to checkout now.", 'mxchat'),
1950 - $cart_count,
1951 - $cart_count > 1 ? esc_html__('s', 'mxchat') : '',
1952 - strip_tags($cart_total)
1953 - ),
1954 - 'redirect_url' => esc_url_raw($checkout_url)
1955 - ]);
1956 - wp_die();
2949 +
2950 + return sanitize_text_field($user_query);
1957 2951 }
1958 2952
1959 -
1960 2953 //very good
1961 2954 private function add_email_to_loops($email) {
1962 2955 // Sanitize the email
1963 2956 $email = sanitize_email($email);
@@ -2041,95 +3034,209 @@
2041 3034
2042 3035 // Default to proceeding with conversation if no specific PDF action is needed
2043 3036 $this->fallbackResponse['text'] = '';
2044 3037 }
3038 +
3039 +
3040 +/**
3041 + * Enhanced fetch_and_split_pdf_pages with SSRF protection
3042 + */
2045 3043 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
3044 + // CLEAR DEBUG LOGGING
3045 + //error_log("=== MXCHAT PDF PROCESSING START ===");
3046 + //error_log("PDF Source: " . $pdf_source);
3047 + //error_log("Max Pages: " . $max_pages);
3048 + //error_log("Session ID: " . ($this->session_id ?? 'not set'));
3049 +
3050 + // Check if Advanced Claude Toolbar is available and enabled
3051 + $claude_available = function_exists('mxchatACT_is_advanced_claude_enabled');
3052 + $claude_enabled = $claude_available ? mxchatACT_is_advanced_claude_enabled() : false;
3053 +
3054 + //error_log("Claude Function Available: " . ($claude_available ? 'YES' : 'NO'));
3055 + //error_log("Claude Enabled: " . ($claude_enabled ? 'YES' : 'NO'));
3056 +
3057 + if ($claude_available && $claude_enabled) {
3058 + //error_log("🚀 ATTEMPTING CLAUDE PROCESSING...");
3059 +
3060 + // Attempt Claude processing first
3061 + $claude_result = apply_filters('mxchat_process_pdf_advanced', false, $pdf_source, $max_pages, $this->session_id);
3062 +
3063 + if ($claude_result !== false && is_array($claude_result) && !empty($claude_result)) {
3064 + //error_log("✅ CLAUDE PROCESSING SUCCESSFUL!");
3065 + //error_log("Claude returned " . count($claude_result) . " processed pages");
3066 +
3067 + // Log first page details for verification
3068 + if (isset($claude_result[0])) {
3069 + $first_page = $claude_result[0];
3070 + //error_log("First page enhanced: " . (isset($first_page['enhanced']) && $first_page['enhanced'] ? 'YES' : 'NO'));
3071 + //error_log("Processing method: " . ($first_page['processing_method'] ?? 'not set'));
3072 + //error_log("First page text preview: " . substr($first_page['text'] ?? '', 0, 100) . "...");
3073 + }
3074 +
3075 + //error_log("=== MXCHAT PDF PROCESSING END (CLAUDE) ===");
3076 + return $claude_result;
3077 + } else {
3078 + //error_log("❌ CLAUDE PROCESSING FAILED or returned invalid result");
3079 + //error_log("Claude result type: " . gettype($claude_result));
3080 + if (is_array($claude_result)) {
3081 + //error_log("Claude result count: " . count($claude_result));
3082 + }
3083 + }
3084 + }
3085 +
3086 + // Fallback to basic processing
3087 + //error_log("🔄 FALLING BACK TO BASIC PDF PROCESSING...");
3088 +
2046 3089 $upload_dir = wp_upload_dir();
2047 3090 $temp_file = null;
2048 -
3091 +
2049 3092 try {
2050 - // Handle URL vs local file
3093 + // Your existing basic processing code here...
3094 + // (I'll include the key parts with debug logging)
3095 +
2051 3096 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
2052 - // Validate and download the file from URL
2053 - $temp_file = wp_tempnam($pdf_source); // Safe temporary file name
2054 - $response = wp_remote_get($pdf_source, ['timeout' => 60]);
2055 -
3097 + //error_log("Downloading PDF from URL...");
3098 +
3099 + // SECURITY FIX: Validate URL before processing
3100 + if (!$this->mxchat_is_safe_pdf_url($pdf_source)) {
3101 + //error_log("❌ SECURITY: Blocked unsafe PDF URL");
3102 + return false;
3103 + }
3104 +
3105 + $temp_file = wp_tempnam($pdf_source);
3106 +
3107 + // SECURITY FIX: Changed from wp_remote_get to wp_safe_remote_get
3108 + $response = wp_safe_remote_get($pdf_source, [
3109 + 'timeout' => 60,
3110 + 'headers' => ['User-Agent' => 'MxChat PDF Processor']
3111 + ]);
3112 +
2056 3113 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
2057 - //error_log(esc_html__("Failed to download PDF. Error: ", 'mxchat') . print_r($response, true));
3114 + $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code($response);
3115 + //error_log("❌ BASIC PROCESSING: Failed to download PDF: " . $error_message);
2058 3116 return false;
2059 3117 }
2060 -
2061 - file_put_contents($temp_file, wp_remote_retrieve_body($response));
2062 -
2063 - // Validate that the downloaded file is a PDF
2064 - $mime_type = mime_content_type($temp_file);
2065 - if ($mime_type !== 'application/pdf') {
2066 - //error_log(esc_html__("Invalid MIME type detected for PDF: ", 'mxchat') . $mime_type);
2067 - unlink($temp_file);
2068 - return false;
3118 +
3119 + global $wp_filesystem;
3120 + if (empty($wp_filesystem)) {
3121 + require_once ABSPATH . 'wp-admin/includes/file.php';
3122 + WP_Filesystem();
2069 3123 }
3124 + $wp_filesystem->put_contents($temp_file, wp_remote_retrieve_body($response), FS_CHMOD_FILE);
3125 + //error_log("✅ PDF downloaded successfully");
2070 3126 } else {
2071 - // For local files, use the provided path directly
2072 3127 $temp_file = $pdf_source;
3128 + //error_log("Using local PDF file: " . $temp_file);
2073 3129 }
2074 -
2075 - // Parse and process the PDF
3130 +
3131 + // Parse PDF
3132 + //error_log("Parsing PDF with basic parser...");
3133 + mxchat_load_pdf_parser();
2076 3134 $parser = new \Smalot\PdfParser\Parser();
2077 3135 $pdf = $parser->parseFile($temp_file);
2078 3136 $pages = $pdf->getPages();
2079 -
3137 +
3138 + //error_log("PDF contains " . count($pages) . " pages");
3139 +
2080 3140 if (count($pages) > $max_pages) {
2081 - //error_log(esc_html__("PDF exceeds the maximum allowed pages: ", 'mxchat') . count($pages));
2082 - if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
3141 + //error_log("❌ BASIC PROCESSING: Too many pages (" . count($pages) . " > " . $max_pages . ")");
3142 + if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
2083 3143 unlink($temp_file);
2084 3144 }
2085 - return esc_html__('too_many_pages', 'mxchat');
3145 + return 'too_many_pages';
2086 3146 }
2087 -
3147 +
2088 3148 $embeddings = [];
3149 + $processed_pages = 0;
3150 +
2089 3151 foreach ($pages as $page_number => $page) {
2090 3152 $text = $page->getText();
2091 -
2092 - // Ensure text is non-empty before generating embeddings
3153 +
2093 3154 if (empty(trim($text))) {
2094 - //error_log(esc_html__("Skipping empty page: ", 'mxchat') . ($page_number + 1));
3155 + //error_log("Skipping empty page: " . ($page_number + 1));
2095 3156 continue;
2096 3157 }
2097 -
3158 +
3159 + $text = $this->mxchat_clean_text($text);
3160 +
2098 3161 $embedding = $this->mxchat_generate_embedding(
2099 - esc_html__("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
3162 + __("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
2100 3163 $this->options['api_key']
2101 3164 );
2102 -
3165 +
2103 3166 if ($embedding) {
2104 3167 $embeddings[] = [
2105 3168 'page_number' => $page_number + 1,
2106 3169 'embedding' => $embedding,
2107 3170 'text' => $text,
3171 + 'enhanced' => false, // CLEARLY MARK AS BASIC
3172 + 'processing_method' => 'basic_pdf_parser'
2108 3173 ];
2109 - } else {
2110 - //error_log(esc_html__("Failed to generate embedding for page ", 'mxchat') . ($page_number + 1));
3174 + $processed_pages++;
2111 3175 }
2112 3176 }
2113 -
2114 - // Clean up downloaded file if it was from URL
2115 - if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
3177 +
3178 + //error_log("✅ BASIC PROCESSING COMPLETE: " . $processed_pages . " pages processed");
3179 +
3180 + // Cleanup
3181 + if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
2116 3182 unlink($temp_file);
2117 3183 }
2118 -
3184 +
3185 + //error_log("=== MXCHAT PDF PROCESSING END (BASIC) ===");
2119 3186 return $embeddings;
2120 -
3187 +
2121 3188 } catch (\Exception $e) {
2122 - // error_log(esc_html__("Error parsing or processing PDF: ", 'mxchat') . $e->getMessage());
2123 -
2124 - // Cleanup in case of exception
3189 + //error_log("❌ BASIC PROCESSING ERROR: " . $e->getMessage());
2125 3190 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
2126 3191 unlink($temp_file);
2127 3192 }
3193 + //error_log("=== MXCHAT PDF PROCESSING END (ERROR) ===");
3194 + return false;
3195 + }
3196 +}
2128 3197
3198 +
3199 +/**
3200 + * Validate PDF URL for security
3201 + * Prevents SSRF attacks by blocking dangerous URLs
3202 + */
3203 +
3204 +private function mxchat_is_safe_pdf_url($url) {
3205 + // Use WordPress core function for comprehensive validation
3206 + // This blocks localhost, private IPs, and reserved IP ranges
3207 + $validated_url = wp_http_validate_url($url);
3208 +
3209 + if ($validated_url === false) {
2129 3210 return false;
2130 3211 }
3212 +
3213 + // Additional check: only allow HTTP/HTTPS schemes
3214 + $parsed = parse_url($url);
3215 + if (!isset($parsed['scheme']) || !in_array($parsed['scheme'], ['http', 'https'], true)) {
3216 + return false;
3217 + }
3218 +
3219 + return true;
2131 3220 }
3221 +
3222 +
3223 +private function mxchat_clean_text($text) {
3224 + // Remove excessive whitespace
3225 + $text = preg_replace('/\s+/', ' ', $text);
3226 +
3227 + // Remove control characters except newlines and tabs
3228 + $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text);
3229 +
3230 + // Normalize line endings
3231 + $text = str_replace(["\r\n", "\r"], "\n", $text);
3232 +
3233 + // Trim whitespace
3234 + $text = trim($text);
3235 +
3236 + return $text;
3237 +}
3238 +
2132 3239 private function find_relevant_pdf_pages($query_embedding, $embeddings) {
2133 3240 //error_log(esc_html__("find_relevant_pdf_pages called.", 'mxchat'));
2134 3241
2135 3242 $most_relevant = null;
@@ -2152,9 +3259,10 @@
2152 3259 }
2153 3260
2154 3261 return [];
2155 3262 }
2156 -// Add this to your class
3263 +
3264 +
2157 3265 public function handle_pdf_upload() {
2158 3266 check_ajax_referer('mxchat_chat_nonce', 'nonce');
2159 3267
2160 3268 if (!isset($_FILES['pdf_file']) || !isset($_POST['session_id'])) {
@@ -2161,12 +3269,30 @@
2161 3269 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
2162 3270 return;
2163 3271 }
2164 3272
3273 + // SECURITY FIX: Check if PDF uploads are enabled in settings
3274 + $options = get_option('mxchat_options', array());
3275 + $show_pdf_button = isset($options['show_pdf_upload_button']) ? $options['show_pdf_upload_button'] : 'on';
3276 +
3277 + if ($show_pdf_button !== 'on') {
3278 + wp_send_json_error(esc_html__('PDF uploads are currently disabled.', 'mxchat'));
3279 + return;
3280 + }
3281 +
2165 3282 $file = $_FILES['pdf_file'];
2166 3283 $session_id = sanitize_text_field($_POST['session_id']);
2167 3284 $original_filename = sanitize_text_field($file['name']);
2168 3285
3286 + // SECURITY FIX: Verify session ownership before allowing upload
3287 + $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
3288 + $session_owner = get_option("mxchat_session_owner_{$session_id}");
3289 +
3290 + if ($session_owner && $session_owner !== $current_user_identifier) {
3291 + wp_send_json_error(esc_html__('Unauthorized access.', 'mxchat'));
3292 + return;
3293 + }
3294 +
2169 3295 $file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
2170 3296 if ($file_type['type'] !== 'application/pdf') {
2171 3297 wp_send_json_error(esc_html__('Invalid file type. Only PDF files are allowed.', 'mxchat'));
2172 3298 return;
@@ -2172,9 +3298,12 @@
2172 3298 return;
2173 3299 }
2174 3300
2175 3301 $upload_dir = wp_upload_dir();
2176 - $pdf_filename = 'mxchat_' . $session_id . '_' . time() . '.pdf';
3302 +
3303 + // SECURITY FIX: Generate random filename without exposing session_id
3304 + $random_string = wp_generate_password(20, false, false); // 20 char alphanumeric string
3305 + $pdf_filename = 'mxchat_' . $random_string . '_' . time() . '.pdf';
2177 3306 $pdf_path = $upload_dir['path'] . '/' . $pdf_filename;
2178 3307
2179 3308 if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
2180 3309 wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
@@ -2205,8 +3334,9 @@
2205 3334 return;
2206 3335 }
2207 3336
2208 3337 if (!empty($embeddings)) {
3338 + // Store the mapping between session and the random filename
2209 3339 set_transient('mxchat_pdf_url_' . $session_id, $pdf_path, HOUR_IN_SECONDS);
2210 3340 set_transient('mxchat_pdf_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
2211 3341 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
2212 3342 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
@@ -2250,450 +3380,357 @@
2250 3380 wp_die();
2251 3381 }
2252 3382
2253 3383
2254 -/**
2255 - * Calls the AI API with the provided prompt.
2256 - *
2257 - * @param string $prompt The prompt to send to the AI.
2258 - * @return array An array containing the AI's response text.
2259 - */
2260 -private function mxchat_call_ai_api( $prompt ) {
2261 - //error_log( esc_html__( 'Calling AI API with the provided prompt.', 'mxchat' ) );
3384 +function mxchat_fetch_new_messages() {
3385 + $session_id = sanitize_text_field($_POST['session_id']);
3386 + $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
3387 + $persistence_enabled = $_POST['persistence_enabled'] === 'true';
3388 + $initial_timestamp = isset($_POST['initial_timestamp']) ? intval($_POST['initial_timestamp']) : 0;
2262 3389
2263 - $api_key = $this->options['api_key'];
2264 - if ( empty( $api_key ) ) {
2265 - //error_log( esc_html__( 'API key is not set.', 'mxchat' ) );
2266 - return [ 'text' => esc_html__( 'API key is not set.', 'mxchat' ) ];
3390 + if (empty($session_id)) {
3391 + //error_log(esc_html__('Fetch new messages error: Session ID missing.', 'mxchat'));
3392 + wp_send_json_error(['message' => esc_html__('Session ID missing.', 'mxchat')]);
3393 + wp_die();
2267 3394 }
2268 3395
2269 - $url = 'https://api.openai.com/v1/chat/completions';
2270 - $messages = [
2271 - [
2272 - 'role' => 'system',
2273 - 'content' => __( 'You are a helpful assistant that provides concise and personalized product recommendations. Someone has asked you for some recommendations please respond very concisely appropriate for an AI chatbot.', 'mxchat' ),
2274 - ],
2275 - [
2276 - 'role' => 'user',
2277 - 'content' => $prompt,
2278 - ],
2279 - ];
3396 + $history = get_option("mxchat_history_{$session_id}", []);
2280 3397
2281 - $args = [
2282 - 'headers' => [
2283 - 'Authorization' => 'Bearer ' . $api_key,
2284 - 'Content-Type' => 'application/json',
2285 - ],
2286 - 'body' => wp_json_encode(
2287 - [
2288 - 'model' => 'gpt-4o',
2289 - 'messages' => $messages,
2290 - 'temperature' => 0.7,
2291 - ]
2292 - ),
2293 - 'timeout' => 10,
2294 - 'method' => 'POST',
2295 - ];
3398 + //error_log("MxChat WhatsApp DEBUG: Fetch new messages for session {$session_id}");
3399 + //error_log("MxChat WhatsApp DEBUG: last_seen_id = " . var_export($last_seen_id, true));
3400 + //error_log("MxChat WhatsApp DEBUG: History count = " . count($history));
3401 + //error_log("MxChat WhatsApp DEBUG: Full history = " . print_r($history, true));
2296 3402
2297 - $response = wp_remote_post( $url, $args );
3403 + $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
3404 + //error_log("MxChat WhatsApp DEBUG: Checking message - ID: " . ($message['id'] ?? 'NO_ID') . ", Role: " . ($message['role'] ?? 'NO_ROLE'));
2298 3405
2299 - if ( is_wp_error( $response ) ) {
2300 - //error_log( esc_html__( 'Error communicating with AI API: ', 'mxchat' ) . $response->get_error_message() );
2301 - return [ 'text' => esc_html__( 'Error communicating with AI API.', 'mxchat' ) ];
2302 - }
3406 + // If persistence is enabled, show all new messages
3407 + if ($persistence_enabled) {
3408 + $has_id = !empty($message['id']);
3409 + $is_agent = $message['role'] === 'agent';
2303 3410
2304 - $body = wp_remote_retrieve_body( $response );
2305 - //error_log( esc_html__( 'API response body: ', 'mxchat' ) . $body );
3411 + // If last_seen_id is empty, 'NaN', or invalid, show all agent messages
3412 + if (empty($last_seen_id) || $last_seen_id === 'NaN' || $last_seen_id === 'undefined') {
3413 + $is_newer = true;
3414 + } else {
3415 + $is_newer = strcmp($message['id'] ?? '', $last_seen_id) > 0;
3416 + }
2306 3417
2307 - $decoded_body = json_decode( $body, true );
2308 - if ( isset( $decoded_body['choices'][0]['message']['content'] ) ) {
2309 - return [ 'text' => $decoded_body['choices'][0]['message']['content'] ];
2310 - } else {
2311 - //error_log( esc_html__( 'Unexpected API response format: ', 'mxchat' ) . wp_json_encode( $decoded_body ) );
2312 - return [ 'text' => esc_html__( 'No response received from AI.', 'mxchat' ) ];
2313 - }
2314 -}
3418 + //error_log("MxChat WhatsApp DEBUG: has_id={$has_id}, is_newer={$is_newer}, is_agent={$is_agent}");
2315 3419
2316 -/**
2317 - * Fetches the AI response for the given prompt.
2318 - *
2319 - * @param string $prompt The prompt to send to the AI.
2320 - * @return string|null The AI's response text or null if not available.
2321 - */
2322 -private function mxchat_fetch_ai_response( $prompt ) {
2323 - $response = $this->mxchat_call_ai_api( $prompt );
2324 - return isset( $response['text'] ) ? $response['text'] : null;
2325 -}
2326 -/**
2327 - * Generates the AI prompt for product recommendations.
2328 - *
2329 - * @param array $recommendations An array of product recommendations.
2330 - * @return string The generated AI prompt.
2331 - */
2332 -private function mxchat_generate_ai_recommendation_prompt($recommendations) {
2333 - $recommendation_list = '';
2334 - foreach ($recommendations as $index => $rec) {
2335 - $number = $index + 1;
2336 - $name = $rec['name'];
2337 - $price = $rec['price'];
2338 - $url = $rec['url'];
2339 - $image = $rec['image'];
2340 - $recommendation_list .= "{$number}. " . esc_html__('Product:', 'mxchat') . " {$name} (" . esc_html__('Price:', 'mxchat') . " \\${$price})\n";
2341 - $recommendation_list .= " [Link]({$url})\n";
2342 - $recommendation_list .= " ![Image]({$image})\n\n";
2343 - }
3420 + return $has_id && $is_newer && $is_agent;
3421 + }
2344 3422
2345 - $prompt = esc_html__('Based on the following list of products, generate a unique, friendly, and personalized response to a user. ', 'mxchat');
2346 - $prompt .= esc_html__('If some products aren\'t exactly what the user asked for but share similar styles or patterns, acknowledge this and explain why you\'re suggesting them. ', 'mxchat');
2347 - $prompt .= esc_html__('For each product, provide a brief justification that clearly explains why it\'s relevant, especially if it\'s a different type of product than requested. ', 'mxchat');
2348 - $prompt .= esc_html__('Please number your responses to match the product numbers.', 'mxchat') . "\n\n";
2349 - $prompt .= esc_html__('Products:', 'mxchat') . "\n\n{$recommendation_list}";
2350 - $prompt .= esc_html__('Please ensure that the number of each product matches the order in which the products are listed.', 'mxchat');
3423 + // If persistence is disabled, only show messages after initial timestamp
3424 + return !empty($message['id']) &&
3425 + $message['role'] === 'agent' &&
3426 + $message['timestamp'] > $initial_timestamp;
3427 + });
2351 3428
2352 - return $prompt;
3429 + //error_log("MxChat WhatsApp DEBUG: Filtered messages count = " . count($new_messages));
3430 +
3431 + // Include current chat mode so frontend can detect agent→AI transitions
3432 + $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
3433 +
3434 + wp_send_json_success([
3435 + 'new_messages' => array_values($new_messages),
3436 + 'chat_mode' => $chat_mode
3437 + ]);
3438 + wp_die();
2353 3439 }
2354 -private function mxchat_generate_recommendations($user_id, $message) {
2355 - // 1. Gather user context
2356 - $user_context = $this->mxchat_get_user_context($user_id);
2357 -
2358 - // Get cart item IDs for filtering
2359 - $cart_item_ids = [];
2360 - if (!empty($user_context['cart_items'])) {
2361 - $cart_item_ids = array_map(function($item) {
2362 - return $item['product_id'];
2363 - }, $user_context['cart_items']);
3440 +public function mxchat_live_agent_handover($message, $user_id, $session_id) {
3441 + // First check if live agents are available
3442 + $live_agent_available = $this->options['live_agent_status'] ?? 'off';
3443 + if ($live_agent_available !== 'on') {
3444 + $away_message = $this->options['live_agent_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
3445 + $this->fallbackResponse = [
3446 + 'text' => $away_message,
3447 + 'html' => '',
3448 + 'images' => [],
3449 + 'chat_mode' => 'ai'
3450 + ];
3451 + wp_send_json([
3452 + 'text' => $away_message,
3453 + 'html' => '',
3454 + 'chat_mode' => 'ai',
3455 + 'session_id' => $session_id
3456 + ]);
3457 + wp_die();
2364 3458 }
2365 3459
2366 - // 2. Get AI recommendation based on context
2367 - $ai_suggestion = $this->mxchat_get_ai_shopping_suggestion($message, $user_context);
2368 - //error_log("AI Shopping Suggestion: " . $ai_suggestion);
2369 -
2370 - // 3. Generate embedding for the AI suggestion
2371 - $suggestion_embedding = $this->mxchat_generate_embedding($ai_suggestion, $this->options['api_key']);
2372 - if (!$suggestion_embedding) {
2373 - //error_log("Could not generate embedding for AI suggestion");
2374 - return ['recommendations' => [], 'sources' => []];
3460 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
3461 +
3462 + if (empty($slack_bot_token)) {
3463 + return false;
2375 3464 }
2376 3465
2377 - // 4. Use existing relevant content function to find matches
2378 - $relevant_content = $this->mxchat_find_relevant_content($suggestion_embedding);
2379 -
2380 - // 5. Extract product URLs from the relevant content
2381 - preg_match_all('/https?:\/\/[^\s<>"\']+?\/product\/[^\s<>"\']+/', $relevant_content, $matches);
2382 -
2383 - $found_products = [];
2384 - if (!empty($matches[0])) {
2385 - foreach ($matches[0] as $url) {
2386 - // Clean the URL
2387 - $url = rtrim($url, '/."\']');
2388 -
2389 - // Get the product slug
2390 - $path = parse_url($url, PHP_URL_PATH);
2391 - $slug = basename(rtrim($path, '/'));
2392 -
2393 - // Find product by slug
2394 - $args = array(
2395 - 'post_type' => 'product',
2396 - 'post_status' => 'publish',
2397 - 'name' => $slug,
2398 - 'posts_per_page' => 1
2399 - );
2400 -
2401 - $products = get_posts($args);
2402 -
2403 - if (!empty($products)) {
2404 - $product_id = $products[0]->ID;
2405 -
2406 - // Skip if product is in cart
2407 - if (in_array($product_id, $cart_item_ids)) {
2408 - //error_log("Skipping product ID $product_id - already in cart");
2409 - continue;
3466 + // Check if channel already exists for this session
3467 + $channel_id = get_option("mxchat_channel_{$session_id}", '');
3468 +
3469 + if (empty($channel_id)) {
3470 + // Create new channel with session ID as name
3471 + $channel_name = $this->generate_channel_name($session_id);
3472 +
3473 + //error_log("Attempting to create channel: $channel_name");
3474 +
3475 + $response = wp_remote_post('https://slack.com/api/conversations.create', [
3476 + 'headers' => [
3477 + 'Content-Type' => 'application/json',
3478 + 'Authorization' => 'Bearer ' . $slack_bot_token
3479 + ],
3480 + 'body' => json_encode([
3481 + 'name' => $channel_name,
3482 + 'is_private' => false // Public channel - anyone in workspace can join
3483 + ])
3484 + ]);
3485 +
3486 + if (!is_wp_error($response)) {
3487 + $response_body = wp_remote_retrieve_body($response);
3488 + $response_data = json_decode($response_body, true);
3489 +
3490 + //error_log("Channel creation response: " . $response_body);
3491 +
3492 + if (isset($response_data['ok']) && $response_data['ok']) {
3493 + $channel_id = $response_data['channel']['id'];
3494 + $actual_channel_name = $response_data['channel']['name'] ?? 'unknown';
3495 + //error_log("Channel created successfully: ID=$channel_id, Name=$actual_channel_name");
3496 + update_option("mxchat_channel_{$session_id}", $channel_id);
3497 +
3498 + // Auto-invite agents to the channel
3499 + $agent_user_ids = $this->options['live_agent_user_ids'] ?? '';
3500 +
3501 + if (!empty($agent_user_ids)) {
3502 + // Parse user IDs (one per line)
3503 + $user_ids = array_filter(array_map('trim', explode("\n", $agent_user_ids)));
3504 +
3505 + foreach ($user_ids as $user_id_to_invite) {
3506 + //error_log("Inviting user to channel: $user_id_to_invite");
3507 +
3508 + $invite_response = wp_remote_post('https://slack.com/api/conversations.invite', [
3509 + 'headers' => [
3510 + 'Content-Type' => 'application/json',
3511 + 'Authorization' => 'Bearer ' . $slack_bot_token
3512 + ],
3513 + 'body' => json_encode([
3514 + 'channel' => $channel_id,
3515 + 'users' => $user_id_to_invite
3516 + ])
3517 + ]);
3518 +
3519 + if (!is_wp_error($invite_response)) {
3520 + $invite_body = wp_remote_retrieve_body($invite_response);
3521 + $invite_data = json_decode($invite_body, true);
3522 + //error_log("Invite response for $user_id_to_invite: " . $invite_body);
3523 +
3524 + if (isset($invite_data['ok']) && $invite_data['ok']) {
3525 + //error_log("Successfully invited user $user_id_to_invite to channel");
3526 + } else {
3527 + //error_log("Failed to invite user $user_id_to_invite: " . ($invite_data['error'] ?? 'Unknown error'));
3528 + }
3529 + } else {
3530 + //error_log("WP Error inviting user $user_id_to_invite: " . $invite_response->get_error_message());
3531 + }
3532 + }
3533 + } else {
3534 + //error_log("No agent user IDs configured for auto-invite");
2410 3535 }
2411 -
2412 - $product = wc_get_product($product_id);
2413 -
2414 - if ($product && $product->is_purchasable() && !in_array($product_id, array_map(function($p) {
2415 - return $p->get_id();
2416 - }, $found_products))) {
2417 - $found_products[] = $product;
2418 - }
3536 + } else {
3537 + //error_log("Channel creation failed: " . ($response_data['error'] ?? 'Unknown error'));
2419 3538 }
3539 + } else {
3540 + //error_log("WP Error creating channel: " . $response->get_error_message());
2420 3541 }
3542 +
3543 + if (empty($channel_id)) {
3544 + return false; // Failed to create channel
3545 + }
2421 3546 }
2422 3547
2423 - // If no products found by URLs, look for product names in the content
2424 - if (empty($found_products)) {
2425 - $products = wc_get_products([
2426 - 'status' => 'publish',
2427 - 'limit' => -1,
2428 - 'return' => 'objects'
2429 - ]);
3548 + // Get recent chat history
3549 + $history = get_option("mxchat_history_{$session_id}", []);
3550 + $recent_history = array_slice($history, -5);
2430 3551
2431 - foreach ($products as $product) {
2432 - // Skip if product is in cart
2433 - if (in_array($product->get_id(), $cart_item_ids)) {
2434 - continue;
2435 - }
2436 -
2437 - $name = $product->get_name();
2438 - if (stripos($relevant_content, $name) !== false) {
2439 - if ($product->is_purchasable() && !in_array($product->get_id(), array_map(function($p) {
2440 - return $p->get_id();
2441 - }, $found_products))) {
2442 - $found_products[] = $product;
2443 - }
2444 - }
3552 + // Format conversation context
3553 + $conversation_context = "";
3554 + if (!empty($recent_history)) {
3555 + $conversation_context = "*Recent Conversation:*\n";
3556 + foreach ($recent_history as $hist_message) {
3557 + $role_display = $hist_message['role'] === 'user' ? 'User' : 'AI';
3558 + $conversation_context .= ">{$role_display}: {$hist_message['content']}\n";
2445 3559 }
3560 + $conversation_context .= "\n";
2446 3561 }
2447 3562
2448 - // Keep searching until we have 4 products or run out of options
2449 - $formatted_recommendations = [];
2450 - foreach ($found_products as $product) {
2451 - if (count($formatted_recommendations) >= 4) break;
3563 + update_option("mxchat_mode_{$session_id}", 'agent');
2452 3564
2453 - $formatted_recommendations[] = [
2454 - 'name' => $product->get_name(),
2455 - 'price' => $product->get_price(),
2456 - 'url' => get_permalink($product->get_id()),
2457 - 'image' => wp_get_attachment_url($product->get_image_id())
2458 - ];
3565 + // Send message to channel
3566 + $channel_message = "🔔 *New Live Agent Request*\n\n";
3567 + $channel_message .= "*Session ID:* `{$session_id}`\n";
3568 + $channel_message .= "*User ID:* `{$user_id}`\n\n";
3569 +
3570 + if (!empty($conversation_context)) {
3571 + $channel_message .= $conversation_context;
2459 3572 }
3573 +
3574 + $channel_message .= "*Current Message:*\n{$message}\n\n";
3575 + $channel_message .= "_Reply directly in this channel - all messages will go to the user_";
2460 3576
2461 - return [
2462 - 'recommendations' => $formatted_recommendations,
2463 - 'sources' => [__('AI-powered personalized recommendations', 'mxchat')]
2464 - ];
2465 -}
2466 -private function mxchat_get_ai_shopping_suggestion($message, $user_context) {
2467 - $prompt = esc_html__("As a shopping assistant, analyze this user's context and generate a specific product search suggestion. ", 'mxchat');
2468 - $prompt .= esc_html__("User's Question: \"{$message}\"\n\n", 'mxchat');
3577 + wp_remote_post('https://slack.com/api/chat.postMessage', [
3578 + 'headers' => [
3579 + 'Content-Type' => 'application/json',
3580 + 'Authorization' => 'Bearer ' . $slack_bot_token
3581 + ],
3582 + 'body' => json_encode([
3583 + 'channel' => $channel_id,
3584 + 'text' => $channel_message,
3585 + 'mrkdwn' => true
3586 + ])
3587 + ]);
2469 3588
2470 - if (!empty($user_context['order_history'])) {
2471 - $prompt .= esc_html__("Their recent orders include:\n", 'mxchat');
2472 - foreach ($user_context['order_history'] as $order) {
2473 - $prompt .= esc_html__("- {$order['name']} ({$order['date']})\n", 'mxchat');
2474 - }
2475 - }
3589 + $success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
3590 + $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
2476 3591
2477 - if (!empty($user_context['cart_items'])) {
2478 - $prompt .= esc_html__("\nThey currently have in their cart:\n", 'mxchat');
2479 - foreach ($user_context['cart_items'] as $item) {
2480 - $prompt .= esc_html__("- {$item['name']}\n", 'mxchat');
2481 - }
2482 - }
3592 + $this->fallbackResponse = [
3593 + 'text' => $success_message,
3594 + 'html' => '',
3595 + 'images' => [],
3596 + 'chat_mode' => 'agent'
3597 + ];
2483 3598
2484 - $prompt .= esc_html__("\nBased on their question and history, suggest a specific search query that would help find the most relevant products. ", 'mxchat');
2485 - $prompt .= esc_html__("Respond with ONLY the search query, nothing else.", 'mxchat');
2486 -
2487 - $response = $this->mxchat_call_ai_api($prompt);
2488 - return isset($response['text']) ? trim($response['text']) : $message;
3599 + wp_send_json([
3600 + 'success' => true,
3601 + 'text' => $success_message,
3602 + 'html' => '',
3603 + 'chat_mode' => 'agent',
3604 + 'session_id' => $session_id,
3605 + 'fallbackResponse' => $this->fallbackResponse
3606 + ]);
3607 + wp_die();
2489 3608 }
2490 -public function mxchat_handle_product_recommendations($message, $user_id, $session_id) {
2491 - try {
2492 - //error_log("Starting product recommendations for user: $user_id, session: $session_id");
2493 3609
2494 - // Pass the message to get context-aware recommendations
2495 - $recommendation_data = $this->mxchat_generate_recommendations($user_id, $message);
2496 - //error_log('Generated recommendation data: ' . wp_json_encode($recommendation_data));
2497 -
2498 - if (empty($recommendation_data['recommendations'])) {
2499 - //error_log("No recommendations found for user: $user_id");
2500 - $this->fallbackResponse = [
2501 - 'text' => __("I couldn't find any product recommendations for you right now. Please try again later!", 'mxchat'),
2502 - ];
2503 - return;
3610 +private function generate_channel_name($session_id) {
3611 + $email = null;
3612 + $name = null;
3613 +
3614 + // 1. First priority: Check if user is logged in and get their info
3615 + if (is_user_logged_in()) {
3616 + $current_user = wp_get_current_user();
3617 + if (!empty($current_user->user_email)) {
3618 + $email = $current_user->user_email;
3619 + //error_log("[DEBUG] Using logged-in user email for channel: {$email}");
2504 3620 }
2505 -
2506 - // Remove duplicates and limit to top 4 recommendations
2507 - $unique_recommendations = [];
2508 - foreach ($recommendation_data['recommendations'] as $rec) {
2509 - $unique_recommendations[$rec['url']] = $rec;
3621 + if (!empty($current_user->display_name)) {
3622 + $name = $current_user->display_name;
3623 + //error_log("[DEBUG] Using logged-in user name for channel: {$name}");
2510 3624 }
2511 -
2512 - $unique_recommendations = array_slice($unique_recommendations, 0, 4);
2513 - //error_log('Top 4 recommendations: ' . wp_json_encode($unique_recommendations));
2514 -
2515 - $recommendations_summary = [];
2516 - foreach ($unique_recommendations as $rec) {
2517 - $recommendations_summary[] = [
2518 - 'name' => $rec['name'],
2519 - 'price' => strip_tags($rec['price']),
2520 - 'url' => $rec['url'],
2521 - 'image' => $rec['image'],
2522 - ];
3625 + }
3626 +
3627 + // 2. Second priority: Check for saved email/name from "require email to chat" option
3628 + if (empty($email)) {
3629 + $email_option_key = "mxchat_email_{$session_id}";
3630 + $saved_email = get_option($email_option_key);
3631 + if (!empty($saved_email)) {
3632 + $email = $saved_email;
3633 + //error_log("[DEBUG] Using saved email from session for channel: {$email}");
2523 3634 }
2524 -
2525 - // Generate AI prompt and fetch response
2526 - $ai_prompt = $this->mxchat_generate_ai_recommendation_prompt($recommendations_summary);
2527 - $ai_response = $this->mxchat_fetch_ai_response($ai_prompt);
2528 -
2529 - if (empty($ai_response)) {
2530 - $this->fallbackResponse = [
2531 - 'text' => __('An unexpected error occurred while generating recommendations. Please try again later.', 'mxchat'),
2532 - ];
2533 - return;
3635 + }
3636 +
3637 + if (empty($name)) {
3638 + $name_option_key = "mxchat_name_{$session_id}";
3639 + $saved_name = get_option($name_option_key);
3640 + if (!empty($saved_name)) {
3641 + $name = $saved_name;
3642 + //error_log("[DEBUG] Using saved name from session for channel: {$name}");
2534 3643 }
2535 -
2536 - // Split the AI's response into lines
2537 - $ai_lines = preg_split('/\r\n|\r|\n/', $ai_response);
2538 -
2539 - // Initialize variables
2540 - $formatted_response = '';
2541 - $justifications = [];
2542 - $current_number = 0;
2543 - $in_introduction = true;
2544 - $introduction = '';
2545 -
2546 - // Parse the AI response to separate the introduction and the justifications
2547 - foreach ($ai_lines as $line) {
2548 - if (preg_match('/^\s*(\d+)\.\s*(.*)$/', $line, $matches)) {
2549 - // This line is a numbered justification
2550 - $current_number = intval($matches[1]) - 1;
2551 - $justifications[$current_number] = $matches[2];
2552 - $in_introduction = false;
2553 - } elseif ($in_introduction) {
2554 - // This line is part of the introduction
2555 - $introduction .= $line . ' ';
2556 - } else {
2557 - // This line is a continuation of the current justification
2558 - if (isset($justifications[$current_number])) {
2559 - $justifications[$current_number] .= ' ' . $line;
2560 - }
3644 + }
3645 +
3646 + // 3. Third priority: Check existing chat transcript for email/name
3647 + if (empty($email) || empty($name)) {
3648 + global $wpdb;
3649 + $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
3650 + $existing_data = $wpdb->get_row($wpdb->prepare(
3651 + "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",
3652 + $session_id
3653 + ));
3654 +
3655 + if ($existing_data) {
3656 + if (empty($email) && !empty($existing_data->user_email)) {
3657 + $email = $existing_data->user_email;
3658 + //error_log("[DEBUG] Using email from chat transcript for channel: {$email}");
2561 3659 }
2562 - }
2563 -
2564 - // Build the formatted response
2565 - if (!empty($introduction)) {
2566 - $formatted_response .= esc_html(trim($introduction)) . "<br><br>";
2567 - }
2568 -
2569 - foreach ($recommendations_summary as $index => $rec) {
2570 - $name = esc_html($rec['name']);
2571 - $price = number_format((float)$rec['price'], 2, '.', '');
2572 - $url = esc_url($rec['url']);
2573 - $image = esc_url($rec['image']);
2574 -
2575 - $formatted_response .= ($index + 1) . ". <strong>" . $name . "</strong> - <strong>$" . $price . "</strong><br>";
2576 - $formatted_response .= "<img src=\"{$image}\" alt=\"{$name}\" style=\"max-width: 200px; height: auto; display: block; margin: 10px 0;\" /><br>";
2577 -
2578 - if (isset($justifications[$index])) {
2579 - $formatted_response .= "<em>" . esc_html(trim($justifications[$index])) . "</em><br><br>";
3660 + if (empty($name) && !empty($existing_data->user_name)) {
3661 + $name = $existing_data->user_name;
3662 + //error_log("[DEBUG] Using name from chat transcript for channel: {$name}");
2580 3663 }
2581 3664 }
2582 -
2583 - $this->fallbackResponse = [
2584 - 'text' => $formatted_response,
2585 - ];
2586 - //error_log('Final formatted response set.');
2587 - } catch (Exception $e) {
2588 - //error_log('Error in mxchat_handle_product_recommendations: ' . $e->getMessage());
2589 - $this->fallbackResponse = [
2590 - 'text' => __('An unexpected error occurred while generating recommendations. Please try again later.', 'mxchat'),
2591 - ];
2592 3665 }
2593 -}
2594 -private function mxchat_get_user_context($user_id) {
2595 - $context = [
2596 - 'order_history' => [],
2597 - 'cart_items' => [],
2598 - 'recently_viewed' => [],
2599 - ];
2600 -
2601 - // Get order history
2602 - if (is_user_logged_in() && $user_id) {
2603 - $orders = wc_get_orders([
2604 - 'customer_id' => $user_id,
2605 - 'limit' => 5, // Last 5 orders
2606 - 'orderby' => 'date',
2607 - 'order' => 'DESC',
2608 - ]);
2609 -
2610 - foreach ($orders as $order) {
2611 - foreach ($order->get_items() as $item) {
2612 - $context['order_history'][] = [
2613 - 'name' => $item->get_name(),
2614 - 'product_id' => $item->get_product_id(),
2615 - 'date' => $order->get_date_created()->format('Y-m-d')
2616 - ];
2617 - }
3666 +
3667 + // 4. Generate channel name based on priority: Name > Email > Session ID
3668 + $channel_name = '';
3669 +
3670 + if (!empty($name)) {
3671 + // Convert name to valid Slack channel name
3672 + $base_name = strtolower(trim($name));
3673 + // Replace spaces and invalid characters
3674 + $base_name = preg_replace('/[^a-z0-9\s]/', '', $base_name);
3675 + $base_name = preg_replace('/\s+/', '-', $base_name);
3676 + $base_name = trim($base_name, '-');
3677 +
3678 + // Get last 4 characters of session ID for uniqueness
3679 + $session_suffix = substr($session_id, -4);
3680 + $channel_name = 'chat-' . $base_name . '-' . strtolower($session_suffix);
3681 +
3682 + // Slack channel names have a 21 character limit
3683 + if (strlen($channel_name) > 21) {
3684 + // Calculate available space for name (21 - 'chat-' - '-' - session_suffix)
3685 + $available_space = 21 - 5 - 1 - strlen($session_suffix); // 'chat-' = 5, '-' = 1
3686 + $truncated_name = substr($base_name, 0, $available_space);
3687 + $truncated_name = rtrim($truncated_name, '-'); // Remove trailing hyphen
3688 + $channel_name = 'chat-' . $truncated_name . '-' . strtolower($session_suffix);
2618 3689 }
2619 - }
2620 -
2621 - // Get cart items
2622 - if (WC()->cart && WC()->cart->get_cart_contents_count() > 0) {
2623 - foreach (WC()->cart->get_cart() as $cart_item) {
2624 - $product = wc_get_product($cart_item['product_id']);
2625 - if ($product) {
2626 - $context['cart_items'][] = [
2627 - 'name' => $product->get_name(),
2628 - 'product_id' => $product->get_id()
2629 - ];
2630 - }
3690 +
3691 + //error_log("[DEBUG] Using name for channel: {$channel_name} (from name: {$name})");
3692 +
3693 + } elseif (!empty($email)) {
3694 + // Convert email to valid Slack channel name (your existing logic)
3695 + $channel_name = 'chat-' . strtolower(str_replace(['@', '.', '+', '_'], ['-at-', '-', '-plus-', '-'], $email));
3696 + // Remove any remaining invalid characters
3697 + $channel_name = preg_replace('/[^a-z0-9\-]/', '', $channel_name);
3698 + // Ensure it doesn't end with a hyphen
3699 + $channel_name = rtrim($channel_name, '-');
3700 + // Slack channel names have a 21 character limit, so truncate if needed
3701 + if (strlen($channel_name) > 21) {
3702 + $channel_name = substr($channel_name, 0, 21);
3703 + $channel_name = rtrim($channel_name, '-'); // Remove trailing hyphen if truncation created one
2631 3704 }
3705 +
3706 + //error_log("[DEBUG] Using email for channel: {$channel_name} (from email: {$email})");
3707 +
3708 + } else {
3709 + // Fallback to session ID if no name or email found
3710 + $channel_name = 'chat-' . strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $session_id));
3711 + //error_log("[DEBUG] No name or email found, using session ID for channel: {$channel_name}");
2632 3712 }
2633 -
2634 - // Get recently viewed items from session
2635 - $viewed_products = WC()->session->get('recently_viewed_products', []);
2636 - foreach ($viewed_products as $product_id) {
2637 - $product = wc_get_product($product_id);
2638 - if ($product) {
2639 - $context['recently_viewed'][] = [
2640 - 'name' => $product->get_name(),
2641 - 'product_id' => $product->get_id()
2642 - ];
2643 - }
3713 +
3714 + // Final validation - ensure channel name meets Slack requirements
3715 + if (strlen($channel_name) > 21) {
3716 + $channel_name = substr($channel_name, 0, 21);
3717 + $channel_name = rtrim($channel_name, '-');
2644 3718 }
2645 -
2646 - return $context;
3719 +
3720 + //error_log("[DEBUG] Generated channel name: {$channel_name}");
3721 + return $channel_name;
2647 3722 }
2648 3723
2649 -
2650 -
2651 -
2652 -
2653 -
2654 -function mxchat_fetch_new_messages() {
2655 - $session_id = sanitize_text_field($_POST['session_id']);
2656 - $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
2657 - $persistence_enabled = $_POST['persistence_enabled'] === 'true';
2658 - $initial_timestamp = isset($_POST['initial_timestamp']) ? intval($_POST['initial_timestamp']) : 0;
2659 -
2660 - if (empty($session_id)) {
2661 - //error_log(esc_html__('Fetch new messages error: Session ID missing.', 'mxchat'));
2662 - wp_send_json_error(['message' => esc_html__('Session ID missing.', 'mxchat')]);
2663 - wp_die();
2664 - }
2665 -
2666 - $history = get_option("mxchat_history_{$session_id}", []);
2667 -
2668 - $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
2669 - // If persistence is enabled, show all new messages
2670 - if ($persistence_enabled) {
2671 - return !empty($message['id']) &&
2672 - strcmp($message['id'], $last_seen_id) > 0 &&
2673 - $message['role'] === 'agent';
2674 - }
2675 -
2676 - // If persistence is disabled, only show messages after initial timestamp
2677 - return !empty($message['id']) &&
2678 - $message['role'] === 'agent' &&
2679 - $message['timestamp'] > $initial_timestamp;
2680 - });
2681 -
2682 - //error_log(esc_html__("New agent messages fetched for session $session_id. Last seen ID: $last_seen_id", 'mxchat'));
2683 -
2684 - wp_send_json_success([
2685 - 'new_messages' => array_values($new_messages)
2686 - ]);
2687 - wp_die();
2688 -}
2689 -
2690 -
2691 -public function mxchat_live_agent_handover($message, $user_id, $session_id) {
2692 - // First check if live agents are available
2693 - $live_agent_available = $this->options['live_agent_status'] ?? 'off';
2694 - if ($live_agent_available !== 'on') {
2695 - $away_message = $this->options['live_agent_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
3724 +/**
3725 + * Telegram Live Agent Handover
3726 + * Creates a forum topic in the Telegram group and notifies agents
3727 + */
3728 +public function mxchat_telegram_live_agent_handover($message, $user_id, $session_id) {
3729 + // Check if Telegram agents are available
3730 + $telegram_available = $this->options['telegram_status'] ?? 'off';
3731 + if ($telegram_available !== 'on') {
3732 + $away_message = $this->options['telegram_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
2696 3733 $this->fallbackResponse = [
2697 3734 'text' => $away_message,
2698 3735 'html' => '',
2699 3736 'images' => [],
@@ -2707,106 +3744,101 @@
2707 3744 ]);
2708 3745 wp_die();
2709 3746 }
2710 3747
2711 - $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2712 - if (empty($slack_webhook_url)) {
3748 + $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
3749 + $telegram_group_id = $this->options['telegram_group_id'] ?? '';
3750 +
3751 + if (empty($telegram_bot_token) || empty($telegram_group_id)) {
2713 3752 return false;
2714 3753 }
2715 3754
2716 - // Get recent chat history (last 5 messages)
3755 + // Check if topic already exists for this session
3756 + $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
3757 +
3758 + if (empty($topic_id)) {
3759 + // Generate topic name
3760 + $topic_name = $this->generate_telegram_topic_name($session_id);
3761 +
3762 + // Random icon color (Telegram forum topic colors)
3763 + $icon_colors = [0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98, 0xFF93B2, 0xFB6F5F];
3764 + $icon_color = $icon_colors[array_rand($icon_colors)];
3765 +
3766 + // Create forum topic
3767 + $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/createForumTopic", [
3768 + 'headers' => ['Content-Type' => 'application/json'],
3769 + 'body' => json_encode([
3770 + 'chat_id' => $telegram_group_id,
3771 + 'name' => $topic_name,
3772 + 'icon_color' => $icon_color
3773 + ])
3774 + ]);
3775 +
3776 + if (!is_wp_error($response)) {
3777 + $response_body = wp_remote_retrieve_body($response);
3778 + $response_data = json_decode($response_body, true);
3779 +
3780 + if (isset($response_data['ok']) && $response_data['ok']) {
3781 + $topic_id = $response_data['result']['message_thread_id'];
3782 + update_option("mxchat_telegram_topic_{$session_id}", $topic_id);
3783 + update_option("mxchat_telegram_group_{$session_id}", $telegram_group_id);
3784 + }
3785 + }
3786 +
3787 + if (empty($topic_id)) {
3788 + return false; // Failed to create topic
3789 + }
3790 + }
3791 +
3792 + // Get recent chat history
2717 3793 $history = get_option("mxchat_history_{$session_id}", []);
2718 - $recent_history = array_slice($history, -5); // Get last 5 messages
3794 + $recent_history = array_slice($history, -5);
2719 3795
2720 - // Format conversation history
3796 + // Format conversation context for Telegram (HTML format)
2721 3797 $conversation_context = "";
2722 3798 if (!empty($recent_history)) {
2723 - $conversation_context = "*Recent Conversation:*\n";
3799 + $conversation_context = "<b>Recent Conversation:</b>\n";
2724 3800 foreach ($recent_history as $hist_message) {
2725 - $role_display = $hist_message['role'] === 'user' ? 'User' : 'AI';
2726 - $conversation_context .= ">{$role_display}: {$hist_message['content']}\n";
3801 + $role_display = $hist_message['role'] === 'user' ? '👤 User' : '🤖 AI';
3802 + $escaped_content = htmlspecialchars($hist_message['content'], ENT_QUOTES, 'UTF-8');
3803 + $conversation_context .= "{$role_display}: {$escaped_content}\n";
2727 3804 }
2728 3805 $conversation_context .= "\n";
2729 3806 }
2730 3807
3808 + // Get user info
3809 + $user_email = get_option("mxchat_email_{$session_id}", 'Not provided');
3810 + $user_name = get_option("mxchat_name_{$session_id}", 'Anonymous');
3811 +
3812 + // Update session mode
2731 3813 update_option("mxchat_mode_{$session_id}", 'agent');
2732 3814
2733 - $webhook_data = [
2734 - 'blocks' => [
2735 - [
2736 - 'type' => 'header',
2737 - 'text' => [
2738 - 'type' => 'plain_text',
2739 - 'text' => '🔔 New Live Agent Request',
2740 - 'emoji' => true
2741 - ]
2742 - ],
2743 - [
2744 - 'type' => 'section',
2745 - 'fields' => [
2746 - [
2747 - 'type' => 'mrkdwn',
2748 - 'text' => sprintf('*User ID:*\n`%s`', $user_id)
2749 - ],
2750 - [
2751 - 'type' => 'mrkdwn',
2752 - 'text' => sprintf('*Session ID:*\n`%s`', $session_id)
2753 - ]
2754 - ]
2755 - ]
2756 - ]
2757 - ];
3815 + // Send initial message to topic
3816 + $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
3817 + $topic_message = "🔔 <b>New Live Agent Request</b>\n\n";
3818 + $topic_message .= "<b>Session ID:</b> <code>{$session_id}</code>\n";
3819 + $topic_message .= "<b>User:</b> {$user_name}\n";
3820 + $topic_message .= "<b>Email:</b> {$user_email}\n\n";
2758 3821
2759 - // Add conversation history if exists
2760 3822 if (!empty($conversation_context)) {
2761 - $webhook_data['blocks'][] = [
2762 - 'type' => 'section',
2763 - 'text' => [
2764 - 'type' => 'mrkdwn',
2765 - 'text' => $conversation_context
2766 - ]
2767 - ];
3823 + $topic_message .= $conversation_context;
2768 3824 }
2769 3825
2770 - // Add the current message
2771 - $webhook_data['blocks'][] = [
2772 - 'type' => 'section',
2773 - 'text' => [
2774 - 'type' => 'mrkdwn',
2775 - 'text' => sprintf('*Current Message:*\n%s', $message)
2776 - ]
2777 - ];
3826 + $topic_message .= "<b>Current Message:</b>\n{$escaped_message}\n\n";
3827 + $topic_message .= "<i>Reply in this topic - messages will be sent to the user</i>\n";
3828 + $topic_message .= "<i>Type #close, #end, #disconnect, or #done to end the session</i>";
2778 3829
2779 - // Add the reply button
2780 - $webhook_data['blocks'][] = [
2781 - 'type' => 'actions',
2782 - 'elements' => [
2783 - [
2784 - 'type' => 'button',
2785 - 'text' => [
2786 - 'type' => 'plain_text',
2787 - 'text' => '✍️ Reply',
2788 - 'emoji' => true
2789 - ],
2790 - 'value' => $session_id,
2791 - 'action_id' => 'reply_to_user',
2792 - 'style' => 'primary'
2793 - ]
2794 - ]
2795 - ];
2796 -
2797 - $response = wp_remote_post($slack_webhook_url, [
2798 - 'body' => json_encode($webhook_data),
2799 - 'headers' => [
2800 - 'Content-Type' => 'application/json',
2801 - ],
3830 + wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
3831 + 'headers' => ['Content-Type' => 'application/json'],
3832 + 'body' => json_encode([
3833 + 'chat_id' => $telegram_group_id,
3834 + 'message_thread_id' => $topic_id,
3835 + 'text' => $topic_message,
3836 + 'parse_mode' => 'HTML'
3837 + ])
2802 3838 ]);
2803 3839
2804 - if (is_wp_error($response)) {
2805 - return false;
2806 - }
2807 -
2808 - $success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
3840 + $success_message = $this->options['telegram_notification_message'] ?? "I've notified a support agent. Please allow a moment for them to respond.";
2809 3841 $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
2810 3842
2811 3843 $this->fallbackResponse = [
2812 3844 'text' => $success_message,
@@ -2824,79 +3856,278 @@
2824 3856 'fallbackResponse' => $this->fallbackResponse
2825 3857 ]);
2826 3858 wp_die();
2827 3859 }
3860 +
3861 +/**
3862 + * Generate topic name for Telegram forum
3863 + */
3864 +private function generate_telegram_topic_name($session_id) {
3865 + $name = null;
3866 + $email = null;
3867 +
3868 + // Check logged in user
3869 + if (is_user_logged_in()) {
3870 + $current_user = wp_get_current_user();
3871 + if (!empty($current_user->display_name)) {
3872 + $name = $current_user->display_name;
3873 + }
3874 + if (!empty($current_user->user_email)) {
3875 + $email = $current_user->user_email;
3876 + }
3877 + }
3878 +
3879 + // Check session data
3880 + if (empty($name)) {
3881 + $name = get_option("mxchat_name_{$session_id}");
3882 + }
3883 + if (empty($email)) {
3884 + $email = get_option("mxchat_email_{$session_id}");
3885 + }
3886 +
3887 + // Generate topic name
3888 + $session_suffix = substr($session_id, -6);
3889 +
3890 + if (!empty($name)) {
3891 + // Clean name for topic (max 128 chars in Telegram)
3892 + $clean_name = preg_replace('/[^\p{L}\p{N}\s\-]/u', '', $name);
3893 + $clean_name = trim($clean_name);
3894 + if (strlen($clean_name) > 50) {
3895 + $clean_name = substr($clean_name, 0, 50);
3896 + }
3897 + return "Chat - {$clean_name} ({$session_suffix})";
3898 + } elseif (!empty($email)) {
3899 + // Use email prefix
3900 + $email_prefix = explode('@', $email)[0];
3901 + if (strlen($email_prefix) > 30) {
3902 + $email_prefix = substr($email_prefix, 0, 30);
3903 + }
3904 + return "Chat - {$email_prefix} ({$session_suffix})";
3905 + }
3906 +
3907 + return "Chat - {$session_suffix}";
3908 +}
3909 +
3910 +/**
3911 + * Send user message to Telegram agent
3912 + */
3913 +public function mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id) {
3914 + $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
3915 + $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
3916 + $group_id = get_option("mxchat_telegram_group_{$session_id}", '');
3917 +
3918 + if (empty($telegram_bot_token) || empty($topic_id) || empty($group_id)) {
3919 + return false;
3920 + }
3921 +
3922 + $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
3923 + $user_message = "👤 <b>User:</b> {$escaped_message}";
3924 +
3925 + $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
3926 + 'headers' => ['Content-Type' => 'application/json'],
3927 + 'body' => json_encode([
3928 + 'chat_id' => $group_id,
3929 + 'message_thread_id' => $topic_id,
3930 + 'text' => $user_message,
3931 + 'parse_mode' => 'HTML'
3932 + ])
3933 + ]);
3934 +
3935 + return !is_wp_error($response);
3936 +}
3937 +
3938 +/**
3939 + * Handle incoming Telegram webhook
3940 + */
3941 +public function handle_telegram_webhook(WP_REST_Request $request) {
3942 + $body = $request->get_body();
3943 + $data = json_decode($body, true);
3944 +
3945 + //error_log('[MxChat Telegram DEBUG] Webhook received: ' . $body);
3946 +
3947 + // Handle message events from forum topics
3948 + if (isset($data['message'])) {
3949 + $message_data = $data['message'];
3950 +
3951 + // Skip if not from a forum topic
3952 + if (!isset($message_data['message_thread_id'])) {
3953 + //error_log('[MxChat Telegram DEBUG] Skipped: No message_thread_id (not a forum topic message)');
3954 + return new WP_REST_Response(['ok' => true]);
3955 + }
3956 +
3957 + // Skip bot messages
3958 + if (isset($message_data['from']['is_bot']) && $message_data['from']['is_bot']) {
3959 + //error_log('[MxChat Telegram DEBUG] Skipped: Message from bot');
3960 + return new WP_REST_Response(['ok' => true]);
3961 + }
3962 +
3963 + $chat_id = $message_data['chat']['id'] ?? '';
3964 + $topic_id = $message_data['message_thread_id'];
3965 + $message_text = $message_data['text'] ?? '';
3966 + $message_id = $message_data['message_id'] ?? '';
3967 + $from = $message_data['from'] ?? [];
3968 + $agent_name = trim(($from['first_name'] ?? '') . ' ' . ($from['last_name'] ?? ''));
3969 + if (empty($agent_name)) {
3970 + $agent_name = $from['username'] ?? 'Agent';
3971 + }
3972 +
3973 + //error_log("[MxChat Telegram DEBUG] Parsed: chat_id={$chat_id}, topic_id={$topic_id}, agent={$agent_name}, text={$message_text}");
3974 +
3975 + // Skip empty messages
3976 + if (empty($message_text)) {
3977 + //error_log('[MxChat Telegram DEBUG] Skipped: Empty message text');
3978 + return new WP_REST_Response(['ok' => true]);
3979 + }
3980 +
3981 + // Find session ID by topic ID - cast to string for comparison
3982 + global $wpdb;
3983 + $topic_id_str = strval($topic_id);
3984 + $session_option = $wpdb->get_var(
3985 + $wpdb->prepare(
3986 + "SELECT option_name FROM {$wpdb->options}
3987 + WHERE option_name LIKE %s
3988 + AND option_value = %s",
3989 + 'mxchat_telegram_topic_%',
3990 + $topic_id_str
3991 + )
3992 + );
3993 +
3994 + //error_log("[MxChat Telegram DEBUG] Looking for topic_id={$topic_id_str} in options, found: " . ($session_option ?: 'NULL'));
3995 +
3996 + if ($session_option) {
3997 + $session_id = str_replace('mxchat_telegram_topic_', '', $session_option);
3998 + //error_log("[MxChat Telegram DEBUG] Session ID: {$session_id}");
3999 +
4000 + // Verify the group ID matches
4001 + $stored_group_id = get_option("mxchat_telegram_group_{$session_id}", '');
4002 + //error_log("[MxChat Telegram DEBUG] Stored group_id={$stored_group_id}, received chat_id={$chat_id}");
4003 +
4004 + if (strval($stored_group_id) != strval($chat_id)) {
4005 + //error_log('[MxChat Telegram DEBUG] Skipped: Group ID mismatch');
4006 + return new WP_REST_Response(['ok' => true]);
4007 + }
4008 +
4009 + // Check for closure commands
4010 + $lower_text = strtolower(trim($message_text));
4011 + if (in_array($lower_text, ['#close', '#end', '#disconnect', '#done'])) {
4012 + //error_log("[MxChat Telegram DEBUG] Closure command received: {$lower_text}");
4013 + // End the live agent session
4014 + update_option("mxchat_mode_{$session_id}", 'ai');
4015 +
4016 + // Save disconnect message
4017 + $disconnect_message = "Live agent session ended. You're now chatting with the AI assistant.";
4018 + $this->mxchat_save_chat_message($session_id, 'bot', $disconnect_message);
4019 +
4020 + // Notify in Telegram
4021 + $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4022 + if (!empty($telegram_bot_token)) {
4023 + wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4024 + 'headers' => ['Content-Type' => 'application/json'],
4025 + 'body' => json_encode([
4026 + 'chat_id' => $chat_id,
4027 + 'message_thread_id' => $topic_id,
4028 + 'text' => "✅ Session closed. User returned to AI chatbot.",
4029 + 'parse_mode' => 'HTML'
4030 + ])
4031 + ]);
4032 +
4033 + // Optionally close the topic
4034 + wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/closeForumTopic", [
4035 + 'headers' => ['Content-Type' => 'application/json'],
4036 + 'body' => json_encode([
4037 + 'chat_id' => $chat_id,
4038 + 'message_thread_id' => $topic_id
4039 + ])
4040 + ]);
4041 + }
4042 +
4043 + return new WP_REST_Response(['ok' => true]);
4044 + }
4045 +
4046 + // Deduplicate messages
4047 + $message_key = md5($session_id . $message_id . $message_text);
4048 + $processed_messages = get_transient('mxchat_telegram_messages_' . $session_id) ?: [];
4049 +
4050 + if (in_array($message_key, $processed_messages)) {
4051 + //error_log('[MxChat Telegram DEBUG] Skipped: Duplicate message');
4052 + return new WP_REST_Response(['ok' => true]);
4053 + }
4054 +
4055 + $processed_messages[] = $message_key;
4056 + if (count($processed_messages) > 50) {
4057 + $processed_messages = array_slice($processed_messages, -50);
4058 + }
4059 + set_transient('mxchat_telegram_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
4060 +
4061 + // Save the agent message - format with agent name prefix for proper parsing
4062 + $formatted_message = "Agent: {$agent_name} - {$message_text}";
4063 + //error_log("[MxChat Telegram DEBUG] Saving agent message: {$formatted_message}");
4064 +
4065 + $this->mxchat_save_chat_message($session_id, 'agent', $formatted_message);
4066 +
4067 + // Verify the message was saved to history
4068 + $history = get_option("mxchat_history_{$session_id}", []);
4069 + $last_message = end($history);
4070 + //error_log("[MxChat Telegram DEBUG] History after save - count: " . count($history) . ", last message role: " . ($last_message['role'] ?? 'none'));
4071 +
4072 + // Send confirmation back to Telegram
4073 + $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4074 + if (!empty($telegram_bot_token)) {
4075 + $confirm_key = 'mxchat_telegram_confirm_' . $message_key;
4076 + if (!get_transient($confirm_key)) {
4077 + wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4078 + 'headers' => ['Content-Type' => 'application/json'],
4079 + 'body' => json_encode([
4080 + 'chat_id' => $chat_id,
4081 + 'message_thread_id' => $topic_id,
4082 + 'text' => "✅ <i>Message sent to user</i>",
4083 + 'parse_mode' => 'HTML',
4084 + 'reply_to_message_id' => $message_id
4085 + ])
4086 + ]);
4087 + set_transient($confirm_key, true, 300);
4088 + }
4089 + }
4090 + } else {
4091 + //error_log("[MxChat Telegram DEBUG] No session found for topic_id={$topic_id}");
4092 + }
4093 + } else {
4094 + //error_log('[MxChat Telegram DEBUG] No message in webhook data');
4095 + }
4096 +
4097 + return new WP_REST_Response(['ok' => true]);
4098 +}
4099 +
2828 4100 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
2829 - $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
4101 + // Check if this is a Telegram agent session
4102 + $telegram_topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
4103 + if (!empty($telegram_topic_id)) {
4104 + return $this->mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id);
4105 + }
2830 4106
2831 - if (empty($slack_webhook_url)) {
2832 - //error_log(esc_html__('Slack Webhook URL is not configured.', 'mxchat'));
4107 + // Otherwise, try Slack
4108 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4109 + $channel_id = get_option("mxchat_channel_{$session_id}", '');
4110 +
4111 + if (empty($slack_bot_token) || empty($channel_id)) {
2833 4112 return false;
2834 4113 }
2835 4114
2836 - $webhook_data = [
2837 - 'blocks' => [
2838 - [
2839 - 'type' => 'header',
2840 - 'text' => [
2841 - 'type' => 'plain_text',
2842 - 'text' => esc_html__('📩 New Chat Message', 'mxchat'),
2843 - 'emoji' => true
2844 - ]
2845 - ],
2846 - [
2847 - 'type' => 'section',
2848 - 'fields' => [
2849 - [
2850 - 'type' => 'mrkdwn',
2851 - 'text' => sprintf(esc_html__('*User ID:*\n`%s`', 'mxchat'), $user_id)
2852 - ],
2853 - [
2854 - 'type' => 'mrkdwn',
2855 - 'text' => sprintf(esc_html__('*Session ID:*\n`%s`', 'mxchat'), $session_id)
2856 - ]
2857 - ]
2858 - ],
2859 - [
2860 - 'type' => 'section',
2861 - 'text' => [
2862 - 'type' => 'mrkdwn',
2863 - 'text' => sprintf(esc_html__('*Message:*\n%s', 'mxchat'), $message)
2864 - ]
2865 - ],
2866 - [
2867 - 'type' => 'actions',
2868 - 'elements' => [
2869 - [
2870 - 'type' => 'button',
2871 - 'text' => [
2872 - 'type' => 'plain_text',
2873 - 'text' => esc_html__('✍️ Reply', 'mxchat'),
2874 - 'emoji' => true
2875 - ],
2876 - 'value' => $session_id,
2877 - 'action_id' => 'reply_to_user',
2878 - 'style' => 'primary'
2879 - ]
2880 - ]
2881 - ]
2882 - ]
2883 - ];
4115 + $user_message = "💬 *User:* {$message}";
2884 4116
2885 - $response = wp_remote_post($slack_webhook_url, [
2886 - 'body' => json_encode($webhook_data),
4117 + $response = wp_remote_post('https://slack.com/api/chat.postMessage', [
2887 4118 'headers' => [
2888 4119 'Content-Type' => 'application/json',
4120 + 'Authorization' => 'Bearer ' . $slack_bot_token
2889 4121 ],
4122 + 'body' => json_encode([
4123 + 'channel' => $channel_id,
4124 + 'text' => $user_message,
4125 + 'mrkdwn' => true
4126 + ])
2890 4127 ]);
2891 4128
2892 - if (is_wp_error($response)) {
2893 - //error_log(esc_html__('Error sending message to Slack: ', 'mxchat') . $response->get_error_message());
2894 - return false;
2895 - }
2896 -
2897 - //error_log(esc_html__('Message sent to Slack successfully.', 'mxchat'));
2898 - return true;
4129 + return !is_wp_error($response);
2899 4130 }
2900 4131 public function handle_slack_interaction(WP_REST_Request $request) {
2901 4132 //error_log('Received Slack interaction');
2902 4133
@@ -2984,17 +4215,16 @@
2984 4215
2985 4216 // Default acknowledgment
2986 4217 return new WP_REST_Response(['ok' => true]);
2987 4218 }
2988 -
2989 4219 public function mxchat_handle_agent_response(WP_REST_Request $request) {
2990 4220 //error_log('Received agent response request');
2991 4221 //error_log('Request data: ' . print_r($request->get_params(), true));
2992 - // error_log('Raw body: ' . file_get_contents('php://input'));
4222 + // //error_log('Raw body: ' . file_get_contents('php://input'));
2993 4223
2994 4224 // Get the data from Slack's slash command format
2995 4225 $command_text = $request->get_param('text');
2996 - // error_log('Command text: ' . $command_text);
4226 + // //error_log('Command text: ' . $command_text);
2997 4227
2998 4228 if (empty($command_text)) {
2999 4229 //error_log(esc_html__('Agent response error: No command text received', 'mxchat'));
3000 4230 return new WP_REST_Response([
@@ -3019,9 +4249,9 @@
3019 4249 // Save the message
3020 4250 $message_id = $this->mxchat_save_chat_message($session_id, 'agent', $message);
3021 4251
3022 4252 if (!$message_id) {
3023 - // error_log('Failed to save agent message');
4253 + // //error_log('Failed to save agent message');
3024 4254 return new WP_REST_Response([
3025 4255 'error' => esc_html__('Failed to save message', 'mxchat')
3026 4256 ], 500);
3027 4257 }
@@ -3031,29 +4261,173 @@
3031 4261 'response_type' => 'in_channel',
3032 4262 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
3033 4263 ], 200);
3034 4264 }
4265 +public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
4266 + // Update mode to AI
4267 + update_option("mxchat_mode_{$session_id}", 'ai');
4268 +
4269 + // Clear any existing PDF context to start fresh
4270 + $this->clear_pdf_transients($session_id);
4271 +
4272 + // Set the response with explicit chat_mode
4273 + $this->fallbackResponse = [
4274 + 'text' => esc_html__('You are now chatting with the AI chatbot.', 'mxchat'),
4275 + 'html' => '',
4276 + 'images' => [],
4277 + 'chat_mode' => 'ai' // Ensure this is set
4278 + ];
4279 +
4280 + // Return the complete response array instead of just true
4281 + return $this->fallbackResponse;
4282 +}
3035 4283
4284 +public function handle_slack_messages(WP_REST_Request $request) {
4285 + // Log the incoming request for debugging
4286 + //error_log('Slack events request received: ' . $request->get_body());
4287 +
4288 + $body = $request->get_body();
4289 + $data = json_decode($body, true);
4290 +
4291 + // Handle Slack URL verification
4292 + if (isset($data['type']) && $data['type'] === 'url_verification') {
4293 + //error_log('Slack URL verification challenge: ' . $data['challenge']);
4294 + return new WP_REST_Response($data['challenge'], 200, ['Content-Type' => 'text/plain']);
4295 + }
4296 +
4297 + // IMPORTANT: Handle Slack's event deduplication
4298 + if (isset($data['event_id'])) {
4299 + $event_id = $data['event_id'];
4300 + $processed_events = get_transient('mxchat_slack_events') ?: [];
4301 +
4302 + // Check if we've already processed this event
4303 + if (in_array($event_id, $processed_events)) {
4304 + //error_log("Duplicate event detected: $event_id");
4305 + return new WP_REST_Response(['ok' => true]);
4306 + }
4307 +
4308 + // Add this event to processed list
4309 + $processed_events[] = $event_id;
4310 + // Keep only last 100 events to prevent memory issues
4311 + if (count($processed_events) > 100) {
4312 + $processed_events = array_slice($processed_events, -100);
4313 + }
4314 + // Store for 1 hour
4315 + set_transient('mxchat_slack_events', $processed_events, HOUR_IN_SECONDS);
4316 + }
4317 +
4318 + // Handle message events
4319 + if (isset($data['event']) && $data['event']['type'] === 'message') {
4320 + $event = $data['event'];
4321 +
4322 + // Skip bot messages and messages with subtypes (like bot_message)
4323 + if (isset($event['bot_id']) || isset($event['subtype'])) {
4324 + return new WP_REST_Response(['ok' => true]);
4325 + }
4326 +
4327 + // Additional check: Skip if this is a threaded reply to our confirmation
4328 + if (isset($event['thread_ts']) && $event['thread_ts'] !== $event['ts']) {
4329 + return new WP_REST_Response(['ok' => true]);
4330 + }
4331 +
4332 + $channel_id = $event['channel'];
4333 + $message_text = $event['text'] ?? '';
4334 + $message_ts = $event['ts'] ?? '';
3036 4335
3037 -public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
3038 - //error_log(esc_html__("Switching back to chatbot mode via intent.", 'mxchat'));
4336 + // Find session ID by looking for matching channel
4337 + global $wpdb;
4338 + $session_option = $wpdb->get_var(
4339 + $wpdb->prepare(
4340 + "SELECT option_name FROM {$wpdb->options}
4341 + WHERE option_name LIKE 'mxchat_channel_%'
4342 + AND option_value = %s",
4343 + $channel_id
4344 + )
4345 + );
3039 4346
3040 - // Just update mode to AI
3041 - update_option("mxchat_mode_{$session_id}", 'ai');
4347 + if ($session_option) {
4348 + $session_id = str_replace('mxchat_channel_', '', $session_option);
3042 4349
3043 - // Initialize states
3044 - $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
3045 - $this->productCardHtml = '';
4350 + // Create a unique key for this specific message
4351 + $message_key = md5($session_id . $message_ts . $message_text);
4352 + $processed_messages = get_transient('mxchat_processed_messages_' . $session_id) ?: [];
3046 4353
3047 - // Set the response message
3048 - $this->fallbackResponse['text'] = esc_html__('You are now chatting with the AI chatbot.', 'mxchat');
4354 + // Check if we've already processed this exact message
4355 + if (in_array($message_key, $processed_messages)) {
4356 + //error_log("Duplicate message detected for session $session_id");
4357 + return new WP_REST_Response(['ok' => true]);
4358 + }
3049 4359
3050 - return true; // Intent was handled
3051 -}
4360 + // Add to processed messages
4361 + $processed_messages[] = $message_key;
4362 + // Keep only last 50 messages per session
4363 + if (count($processed_messages) > 50) {
4364 + $processed_messages = array_slice($processed_messages, -50);
4365 + }
4366 + set_transient('mxchat_processed_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
3052 4367
4368 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
3053 4369
4370 + // Handle agent ending the chat — transfer back to AI
4371 + // Format: "!endchat" or "!endchat <custom message to user>"
4372 + if (preg_match('/^!endchat\b/i', trim($message_text))) {
4373 + update_option("mxchat_mode_{$session_id}", 'ai');
3054 4374
4375 + // Extract custom message after !endchat, or use empty string
4376 + $custom_message = trim(preg_replace('/^!endchat\s*/i', '', trim($message_text)));
3055 4377
4378 + // Send the agent's custom farewell message if provided
4379 + if (!empty($custom_message)) {
4380 + $this->mxchat_save_chat_message($session_id, 'agent', $custom_message);
4381 + }
4382 +
4383 + // Confirm in Slack channel
4384 + if (!empty($slack_bot_token)) {
4385 + wp_remote_post('https://slack.com/api/chat.postMessage', [
4386 + 'headers' => [
4387 + 'Content-Type' => 'application/json',
4388 + 'Authorization' => 'Bearer ' . $slack_bot_token
4389 + ],
4390 + 'body' => json_encode([
4391 + 'channel' => $channel_id,
4392 + 'text' => "✅ *Chat ended.* User has been transferred back to AI mode.",
4393 + 'mrkdwn' => true
4394 + ])
4395 + ]);
4396 + }
4397 +
4398 + return new WP_REST_Response(['ok' => true]);
4399 + }
4400 +
4401 + // Save the agent message
4402 + $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
4403 +
4404 + // Send confirmation back to Slack (only once)
4405 + if (!empty($slack_bot_token)) {
4406 + // Use a transient to prevent duplicate confirmations
4407 + $confirm_key = 'mxchat_confirm_' . $message_key;
4408 + if (!get_transient($confirm_key)) {
4409 + wp_remote_post('https://slack.com/api/chat.postMessage', [
4410 + 'headers' => [
4411 + 'Content-Type' => 'application/json',
4412 + 'Authorization' => 'Bearer ' . $slack_bot_token
4413 + ],
4414 + 'body' => json_encode([
4415 + 'channel' => $channel_id,
4416 + 'text' => "✅ _Message sent to user_",
4417 + 'thread_ts' => $event['ts'] // Reply in thread
4418 + ])
4419 + ]);
4420 + // Set transient to prevent duplicate confirmations
4421 + set_transient($confirm_key, true, 300); // 5 minutes
4422 + }
4423 + }
4424 + }
4425 + }
4426 +
4427 + return new WP_REST_Response(['ok' => true]);
4428 +}
4429 +
3056 4430 // For the word upload handler
3057 4431 public function mxchat_handle_word_upload() {
3058 4432 // Delegate to word handler
3059 4433 $this->word_handler->mxchat_handle_word_upload();
@@ -3076,21 +4450,102 @@
3076 4450 return MxChat_User::mxchat_get_user_identifier();
3077 4451 }
3078 4452
3079 4453 private function mxchat_generate_embedding($text, $api_key) {
3080 - $endpoint = 'https://api.openai.com/v1/embeddings';
3081 -
3082 - $body = wp_json_encode([
3083 - 'input' => $text,
3084 - 'model' => 'text-embedding-ada-002'
3085 - ]);
3086 -
4454 + try {
4455 + // Get options and selected model
4456 + $options = get_option('mxchat_options');
4457 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4458 +
4459 + // Determine endpoint and API key based on model
4460 + if (strpos($selected_model, 'voyage') === 0) {
4461 + $endpoint = 'https://api.voyageai.com/v1/embeddings';
4462 + $api_key = $options['voyage_api_key'] ?? '';
4463 +
4464 + // Check if Voyage API key is missing
4465 + if (empty($api_key)) {
4466 + //error_log('Voyage API key is missing');
4467 + return [
4468 + 'error' => esc_html__('Voyage AI API key is not configured', 'mxchat'),
4469 + 'error_code' => 'missing_voyage_api_key'
4470 + ];
4471 + }
4472 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
4473 + $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
4474 + $api_key = $options['gemini_api_key'] ?? '';
4475 +
4476 + // Check if Gemini API key is missing
4477 + if (empty($api_key)) {
4478 + //error_log('Gemini API key is missing');
4479 + return [
4480 + 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
4481 + 'error_code' => 'missing_gemini_api_key'
4482 + ];
4483 + }
4484 + } else {
4485 + $endpoint = 'https://api.openai.com/v1/embeddings';
4486 + // Use the passed API key for OpenAI
4487 +
4488 + // Check if OpenAI API key is missing
4489 + if (empty($api_key)) {
4490 + //error_log('OpenAI API key is missing');
4491 + return [
4492 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
4493 + 'error_code' => 'missing_openai_api_key'
4494 + ];
4495 + }
4496 + }
4497 +
4498 + // Check if text is empty
4499 + if (empty($text)) {
4500 + //error_log('Empty text provided for embedding generation');
4501 + return [
4502 + 'error' => esc_html__('No text provided for embedding generation', 'mxchat'),
4503 + 'error_code' => 'empty_embedding_text'
4504 + ];
4505 + }
4506 +
4507 + // Prepare request body based on provider
4508 + if (strpos($selected_model, 'gemini-embedding') === 0) {
4509 + // Gemini API format
4510 + $request_body = [
4511 + 'model' => 'models/' . $selected_model,
4512 + 'content' => [
4513 + 'parts' => [
4514 + ['text' => $text]
4515 + ]
4516 + ],
4517 + 'outputDimensionality' => 1536
4518 + ];
4519 +
4520 + // Prepare headers for Gemini (API key as query parameter)
4521 + $endpoint .= '?key=' . $api_key;
4522 + $headers = [
4523 + 'Content-Type' => 'application/json'
4524 + ];
4525 + } else {
4526 + // OpenAI/Voyage API format
4527 + $request_body = [
4528 + 'input' => $text,
4529 + 'model' => $selected_model
4530 + ];
4531 +
4532 + // Add output_dimension for voyage-3-large
4533 + if ($selected_model === 'voyage-3-large') {
4534 + $request_body['output_dimension'] = 2048;
4535 + }
4536 +
4537 + // Prepare headers for OpenAI/Voyage
4538 + $headers = [
4539 + 'Content-Type' => 'application/json',
4540 + 'Authorization' => 'Bearer ' . $api_key
4541 + ];
4542 + }
4543 +
4544 + // Prepare request arguments
3087 4545 $args = [
3088 - 'body' => $body,
3089 - 'headers' => [
3090 - 'Content-Type' => 'application/json',
3091 - 'Authorization' => 'Bearer ' . $api_key,
3092 - ],
4546 + 'body' => wp_json_encode($request_body),
4547 + 'headers' => $headers,
3093 4548 'timeout' => 60,
3094 4549 'redirection' => 5,
3095 4550 'blocking' => true,
3096 4551 'httpversion' => '1.0',
@@ -3095,63 +4550,198 @@
3095 4550 'blocking' => true,
3096 4551 'httpversion' => '1.0',
3097 4552 'sslverify' => true,
3098 4553 ];
3099 -
4554 +
4555 + // Make the request
3100 4556 $response = wp_remote_post($endpoint, $args);
3101 -
4557 +
4558 + // Handle WordPress errors
3102 4559 if (is_wp_error($response)) {
3103 - return null;
4560 + $error_message = $response->get_error_message();
4561 + //error_log('Embedding Generation Error: ' . $error_message);
4562 + return [
4563 + 'error' => esc_html__('Connection error when generating embeddings: ', 'mxchat') . esc_html($error_message),
4564 + 'error_code' => 'embedding_connection_error'
4565 + ];
3104 4566 }
4567 +
4568 + // Check HTTP status code
4569 + $status_code = wp_remote_retrieve_response_code($response);
4570 + if ($status_code !== 200) {
4571 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
4572 +
4573 + $error_message = isset($response_body['error']['message'])
4574 + ? $response_body['error']['message']
4575 + : 'HTTP Error ' . $status_code;
4576 +
4577 + $error_type = isset($response_body['error']['type'])
4578 + ? $response_body['error']['type']
4579 + : 'unknown';
4580 +
4581 + //error_log('Embedding API HTTP Error: ' . $status_code . ' - ' . $error_message);
4582 +
4583 + // Handle specific error types
4584 + switch ($error_type) {
4585 + case 'invalid_request_error':
4586 + if (strpos($error_message, 'API key') !== false) {
4587 + return [
4588 + 'error' => esc_html__('Invalid API key for embedding generation. Please check your API key configuration.', 'mxchat'),
4589 + 'error_code' => 'embedding_invalid_api_key'
4590 + ];
4591 + }
4592 + break;
4593 +
4594 + case 'authentication_error':
4595 + return [
4596 + 'error' => esc_html__('Authentication failed for embedding generation. Please check your API key.', 'mxchat'),
4597 + 'error_code' => 'embedding_auth_error'
4598 + ];
4599 +
4600 + case 'rate_limit_exceeded':
4601 + return [
4602 + 'error' => esc_html__('Rate limit exceeded for embedding generation. Please try again later.', 'mxchat'),
4603 + 'error_code' => 'embedding_rate_limit'
4604 + ];
4605 +
4606 + case 'quota_exceeded':
4607 + return [
4608 + 'error' => esc_html__('API quota exceeded for embedding generation. Please check your billing details.', 'mxchat'),
4609 + 'error_code' => 'embedding_quota_exceeded'
4610 + ];
4611 + }
4612 +
4613 + // Generic error fallback
4614 + return [
4615 + 'error' => esc_html__('Embedding API error - check embedding API key.: ', 'mxchat') . esc_html($error_message),
4616 + 'error_code' => 'embedding_api_error',
4617 + 'status_code' => $status_code
4618 + ];
4619 + }
4620 +
4621 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
4622 +
4623 + // Handle different response formats based on provider
4624 + if (strpos($selected_model, 'gemini-embedding') === 0) {
4625 + // Gemini API response format
4626 + if (isset($response_body['embedding']['values']) && is_array($response_body['embedding']['values'])) {
4627 + return $response_body['embedding']['values'];
4628 + } else {
4629 + //error_log('Invalid Gemini embedding response: ' . wp_json_encode($response_body));
4630 + return [
4631 + 'error' => esc_html__('Received invalid embedding data from the Gemini API.', 'mxchat'),
4632 + 'error_code' => 'invalid_gemini_embedding_response'
4633 + ];
4634 + }
4635 + } else {
4636 + // OpenAI/Voyage API response format
4637 + if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
4638 + return $response_body['data'][0]['embedding'];
4639 + } else {
4640 + //error_log('Invalid embedding response: ' . wp_json_encode($response_body));
4641 + return [
4642 + 'error' => esc_html__('Received invalid embedding data from the API.', 'mxchat'),
4643 + 'error_code' => 'invalid_embedding_response'
4644 + ];
4645 + }
4646 + }
4647 + } catch (Exception $e) {
4648 + //error_log('Embedding Exception: ' . $e->getMessage());
4649 + return [
4650 + 'error' => esc_html__('System error when generating embeddings: ', 'mxchat') . esc_html($e->getMessage()),
4651 + 'error_code' => 'embedding_exception'
4652 + ];
4653 + }
4654 +}
3105 4655
3106 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
3107 4656
3108 - if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
3109 - return $response_body['data'][0]['embedding'];
4657 +private function mxchat_find_relevant_content($user_embedding, $bot_id = 'default', $user_query = '') {
4658 + //error_log("MXCHAT DEBUG: find_relevant_content called with bot_id: " . $bot_id);
4659 +
4660 + // Check for OpenAI Vector Store first (takes priority when enabled)
4661 + $bot_vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
4662 +
4663 + if ($bot_vectorstore_config['use_vectorstore']) {
4664 + // Get current model to verify it's an OpenAI model
4665 + $bot_options = $this->get_bot_options($bot_id);
4666 + $mxchat_options = get_option('mxchat_options', array());
4667 + $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
4668 + $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
4669 +
4670 + if ($this->is_openai_chat_model($selected_model)) {
4671 + //error_log("MXCHAT DEBUG: Using OpenAI Vector Store for knowledge retrieval");
4672 + return $this->find_relevant_content_openai_vectorstore($user_query, $bot_id, $bot_vectorstore_config);
3110 4673 } else {
3111 - return null;
4674 + //error_log("MXCHAT DEBUG: Vector Store enabled but model is not OpenAI (" . $selected_model . "), skipping Vector Store");
3112 4675 }
3113 4676 }
3114 4677
4678 + // Get bot-specific Pinecone configuration
4679 + $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
3115 4680
3116 -private function mxchat_find_relevant_content($user_embedding) {
3117 - //error_log('MXChat Vector Search: Starting content search...');
4681 + // Debug: Log the Pinecone configuration
4682 + //error_log("MXCHAT DEBUG: Pinecone config for bot '$bot_id':");
4683 + //error_log(" - use_pinecone: " . ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false'));
4684 + //error_log(" - api_key: " . (empty($bot_pinecone_config['api_key']) ? 'EMPTY' : 'SET (hidden)'));
4685 + //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'NOT SET'));
4686 + //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'NOT SET'));
3118 4687
3119 - // Retrieve the add-on settings from the database.
3120 - $addon_options = get_option('mxchat_pinecone_addon_options', array());
4688 + // Determine whether to use Pinecone based on bot configuration
4689 + $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
3121 4690
3122 - // Determine whether Pinecone is enabled.
3123 - // We expect the sanitized setting to be a string '1' if enabled, otherwise '0'.
3124 - $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
4691 + //error_log("MXCHAT DEBUG: Using " . ($use_pinecone ? "Pinecone" : "WordPress Database") . " for knowledge retrieval");
3125 4692
3126 - //error_log('Pinecone enabled flag: ' . $use_pinecone);
3127 -
3128 - if ($use_pinecone === 1) {
3129 - //error_log('MXChat Vector Search: Using Pinecone database');
3130 - return $this->find_relevant_content_pinecone($user_embedding);
4693 + if ($use_pinecone) {
4694 + return $this->find_relevant_content_pinecone($user_embedding, $bot_id, $bot_pinecone_config);
3131 4695 } else {
3132 - //error_log('MXChat Vector Search: Using WordPress database');
3133 - return $this->find_relevant_content_wordpress($user_embedding);
4696 + return $this->find_relevant_content_wordpress($user_embedding, $bot_id);
3134 4697 }
3135 4698 }
3136 4699
3137 -private function find_relevant_content_wordpress($user_embedding) {
4700 +private function find_relevant_content_wordpress($user_embedding, $bot_id = 'default') {
3138 4701 global $wpdb;
3139 4702 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3140 - $cache_key = 'mxchat_system_prompt_embeddings';
4703 + $cache_key = 'mxchat_system_prompt_embeddings_' . $bot_id;
3141 4704 $batch_size = 500;
3142 4705
4706 + // Initialize similarity analysis storage
4707 + $this->last_similarity_analysis = [
4708 + 'knowledge_base_type' => 'WordPress Database',
4709 + 'bot_id' => $bot_id,
4710 + 'top_matches' => [],
4711 + 'threshold_used' => 0,
4712 + 'total_checked' => 0
4713 + ];
4714 +
4715 + // NEW: Initialize valid URLs array
4716 + $valid_urls = [];
4717 +
4718 + // Get bot-specific options for similarity threshold
4719 + $bot_options = $this->get_bot_options($bot_id);
4720 + $current_options = !empty($bot_options) ? $bot_options : $this->options;
4721 +
3143 4722 // Retrieve embeddings from cache or database
3144 4723 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3145 4724 if ($embeddings === false) {
4725 + // Cache miss - load embeddings from database WITH CONTENT and ROLE RESTRICTION for testing
3146 4726 $embeddings = [];
3147 4727 $offset = 0;
3148 4728
3149 - // Load in batches and build cache
3150 4729 do {
4730 + // Add bot_id filter if not default and if bot_metadata column exists
4731 + $bot_filter = '';
4732 + if ($bot_id !== 'default') {
4733 + // Check if bot_metadata column exists
4734 + $column_exists = $wpdb->get_var("SHOW COLUMNS FROM {$system_prompt_table} LIKE 'bot_metadata'");
4735 + if ($column_exists) {
4736 + $bot_filter = $wpdb->prepare(" AND (bot_metadata = %s OR bot_metadata IS NULL OR bot_metadata = '')", $bot_id);
4737 + }
4738 + }
4739 +
3151 4740 $query = $wpdb->prepare(
3152 - "SELECT id, embedding_vector
4741 + "SELECT id, embedding_vector, article_content, source_url, role_restriction
3153 4742 FROM {$system_prompt_table}
4743 + WHERE 1=1 {$bot_filter}
3154 4744 LIMIT %d OFFSET %d",
3155 4745 $batch_size,
3156 4746 $offset
3157 4747 );
@@ -3162,113 +4752,426 @@
3162 4752 }
3163 4753
3164 4754 $embeddings = array_merge($embeddings, $batch);
3165 4755 $offset += $batch_size;
3166 -
3167 - // Free memory
3168 4756 unset($batch);
3169 -
3170 4757 } while (true);
3171 4758
3172 4759 if (empty($embeddings)) {
3173 - return ''; // Return an empty string if no embeddings found
4760 + // Store empty array for valid URLs since no content found
4761 + $this->current_valid_urls = [];
4762 + return '';
3174 4763 }
4764 +
4765 + // Cache embeddings for future use (but note: this now includes content and role restrictions)
3175 4766 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3176 4767 }
3177 4768
3178 - // Initialize array to store relevant results with similarity scores
3179 - $relevant_results = [];
3180 - // Iterate through embeddings to calculate similarity
4769 + // Get knowledge manager instance for role checking
4770 + $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
4771 +
4772 + // Get base similarity threshold from bot options or default options
4773 + $similarity_threshold = isset($current_options['similarity_threshold'])
4774 + ? ((int) $current_options['similarity_threshold']) / 100
4775 + : 0.35;
4776 +
4777 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
4778 +
4779 + // Calculate similarities and build results array
4780 + $all_similarities = [];
4781 + $url_groups = array(); // NEW: Group by source_url for chunk reassembly
4782 +
3181 4783 foreach ($embeddings as $embedding) {
3182 4784 $database_embedding = $embedding->embedding_vector
3183 4785 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3184 4786 : null;
4787 +
3185 4788 if (is_array($database_embedding) && is_array($user_embedding)) {
3186 4789 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
3187 - $relevant_results[] = [
3188 - 'id' => $embedding->id,
3189 - 'similarity' => $similarity
4790 +
4791 + // Check role access
4792 + $role_restriction = $embedding->role_restriction ?? 'public';
4793 + $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
4794 +
4795 + // Store ALL similarities for testing (top 10)
4796 + $source_display = '';
4797 + $source_url = $embedding->source_url ?? '';
4798 + if (!empty($source_url) && $source_url !== '#') {
4799 + $source_display = $source_url;
4800 + } else {
4801 + $content_preview = strip_tags($embedding->article_content ?? '');
4802 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
4803 + $source_display = substr(trim($content_preview), 0, 50) . '...';
4804 + }
4805 +
4806 + // Parse chunk metadata for display
4807 + $article_content_for_parse = $embedding->article_content ?? '';
4808 + $parsed_for_display = MxChat_Chunker::parse_stored_chunk($article_content_for_parse);
4809 + $is_chunk = $parsed_for_display['is_chunked'];
4810 + $chunk_meta = $parsed_for_display['metadata'];
4811 +
4812 + $all_similarities[] = [
4813 + 'document_id' => $embedding->id,
4814 + 'similarity' => $similarity,
4815 + 'similarity_percentage' => round($similarity * 100, 2),
4816 + 'above_threshold' => $similarity >= $similarity_threshold,
4817 + 'source_display' => $source_display,
4818 + 'content_preview' => substr(strip_tags($parsed_for_display['text'] ?? ''), 0, 100) . '...',
4819 + 'used_for_context' => false,
4820 + 'role_restriction' => $role_restriction,
4821 + 'has_access' => $has_access,
4822 + 'filtered_out' => !$has_access,
4823 + 'is_chunk' => $is_chunk,
4824 + 'chunk_index' => $is_chunk ? ($chunk_meta['chunk_index'] ?? 0) : null,
4825 + 'total_chunks' => $is_chunk ? ($chunk_meta['total_chunks'] ?? 1) : null
3190 4826 ];
4827 +
4828 + // Only consider results above threshold AND with access for content retrieval
4829 + if ($similarity >= $similarity_threshold && $has_access) {
4830 + // Parse chunk metadata if present
4831 + $article_content = $embedding->article_content ?? '';
4832 + $parsed = MxChat_Chunker::parse_stored_chunk($article_content);
4833 + $is_chunked = $parsed['is_chunked'];
4834 + $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
4835 + $text_content = $parsed['text'];
4836 +
4837 + // Use a unique key for manual entries without a source URL
4838 + $group_key = !empty($source_url) ? $source_url : '_manual_' . $embedding->id;
4839 +
4840 + // Group by source URL (or unique key for manual entries)
4841 + if (!isset($url_groups[$group_key])) {
4842 + $url_groups[$group_key] = array(
4843 + 'source_url' => $source_url,
4844 + 'best_score' => 0,
4845 + 'is_chunked' => $is_chunked,
4846 + 'chunks' => array(),
4847 + 'single_text' => '',
4848 + 'single_id' => null
4849 + );
4850 + }
4851 +
4852 + // Track best score for this group
4853 + if ($similarity > $url_groups[$group_key]['best_score']) {
4854 + $url_groups[$group_key]['best_score'] = $similarity;
4855 + }
4856 +
4857 + // Store chunk info or single text
4858 + if ($is_chunked) {
4859 + $url_groups[$group_key]['is_chunked'] = true;
4860 + $url_groups[$group_key]['chunks'][] = array(
4861 + 'id' => $embedding->id,
4862 + 'score' => $similarity,
4863 + 'chunk_index' => $chunk_index,
4864 + 'text' => $text_content
4865 + );
4866 + } else {
4867 + $url_groups[$group_key]['single_text'] = $text_content;
4868 + $url_groups[$group_key]['single_id'] = $embedding->id;
4869 + }
4870 + }
3191 4871 }
3192 - // Free memory
4872 +
3193 4873 unset($database_embedding);
3194 4874 }
3195 4875
3196 - // Retrieve the similarity threshold
3197 - $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
4876 + // Sort ALL similarities for testing display (highest first)
4877 + usort($all_similarities, function ($a, $b) {
4878 + return $b['similarity'] <=> $a['similarity'];
4879 + });
3198 4880
3199 - // Filter and sort relevant results by similarity
3200 - $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
3201 - return $result['similarity'] >= $similarity_threshold;
4881 + // Sort URL groups by best score (highest first)
4882 + uasort($url_groups, function($a, $b) {
4883 + return $b['best_score'] <=> $a['best_score'];
3202 4884 });
3203 - usort($relevant_results, function ($a, $b) {
3204 - return $b['similarity'] <=> $a['similarity'];
3205 - });
3206 4885
3207 - // Limit to the top 5 results
3208 - $top_results = array_slice($relevant_results, 0, 5);
4886 + // Get RAG sources limit from options (default 6, min 3, max 10)
4887 + $rag_sources_limit = isset($options['rag_sources_limit']) ? intval($options['rag_sources_limit']) : 3;
4888 + if ($rag_sources_limit < 3) $rag_sources_limit = 3;
4889 + if ($rag_sources_limit > 10) $rag_sources_limit = 10;
3209 4890
3210 - // Initialize the final content
4891 + // Take top N unique URLs based on user setting
4892 + $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
4893 +
4894 + // Track which document IDs are used for context
4895 + $used_document_ids = [];
4896 + foreach ($top_urls as $group) {
4897 + if ($group['is_chunked']) {
4898 + foreach ($group['chunks'] as $chunk) {
4899 + $used_document_ids[] = $chunk['id'];
4900 + }
4901 + } elseif ($group['single_id']) {
4902 + $used_document_ids[] = $group['single_id'];
4903 + }
4904 + }
4905 +
4906 + // Update the all_similarities array to mark which were actually used
4907 + foreach ($all_similarities as &$similarity_item) {
4908 + $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
4909 + }
4910 +
4911 + // Store top 10 for testing panel
4912 + $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
4913 + $this->last_similarity_analysis['total_checked'] = count($embeddings);
4914 +
4915 + // Initialize final content
3211 4916 $content = '';
4917 + $matches_used = 0;
4918 + $total_chunks_used = 0;
4919 + $max_total_chunks = isset($options['rag_chunks_limit']) ? intval($options['rag_chunks_limit']) : 15;
4920 + if ($max_total_chunks < 8) $max_total_chunks = 8;
4921 + if ($max_total_chunks > 20) $max_total_chunks = 20;
4922 + $max_chunks_per_source = 5; // Cap per individual source to limit token usage
3212 4923
3213 - // Fetch and combine content for the top results
3214 - foreach ($top_results as $result) {
3215 - $chunk_content = $this->fetch_content_with_product_links($result['id']);
3216 - // Check if the content is PDF-related and add surrounding pages
3217 - if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
3218 - $surrounding_content = $wpdb->get_results($wpdb->prepare(
3219 - "SELECT article_content FROM {$system_prompt_table}
3220 - WHERE id IN (
3221 - (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
3222 - (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
3223 - )",
3224 - $result['id'],
3225 - $result['id']
3226 - ));
3227 - // Add previous content if it exists
3228 - if (!empty($surrounding_content[0])) {
3229 - $content .= $surrounding_content[0]->article_content . "\n\n";
4924 + // Check if citation links are enabled (default to 'on' for backwards compatibility)
4925 + // Use fresh options to ensure we get the latest setting value
4926 + $fresh_options = get_option('mxchat_options', []);
4927 + $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
4928 +
4929 + // Build content from top sources
4930 + foreach ($top_urls as $group_key => $group) {
4931 + $source_url = $group['source_url']; // Use actual source_url, not the group key
4932 +
4933 + // Stop if we've hit the total chunk limit
4934 + if ($total_chunks_used >= $max_total_chunks) {
4935 + break;
4936 + }
4937 +
4938 + $full_text = '';
4939 + $chunks_in_this_source = 1; // Default for non-chunked content
4940 +
4941 + if ($group['is_chunked']) {
4942 + // Calculate how many chunks we can still use (respect both total and per-source caps)
4943 + $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
4944 +
4945 + // Fetch chunks for this URL with limit
4946 + $full_text = $this->reassemble_chunks_from_wordpress($source_url, $chunks_remaining, $chunks_in_this_source);
4947 +
4948 + // If fetching all chunks fails, fall back to matched chunks
4949 + if (empty($full_text)) {
4950 + // Sort matched chunks by index and concatenate
4951 + usort($group['chunks'], function($a, $b) {
4952 + return $a['chunk_index'] <=> $b['chunk_index'];
4953 + });
4954 +
4955 + $chunk_texts = array();
4956 + $chunks_in_this_source = 0;
4957 + foreach ($group['chunks'] as $chunk) {
4958 + if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
4959 + break;
4960 + }
4961 + $chunk_texts[] = $chunk['text'];
4962 + $chunks_in_this_source++;
4963 + }
4964 + $full_text = implode("\n\n", $chunk_texts);
3230 4965 }
3231 - // Add the main chunk content
3232 - $content .= $chunk_content . "\n\n";
3233 - // Add next content if it exists
3234 - if (!empty($surrounding_content[1])) {
3235 - $content .= $surrounding_content[1]->article_content . "\n\n";
4966 + } else {
4967 + $full_text = $group['single_text'];
4968 + $chunks_in_this_source = 1;
4969 + }
4970 +
4971 + if (!empty($full_text)) {
4972 + // Strip URLs from content if citation links are disabled
4973 + if (!$citation_links_enabled) {
4974 + $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
4975 + $full_text = preg_replace('/\s+/', ' ', trim($full_text)); // Clean up extra spaces
3236 4976 }
4977 +
4978 + // Use numbered reference for URL-based entries, plain info label for manual entries
4979 + if (!empty($source_url) && $source_url !== '#') {
4980 + $matches_used++;
4981 + $content .= "## Reference " . $matches_used . " ##\n";
4982 + $content .= $full_text . "\n\n";
4983 +
4984 + // Only include citation URLs if citation links are enabled
4985 + if ($citation_links_enabled) {
4986 + $valid_urls[] = $source_url;
4987 + $content .= "URL: " . $source_url . "\n\n";
4988 + }
4989 + } else {
4990 + // Manual entry — no reference number, no citation
4991 + $content .= "## Information ##\n";
4992 + $content .= $full_text . "\n\n";
4993 + }
4994 +
4995 + // Extract any URLs from the text content itself (only if citation links enabled)
4996 + if ($citation_links_enabled) {
4997 + preg_match_all(
4998 + '#\bhttps?://[^\s<>"\']+#i',
4999 + $full_text,
5000 + $content_urls
5001 + );
5002 + if (!empty($content_urls[0])) {
5003 + $valid_urls = array_merge($valid_urls, $content_urls[0]);
5004 + }
5005 + }
5006 +
5007 + $total_chunks_used += $chunks_in_this_source;
5008 + }
5009 + }
5010 +
5011 + // NEW: Store unique valid URLs for validation
5012 + $this->current_valid_urls = array_unique($valid_urls);
5013 +
5014 + // Store sources and chunks counts for testing/transcript display
5015 + $this->last_similarity_analysis['sources_used'] = $matches_used;
5016 + $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
5017 +
5018 + // Add response guidelines
5019 + if (empty($top_urls)) {
5020 + $content = "No reference information was found for this query.\n\n";
5021 + } else {
5022 + // Build response guidelines based on citation links setting
5023 + $content .= "\n## Response Guidelines ##\n" .
5024 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
5025 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
5026 + "If you don't have specific information or are uncertain about any details, it's always " .
5027 + "better to honestly say you don't know rather than making up or guessing at answers. " .
5028 + "When information is incomplete, let them know you are unsure.\n\n";
5029 +
5030 + // Only add hyperlink instructions if citation links are enabled
5031 + if ($citation_links_enabled) {
5032 + $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
5033 + "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
5034 + "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
3237 5035 } else {
3238 - // For non-PDF content, add directly
3239 - $content .= $chunk_content . "\n\n";
5036 + $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
5037 + "Simply provide helpful answers based on the reference information without citing sources.";
3240 5038 }
3241 5039 }
3242 5040
3243 5041 return trim($content);
3244 5042 }
5043 +
3245 5044 /**
3246 - * Find relevant content in Pinecone vector database
5045 + * Fetch and reassemble chunks for a URL from WordPress database
5046 + *
5047 + * @param string $source_url The source URL to fetch chunks for
5048 + * @param int $max_chunks Maximum number of chunks to return (0 = unlimited)
5049 + * @param int &$chunk_count Reference to store the actual number of chunks returned
5050 + * @return string Reassembled content from chunks
3247 5051 */
3248 -private function find_relevant_content_pinecone($user_embedding) {
3249 - $options = get_option('mxchat_pinecone_addon_options', array());
3250 - $api_key = $options['mxchat_pinecone_api_key'] ?? '';
3251 - $host = $options['mxchat_pinecone_host'] ?? '';
5052 +private function reassemble_chunks_from_wordpress($source_url, $max_chunks = 0, &$chunk_count = 0) {
5053 + global $wpdb;
5054 + $table = $wpdb->prefix . 'mxchat_system_prompt_content';
3252 5055
3253 - if (empty($host) || empty($api_key)) {
3254 - //error_log('Pinecone credentials not properly configured');
5056 + // Fetch all rows with this source_url
5057 + $rows = $wpdb->get_results($wpdb->prepare(
5058 + "SELECT article_content FROM {$table}
5059 + WHERE source_url = %s
5060 + ORDER BY id ASC",
5061 + $source_url
5062 + ));
5063 +
5064 + if (empty($rows)) {
5065 + $chunk_count = 0;
3255 5066 return '';
3256 5067 }
3257 5068
3258 - // Get similarity threshold from WordPress settings
3259 - $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
5069 + // Parse and sort chunks by index
5070 + $chunks = array();
5071 + foreach ($rows as $row) {
5072 + $parsed = MxChat_Chunker::parse_stored_chunk($row->article_content);
3260 5073
5074 + if ($parsed['is_chunked']) {
5075 + $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
5076 + $chunks[$chunk_index] = $parsed['text'];
5077 + } else {
5078 + // Non-chunked content - just return it
5079 + $chunks[] = $parsed['text'];
5080 + }
5081 + }
5082 +
5083 + // Sort by chunk index
5084 + ksort($chunks);
5085 +
5086 + // Apply chunk limit if specified
5087 + if ($max_chunks > 0 && count($chunks) > $max_chunks) {
5088 + $chunks = array_slice($chunks, 0, $max_chunks, true);
5089 + }
5090 +
5091 + // Store actual chunk count
5092 + $chunk_count = count($chunks);
5093 +
5094 + // Reassemble content
5095 + return implode("\n\n", $chunks);
5096 +}
5097 +
5098 +private function find_relevant_content_pinecone($user_embedding, $bot_id = 'default', $bot_config = null) {
5099 + global $wpdb;
5100 +
5101 + //error_log("MXCHAT DEBUG: find_relevant_content_pinecone called");
5102 + //error_log(" - bot_id: " . $bot_id);
5103 + //error_log(" - user_embedding is array: " . (is_array($user_embedding) ? 'yes' : 'no'));
5104 + //error_log(" - user_embedding count: " . (is_array($user_embedding) ? count($user_embedding) : 'N/A'));
5105 +
5106 + // Use bot-specific config or fall back to default
5107 + if ($bot_config === null) {
5108 + $bot_config = $this->get_bot_pinecone_config($bot_id);
5109 + }
5110 +
5111 + $api_key = $bot_config['api_key'] ?? '';
5112 + $host = $bot_config['host'] ?? '';
5113 + $namespace = $bot_config['namespace'] ?? '';
5114 +
5115 + //error_log("MXCHAT DEBUG: Pinecone query parameters:");
5116 + //error_log(" - API Key: " . (empty($api_key) ? 'EMPTY - ERROR!' : 'Present (length: ' . strlen($api_key) . ')'));
5117 + //error_log(" - Host: " . (empty($host) ? 'EMPTY - ERROR!' : $host));
5118 + //error_log(" - Namespace: " . (empty($namespace) ? 'EMPTY (will use default)' : $namespace));
5119 +
5120 + // Initialize similarity analysis storage
5121 + $this->last_similarity_analysis = [
5122 + 'knowledge_base_type' => 'Pinecone',
5123 + 'bot_id' => $bot_id,
5124 + 'namespace' => $namespace,
5125 + 'top_matches' => [],
5126 + 'threshold_used' => 0,
5127 + 'total_checked' => 0
5128 + ];
5129 +
5130 + // NEW: Initialize valid URLs array
5131 + $valid_urls = [];
5132 +
5133 + if (empty($host) || empty($api_key)) {
5134 + //error_log("MXCHAT DEBUG ERROR: Missing Pinecone host or API key!");
5135 + //error_log(" - Host empty: " . (empty($host) ? 'YES' : 'NO'));
5136 + //error_log(" - API key empty: " . (empty($api_key) ? 'YES' : 'NO'));
5137 + // Store empty array for valid URLs since we can't proceed
5138 + $this->current_valid_urls = [];
5139 + return '';
5140 + }
5141 +
5142 + // Get knowledge manager instance for role checking
5143 + $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
5144 +
5145 + // Get the similarity threshold from the bot options or main options
5146 + $bot_options = $this->get_bot_options($bot_id);
5147 + $current_options = !empty($bot_options) ? $bot_options : get_option('mxchat_options', []);
5148 +
5149 + $similarity_threshold = isset($current_options['similarity_threshold'])
5150 + ? ((int) $current_options['similarity_threshold']) / 100
5151 + : 0.35;
5152 +
5153 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
5154 +
3261 5155 // Prepare the query request for Pinecone
3262 5156 $api_endpoint = "https://{$host}/query";
3263 -
5157 +
3264 5158 $request_body = array(
3265 5159 'vector' => $user_embedding,
3266 - 'topK' => 5,
5160 + 'topK' => 50, // Increased for chunked content grouping - need more candidates to find top N unique URLs
3267 5161 'includeMetadata' => true,
3268 5162 'includeValues' => true
3269 5163 );
3270 -
5164 +
5165 + // Add namespace if specified for this bot
5166 + if (!empty($namespace)) {
5167 + $request_body['namespace'] = $namespace;
5168 + }
5169 +
5170 + //error_log("MXCHAT DEBUG: About to call Pinecone API");
5171 + //error_log(" - Endpoint: " . $api_endpoint);
5172 + //error_log(" - Namespace in request: " . (!empty($namespace) ? $namespace : 'NOT SET'));
5173 +
3271 5174 $response = wp_remote_post($api_endpoint, array(
3272 5175 'headers' => array(
3273 5176 'Api-Key' => $api_key,
3274 5177 'accept' => 'application/json',
@@ -3276,46 +5179,872 @@
3276 5179 ),
3277 5180 'body' => wp_json_encode($request_body),
3278 5181 'timeout' => 30
3279 5182 ));
3280 -
5183 +
3281 5184 if (is_wp_error($response)) {
3282 - //error_log('Pinecone query error: ' . $response->get_error_message());
5185 + //error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
5186 + // Store empty array for valid URLs
5187 + $this->current_valid_urls = [];
3283 5188 return '';
3284 5189 }
3285 -
5190 +
3286 5191 $response_code = wp_remote_retrieve_response_code($response);
5192 + //error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
5193 +
3287 5194 if ($response_code !== 200) {
3288 - //error_log('Pinecone API error: ' . wp_remote_retrieve_body($response));
5195 + $response_body = wp_remote_retrieve_body($response);
5196 + //error_log("MXCHAT DEBUG ERROR: Pinecone API error response: " . substr($response_body, 0, 500));
5197 + // Store empty array for valid URLs
5198 + $this->current_valid_urls = [];
3289 5199 return '';
3290 5200 }
3291 -
3292 - $results = json_decode(wp_remote_retrieve_body($response), true);
5201 +
5202 + // ADD DETAILED DEBUG SECTION HERE
5203 + $response_body = wp_remote_retrieve_body($response);
5204 + //error_log("MXCHAT DEBUG: Raw Pinecone response length: " . strlen($response_body));
5205 +
5206 + $results = json_decode($response_body, true);
5207 +
5208 + if (json_last_error() !== JSON_ERROR_NONE) {
5209 + //error_log("MXCHAT DEBUG ERROR: JSON decode error: " . json_last_error_msg());
5210 + //error_log("MXCHAT DEBUG: First 500 chars of response: " . substr($response_body, 0, 500));
5211 + // Store empty array for valid URLs
5212 + $this->current_valid_urls = [];
5213 + return '';
5214 + }
5215 +
5216 + //error_log("MXCHAT DEBUG: Pinecone response structure:");
5217 + //error_log(" - Has 'matches' key: " . (isset($results['matches']) ? 'yes' : 'no'));
5218 + //error_log(" - Has 'namespace' key: " . (isset($results['namespace']) ? 'yes (' . $results['namespace'] . ')' : 'no'));
5219 +
3293 5220 if (empty($results['matches'])) {
5221 + //error_log("MXCHAT DEBUG: No matches found in Pinecone response");
5222 + //error_log("MXCHAT DEBUG: Response keys: " . implode(', ', array_keys($results)));
5223 + // Store empty array for valid URLs
5224 + $this->current_valid_urls = [];
3294 5225 return '';
3295 5226 }
3296 -
5227 +
5228 + //error_log("MXCHAT DEBUG: Found " . count($results['matches']) . " matches in Pinecone");
5229 +
5230 + // Log first match details for debugging
5231 + if (!empty($results['matches'][0])) {
5232 + $first_match = $results['matches'][0];
5233 + //error_log("MXCHAT DEBUG: First match details:");
5234 + //error_log(" - Score: " . ($first_match['score'] ?? 'no score'));
5235 + //error_log(" - Has metadata: " . (isset($first_match['metadata']) ? 'yes' : 'no'));
5236 + if (isset($first_match['metadata'])) {
5237 + //error_log(" - Metadata keys: " . implode(', ', array_keys($first_match['metadata'])));
5238 + }
5239 + }
5240 +
3297 5241 // Initialize the final content
3298 5242 $content = '';
5243 + $matches_used = 0;
5244 + $matches_used_for_context = [];
5245 + $total_chunks_used = 0;
5246 + $max_total_chunks = isset($current_options['rag_chunks_limit']) ? intval($current_options['rag_chunks_limit']) : 15;
5247 + if ($max_total_chunks < 8) $max_total_chunks = 8;
5248 + if ($max_total_chunks > 20) $max_total_chunks = 20;
5249 + $max_chunks_per_source = 5; // Cap per individual source to limit token usage
3299 5250
3300 - // Process each match
3301 - foreach ($results['matches'] as $match) {
5251 + // Check if citation links are enabled (default to 'on' for backwards compatibility)
5252 + // Use fresh options to ensure we get the latest setting value
5253 + $fresh_options = get_option('mxchat_options', []);
5254 + $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
5255 +
5256 + // NEW CHUNKING LOGIC: Group results by source_url for chunk reassembly
5257 + $url_groups = array();
5258 +
5259 + foreach ($results['matches'] as $index => $match) {
3302 5260 // Skip if similarity is below threshold
3303 5261 if ($match['score'] < $similarity_threshold) {
3304 5262 continue;
3305 5263 }
3306 5264
3307 - if (!empty($match['metadata']['text']) && !empty($match['metadata']['source_url'])) {
3308 - // Add content with citation
3309 - $content .= $match['metadata']['text'] . "\n";
3310 - $content .= "Source: " . $match['metadata']['source_url'] . "\n\n";
5265 + $metadata = $match['metadata'] ?? array();
5266 + $source_url = $metadata['source_url'] ?? '';
5267 + $match_id = $match['id'] ?? '';
5268 +
5269 + // LAZY ROLE CHECK: Only check role for content we're actually considering
5270 + $role_restriction = $this->get_single_vector_role($match_id, $metadata);
5271 + $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
5272 +
5273 + // Skip if user doesn't have access
5274 + if (!$has_access) {
5275 + continue;
3311 5276 }
5277 +
5278 + // Use a unique key for manual entries without a source URL
5279 + $group_key = !empty($source_url) ? $source_url : '_manual_' . $match_id;
5280 +
5281 + // Group by source URL (or unique key for manual entries)
5282 + if (!isset($url_groups[$group_key])) {
5283 + $url_groups[$group_key] = array(
5284 + 'source_url' => $source_url,
5285 + 'best_score' => 0,
5286 + 'is_chunked' => isset($metadata['is_chunked']) && $metadata['is_chunked'],
5287 + 'chunks' => array(),
5288 + 'single_text' => ''
5289 + );
5290 + }
5291 +
5292 + // Track best score for this group
5293 + if ($match['score'] > $url_groups[$group_key]['best_score']) {
5294 + $url_groups[$group_key]['best_score'] = $match['score'];
5295 + }
5296 +
5297 + // Store chunk info or single text
5298 + if ($url_groups[$group_key]['is_chunked']) {
5299 + $url_groups[$group_key]['chunks'][] = array(
5300 + 'id' => $match_id,
5301 + 'score' => $match['score'],
5302 + 'chunk_index' => $metadata['chunk_index'] ?? 0,
5303 + 'text' => $metadata['text'] ?? ''
5304 + );
5305 + } else {
5306 + // Non-chunked content - just store the text
5307 + $url_groups[$group_key]['single_text'] = $metadata['text'] ?? '';
5308 + $url_groups[$group_key]['single_id'] = $match_id;
5309 + }
3312 5310 }
3313 5311
5312 + // Sort URL groups by best score (highest first)
5313 + uasort($url_groups, function($a, $b) {
5314 + return $b['best_score'] <=> $a['best_score'];
5315 + });
5316 +
5317 + // Get RAG sources limit from options (default 6, min 3, max 10)
5318 + $rag_sources_limit = isset($current_options['rag_sources_limit']) ? intval($current_options['rag_sources_limit']) : 3;
5319 + if ($rag_sources_limit < 3) $rag_sources_limit = 3;
5320 + if ($rag_sources_limit > 10) $rag_sources_limit = 10;
5321 +
5322 + // Take top N unique URLs based on user setting
5323 + $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
5324 +
5325 + // Track which match IDs are actually used for context
5326 + foreach ($top_urls as $group) {
5327 + if ($group['is_chunked']) {
5328 + foreach ($group['chunks'] as $chunk) {
5329 + $matches_used_for_context[] = $chunk['id'];
5330 + }
5331 + } elseif (!empty($group['single_id'])) {
5332 + $matches_used_for_context[] = $group['single_id'];
5333 + }
5334 + }
5335 +
5336 + // Build content from top sources
5337 + foreach ($top_urls as $group_key => $group) {
5338 + $source_url = $group['source_url']; // Use actual source_url, not the group key
5339 +
5340 + // Stop if we've hit the total chunk limit
5341 + if ($total_chunks_used >= $max_total_chunks) {
5342 + break;
5343 + }
5344 +
5345 + $full_text = '';
5346 + $chunks_in_this_source = 1; // Default for non-chunked content
5347 +
5348 + if ($group['is_chunked']) {
5349 + // Calculate how many chunks we can still use (respect both total and per-source caps)
5350 + $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
5351 +
5352 + // Fetch chunks for this URL with limit
5353 + $full_text = $this->reassemble_chunks_from_pinecone($source_url, $bot_config, $chunks_remaining, $chunks_in_this_source);
5354 +
5355 + // If fetching all chunks fails, fall back to matched chunks
5356 + if (empty($full_text)) {
5357 + // Sort matched chunks by index and concatenate
5358 + usort($group['chunks'], function($a, $b) {
5359 + return $a['chunk_index'] <=> $b['chunk_index'];
5360 + });
5361 +
5362 + $chunk_texts = array();
5363 + $chunks_in_this_source = 0;
5364 + foreach ($group['chunks'] as $chunk) {
5365 + if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
5366 + break;
5367 + }
5368 + $chunk_texts[] = $chunk['text'];
5369 + $chunks_in_this_source++;
5370 + }
5371 + $full_text = implode("\n\n", $chunk_texts);
5372 + }
5373 + } else {
5374 + $full_text = $group['single_text'];
5375 + $chunks_in_this_source = 1;
5376 + }
5377 +
5378 + if (!empty($full_text)) {
5379 + // Strip URLs from content if citation links are disabled
5380 + if (!$citation_links_enabled) {
5381 + $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
5382 + $full_text = preg_replace('/\s+/', ' ', trim($full_text)); // Clean up extra spaces
5383 + }
5384 +
5385 + // Use numbered reference for URL-based entries, plain info label for manual entries
5386 + if (!empty($source_url) && $source_url !== '#') {
5387 + $matches_used++;
5388 + $content .= "## Reference " . $matches_used . " ##\n";
5389 + $content .= $full_text . "\n\n";
5390 +
5391 + // Only include citation URLs if citation links are enabled
5392 + if ($citation_links_enabled) {
5393 + $valid_urls[] = $source_url;
5394 + $content .= "URL: " . $source_url . "\n\n";
5395 + }
5396 + } else {
5397 + // Manual entry — no reference number, no citation
5398 + $content .= "## Information ##\n";
5399 + $content .= $full_text . "\n\n";
5400 + }
5401 +
5402 + // Extract any URLs from the text content itself (only if citation links enabled)
5403 + if ($citation_links_enabled) {
5404 + preg_match_all(
5405 + '#\bhttps?://[^\s<>"\']+#i',
5406 + $full_text,
5407 + $content_urls
5408 + );
5409 + if (!empty($content_urls[0])) {
5410 + $valid_urls = array_merge($valid_urls, $content_urls[0]);
5411 + }
5412 + }
5413 +
5414 + $total_chunks_used += $chunks_in_this_source;
5415 + }
5416 + }
5417 +
5418 + // Process ALL matches for testing data (top 10) - with role checking for testing display
5419 + $all_matches = [];
5420 + foreach ($results['matches'] as $index => $match) {
5421 + if ($index >= 10) break; // Limit to top 10 for testing
5422 +
5423 + $match_id = $match['id'] ?? '';
5424 +
5425 + // Check role access for testing display (use cache if available)
5426 + $role_restriction = $this->get_single_vector_role($match_id, $match['metadata']);
5427 + $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
5428 +
5429 + $source_display = '';
5430 + if (!empty($match['metadata']['source_url'])) {
5431 + $source_display = $match['metadata']['source_url'];
5432 + } else {
5433 + $content_preview = strip_tags($match['metadata']['text'] ?? '');
5434 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
5435 + $source_display = substr(trim($content_preview), 0, 50) . '...';
5436 + }
5437 +
5438 + $match_id_for_display = $match['id'] ?? $index;
5439 +
5440 + // Check for chunk metadata in Pinecone
5441 + $is_chunk = isset($match['metadata']['is_chunked']) && $match['metadata']['is_chunked'];
5442 + $chunk_index = isset($match['metadata']['chunk_index']) ? intval($match['metadata']['chunk_index']) : null;
5443 + $total_chunks = isset($match['metadata']['total_chunks']) ? intval($match['metadata']['total_chunks']) : null;
5444 +
5445 + // Also detect chunk from vector ID pattern: {hash}_chunk_{index}
5446 + if (!$is_chunk && MxChat_Chunker::is_chunk_vector_id($match_id_for_display)) {
5447 + $is_chunk = true;
5448 + }
5449 +
5450 + $all_matches[] = [
5451 + 'document_id' => $match_id_for_display,
5452 + 'similarity' => $match['score'],
5453 + 'similarity_percentage' => round($match['score'] * 100, 2),
5454 + 'above_threshold' => $match['score'] >= $similarity_threshold,
5455 + 'source_display' => $source_display,
5456 + 'content_preview' => substr(strip_tags($match['metadata']['text'] ?? ''), 0, 100) . '...',
5457 + 'used_for_context' => in_array($match_id_for_display, $matches_used_for_context),
5458 + 'role_restriction' => $role_restriction,
5459 + 'has_access' => $has_access,
5460 + 'filtered_out' => !$has_access,
5461 + 'is_chunk' => $is_chunk,
5462 + 'chunk_index' => $chunk_index,
5463 + 'total_chunks' => $total_chunks
5464 + ];
5465 + }
5466 +
5467 + // Store for testing panel
5468 + $this->last_similarity_analysis['top_matches'] = $all_matches;
5469 + $this->last_similarity_analysis['total_checked'] = count($results['matches']);
5470 + $this->last_similarity_analysis['sources_used'] = $matches_used;
5471 + $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
5472 +
5473 + // NEW: Store unique valid URLs for validation
5474 + $this->current_valid_urls = array_unique($valid_urls);
5475 +
5476 + // Add response guidelines
5477 + if ($matches_used === 0) {
5478 + $content = "No reference information was found for this query.\n\n";
5479 + } else {
5480 + // Build response guidelines based on citation links setting
5481 + $content .= "\n## Response Guidelines ##\n" .
5482 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
5483 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
5484 + "If you don't have specific information or are uncertain about any details, it's always " .
5485 + "better to honestly say you don't know rather than making up or guessing at answers. " .
5486 + "When information is incomplete, let them know you are unsure.\n\n";
5487 +
5488 + // Only add hyperlink instructions if citation links are enabled
5489 + if ($citation_links_enabled) {
5490 + $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
5491 + "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
5492 + "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
5493 + } else {
5494 + $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
5495 + "Simply provide helpful answers based on the reference information without citing sources.";
5496 + }
5497 + }
5498 +
3314 5499 return trim($content);
3315 5500 }
3316 5501
5502 +/**
5503 + * Get role restriction for a single vector (with caching)
5504 + */
5505 +private function get_single_vector_role($vector_id, $metadata = array()) {
5506 + global $wpdb;
5507 +
5508 + if (empty($vector_id)) {
5509 + return 'public';
5510 + }
5511 +
5512 + // Check cache first
5513 + $cache_key = 'mxchat_vector_role_' . $vector_id;
5514 + $cached_role = wp_cache_get($cache_key, 'mxchat_vector_roles');
5515 +
5516 + if ($cached_role !== false) {
5517 + return $cached_role;
5518 + }
5519 +
5520 + $role_restriction = 'public';
5521 +
5522 + // First try Pinecone metadata
5523 + if (!empty($metadata['role_restriction'])) {
5524 + $role_restriction = $metadata['role_restriction'];
5525 + } else {
5526 + // Check WordPress table for user-modified roles
5527 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
5528 + $stored_role = $wpdb->get_var($wpdb->prepare(
5529 + "SELECT role_restriction FROM {$roles_table} WHERE vector_id = %s",
5530 + $vector_id
5531 + ));
5532 +
5533 + if ($stored_role) {
5534 + $role_restriction = $stored_role;
5535 + }
5536 + }
5537 +
5538 + // Cache individual role for 1 hour
5539 + wp_cache_set($cache_key, $role_restriction, 'mxchat_vector_roles', 3600);
3317 5540
5541 + return $role_restriction;
5542 +}
5543 +
5544 +/**
5545 + * Fetch and reassemble all chunks for a URL from Pinecone
5546 + *
5547 + * @param string $source_url The source URL to fetch chunks for
5548 + * @param array $bot_config Bot-specific Pinecone configuration
5549 + * @return string Reassembled content from all chunks
5550 + */
5551 +private function reassemble_chunks_from_pinecone($source_url, $bot_config, $max_chunks = 0, &$chunk_count = 0) {
5552 + $api_key = $bot_config['api_key'] ?? '';
5553 + $host = $bot_config['host'] ?? '';
5554 + $namespace = $bot_config['namespace'] ?? '';
5555 +
5556 + if (empty($host) || empty($api_key)) {
5557 + $chunk_count = 0;
5558 + return '';
5559 + }
5560 +
5561 + $base_hash = md5($source_url);
5562 +
5563 + // Use Pinecone list API to find all chunk vectors with this prefix
5564 + $list_url = "https://{$host}/vectors/list";
5565 +
5566 + // Limit to max_chunks if specified, otherwise fetch up to 100
5567 + $fetch_limit = ($max_chunks > 0 && $max_chunks < 100) ? $max_chunks : 100;
5568 +
5569 + $list_body = array(
5570 + 'prefix' => $base_hash . '_chunk_',
5571 + 'limit' => $fetch_limit
5572 + );
5573 +
5574 + if (!empty($namespace)) {
5575 + $list_body['namespace'] = $namespace;
5576 + }
5577 +
5578 + $list_response = wp_remote_post($list_url, array(
5579 + 'headers' => array(
5580 + 'Api-Key' => $api_key,
5581 + 'accept' => 'application/json',
5582 + 'content-type' => 'application/json'
5583 + ),
5584 + 'body' => wp_json_encode($list_body),
5585 + 'timeout' => 30
5586 + ));
5587 +
5588 + if (is_wp_error($list_response)) {
5589 + //error_log('[MXCHAT-CHUNK] List API error: ' . $list_response->get_error_message());
5590 + return '';
5591 + }
5592 +
5593 + $list_data = json_decode(wp_remote_retrieve_body($list_response), true);
5594 +
5595 + if (empty($list_data['vectors'])) {
5596 + //error_log('[MXCHAT-CHUNK] No chunk vectors found for URL: ' . $source_url);
5597 + return '';
5598 + }
5599 +
5600 + // Extract vector IDs
5601 + $vector_ids = array();
5602 + foreach ($list_data['vectors'] as $vector) {
5603 + if (isset($vector['id'])) {
5604 + $vector_ids[] = $vector['id'];
5605 + }
5606 + }
5607 +
5608 + if (empty($vector_ids)) {
5609 + return '';
5610 + }
5611 +
5612 + // Fetch all chunk content
5613 + $fetch_url = "https://{$host}/vectors/fetch";
5614 +
5615 + $fetch_body = array(
5616 + 'ids' => $vector_ids
5617 + );
5618 +
5619 + if (!empty($namespace)) {
5620 + $fetch_body['namespace'] = $namespace;
5621 + }
5622 +
5623 + $fetch_response = wp_remote_post($fetch_url, array(
5624 + 'headers' => array(
5625 + 'Api-Key' => $api_key,
5626 + 'accept' => 'application/json',
5627 + 'content-type' => 'application/json'
5628 + ),
5629 + 'body' => wp_json_encode($fetch_body),
5630 + 'timeout' => 30
5631 + ));
5632 +
5633 + if (is_wp_error($fetch_response)) {
5634 + //error_log('[MXCHAT-CHUNK] Fetch API error: ' . $fetch_response->get_error_message());
5635 + return '';
5636 + }
5637 +
5638 + $fetch_data = json_decode(wp_remote_retrieve_body($fetch_response), true);
5639 +
5640 + if (empty($fetch_data['vectors'])) {
5641 + return '';
5642 + }
5643 +
5644 + // Sort chunks by index and reassemble
5645 + $chunks = array();
5646 + foreach ($fetch_data['vectors'] as $id => $vector) {
5647 + $metadata = $vector['metadata'] ?? array();
5648 + $chunk_index = $metadata['chunk_index'] ?? 0;
5649 + $text = $metadata['text'] ?? '';
5650 +
5651 + // Store chunk with its index
5652 + $chunks[$chunk_index] = $text;
5653 + }
5654 +
5655 + // Sort by chunk index
5656 + ksort($chunks);
5657 +
5658 + // Apply chunk limit if specified
5659 + if ($max_chunks > 0 && count($chunks) > $max_chunks) {
5660 + $chunks = array_slice($chunks, 0, $max_chunks, true);
5661 + }
5662 +
5663 + // Store actual chunk count
5664 + $chunk_count = count($chunks);
5665 +
5666 + // Reassemble content
5667 + return implode("\n\n", $chunks);
5668 +}
5669 +
5670 +/**
5671 + * Search for relevant content using OpenAI Vector Store (File Search)
5672 + *
5673 + * @param string $user_query The user's query text
5674 + * @param string $bot_id The bot ID
5675 + * @param array $vectorstore_config Vector Store configuration
5676 + * @return string Formatted context string with references
5677 + */
5678 +private function find_relevant_content_openai_vectorstore($user_query, $bot_id = 'default', $vectorstore_config = array()) {
5679 + //error_log("MXCHAT DEBUG: find_relevant_content_openai_vectorstore called");
5680 + //error_log(" - bot_id: " . $bot_id);
5681 + //error_log(" - user_query length: " . strlen($user_query));
5682 +
5683 + // Get OpenAI API key
5684 + $mxchat_options = get_option('mxchat_options', array());
5685 + $api_key = $mxchat_options['api_key'] ?? '';
5686 +
5687 + // Reset vectorstore error tracking
5688 + $this->last_vectorstore_error = null;
5689 +
5690 + if (empty($api_key)) {
5691 + //error_log("MXCHAT DEBUG ERROR: OpenAI API key not configured");
5692 + $this->last_vectorstore_error = 'Vector Store search failed: OpenAI API key is not configured.';
5693 + $this->current_valid_urls = [];
5694 + return '';
5695 + }
5696 +
5697 + // Get Vector Store configuration
5698 + if (empty($vectorstore_config)) {
5699 + $vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
5700 + }
5701 +
5702 + $vectorstore_ids_string = $vectorstore_config['vectorstore_ids'] ?? '';
5703 + $max_results = $vectorstore_config['max_results'] ?? 5;
5704 +
5705 + if (empty($vectorstore_ids_string)) {
5706 + //error_log("MXCHAT DEBUG ERROR: No Vector Store IDs configured");
5707 + $this->last_vectorstore_error = 'Vector Store search failed: No Vector Store IDs are configured for this bot.';
5708 + $this->current_valid_urls = [];
5709 + return '';
5710 + }
5711 +
5712 + // Parse Vector Store IDs
5713 + $vectorstore_ids = array_map('trim', explode(',', $vectorstore_ids_string));
5714 + $vectorstore_ids = array_filter($vectorstore_ids); // Remove empty values
5715 +
5716 + //error_log("MXCHAT DEBUG: Vector Store IDs: " . implode(', ', $vectorstore_ids));
5717 + //error_log("MXCHAT DEBUG: Max results: " . $max_results);
5718 +
5719 + // Initialize similarity analysis storage
5720 + $this->last_similarity_analysis = [
5721 + 'knowledge_base_type' => 'OpenAI Vector Store',
5722 + 'bot_id' => $bot_id,
5723 + 'vectorstore_ids' => $vectorstore_ids,
5724 + 'top_matches' => [],
5725 + 'threshold_used' => 0,
5726 + 'total_checked' => 0
5727 + ];
5728 +
5729 + $valid_urls = [];
5730 +
5731 + // Get the selected model
5732 + $bot_options = $this->get_bot_options($bot_id);
5733 + $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
5734 + $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
5735 +
5736 + // Verify it's an OpenAI model
5737 + if (!$this->is_openai_chat_model($selected_model)) {
5738 + //error_log("MXCHAT DEBUG ERROR: Vector Store search requires OpenAI model. Current: " . $selected_model);
5739 + $this->last_vectorstore_error = 'Vector Store search requires an OpenAI model. Current model: ' . $selected_model;
5740 + $this->current_valid_urls = [];
5741 + return '';
5742 + }
5743 +
5744 + // Use OpenAI Responses API with file_search tool
5745 + $request_body = array(
5746 + 'model' => $selected_model,
5747 + 'input' => $user_query,
5748 + 'tools' => array(
5749 + array(
5750 + 'type' => 'file_search',
5751 + 'vector_store_ids' => $vectorstore_ids,
5752 + 'max_num_results' => intval($max_results)
5753 + )
5754 + ),
5755 + 'include' => array('output[*].file_search_call.search_results')
5756 + );
5757 +
5758 + //error_log("MXCHAT VECTORSTORE: ========== REQUEST START ==========");
5759 + //error_log("MXCHAT VECTORSTORE: Model: " . $selected_model);
5760 + //error_log("MXCHAT VECTORSTORE: Query: " . substr($user_query, 0, 200));
5761 + //error_log("MXCHAT VECTORSTORE: Vector Store IDs: " . implode(', ', $vectorstore_ids));
5762 + //error_log("MXCHAT VECTORSTORE: Max Results: " . $max_results);
5763 + //error_log("MXCHAT VECTORSTORE: Request body: " . wp_json_encode($request_body));
5764 +
5765 + $response = wp_remote_post('https://api.openai.com/v1/responses', array(
5766 + 'headers' => array(
5767 + 'Authorization' => 'Bearer ' . $api_key,
5768 + 'Content-Type' => 'application/json'
5769 + ),
5770 + 'body' => wp_json_encode($request_body),
5771 + 'timeout' => 60
5772 + ));
5773 +
5774 + if (is_wp_error($response)) {
5775 + //error_log("MXCHAT VECTORSTORE ERROR: WP Error: " . $response->get_error_message());
5776 + $this->last_vectorstore_error = 'Vector Store API request failed: ' . $response->get_error_message();
5777 + $this->current_valid_urls = [];
5778 + return '';
5779 + }
5780 +
5781 + $response_code = wp_remote_retrieve_response_code($response);
5782 + //error_log("MXCHAT VECTORSTORE: Response code: " . $response_code);
5783 +
5784 + $response_body = wp_remote_retrieve_body($response);
5785 + //error_log("MXCHAT VECTORSTORE: Raw response (first 2000 chars): " . substr($response_body, 0, 2000));
5786 +
5787 + if ($response_code !== 200) {
5788 + //error_log("MXCHAT VECTORSTORE ERROR: API error response: " . $response_body);
5789 + $api_error_detail = '';
5790 + $decoded_error = json_decode($response_body, true);
5791 + if (isset($decoded_error['error']['message'])) {
5792 + $api_error_detail = $decoded_error['error']['message'];
5793 + }
5794 + $this->last_vectorstore_error = 'Vector Store API returned HTTP ' . $response_code . ($api_error_detail ? ': ' . $api_error_detail : '');
5795 + $this->current_valid_urls = [];
5796 + return '';
5797 + }
5798 + $result = json_decode($response_body, true);
5799 +
5800 + if (json_last_error() !== JSON_ERROR_NONE) {
5801 + //error_log("MXCHAT VECTORSTORE ERROR: JSON decode error: " . json_last_error_msg());
5802 + $this->last_vectorstore_error = 'Vector Store response could not be parsed: ' . json_last_error_msg();
5803 + $this->current_valid_urls = [];
5804 + return '';
5805 + }
5806 +
5807 + // Debug: Log the structure of the result
5808 + //error_log("MXCHAT VECTORSTORE: Result keys: " . implode(', ', array_keys($result)));
5809 + if (isset($result['output'])) {
5810 + //error_log("MXCHAT VECTORSTORE: Output count: " . count($result['output']));
5811 + foreach ($result['output'] as $idx => $out) {
5812 + //error_log("MXCHAT VECTORSTORE: Output[$idx] type: " . ($out['type'] ?? 'unknown'));
5813 + //error_log("MXCHAT VECTORSTORE: Output[$idx] keys: " . implode(', ', array_keys($out)));
5814 + }
5815 + } else {
5816 + //error_log("MXCHAT VECTORSTORE: No 'output' key in result!");
5817 + }
5818 +
5819 + // Extract file search results from the response
5820 + $content = '';
5821 + $matches_used = 0;
5822 + $all_matches = [];
5823 +
5824 + // The Responses API returns output array with tool results
5825 + if (isset($result['output']) && is_array($result['output'])) {
5826 + foreach ($result['output'] as $output_item) {
5827 + // Look for file_search_call results
5828 + if (isset($output_item['type']) && $output_item['type'] === 'file_search_call') {
5829 + //error_log("MXCHAT VECTORSTORE: Found file_search_call output item");
5830 + //error_log("MXCHAT VECTORSTORE: file_search_call keys: " . implode(', ', array_keys($output_item)));
5831 +
5832 + // Check for search_results in the output item directly
5833 + $search_results = $output_item['search_results'] ?? $output_item['results'] ?? [];
5834 + //error_log("MXCHAT VECTORSTORE: Search results count: " . count($search_results));
5835 +
5836 + if (empty($search_results)) {
5837 + //error_log("MXCHAT VECTORSTORE: No search results found in file_search_call");
5838 + //error_log("MXCHAT VECTORSTORE: file_search_call content: " . wp_json_encode($output_item));
5839 + }
5840 +
5841 + foreach ($search_results as $index => $search_result) {
5842 + $filename = $search_result['filename'] ?? '';
5843 + $score = $search_result['score'] ?? 0;
5844 + $text_content = '';
5845 +
5846 + // Extract text content from the result
5847 + // The text can be directly on the result OR nested under content array
5848 + if (isset($search_result['text']) && !empty($search_result['text'])) {
5849 + // Direct text field (OpenAI's actual format)
5850 + $text_content = $search_result['text'];
5851 + //error_log("MXCHAT VECTORSTORE: Found text directly on result[$index], length: " . strlen($text_content));
5852 + } elseif (isset($search_result['content']) && is_array($search_result['content'])) {
5853 + // Nested content array format
5854 + foreach ($search_result['content'] as $content_item) {
5855 + if (isset($content_item['text'])) {
5856 + $text_content .= $content_item['text'] . "\n";
5857 + }
5858 + }
5859 + //error_log("MXCHAT VECTORSTORE: Found text in content array for result[$index], length: " . strlen($text_content));
5860 + } else {
5861 + //error_log("MXCHAT VECTORSTORE: No text found for result[$index]. Keys: " . implode(', ', array_keys($search_result)));
5862 + }
5863 +
5864 + if (!empty($text_content)) {
5865 + $content .= "## Reference " . ($matches_used + 1) . " ##\n";
5866 + $content .= trim($text_content) . "\n\n";
5867 +
5868 + if (!empty($filename)) {
5869 + $content .= "Source: " . $filename . "\n\n";
5870 + }
5871 +
5872 + // Extract URLs from content
5873 + preg_match_all(
5874 + '#\bhttps?://[^\s<>"\']+#i',
5875 + $text_content,
5876 + $content_urls
5877 + );
5878 + if (!empty($content_urls[0])) {
5879 + $valid_urls = array_merge($valid_urls, $content_urls[0]);
5880 + }
5881 +
5882 + $matches_used++;
5883 + }
5884 +
5885 + // Store for similarity analysis
5886 + $all_matches[] = [
5887 + 'document_id' => $filename ?: ('result_' . $index),
5888 + 'similarity' => $score,
5889 + 'similarity_percentage' => round($score * 100, 2),
5890 + 'above_threshold' => true,
5891 + 'source_display' => $filename,
5892 + 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
5893 + 'used_for_context' => true,
5894 + 'role_restriction' => 'public',
5895 + 'has_access' => true,
5896 + 'filtered_out' => false
5897 + ];
5898 + }
5899 + }
5900 +
5901 + // Also check for message content with annotations (citations)
5902 + if (isset($output_item['type']) && $output_item['type'] === 'message') {
5903 + if (isset($output_item['content']) && is_array($output_item['content'])) {
5904 + foreach ($output_item['content'] as $content_block) {
5905 + if (isset($content_block['annotations']) && is_array($content_block['annotations'])) {
5906 + foreach ($content_block['annotations'] as $annotation) {
5907 + if (isset($annotation['filename'])) {
5908 + $filename = $annotation['filename'];
5909 + $score = $annotation['score'] ?? 0;
5910 + $text_content = '';
5911 +
5912 + if (isset($annotation['content']) && is_array($annotation['content'])) {
5913 + foreach ($annotation['content'] as $ann_content) {
5914 + if (isset($ann_content['text'])) {
5915 + $text_content .= $ann_content['text'] . "\n";
5916 + }
5917 + }
5918 + }
5919 +
5920 + if (!empty($text_content) && $matches_used < $max_results) {
5921 + $content .= "## Reference " . ($matches_used + 1) . " ##\n";
5922 + $content .= trim($text_content) . "\n\n";
5923 + $content .= "Source: " . $filename . "\n\n";
5924 +
5925 + preg_match_all(
5926 + '#\bhttps?://[^\s<>"\']+#i',
5927 + $text_content,
5928 + $content_urls
5929 + );
5930 + if (!empty($content_urls[0])) {
5931 + $valid_urls = array_merge($valid_urls, $content_urls[0]);
5932 + }
5933 +
5934 + $matches_used++;
5935 +
5936 + $all_matches[] = [
5937 + 'document_id' => $filename,
5938 + 'similarity' => $score,
5939 + 'similarity_percentage' => round($score * 100, 2),
5940 + 'above_threshold' => true,
5941 + 'source_display' => $filename,
5942 + 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
5943 + 'used_for_context' => true,
5944 + 'role_restriction' => 'public',
5945 + 'has_access' => true,
5946 + 'filtered_out' => false
5947 + ];
5948 + }
5949 + }
5950 + }
5951 + }
5952 + }
5953 + }
5954 + }
5955 + }
5956 + }
5957 +
5958 + // Store for testing panel
5959 + $this->last_similarity_analysis['top_matches'] = $all_matches;
5960 + $this->last_similarity_analysis['total_checked'] = count($all_matches);
5961 +
5962 + // Store unique valid URLs for validation
5963 + $this->current_valid_urls = array_unique($valid_urls);
5964 +
5965 + //error_log("MXCHAT VECTORSTORE: ========== SEARCH COMPLETE ==========");
5966 + //error_log("MXCHAT VECTORSTORE: Matches used: " . $matches_used);
5967 + //error_log("MXCHAT VECTORSTORE: All matches count: " . count($all_matches));
5968 + //error_log("MXCHAT VECTORSTORE: Content length: " . strlen($content));
5969 + if ($matches_used > 0) {
5970 + //error_log("MXCHAT VECTORSTORE: Content preview: " . substr($content, 0, 500));
5971 + }
5972 +
5973 + // Check if citation links are enabled
5974 + $citation_links_enabled = ($mxchat_options['citation_links_toggle'] ?? 'on') === 'on';
5975 +
5976 + // Add response guidelines
5977 + if ($matches_used === 0) {
5978 + //error_log("MXCHAT VECTORSTORE: No matches found - returning empty reference message");
5979 + $content = "No reference information was found for this query.\n\n";
5980 + } else {
5981 + // Build response guidelines based on citation links setting
5982 + $content .= "\n## Response Guidelines ##\n" .
5983 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
5984 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
5985 + "If you don't have specific information or are uncertain about any details, it's always " .
5986 + "better to honestly say you don't know rather than making up or guessing at answers. " .
5987 + "When information is incomplete, let them know you are unsure.\n\n";
5988 +
5989 + // Only add hyperlink instructions if citation links are enabled
5990 + if ($citation_links_enabled) {
5991 + $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
5992 + "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about.";
5993 + } else {
5994 + $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
5995 + "Simply provide helpful answers based on the reference information without citing sources.";
5996 + }
5997 + }
5998 +
5999 + //error_log("MXCHAT DEBUG: Vector Store search complete. Matches used: " . $matches_used);
6000 +
6001 + return trim($content);
6002 +}
6003 +
6004 +/**
6005 + * Check if the given model is an OpenAI chat model
6006 + *
6007 + * @param string $model The model ID
6008 + * @return bool True if it's an OpenAI model
6009 + */
6010 +private function is_openai_chat_model($model) {
6011 + $openai_prefixes = array('gpt-', 'o1-', 'o3-');
6012 + foreach ($openai_prefixes as $prefix) {
6013 + if (strpos($model, $prefix) === 0) {
6014 + return true;
6015 + }
6016 + }
6017 + return false;
6018 +}
6019 +
6020 +/**
6021 + * Get bot-specific Vector Store configuration
6022 + *
6023 + * @param string $bot_id The bot ID
6024 + * @return array Configuration array
6025 + */
6026 +private function get_bot_vectorstore_config($bot_id = 'default') {
6027 + $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
6028 +
6029 + // Default global settings
6030 + $default_config = array(
6031 + 'use_vectorstore' => ($vectorstore_options['mxchat_use_openai_vectorstore'] ?? '0') === '1',
6032 + 'vectorstore_ids' => $vectorstore_options['mxchat_vectorstore_ids'] ?? '',
6033 + 'max_results' => $vectorstore_options['mxchat_vectorstore_max_results'] ?? 5
6034 + );
6035 +
6036 + // Allow multi-bot plugin to override with bot-specific settings
6037 + $bot_config = apply_filters('mxchat_get_bot_vectorstore_config', $default_config, $bot_id);
6038 +
6039 + // Preserve max_results from global settings if not set in bot config
6040 + if (!isset($bot_config['max_results'])) {
6041 + $bot_config['max_results'] = $default_config['max_results'];
6042 + }
6043 +
6044 + return $bot_config;
6045 +}
6046 +
3318 6047 private function mxchat_find_relevant_products($user_embedding) {
3319 6048 //error_log('MXChat Vector Search: Starting product search...');
3320 6049
3321 6050 // Retrieve the add-on settings from the database
@@ -3333,9 +6062,8 @@
3333 6062 //error_log('MXChat Vector Search: Using WordPress database for products');
3334 6063 return $this->find_relevant_products_wordpress($user_embedding);
3335 6064 }
3336 6065 }
3337 -
3338 6066 private function find_relevant_products_wordpress($user_embedding) {
3339 6067 global $wpdb;
3340 6068 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3341 6069 $cache_key = 'mxchat_system_prompt_embeddings';
@@ -3399,9 +6127,9 @@
3399 6127 usort($relevant_results, function ($a, $b) {
3400 6128 return $b['similarity'] <=> $a['similarity'];
3401 6129 });
3402 6130
3403 - $top_results = array_slice($relevant_results, 0, 5);
6131 + $top_results = array_slice($relevant_results, 0, 3);
3404 6132 $content = '';
3405 6133
3406 6134 foreach ($top_results as $result) {
3407 6135 $chunk_content = $this->fetch_content_with_product_links($result['id']);
@@ -3410,9 +6138,9 @@
3410 6138
3411 6139 return trim($content);
3412 6140 }
3413 6141
3414 -// Modified search function with correct filter syntax
6142 +
3415 6143 private function find_relevant_products_pinecone($user_embedding) {
3416 6144 //error_log('Starting Pinecone product search...');
3417 6145
3418 6146 $options = get_option('mxchat_pinecone_addon_options', array());
@@ -3509,321 +6237,2130 @@
3509 6237
3510 6238 return null;
3511 6239 }
3512 6240
3513 -// Function definition
3514 -private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $conversation_history) {
6241 +/**
6242 + * Get system instructions for a specific bot or default
6243 + * Checks for multi-bot add-on and uses bot-specific instructions if available
6244 + * Automatically strips URLs if citation links are disabled
6245 + * Replaces {visitor_name} placeholder with actual visitor name if available
6246 + *
6247 + * @param string $bot_id The bot ID to get instructions for
6248 + * @param string $session_id Optional session ID to lookup visitor name
6249 + */
6250 +private function get_system_instructions($bot_id = 'default', $session_id = '') {
6251 + $instructions = '';
6252 +
6253 + // Check if multi-bot add-on is active
6254 + if (class_exists('MxChat_Multi_Bot_Core_Manager') && $bot_id !== 'default') {
6255 + // Get bot-specific options from multi-bot add-on
6256 + $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
6257 +
6258 + // If bot has custom system instructions, use those
6259 + if (!empty($bot_options['system_prompt_instructions'])) {
6260 + $instructions = $bot_options['system_prompt_instructions'];
6261 + }
6262 + }
6263 +
6264 + // Fall back to default system instructions
6265 + if (empty($instructions)) {
6266 + $instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
6267 + }
6268 +
6269 + // Check if citation links are disabled - if so, strip URLs from instructions
6270 + $fresh_options = get_option('mxchat_options', []);
6271 + $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
6272 +
6273 + if (!$citation_links_enabled && !empty($instructions)) {
6274 + $instructions = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $instructions);
6275 + $instructions = preg_replace('/\s+/', ' ', trim($instructions)); // Clean up extra spaces
6276 + }
6277 +
6278 + // Replace {visitor_name} placeholder with actual visitor name if available
6279 + if (!empty($instructions) && !empty($session_id) && stripos($instructions, '{visitor_name}') !== false) {
6280 + $name_option_key = "mxchat_name_{$session_id}";
6281 + $visitor_name = get_option($name_option_key, '');
6282 +
6283 + if (!empty($visitor_name)) {
6284 + $instructions = str_ireplace('{visitor_name}', sanitize_text_field($visitor_name), $instructions);
6285 + } else {
6286 + // Remove placeholder if no name is available
6287 + $instructions = str_ireplace('{visitor_name}', '', $instructions);
6288 + $instructions = preg_replace('/\s{2,}/', ' ', trim($instructions)); // Clean up extra spaces
6289 + }
6290 + }
6291 +
6292 + // Allow developers to filter system instructions and process shortcodes
6293 + $instructions = apply_filters('mxchat_system_instructions', $instructions, $bot_id, $session_id);
6294 + $instructions = do_shortcode($instructions);
6295 +
6296 + return $instructions;
6297 +}
6298 +/**
6299 + * Get the current bot ID from session or request context
6300 + */
6301 +private function get_current_bot_id($session_id = '') {
6302 + // First, check if bot_id is passed in the current request
6303 + if (isset($_POST['bot_id']) && !empty($_POST['bot_id'])) {
6304 + return sanitize_key($_POST['bot_id']);
6305 + }
6306 +
6307 + // If not in POST, try to get it from session data
6308 + if (!empty($session_id)) {
6309 + $bot_id = get_option("mxchat_session_bot_{$session_id}", '');
6310 + if (!empty($bot_id)) {
6311 + return $bot_id;
6312 + }
6313 + }
6314 +
6315 + // Fall back to default
6316 + return 'default';
6317 +}
6318 +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') {
3515 6319 try {
3516 6320 if (!$relevant_content) {
3517 - return esc_html__("I'm sorry, I couldn't find relevant information on that topic.", 'mxchat');
6321 + $error_response = [
6322 + 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
6323 + 'error_code' => 'no_relevant_content'
6324 + ];
6325 +
6326 + if ($testing_data !== null) {
6327 + $error_response['testing_data'] = $testing_data;
6328 + }
6329 +
6330 + return $error_response;
3518 6331 }
3519 -
3520 - // Ensure conversation_history is an array
6332 +
3521 6333 if (!is_array($conversation_history)) {
3522 6334 $conversation_history = array();
3523 6335 }
6336 +
6337 + // Check if this is an OpenRouter model
6338 + if ($selected_model === 'openrouter') {
6339 + // Get the actual OpenRouter model from options
6340 + $openrouter_selected_model = $this->options['openrouter_selected_model'] ?? '';
6341 +
6342 + if (empty($openrouter_selected_model)) {
6343 + $error_response = [
6344 + 'error' => esc_html__('No OpenRouter model selected. Please select a model in settings.', 'mxchat'),
6345 + 'error_code' => 'no_openrouter_model_selected'
6346 + ];
6347 + if ($testing_data !== null) {
6348 + $error_response['testing_data'] = $testing_data;
6349 + }
6350 + return $error_response;
6351 + }
6352 +
6353 + if (empty($openrouter_api_key)) {
6354 + $error_response = [
6355 + 'error' => esc_html__('OpenRouter API key is not configured', 'mxchat'),
6356 + 'error_code' => 'missing_openrouter_api_key'
6357 + ];
6358 + if ($testing_data !== null) {
6359 + $error_response['testing_data'] = $testing_data;
6360 + }
6361 + return $error_response;
6362 + }
6363 +
6364 + if ($streaming) {
6365 + return $this->mxchat_generate_response_openrouter_stream(
6366 + $openrouter_selected_model,
6367 + $openrouter_api_key,
6368 + $conversation_history,
6369 + $relevant_content,
6370 + $session_id,
6371 + $testing_data
6372 + );
6373 + } else {
6374 + $response = $this->mxchat_generate_response_openrouter(
6375 + $openrouter_selected_model,
6376 + $openrouter_api_key,
6377 + $conversation_history,
6378 + $relevant_content
6379 + );
6380 + }
6381 +
6382 + if (is_array($response) && isset($response['error'])) {
6383 + if ($testing_data !== null) {
6384 + $response['testing_data'] = $testing_data;
6385 + }
6386 + return $response;
6387 + }
6388 +
6389 + return $response;
6390 + }
3524 6391
3525 - // Get selected model with default fallback
3526 - $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
3527 -
3528 6392 // Extract model prefix to determine the provider
3529 6393 $model_parts = explode('-', $selected_model);
3530 6394 $provider = strtolower($model_parts[0]);
3531 -
6395 +
3532 6396 // Handle model selection based on provider prefix
3533 6397 switch ($provider) {
3534 - case 'claude':
3535 - if (empty($claude_api_key)) {
3536 - throw new Exception(esc_html__('Claude API key is not configured', 'mxchat'));
6398 + case 'gemini':
6399 + if (empty($gemini_api_key)) {
6400 + $error_response = [
6401 + 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
6402 + 'error_code' => 'missing_gemini_api_key'
6403 + ];
6404 + if ($testing_data !== null) {
6405 + $error_response['testing_data'] = $testing_data;
6406 + }
6407 + return $error_response;
3537 6408 }
3538 - return $this->mxchat_generate_response_claude(
6409 + $response = $this->mxchat_generate_response_gemini(
3539 6410 $selected_model,
3540 - $claude_api_key,
6411 + $gemini_api_key,
3541 6412 $conversation_history,
3542 6413 $relevant_content
3543 6414 );
3544 -
6415 + break;
6416 +
6417 + case 'claude':
6418 + if (empty($claude_api_key)) {
6419 + $error_response = [
6420 + 'error' => esc_html__('Claude API key is not configured', 'mxchat'),
6421 + 'error_code' => 'missing_claude_api_key'
6422 + ];
6423 + if ($testing_data !== null) {
6424 + $error_response['testing_data'] = $testing_data;
6425 + }
6426 + return $error_response;
6427 + }
6428 + if ($streaming) {
6429 + return $this->mxchat_generate_response_claude_stream(
6430 + $selected_model,
6431 + $claude_api_key,
6432 + $conversation_history,
6433 + $relevant_content,
6434 + $session_id,
6435 + $testing_data
6436 + );
6437 + } else {
6438 + $response = $this->mxchat_generate_response_claude(
6439 + $selected_model,
6440 + $claude_api_key,
6441 + $conversation_history,
6442 + $relevant_content
6443 + );
6444 + }
6445 + break;
6446 +
3545 6447 case 'grok':
3546 6448 if (empty($xai_api_key)) {
3547 - throw new Exception(esc_html__('X.AI API key is not configured', 'mxchat'));
6449 + $error_response = [
6450 + 'error' => esc_html__('X.AI API key is not configured', 'mxchat'),
6451 + 'error_code' => 'missing_xai_api_key'
6452 + ];
6453 + if ($testing_data !== null) {
6454 + $error_response['testing_data'] = $testing_data;
6455 + }
6456 + return $error_response;
3548 6457 }
3549 - return $this->mxchat_generate_response_xai(
3550 - $selected_model,
3551 - $xai_api_key,
3552 - $conversation_history,
3553 - $relevant_content
3554 - );
3555 -
6458 + if ($streaming) {
6459 + return $this->mxchat_generate_response_xai_stream(
6460 + $selected_model,
6461 + $xai_api_key,
6462 + $conversation_history,
6463 + $relevant_content,
6464 + $session_id,
6465 + $testing_data
6466 + );
6467 + } else {
6468 + $response = $this->mxchat_generate_response_xai(
6469 + $selected_model,
6470 + $xai_api_key,
6471 + $conversation_history,
6472 + $relevant_content
6473 + );
6474 + }
6475 + break;
6476 +
3556 6477 case 'deepseek':
3557 6478 if (empty($deepseek_api_key)) {
3558 - throw new Exception(esc_html__('DeepSeek API key is not configured', 'mxchat'));
6479 + $error_response = [
6480 + 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
6481 + 'error_code' => 'missing_deepseek_api_key'
6482 + ];
6483 + if ($testing_data !== null) {
6484 + $error_response['testing_data'] = $testing_data;
6485 + }
6486 + return $error_response;
3559 6487 }
3560 - return $this->mxchat_generate_response_deepseek(
3561 - $selected_model,
3562 - $deepseek_api_key,
3563 - $conversation_history,
3564 - $relevant_content
3565 - );
3566 -
6488 + if ($streaming) {
6489 + return $this->mxchat_generate_response_deepseek_stream(
6490 + $selected_model,
6491 + $deepseek_api_key,
6492 + $conversation_history,
6493 + $relevant_content,
6494 + $session_id,
6495 + $testing_data
6496 + );
6497 + } else {
6498 + $response = $this->mxchat_generate_response_deepseek(
6499 + $selected_model,
6500 + $deepseek_api_key,
6501 + $conversation_history,
6502 + $relevant_content
6503 + );
6504 + }
6505 + break;
6506 +
3567 6507 case 'gpt':
6508 + case 'o1':
3568 6509 if (empty($api_key)) {
3569 - throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
6510 + $error_response = [
6511 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
6512 + 'error_code' => 'missing_openai_api_key'
6513 + ];
6514 + if ($testing_data !== null) {
6515 + $error_response['testing_data'] = $testing_data;
6516 + }
6517 + return $error_response;
3570 6518 }
3571 - return $this->mxchat_generate_response_openai(
3572 - $selected_model,
3573 - $api_key,
3574 - $conversation_history,
3575 - $relevant_content
3576 - );
3577 6519
6520 + // Check if web search is enabled for this OpenAI model
6521 + $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
6522 + // Models that don't support web search
6523 + $unsupported_web_search_models = array('gpt-4.1-nano');
6524 + $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
6525 +
6526 + if ($web_search_enabled && $model_supports_web_search) {
6527 + // Use Responses API (required for some models, or when web search is enabled)
6528 + return $this->mxchat_generate_response_openai_web_search(
6529 + $selected_model,
6530 + $api_key,
6531 + $conversation_history,
6532 + $relevant_content,
6533 + $session_id,
6534 + $testing_data,
6535 + $streaming
6536 + );
6537 + } elseif ($streaming) {
6538 + return $this->mxchat_generate_response_openai_stream(
6539 + $selected_model,
6540 + $api_key,
6541 + $conversation_history,
6542 + $relevant_content,
6543 + $session_id,
6544 + $testing_data
6545 + );
6546 + } else {
6547 + $response = $this->mxchat_generate_response_openai(
6548 + $selected_model,
6549 + $api_key,
6550 + $conversation_history,
6551 + $relevant_content
6552 + );
6553 + }
6554 + break;
6555 +
3578 6556 default:
3579 - // Default to OpenAI for custom models or unrecognized prefixes
3580 6557 if (empty($api_key)) {
3581 - throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
6558 + $error_response = [
6559 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
6560 + 'error_code' => 'missing_openai_api_key'
6561 + ];
6562 + if ($testing_data !== null) {
6563 + $error_response['testing_data'] = $testing_data;
6564 + }
6565 + return $error_response;
3582 6566 }
3583 - return $this->mxchat_generate_response_openai(
3584 - $selected_model,
3585 - $api_key,
3586 - $conversation_history,
3587 - $relevant_content
6567 +
6568 + // Check if web search is enabled (default case also handles OpenAI models)
6569 + $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
6570 + $unsupported_web_search_models = array('gpt-4.1-nano');
6571 + $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
6572 +
6573 + if ($web_search_enabled && $model_supports_web_search) {
6574 + return $this->mxchat_generate_response_openai_web_search(
6575 + $selected_model,
6576 + $api_key,
6577 + $conversation_history,
6578 + $relevant_content,
6579 + $session_id,
6580 + $testing_data,
6581 + $streaming
6582 + );
6583 + } elseif ($streaming) {
6584 + return $this->mxchat_generate_response_openai_stream(
6585 + $selected_model,
6586 + $api_key,
6587 + $conversation_history,
6588 + $relevant_content,
6589 + $session_id,
6590 + $testing_data
6591 + );
6592 + } else {
6593 + $response = $this->mxchat_generate_response_openai(
6594 + $selected_model,
6595 + $api_key,
6596 + $conversation_history,
6597 + $relevant_content
6598 + );
6599 + }
6600 + break;
6601 + }
6602 +
6603 + if (is_array($response) && isset($response['error'])) {
6604 + if ($testing_data !== null) {
6605 + $response['testing_data'] = $testing_data;
6606 + }
6607 + return $response;
6608 + }
6609 +
6610 + return $response;
6611 +
6612 + } catch (Exception $e) {
6613 + $error_response = [
6614 + 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
6615 + 'error_code' => 'system_exception',
6616 + 'exception_details' => $e->getMessage()
6617 + ];
6618 +
6619 + if ($testing_data !== null) {
6620 + $error_response['testing_data'] = $testing_data;
6621 + }
6622 +
6623 + return $error_response;
6624 + }
6625 +}
6626 +private function mxchat_generate_response_openrouter_stream($selected_model, $openrouter_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
6627 + try {
6628 + $bot_id = $this->get_current_bot_id($session_id);
6629 + $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
6630 +
6631 + if (!is_array($conversation_history)) {
6632 + $conversation_history = array();
6633 + }
6634 +
6635 + $formatted_conversation = array();
6636 +
6637 + $formatted_conversation[] = array(
6638 + 'role' => 'system',
6639 + 'content' => $system_prompt_instructions . " " . $relevant_content
6640 + );
6641 +
6642 + foreach ($conversation_history as $message) {
6643 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
6644 + $role = $message['role'];
6645 + if ($role === 'bot' || $role === 'agent') {
6646 + $role = 'assistant';
6647 + }
6648 + if (!in_array($role, ['system', 'assistant', 'user'])) {
6649 + $role = 'user';
6650 + }
6651 + $formatted_conversation[] = array(
6652 + 'role' => $role,
6653 + 'content' => $message['content']
3588 6654 );
6655 + }
3589 6656 }
6657 +
6658 + if (headers_sent() || !function_exists('curl_init')) {
6659 + $regular_response = $this->mxchat_generate_response_openrouter(
6660 + $selected_model,
6661 + $openrouter_api_key,
6662 + $conversation_history,
6663 + $relevant_content
6664 + );
6665 +
6666 + // Save bot response to transcript
6667 + if (!empty($regular_response) && !empty($session_id)) {
6668 + $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
6669 + }
6670 +
6671 + $response_data = [
6672 + 'text' => $regular_response,
6673 + 'html' => '',
6674 + 'session_id' => $session_id
6675 + ];
6676 +
6677 + if ($testing_data !== null) {
6678 + $response_data['testing_data'] = $testing_data;
6679 + }
6680 +
6681 + header('Content-Type: application/json');
6682 + echo json_encode($response_data);
6683 + return true;
6684 + }
6685 +
6686 + $body = json_encode([
6687 + 'model' => $selected_model,
6688 + 'messages' => $formatted_conversation,
6689 + 'temperature' => 1,
6690 + 'stream' => true
6691 + ]);
6692 +
6693 + // Setup streaming headers now that we know we're actually streaming
6694 + $this->setup_streaming_headers();
6695 +
6696 + $ch = curl_init();
6697 + curl_setopt($ch, CURLOPT_URL, 'https://openrouter.ai/api/v1/chat/completions');
6698 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
6699 + curl_setopt($ch, CURLOPT_POST, true);
6700 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
6701 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
6702 + 'Content-Type: application/json',
6703 + 'Authorization: Bearer ' . $openrouter_api_key,
6704 + 'HTTP-Referer: ' . home_url(),
6705 + 'X-Title: ' . get_bloginfo('name')
6706 + ));
6707 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
6708 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
6709 +
6710 + $full_response = '';
6711 + $stream_started = false;
6712 + $buffer = '';
6713 +
6714 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
6715 + if (!$stream_started && $testing_data !== null) {
6716 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
6717 + flush();
6718 + $stream_started = true;
6719 + }
6720 +
6721 + $buffer .= $data;
6722 + $lines = explode("\n", $buffer);
6723 + $buffer = array_pop($lines);
6724 +
6725 + foreach ($lines as $line) {
6726 + if (trim($line) === '') {
6727 + continue;
6728 + }
6729 +
6730 + if (strpos($line, 'data: ') !== 0) {
6731 + continue;
6732 + }
6733 +
6734 + $json_str = substr($line, 6);
6735 +
6736 + if (trim($json_str) === '[DONE]') {
6737 + echo "data: [DONE]\n\n";
6738 + flush();
6739 + continue;
6740 + }
6741 +
6742 + $json = json_decode(trim($json_str), true);
6743 + if ($json && isset($json['choices'][0]['delta']['content'])) {
6744 + $content = $json['choices'][0]['delta']['content'];
6745 + $full_response .= $content;
6746 +
6747 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
6748 + flush();
6749 + }
6750 + }
6751 +
6752 + return strlen($data);
6753 + });
6754 +
6755 + $response = curl_exec($ch);
6756 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
6757 +
6758 + if (curl_errno($ch) || $http_code !== 200) {
6759 + curl_close($ch);
6760 +
6761 + $regular_response = $this->mxchat_generate_response_openrouter(
6762 + $selected_model,
6763 + $openrouter_api_key,
6764 + $conversation_history,
6765 + $relevant_content
6766 + );
6767 +
6768 + $response_data = [
6769 + 'text' => $regular_response,
6770 + 'html' => '',
6771 + 'session_id' => $session_id
6772 + ];
6773 +
6774 + if ($testing_data !== null) {
6775 + $response_data['testing_data'] = $testing_data;
6776 + }
6777 +
6778 + header('Content-Type: application/json');
6779 + echo json_encode($response_data);
6780 + return true;
6781 + }
6782 +
6783 + curl_close($ch);
6784 +
6785 + if (!empty($full_response) && !empty($session_id)) {
6786 + // Prepare RAG context for streaming response
6787 + $rag_context_for_storage = null;
6788 + $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
6789 + $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
6790 +
6791 + if ($has_rag_data || $has_action_data) {
6792 + $rag_context_for_storage = [];
6793 +
6794 + if ($has_rag_data) {
6795 + $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
6796 + $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
6797 + $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
6798 + $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
6799 + $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
6800 + }
6801 +
6802 + if ($has_action_data) {
6803 + $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
6804 + }
6805 + }
6806 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
6807 + }
6808 +
6809 + return true;
6810 +
3590 6811 } catch (Exception $e) {
3591 - //error_log('MXChat Error: ' . $e->getMessage());
3592 - return sprintf(
3593 - esc_html__('An error occurred: %s', 'mxchat'),
3594 - esc_html($e->getMessage())
6812 + $regular_response = $this->mxchat_generate_response_openrouter(
6813 + $selected_model,
6814 + $openrouter_api_key,
6815 + $conversation_history,
6816 + $relevant_content
3595 6817 );
6818 +
6819 + $response_data = [
6820 + 'text' => $regular_response,
6821 + 'html' => '',
6822 + 'session_id' => $session_id
6823 + ];
6824 +
6825 + if ($testing_data !== null) {
6826 + $response_data['testing_data'] = $testing_data;
6827 + }
6828 +
6829 + header('Content-Type: application/json');
6830 + echo json_encode($response_data);
6831 + return true;
3596 6832 }
3597 6833 }
6834 +private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
6835 + try {
6836 + $bot_id = $this->get_current_bot_id($session_id);
6837 +
6838 + // Get system prompt instructions using centralized function
6839 + $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
6840 +
6841 + // Ensure conversation_history is an array
6842 + if (!is_array($conversation_history)) {
6843 + $conversation_history = array();
6844 + }
3598 6845
6846 + // Format conversation history for OpenAI
6847 + $formatted_conversation = array();
3599 6848
3600 -private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
3601 - // Ensure conversation_history is an array
3602 - if (!is_array($conversation_history)) {
3603 - $conversation_history = array();
3604 - }
6849 + $formatted_conversation[] = array(
6850 + 'role' => 'system',
6851 + 'content' => $system_prompt_instructions . " " . $relevant_content
6852 + );
3605 6853
3606 - // Get system prompt instructions from options
3607 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
6854 + foreach ($conversation_history as $message) {
6855 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
6856 + $role = $message['role'];
6857 + if ($role === 'bot' || $role === 'agent') {
6858 + $role = 'assistant';
6859 + }
6860 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
6861 + $role = 'user';
6862 + }
6863 + $formatted_conversation[] = array(
6864 + 'role' => $role,
6865 + 'content' => $message['content']
6866 + );
6867 + }
6868 + }
3608 6869
3609 - // Create a new array for the formatted conversation
3610 - $formatted_conversation = array();
6870 + // Check if we can actually stream
6871 + if (headers_sent() || !function_exists('curl_init')) {
6872 + // Fallback to regular response with testing data
6873 + $regular_response = $this->mxchat_generate_response_openai(
6874 + $selected_model,
6875 + $api_key,
6876 + $conversation_history,
6877 + $relevant_content
6878 + );
6879 +
6880 + // Save bot response to transcript
6881 + if (!empty($regular_response) && !empty($session_id)) {
6882 + $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
6883 + }
6884 +
6885 + $response_data = [
6886 + 'text' => $regular_response,
6887 + 'html' => '',
6888 + 'session_id' => $session_id
6889 + ];
6890 +
6891 + if ($testing_data !== null) {
6892 + $response_data['testing_data'] = $testing_data;
6893 + }
6894 +
6895 + header('Content-Type: application/json');
6896 + echo json_encode($response_data);
6897 + return true;
6898 + }
3611 6899
3612 - // Add system message first
3613 - $formatted_conversation[] = array(
3614 - 'role' => 'system',
3615 - 'content' => $system_prompt_instructions . " " . $relevant_content
3616 - );
6900 + // Check if this is a GPT-5 model (supports reasoning_effort parameter)
6901 + $is_gpt5_model = (
6902 + strpos($selected_model, 'gpt-5') === 0 ||
6903 + $selected_model === 'gpt-5.2' ||
6904 + $selected_model === 'gpt-5.1-2025-11-13' ||
6905 + $selected_model === 'gpt-5' ||
6906 + $selected_model === 'gpt-5-mini' ||
6907 + $selected_model === 'gpt-5-nano'
6908 + );
3617 6909
3618 - // Add the rest of the conversation history
3619 - foreach ($conversation_history as $message) {
3620 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3621 - $role = $message['role'];
6910 + // Build request body with optimal settings for fast streaming
6911 + $request_body = [
6912 + 'model' => $selected_model,
6913 + 'messages' => $formatted_conversation,
6914 + 'temperature' => 1,
6915 + 'stream' => true
6916 + ];
3622 6917
3623 - // Convert roles to supported format
3624 - if ($role === 'bot' || $role === 'agent') {
3625 - $role = 'assistant';
6918 + // Add reasoning_effort only for GPT-5 models that support it
6919 + // These chat models don't support reasoning_effort parameter
6920 + $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');
6921 + if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
6922 + // GPT-5.1 uses 'low' instead of 'minimal'
6923 + if ($selected_model === 'gpt-5.1-2025-11-13') {
6924 + $request_body['reasoning_effort'] = 'low';
6925 + } elseif ($selected_model === 'gpt-5.4') {
6926 + $request_body['reasoning_effort'] = 'none';
6927 + } else {
6928 + $request_body['reasoning_effort'] = 'minimal';
3626 6929 }
3627 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3628 - $role = 'user';
6930 + }
6931 +
6932 + $body = json_encode($request_body);
6933 +
6934 + // Setup streaming headers now that we know we're actually streaming
6935 + $this->setup_streaming_headers();
6936 +
6937 + // Use cURL for streaming support
6938 + $ch = curl_init();
6939 + curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
6940 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
6941 + curl_setopt($ch, CURLOPT_POST, true);
6942 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
6943 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
6944 + 'Content-Type: application/json',
6945 + 'Authorization: Bearer ' . $api_key
6946 + ));
6947 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
6948 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
6949 +
6950 + $full_response = ''; // Accumulate full response for saving
6951 + $stream_started = false;
6952 + $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
6953 +
6954 + // Buffer control for real-time streaming
6955 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
6956 + // Send testing data as the first event if available
6957 + if (!$stream_started && $testing_data !== null) {
6958 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
6959 + flush();
6960 + $stream_started = true;
3629 6961 }
6962 +
6963 + // CRITICAL FIX: Append new data to buffer
6964 + $buffer .= $data;
6965 +
6966 + // Process complete lines only
6967 + $lines = explode("\n", $buffer);
6968 +
6969 + // CRITICAL FIX: Keep the last incomplete line in the buffer
6970 + // The last element might be incomplete, so keep it in buffer
6971 + $buffer = array_pop($lines);
6972 +
6973 + foreach ($lines as $line) {
6974 + // Skip empty lines
6975 + if (trim($line) === '') {
6976 + continue;
6977 + }
6978 +
6979 + // Only process lines that start with "data: "
6980 + if (strpos($line, 'data: ') !== 0) {
6981 + continue;
6982 + }
6983 +
6984 + $json_str = substr($line, 6); // Remove 'data: ' prefix
6985 +
6986 + if (trim($json_str) === '[DONE]') {
6987 + echo "data: [DONE]\n\n";
6988 + flush();
6989 + continue;
6990 + }
6991 +
6992 + // Try to decode JSON
6993 + $json = json_decode(trim($json_str), true);
6994 + if ($json && isset($json['choices'][0]['delta']['content'])) {
6995 + $content = $json['choices'][0]['delta']['content'];
6996 + $full_response .= $content; // Accumulate the full response
6997 +
6998 + // Send as SSE format
6999 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
7000 + flush();
7001 + }
7002 + }
7003 +
7004 + return strlen($data);
7005 + });
7006 +
7007 + $response = curl_exec($ch);
7008 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7009 +
7010 + if (curl_errno($ch) || $http_code !== 200) {
7011 + $curl_error = curl_error($ch);
7012 + curl_close($ch);
3630 7013
3631 - $formatted_conversation[] = array(
3632 - 'role' => $role,
3633 - 'content' => $message['content']
7014 + // Fallback to regular response
7015 + $regular_response = $this->mxchat_generate_response_openai(
7016 + $selected_model,
7017 + $api_key,
7018 + $conversation_history,
7019 + $relevant_content
3634 7020 );
7021 +
7022 + // FIXED: Check if regular response returned an error
7023 + if (is_array($regular_response) && isset($regular_response['error'])) {
7024 + // Send error in SSE format since we're in streaming mode
7025 + echo "data: " . json_encode([
7026 + 'error' => true,
7027 + 'error_message' => $regular_response['error'],
7028 + 'error_code' => $regular_response['error_code'] ?? 'api_error',
7029 + 'text' => $regular_response['error'],
7030 + 'message' => $regular_response['error']
7031 + ]) . "\n\n";
7032 + echo "data: [DONE]\n\n";
7033 + flush();
7034 + return true;
7035 + }
7036 +
7037 + $response_data = [
7038 + 'text' => $regular_response,
7039 + 'html' => '',
7040 + 'session_id' => $session_id
7041 + ];
7042 +
7043 + if ($testing_data !== null) {
7044 + $response_data['testing_data'] = $testing_data;
7045 + }
7046 +
7047 + header('Content-Type: application/json');
7048 + echo json_encode($response_data);
7049 + return true;
3635 7050 }
7051 +
7052 + curl_close($ch);
7053 +
7054 + // Save the complete response to maintain chat persistence
7055 + if (!empty($full_response) && !empty($session_id)) {
7056 + // Prepare RAG context for streaming response
7057 + $rag_context_for_storage = null;
7058 + $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7059 + $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7060 +
7061 + if ($has_rag_data || $has_action_data) {
7062 + $rag_context_for_storage = [];
7063 +
7064 + if ($has_rag_data) {
7065 + $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7066 + $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7067 + $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7068 + $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7069 + $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7070 + }
7071 +
7072 + if ($has_action_data) {
7073 + $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7074 + }
7075 + }
7076 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
7077 + }
7078 +
7079 + return true; // Indicate streaming completed successfully
7080 +
7081 + } catch (Exception $e) {
7082 + // Fallback to regular response
7083 + $regular_response = $this->mxchat_generate_response_openai(
7084 + $selected_model,
7085 + $api_key,
7086 + $conversation_history,
7087 + $relevant_content
7088 + );
7089 +
7090 + // FIXED: Check if regular response returned an error
7091 + if (is_array($regular_response) && isset($regular_response['error'])) {
7092 + // Send error in SSE format since we're in streaming mode
7093 + echo "data: " . json_encode([
7094 + 'error' => true,
7095 + 'error_message' => $regular_response['error'],
7096 + 'error_code' => $regular_response['error_code'] ?? 'api_error',
7097 + 'text' => $regular_response['error'],
7098 + 'message' => $regular_response['error']
7099 + ]) . "\n\n";
7100 + echo "data: [DONE]\n\n";
7101 + flush();
7102 + return true;
7103 + }
7104 +
7105 + $response_data = [
7106 + 'text' => $regular_response,
7107 + 'html' => '',
7108 + 'session_id' => $session_id
7109 + ];
7110 +
7111 + if ($testing_data !== null) {
7112 + $response_data['testing_data'] = $testing_data;
7113 + }
7114 +
7115 + header('Content-Type: application/json');
7116 + echo json_encode($response_data);
7117 + return true;
3636 7118 }
7119 +}
3637 7120
3638 - $body = json_encode([
3639 - 'model' => $selected_model,
3640 - 'messages' => $formatted_conversation,
3641 - 'temperature' => 0.8,
3642 - 'stream' => false
3643 - ]);
7121 +/**
7122 + * Generate response using OpenAI Responses API with web search tool
7123 + * This uses the newer Responses API which supports web search functionality
7124 + */
7125 +private function mxchat_generate_response_openai_web_search($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null, $streaming = false) {
7126 + try {
7127 + $bot_id = $this->get_current_bot_id($session_id);
7128 + $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
3644 7129
3645 - $args = [
3646 - 'body' => $body,
3647 - 'headers' => [
3648 - 'Content-Type' => 'application/json',
3649 - 'Authorization' => 'Bearer ' . $deepseek_api_key,
3650 - ],
3651 - 'timeout' => 60,
3652 - 'redirection' => 5,
3653 - 'blocking' => true,
3654 - 'httpversion' => '1.0',
3655 - 'sslverify' => true,
3656 - ];
7130 + if (!is_array($conversation_history)) {
7131 + $conversation_history = array();
7132 + }
3657 7133
3658 - $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
7134 + // Build the input for Responses API
7135 + // The Responses API uses a different format - we need to construct the input properly
7136 + $input_parts = [];
3659 7137
7138 + // Add system instructions as context
7139 + $system_context = $system_prompt_instructions . "\n\n" . $relevant_content;
7140 +
7141 + // Build conversation as input items for Responses API
7142 + foreach ($conversation_history as $message) {
7143 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
7144 + $role = $message['role'];
7145 + if ($role === 'bot' || $role === 'agent') {
7146 + $role = 'assistant';
7147 + }
7148 + if (!in_array($role, ['assistant', 'user'])) {
7149 + $role = 'user';
7150 + }
7151 + $input_parts[] = [
7152 + 'type' => 'message',
7153 + 'role' => $role,
7154 + 'content' => $message['content']
7155 + ];
7156 + }
7157 + }
7158 +
7159 + // Build request body for Responses API
7160 + $request_body = [
7161 + 'model' => $selected_model,
7162 + 'input' => $input_parts,
7163 + 'instructions' => $system_context,
7164 + 'stream' => $streaming
7165 + ];
7166 +
7167 + // Only add web search tool if web search is enabled in settings
7168 + $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
7169 + if ($web_search_enabled) {
7170 + $request_body['tools'] = [
7171 + ['type' => 'web_search']
7172 + ];
7173 + }
7174 +
7175 + // Add reasoning effort for supported models
7176 + $is_gpt5_model = strpos($selected_model, 'gpt-5') === 0;
7177 + $no_reasoning_web = array('gpt-5.2', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
7178 + if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_web, true)) {
7179 + if ($selected_model === 'gpt-5.1-2025-11-13') {
7180 + $request_body['reasoning'] = ['effort' => 'low'];
7181 + } elseif ($selected_model === 'gpt-5.4') {
7182 + $request_body['reasoning'] = ['effort' => 'low'];
7183 + }
7184 + }
7185 +
7186 + //error_log("MXCHAT WEB SEARCH: Request body: " . json_encode($request_body));
7187 +
7188 + if ($streaming) {
7189 + return $this->mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data);
7190 + } else {
7191 + return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7192 + }
7193 +
7194 + } catch (Exception $e) {
7195 + //error_log("MXCHAT WEB SEARCH ERROR: " . $e->getMessage());
7196 + return [
7197 + 'error' => sprintf(esc_html__('Web search error: %s', 'mxchat'), esc_html($e->getMessage())),
7198 + 'error_code' => 'web_search_exception'
7199 + ];
7200 + }
7201 +}
7202 +
7203 +/**
7204 + * Handle non-streaming web search response
7205 + */
7206 +private function mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data) {
7207 + $request_body['stream'] = false;
7208 +
7209 + $response = wp_remote_post('https://api.openai.com/v1/responses', array(
7210 + 'headers' => array(
7211 + 'Authorization' => 'Bearer ' . $api_key,
7212 + 'Content-Type' => 'application/json'
7213 + ),
7214 + 'body' => json_encode($request_body),
7215 + 'timeout' => 90
7216 + ));
7217 +
3660 7218 if (is_wp_error($response)) {
3661 - //error_log('DeepSeek API Error: ' . $response->get_error_message());
3662 - return "Sorry, there was an error processing your request.";
7219 + //error_log("MXCHAT WEB SEARCH ERROR: WP Error: " . $response->get_error_message());
7220 + return [
7221 + 'error' => esc_html__('Failed to connect to OpenAI web search API', 'mxchat'),
7222 + 'error_code' => 'web_search_connection_error'
7223 + ];
3663 7224 }
3664 7225
7226 + $response_code = wp_remote_retrieve_response_code($response);
3665 7227 $response_body = wp_remote_retrieve_body($response);
3666 - $decoded_response = json_decode($response_body, true);
3667 7228
3668 - if (isset($decoded_response['choices'][0]['message']['content'])) {
3669 - return trim($decoded_response['choices'][0]['message']['content']);
3670 - } else {
3671 - //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
3672 - return "Sorry, I couldn't process that request.";
7229 + //error_log("MXCHAT WEB SEARCH: Response code: " . $response_code);
7230 + //error_log("MXCHAT WEB SEARCH: Response body (first 2000): " . substr($response_body, 0, 2000));
7231 +
7232 + if ($response_code !== 200) {
7233 + $error_data = json_decode($response_body, true);
7234 + $error_message = $error_data['error']['message'] ?? 'Unknown API error';
7235 + return [
7236 + 'error' => sprintf(esc_html__('OpenAI API error: %s', 'mxchat'), esc_html($error_message)),
7237 + 'error_code' => 'web_search_api_error'
7238 + ];
3673 7239 }
7240 +
7241 + $result = json_decode($response_body, true);
7242 +
7243 + if (json_last_error() !== JSON_ERROR_NONE) {
7244 + return [
7245 + 'error' => esc_html__('Invalid response from OpenAI', 'mxchat'),
7246 + 'error_code' => 'web_search_json_error'
7247 + ];
7248 + }
7249 +
7250 + // Extract the response text and citations from Responses API format
7251 + $output_text = '';
7252 + $citations = [];
7253 +
7254 + if (isset($result['output'])) {
7255 + foreach ($result['output'] as $output_item) {
7256 + if ($output_item['type'] === 'message' && isset($output_item['content'])) {
7257 + foreach ($output_item['content'] as $content_item) {
7258 + if ($content_item['type'] === 'output_text') {
7259 + $output_text .= $content_item['text'];
7260 +
7261 + // Extract citations/annotations
7262 + if (isset($content_item['annotations'])) {
7263 + foreach ($content_item['annotations'] as $annotation) {
7264 + if ($annotation['type'] === 'url_citation') {
7265 + $citations[] = [
7266 + 'url' => $annotation['url'],
7267 + 'title' => $annotation['title'] ?? ''
7268 + ];
7269 + }
7270 + }
7271 + }
7272 + }
7273 + }
7274 + }
7275 + }
7276 + }
7277 +
7278 + // If we have citations, append them to the response
7279 + if (!empty($citations)) {
7280 + $output_text .= "\n\n**Sources:**\n";
7281 + $seen_urls = [];
7282 + foreach ($citations as $citation) {
7283 + if (!in_array($citation['url'], $seen_urls)) {
7284 + $seen_urls[] = $citation['url'];
7285 + $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
7286 + $output_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
7287 + }
7288 + }
7289 + }
7290 +
7291 + // Save to transcript
7292 + if (!empty($output_text) && !empty($session_id)) {
7293 + $this->mxchat_save_chat_message($session_id, 'bot', $output_text);
7294 + }
7295 +
7296 + return $output_text;
3674 7297 }
3675 7298
3676 -private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
3677 - // Ensure conversation_history is an array
3678 - if (!is_array($conversation_history)) {
3679 - $conversation_history = array();
7299 +/**
7300 + * Handle streaming web search response using Responses API
7301 + */
7302 +private function mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data) {
7303 + $request_body['stream'] = true;
7304 +
7305 + // Check if we can stream
7306 + if (headers_sent() || !function_exists('curl_init')) {
7307 + // Fallback to non-streaming
7308 + return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
3680 7309 }
3681 7310
3682 - // Get system prompt instructions from options
3683 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
7311 + // Setup streaming headers
7312 + $this->setup_streaming_headers();
3684 7313
3685 - // Create a new array for the formatted conversation
3686 - $formatted_conversation = array();
7314 + $ch = curl_init();
7315 + curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/responses');
7316 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
7317 + curl_setopt($ch, CURLOPT_POST, true);
7318 + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($request_body));
7319 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
7320 + 'Content-Type: application/json',
7321 + 'Authorization: Bearer ' . $api_key
7322 + ));
7323 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7324 + curl_setopt($ch, CURLOPT_TIMEOUT, 120);
3687 7325
3688 - // Add system message first
3689 - $formatted_conversation[] = array(
3690 - 'role' => 'system',
3691 - 'content' => $system_prompt_instructions . " " . $relevant_content
3692 - );
7326 + $full_response = '';
7327 + $stream_started = false;
7328 + $buffer = '';
7329 + $citations = [];
3693 7330
3694 - // Add the rest of the conversation history
3695 - foreach ($conversation_history as $message) {
3696 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3697 - $role = $message['role'];
7331 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$citations, $testing_data) {
7332 + // Send testing data as first event if available
7333 + if (!$stream_started && $testing_data !== null) {
7334 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
7335 + flush();
7336 + $stream_started = true;
7337 + }
3698 7338
3699 - // Convert roles to supported format
3700 - if ($role === 'bot' || $role === 'agent') {
3701 - $role = 'assistant';
7339 + $buffer .= $data;
7340 + $lines = explode("\n", $buffer);
7341 + $buffer = array_pop($lines);
7342 +
7343 + foreach ($lines as $line) {
7344 + if (trim($line) === '') continue;
7345 + if (strpos($line, 'data: ') !== 0) continue;
7346 +
7347 + $json_str = substr($line, 6);
7348 +
7349 + if (trim($json_str) === '[DONE]') {
7350 + // Append citations if we have any
7351 + if (!empty($citations)) {
7352 + $citation_text = "\n\n**Sources:**\n";
7353 + $seen_urls = [];
7354 + foreach ($citations as $citation) {
7355 + if (!in_array($citation['url'], $seen_urls)) {
7356 + $seen_urls[] = $citation['url'];
7357 + $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
7358 + $citation_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
7359 + }
7360 + }
7361 + echo "data: " . json_encode(['content' => $citation_text]) . "\n\n";
7362 + $full_response .= $citation_text;
7363 + flush();
7364 + }
7365 + echo "data: [DONE]\n\n";
7366 + flush();
7367 + continue;
3702 7368 }
3703 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3704 - $role = 'user';
7369 +
7370 + $json = json_decode(trim($json_str), true);
7371 + if (!$json) continue;
7372 +
7373 + // Handle Responses API streaming events
7374 + // The format is different from Chat Completions
7375 + if (isset($json['type'])) {
7376 + switch ($json['type']) {
7377 + case 'response.output_text.delta':
7378 + // Text content delta
7379 + if (isset($json['delta'])) {
7380 + $content = $json['delta'];
7381 + $full_response .= $content;
7382 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
7383 + flush();
7384 + }
7385 + break;
7386 +
7387 + case 'response.output_item.done':
7388 + // Check for citations in completed items
7389 + if (isset($json['item']['content'])) {
7390 + foreach ($json['item']['content'] as $content_item) {
7391 + if (isset($content_item['annotations'])) {
7392 + foreach ($content_item['annotations'] as $annotation) {
7393 + if ($annotation['type'] === 'url_citation') {
7394 + $citations[] = [
7395 + 'url' => $annotation['url'],
7396 + 'title' => $annotation['title'] ?? ''
7397 + ];
7398 + }
7399 + }
7400 + }
7401 + }
7402 + }
7403 + break;
7404 + }
3705 7405 }
7406 + }
3706 7407
3707 - $formatted_conversation[] = array(
3708 - 'role' => $role,
3709 - 'content' => $message['content']
7408 + return strlen($data);
7409 + });
7410 +
7411 + $response = curl_exec($ch);
7412 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7413 +
7414 + if (curl_errno($ch) || $http_code !== 200) {
7415 + $curl_error = curl_error($ch);
7416 + curl_close($ch);
7417 +
7418 + //error_log("MXCHAT WEB SEARCH STREAM ERROR: HTTP $http_code, cURL error: $curl_error");
7419 +
7420 + // Fallback to non-streaming
7421 + $fallback_response = $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
7422 +
7423 + if (is_array($fallback_response) && isset($fallback_response['error'])) {
7424 + echo "data: " . json_encode([
7425 + 'error' => true,
7426 + 'error_message' => $fallback_response['error'],
7427 + 'error_code' => $fallback_response['error_code'] ?? 'web_search_error'
7428 + ]) . "\n\n";
7429 + echo "data: [DONE]\n\n";
7430 + flush();
7431 + return true;
7432 + }
7433 +
7434 + $response_data = [
7435 + 'text' => $fallback_response,
7436 + 'html' => '',
7437 + 'session_id' => $session_id
7438 + ];
7439 + if ($testing_data !== null) {
7440 + $response_data['testing_data'] = $testing_data;
7441 + }
7442 + header('Content-Type: application/json');
7443 + echo json_encode($response_data);
7444 + return true;
7445 + }
7446 +
7447 + curl_close($ch);
7448 +
7449 + // Save the complete response
7450 + if (!empty($full_response) && !empty($session_id)) {
7451 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
7452 + }
7453 +
7454 + return true;
7455 +}
7456 +
7457 +private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
7458 + try {
7459 + // Get bot ID from session or request
7460 + $bot_id = $this->get_current_bot_id($session_id);
7461 +
7462 + // Get system prompt instructions using centralized function
7463 + $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
7464 + // Ensure conversation_history is an array
7465 + if (!is_array($conversation_history)) {
7466 + $conversation_history = array();
7467 + }
7468 +
7469 + // Clean and validate conversation history
7470 + foreach ($conversation_history as &$message) {
7471 + // Convert bot and agent roles to assistant
7472 + if ($message['role'] === 'bot' || $message['role'] === 'agent') {
7473 + $message['role'] = 'assistant';
7474 + }
7475 +
7476 + // Remove unsupported roles - Claude only supports 'assistant' and 'user'
7477 + if (!in_array($message['role'], ['assistant', 'user'])) {
7478 + $message['role'] = 'user';
7479 + }
7480 +
7481 + // Ensure content field exists
7482 + if (!isset($message['content']) || empty($message['content'])) {
7483 + $message['content'] = '';
7484 + }
7485 +
7486 + // Remove any unsupported fields
7487 + $message = array_intersect_key($message, array_flip(['role', 'content']));
7488 + }
7489 +
7490 + // Add relevant content as the latest user message
7491 + $conversation_history[] = [
7492 + 'role' => 'user',
7493 + 'content' => $relevant_content
7494 + ];
7495 +
7496 + // Prepare the request body with stream: true
7497 + $body = json_encode([
7498 + 'model' => $selected_model,
7499 + 'messages' => $conversation_history,
7500 + 'max_tokens' => 1000,
7501 + 'temperature' => 0.8,
7502 + 'system' => $system_prompt_instructions,
7503 + 'stream' => true
7504 + ]);
7505 +
7506 + // Check if we can actually stream (headers not sent, etc.)
7507 + if (headers_sent() || !function_exists('curl_init')) {
7508 + // Fallback to regular response with testing data
7509 + //error_log("MxChat: Streaming not possible, falling back to regular response");
7510 + $regular_response = $this->mxchat_generate_response_claude(
7511 + $selected_model,
7512 + $claude_api_key,
7513 + array_slice($conversation_history, 0, -1), // Remove the added content
7514 + $relevant_content
3710 7515 );
7516 +
7517 + // Save bot response to transcript
7518 + if (!empty($regular_response) && !empty($session_id)) {
7519 + $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
7520 + }
7521 +
7522 + // Return as JSON with testing data
7523 + $response_data = [
7524 + 'text' => $regular_response,
7525 + 'html' => '',
7526 + 'session_id' => $session_id
7527 + ];
7528 +
7529 + if ($testing_data !== null) {
7530 + $response_data['testing_data'] = $testing_data;
7531 + //error_log("MxChat Testing: Added testing data to Claude fallback response");
7532 + }
7533 +
7534 + // Clear any streaming headers and send JSON
7535 + if (headers_sent() === false) {
7536 + header('Content-Type: application/json');
7537 + }
7538 + echo json_encode($response_data);
7539 + return true; // Indicate we handled the response
3711 7540 }
7541 +
7542 + // Setup streaming headers now that we know we're actually streaming
7543 + $this->setup_streaming_headers();
7544 +
7545 + // Use cURL for streaming support
7546 + $ch = curl_init();
7547 + curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
7548 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
7549 + curl_setopt($ch, CURLOPT_POST, true);
7550 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
7551 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
7552 + 'Content-Type: application/json',
7553 + 'x-api-key: ' . $claude_api_key,
7554 + 'anthropic-version: 2023-06-01'
7555 + ));
7556 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7557 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
7558 +
7559 + $full_response = ''; // Accumulate full response for saving
7560 + $stream_started = false;
7561 + $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
7562 +
7563 + // Buffer control for real-time streaming
7564 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
7565 + // Send testing data as the first event if available
7566 + if (!$stream_started && $testing_data !== null) {
7567 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
7568 + flush();
7569 + $stream_started = true;
7570 + //error_log("MxChat Testing: Sent testing data in Claude stream");
7571 + }
7572 +
7573 + // CRITICAL FIX: Append new data to buffer
7574 + $buffer .= $data;
7575 +
7576 + // Process complete lines only
7577 + $lines = explode("\n", $buffer);
7578 +
7579 + // CRITICAL FIX: Keep the last incomplete line in the buffer
7580 + // The last element might be incomplete, so keep it in buffer
7581 + $buffer = array_pop($lines);
7582 +
7583 + foreach ($lines as $line) {
7584 + if (trim($line) === '') {
7585 + continue;
7586 + }
7587 +
7588 + // Claude uses event: and data: format
7589 + if (strpos($line, 'event: ') === 0) {
7590 + // Store the event type for the next data line
7591 + continue;
7592 + }
7593 +
7594 + if (strpos($line, 'data: ') === 0) {
7595 + $json_str = substr($line, 6); // Remove 'data: ' prefix
7596 +
7597 + $json = json_decode(trim($json_str), true);
7598 + if (json_last_error() !== JSON_ERROR_NONE) {
7599 + continue;
7600 + }
7601 +
7602 + // Handle different event types
7603 + if (isset($json['type'])) {
7604 + switch ($json['type']) {
7605 + case 'content_block_delta':
7606 + if (isset($json['delta']['text'])) {
7607 + $content = $json['delta']['text'];
7608 + $full_response .= $content; // Accumulate
7609 + // Send as SSE format compatible with your frontend
7610 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
7611 + flush();
7612 + }
7613 + break;
7614 +
7615 + case 'message_stop':
7616 + echo "data: [DONE]\n\n";
7617 + flush();
7618 + break;
7619 +
7620 + case 'error':
7621 + echo "data: " . json_encode(['error' => $json['error']['message'] ?? 'Unknown error']) . "\n\n";
7622 + flush();
7623 + break;
7624 + }
7625 + }
7626 + }
7627 + }
7628 +
7629 + return strlen($data);
7630 + });
7631 +
7632 + $response = curl_exec($ch);
7633 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7634 +
7635 + if (curl_errno($ch)) {
7636 + curl_close($ch);
7637 + throw new Exception('cURL Error: ' . curl_error($ch));
7638 + }
7639 +
7640 + curl_close($ch);
7641 +
7642 + if ($http_code !== 200) {
7643 + // Fallback to regular response
7644 + //error_log("MxChat: Claude streaming failed with HTTP $http_code, falling back");
7645 + $regular_response = $this->mxchat_generate_response_claude(
7646 + $selected_model,
7647 + $claude_api_key,
7648 + array_slice($conversation_history, 0, -1), // Remove the added content
7649 + $relevant_content
7650 + );
7651 +
7652 + // FIXED: Check if regular response returned an error
7653 + if (is_array($regular_response) && isset($regular_response['error'])) {
7654 + // Send error in SSE format since we're in streaming mode
7655 + echo "data: " . json_encode([
7656 + 'error' => true,
7657 + 'error_message' => $regular_response['error'],
7658 + 'error_code' => $regular_response['error_code'] ?? 'api_error',
7659 + 'text' => $regular_response['error'],
7660 + 'message' => $regular_response['error']
7661 + ]) . "\n\n";
7662 + echo "data: [DONE]\n\n";
7663 + flush();
7664 + return true;
7665 + }
7666 +
7667 + $response_data = [
7668 + 'text' => $regular_response,
7669 + 'html' => '',
7670 + 'session_id' => $session_id
7671 + ];
7672 +
7673 + if ($testing_data !== null) {
7674 + $response_data['testing_data'] = $testing_data;
7675 + //error_log("MxChat Testing: Added testing data to Claude error fallback");
7676 + }
7677 +
7678 + header('Content-Type: application/json');
7679 + echo json_encode($response_data);
7680 + return true;
7681 + }
7682 +
7683 + // Save the complete response to maintain chat persistence
7684 + if (!empty($full_response) && !empty($session_id)) {
7685 + // Prepare RAG context for streaming response
7686 + $rag_context_for_storage = null;
7687 + $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7688 + $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7689 +
7690 + if ($has_rag_data || $has_action_data) {
7691 + $rag_context_for_storage = [];
7692 +
7693 + if ($has_rag_data) {
7694 + $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7695 + $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7696 + $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7697 + $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7698 + $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7699 + }
7700 +
7701 + if ($has_action_data) {
7702 + $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7703 + }
7704 + }
7705 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
7706 + }
7707 +
7708 + return true; // Indicate streaming completed successfully
7709 +
7710 + } catch (Exception $e) {
7711 + //error_log("MxChat Claude streaming exception: " . $e->getMessage());
7712 +
7713 + // Fallback to regular response on exception
7714 + $regular_response = $this->mxchat_generate_response_claude(
7715 + $selected_model,
7716 + $claude_api_key,
7717 + $conversation_history,
7718 + $relevant_content
7719 + );
7720 +
7721 + // FIXED: Check if regular response returned an error
7722 + if (is_array($regular_response) && isset($regular_response['error'])) {
7723 + // Send error in SSE format since we're in streaming mode
7724 + echo "data: " . json_encode([
7725 + 'error' => true,
7726 + 'error_message' => $regular_response['error'],
7727 + 'error_code' => $regular_response['error_code'] ?? 'api_error',
7728 + 'text' => $regular_response['error'],
7729 + 'message' => $regular_response['error']
7730 + ]) . "\n\n";
7731 + echo "data: [DONE]\n\n";
7732 + flush();
7733 + return true;
7734 + }
7735 +
7736 + $response_data = [
7737 + 'text' => $regular_response,
7738 + 'html' => '',
7739 + 'session_id' => $session_id
7740 + ];
7741 +
7742 + if ($testing_data !== null) {
7743 + $response_data['testing_data'] = $testing_data;
7744 + //error_log("MxChat Testing: Added testing data to Claude exception fallback");
7745 + }
7746 +
7747 + header('Content-Type: application/json');
7748 + echo json_encode($response_data);
7749 + return true;
3712 7750 }
7751 +}
7752 +private function mxchat_generate_response_xai_stream($selected_model, $xai_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
7753 + try {
7754 + // Get bot ID from session or request
7755 + $bot_id = $this->get_current_bot_id($session_id);
7756 +
7757 + // Get system prompt instructions using centralized function
7758 + $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
7759 +
7760 + // Ensure conversation_history is an array
7761 + if (!is_array($conversation_history)) {
7762 + $conversation_history = array();
7763 + }
3713 7764
3714 - $body = json_encode([
3715 - 'model' => $selected_model,
3716 - 'messages' => $formatted_conversation,
3717 - 'temperature' => 0.8,
3718 - 'stream' => false
3719 - ]);
7765 + // Format conversation history for X.AI (same as OpenAI format)
7766 + $formatted_conversation = array();
3720 7767
3721 - $args = [
3722 - 'body' => $body,
3723 - 'headers' => [
3724 - 'Content-Type' => 'application/json',
3725 - 'Authorization' => 'Bearer ' . $api_key,
3726 - ],
3727 - 'timeout' => 60,
3728 - 'redirection' => 5,
3729 - 'blocking' => true,
3730 - 'httpversion' => '1.0',
3731 - 'sslverify' => true,
3732 - ];
7768 + $formatted_conversation[] = array(
7769 + 'role' => 'system',
7770 + 'content' => $system_prompt_instructions . " " . $relevant_content
7771 + );
3733 7772
3734 - $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
7773 + foreach ($conversation_history as $message) {
7774 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
7775 + $role = $message['role'];
7776 + if ($role === 'bot' || $role === 'agent') {
7777 + $role = 'assistant';
7778 + }
7779 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
7780 + $role = 'user';
7781 + }
7782 + $formatted_conversation[] = array(
7783 + 'role' => $role,
7784 + 'content' => $message['content']
7785 + );
7786 + }
7787 + }
3735 7788
3736 - if (is_wp_error($response)) {
3737 - //error_log('OpenAI API Error: ' . $response->get_error_message());
3738 - return "Sorry, there was an error processing your request.";
3739 - }
7789 + // Check if we can actually stream
7790 + if (headers_sent() || !function_exists('curl_init')) {
7791 + // Fallback to regular response with testing data
7792 + //error_log("MxChat: X.AI streaming not possible, falling back to regular response");
7793 + $regular_response = $this->mxchat_generate_response_xai(
7794 + $selected_model,
7795 + $xai_api_key,
7796 + $conversation_history,
7797 + $relevant_content
7798 + );
7799 +
7800 + // Save bot response to transcript
7801 + if (!empty($regular_response) && !empty($session_id)) {
7802 + $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
7803 + }
7804 +
7805 + $response_data = [
7806 + 'text' => $regular_response,
7807 + 'html' => '',
7808 + 'session_id' => $session_id
7809 + ];
7810 +
7811 + if ($testing_data !== null) {
7812 + $response_data['testing_data'] = $testing_data;
7813 + //error_log("MxChat Testing: Added testing data to X.AI fallback response");
7814 + }
7815 +
7816 + header('Content-Type: application/json');
7817 + echo json_encode($response_data);
7818 + return true;
7819 + }
3740 7820
3741 - $response_body = wp_remote_retrieve_body($response);
3742 - $decoded_response = json_decode($response_body, true);
7821 + // Prepare the request body with stream: true
7822 + $body = json_encode([
7823 + 'model' => $selected_model,
7824 + 'messages' => $formatted_conversation,
7825 + 'temperature' => 0.8,
7826 + 'stream' => true
7827 + ]);
3743 7828
3744 - if (isset($decoded_response['choices'][0]['message']['content'])) {
3745 - return trim($decoded_response['choices'][0]['message']['content']);
3746 - } else {
3747 - //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
3748 - return "Sorry, I couldn't process that request.";
7829 + // Setup streaming headers now that we know we're actually streaming
7830 + $this->setup_streaming_headers();
7831 +
7832 + // Use cURL for streaming support
7833 + $ch = curl_init();
7834 + curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
7835 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
7836 + curl_setopt($ch, CURLOPT_POST, true);
7837 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
7838 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
7839 + 'Content-Type: application/json',
7840 + 'Authorization: Bearer ' . $xai_api_key
7841 + ));
7842 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7843 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
7844 +
7845 + $full_response = ''; // Accumulate full response for saving
7846 + $stream_started = false;
7847 + $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
7848 +
7849 + // Buffer control for real-time streaming
7850 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
7851 + // Send testing data as the first event if available
7852 + if (!$stream_started && $testing_data !== null) {
7853 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
7854 + flush();
7855 + $stream_started = true;
7856 + //error_log("MxChat Testing: Sent testing data in X.AI stream");
7857 + }
7858 +
7859 + // CRITICAL FIX: Append new data to buffer
7860 + $buffer .= $data;
7861 +
7862 + // Process complete lines only
7863 + $lines = explode("\n", $buffer);
7864 +
7865 + // CRITICAL FIX: Keep the last incomplete line in the buffer
7866 + // The last element might be incomplete, so keep it in buffer
7867 + $buffer = array_pop($lines);
7868 +
7869 + foreach ($lines as $line) {
7870 + // Skip empty lines
7871 + if (trim($line) === '') {
7872 + continue;
7873 + }
7874 +
7875 + // Only process lines that start with "data: "
7876 + if (strpos($line, 'data: ') !== 0) {
7877 + continue;
7878 + }
7879 +
7880 + $json_str = substr($line, 6); // Remove 'data: ' prefix
7881 +
7882 + if (trim($json_str) === '[DONE]') {
7883 + echo "data: [DONE]\n\n";
7884 + flush();
7885 + continue;
7886 + }
7887 +
7888 + // Try to decode JSON
7889 + $json = json_decode(trim($json_str), true);
7890 + if ($json && isset($json['choices'][0]['delta']['content'])) {
7891 + $content = $json['choices'][0]['delta']['content'];
7892 + $full_response .= $content; // Accumulate
7893 + // Send as SSE format
7894 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
7895 + flush();
7896 + }
7897 + }
7898 +
7899 + return strlen($data);
7900 + });
7901 +
7902 + $response = curl_exec($ch);
7903 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7904 +
7905 + if (curl_errno($ch) || $http_code !== 200) {
7906 + curl_close($ch);
7907 +
7908 + // Fallback to regular response
7909 + //error_log("MxChat: X.AI streaming failed, falling back");
7910 + $regular_response = $this->mxchat_generate_response_xai(
7911 + $selected_model,
7912 + $xai_api_key,
7913 + $conversation_history,
7914 + $relevant_content
7915 + );
7916 +
7917 + $response_data = [
7918 + 'text' => $regular_response,
7919 + 'html' => '',
7920 + 'session_id' => $session_id
7921 + ];
7922 +
7923 + if ($testing_data !== null) {
7924 + $response_data['testing_data'] = $testing_data;
7925 + //error_log("MxChat Testing: Added testing data to X.AI error fallback");
7926 + }
7927 +
7928 + header('Content-Type: application/json');
7929 + echo json_encode($response_data);
7930 + return true;
7931 + }
7932 +
7933 + curl_close($ch);
7934 +
7935 + // Save the complete response to maintain chat persistence
7936 + if (!empty($full_response) && !empty($session_id)) {
7937 + // Prepare RAG context for streaming response
7938 + $rag_context_for_storage = null;
7939 + $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7940 + $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7941 +
7942 + if ($has_rag_data || $has_action_data) {
7943 + $rag_context_for_storage = [];
7944 +
7945 + if ($has_rag_data) {
7946 + $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7947 + $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7948 + $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7949 + $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7950 + $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7951 + }
7952 +
7953 + if ($has_action_data) {
7954 + $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7955 + }
7956 + }
7957 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
7958 + }
7959 +
7960 + return true; // Indicate streaming completed successfully
7961 +
7962 + } catch (Exception $e) {
7963 + //error_log("MxChat X.AI streaming exception: " . $e->getMessage());
7964 +
7965 + // Fallback to regular response
7966 + $regular_response = $this->mxchat_generate_response_xai(
7967 + $selected_model,
7968 + $xai_api_key,
7969 + $conversation_history,
7970 + $relevant_content
7971 + );
7972 +
7973 + $response_data = [
7974 + 'text' => $regular_response,
7975 + 'html' => '',
7976 + 'session_id' => $session_id
7977 + ];
7978 +
7979 + if ($testing_data !== null) {
7980 + $response_data['testing_data'] = $testing_data;
7981 + //error_log("MxChat Testing: Added testing data to X.AI exception fallback");
7982 + }
7983 +
7984 + header('Content-Type: application/json');
7985 + echo json_encode($response_data);
7986 + return true;
3749 7987 }
3750 7988 }
3751 -private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
3752 - // Get system prompt instructions from options
3753 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
7989 +private function mxchat_generate_response_deepseek_stream($selected_model, $deepseek_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
7990 + try {
7991 + // Get bot ID from session or request
7992 + $bot_id = $this->get_current_bot_id($session_id);
7993 +
7994 + // Get system prompt instructions using centralized function
7995 + $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
7996 +
7997 + // Ensure conversation_history is an array
7998 + if (!is_array($conversation_history)) {
7999 + $conversation_history = array();
8000 + }
3754 8001
3755 - // Add system prompt to relevant content
3756 - $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
8002 + // Format conversation history for DeepSeek
8003 + $formatted_conversation = array();
3757 8004
3758 - // Prepend system instructions to the conversation history
3759 - array_unshift($conversation_history, [
3760 - 'role' => 'system',
3761 - 'content' => "Here are your instructions: " . $content_with_instructions
3762 - ]);
8005 + $formatted_conversation[] = array(
8006 + 'role' => 'system',
8007 + 'content' => $system_prompt_instructions . " " . $relevant_content
8008 + );
3763 8009
3764 - // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3765 - foreach ($conversation_history as &$message) {
3766 - if ($message['role'] === 'bot') {
3767 - $message['role'] = 'assistant';
3768 - } elseif ($message['role'] === 'agent') {
3769 - // Tag the message as coming from a live agent
3770 - $message['role'] = 'assistant';
3771 - if (!isset($message['metadata'])) {
3772 - $message['metadata'] = ['source' => 'live_agent'];
8010 + foreach ($conversation_history as $message) {
8011 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
8012 + $role = $message['role'];
8013 + if ($role === 'bot' || $role === 'agent') {
8014 + $role = 'assistant';
8015 + }
8016 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
8017 + $role = 'user';
8018 + }
8019 + $formatted_conversation[] = array(
8020 + 'role' => $role,
8021 + 'content' => $message['content']
8022 + );
3773 8023 }
3774 8024 }
3775 8025
3776 - // Ensure all roles are valid
3777 - if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3778 - $message['role'] = 'user'; // Default to 'user'
8026 + // Check if we can actually stream
8027 + if (headers_sent() || !function_exists('curl_init')) {
8028 + // Fallback to regular response with testing data
8029 + //error_log("MxChat: DeepSeek streaming not possible, falling back to regular response");
8030 + $regular_response = $this->mxchat_generate_response_deepseek(
8031 + $selected_model,
8032 + $deepseek_api_key,
8033 + $conversation_history,
8034 + $relevant_content
8035 + );
8036 +
8037 + // Save bot response to transcript
8038 + if (!empty($regular_response) && !empty($session_id)) {
8039 + $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
8040 + }
8041 +
8042 + $response_data = [
8043 + 'text' => $regular_response,
8044 + 'html' => '',
8045 + 'session_id' => $session_id
8046 + ];
8047 +
8048 + if ($testing_data !== null) {
8049 + $response_data['testing_data'] = $testing_data;
8050 + //error_log("MxChat Testing: Added testing data to DeepSeek fallback response");
8051 + }
8052 +
8053 + header('Content-Type: application/json');
8054 + echo json_encode($response_data);
8055 + return true;
3779 8056 }
8057 +
8058 + // Prepare the request body with stream: true
8059 + $body = json_encode([
8060 + 'model' => $selected_model,
8061 + 'messages' => $formatted_conversation,
8062 + 'temperature' => 0.8,
8063 + 'stream' => true
8064 + ]);
8065 +
8066 + // Setup streaming headers now that we know we're actually streaming
8067 + $this->setup_streaming_headers();
8068 +
8069 + // Use cURL for streaming support
8070 + $ch = curl_init();
8071 + curl_setopt($ch, CURLOPT_URL, 'https://api.deepseek.com/v1/chat/completions');
8072 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
8073 + curl_setopt($ch, CURLOPT_POST, true);
8074 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
8075 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
8076 + 'Content-Type: application/json',
8077 + 'Authorization: Bearer ' . $deepseek_api_key
8078 + ));
8079 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
8080 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
8081 +
8082 + $full_response = ''; // Accumulate full response for saving
8083 + $stream_started = false;
8084 + $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
8085 +
8086 + // Buffer control for real-time streaming
8087 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
8088 + // Send testing data as the first event if available
8089 + if (!$stream_started && $testing_data !== null) {
8090 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
8091 + flush();
8092 + $stream_started = true;
8093 + //error_log("MxChat Testing: Sent testing data in DeepSeek stream");
8094 + }
8095 +
8096 + // CRITICAL FIX: Append new data to buffer
8097 + $buffer .= $data;
8098 +
8099 + // Process complete lines only
8100 + $lines = explode("\n", $buffer);
8101 +
8102 + // CRITICAL FIX: Keep the last incomplete line in the buffer
8103 + // The last element might be incomplete, so keep it in buffer
8104 + $buffer = array_pop($lines);
8105 +
8106 + foreach ($lines as $line) {
8107 + // Skip empty lines
8108 + if (trim($line) === '') {
8109 + continue;
8110 + }
8111 +
8112 + // Only process lines that start with "data: "
8113 + if (strpos($line, 'data: ') !== 0) {
8114 + continue;
8115 + }
8116 +
8117 + $json_str = substr($line, 6); // Remove 'data: ' prefix
8118 +
8119 + if (trim($json_str) === '[DONE]') {
8120 + echo "data: [DONE]\n\n";
8121 + flush();
8122 + continue;
8123 + }
8124 +
8125 + // Try to decode JSON
8126 + $json = json_decode(trim($json_str), true);
8127 + if ($json && isset($json['choices'][0]['delta']['content'])) {
8128 + $content = $json['choices'][0]['delta']['content'];
8129 + $full_response .= $content; // Accumulate the full response
8130 +
8131 + // Send as SSE format
8132 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
8133 + flush();
8134 + }
8135 + }
8136 +
8137 + return strlen($data);
8138 + });
8139 +
8140 + $response = curl_exec($ch);
8141 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
8142 +
8143 + if (curl_errno($ch) || $http_code !== 200) {
8144 + $curl_error = curl_error($ch);
8145 + curl_close($ch);
8146 +
8147 + // Log the specific error for debugging
8148 + //error_log("MxChat: DeepSeek streaming failed - HTTP: $http_code, cURL: $curl_error");
8149 +
8150 + // Fallback to regular response
8151 + $regular_response = $this->mxchat_generate_response_deepseek(
8152 + $selected_model,
8153 + $deepseek_api_key,
8154 + $conversation_history,
8155 + $relevant_content
8156 + );
8157 +
8158 + // Handle error response from regular function
8159 + if (is_array($regular_response) && isset($regular_response['error'])) {
8160 + if ($testing_data !== null) {
8161 + $regular_response['testing_data'] = $testing_data;
8162 + }
8163 + header('Content-Type: application/json');
8164 + echo json_encode($regular_response);
8165 + return true;
8166 + }
8167 +
8168 + $response_data = [
8169 + 'text' => $regular_response,
8170 + 'html' => '',
8171 + 'session_id' => $session_id
8172 + ];
8173 +
8174 + if ($testing_data !== null) {
8175 + $response_data['testing_data'] = $testing_data;
8176 + //error_log("MxChat Testing: Added testing data to DeepSeek error fallback");
8177 + }
8178 +
8179 + header('Content-Type: application/json');
8180 + echo json_encode($response_data);
8181 + return true;
8182 + }
8183 +
8184 + curl_close($ch);
8185 +
8186 + // Save the complete response to maintain chat persistence
8187 + if (!empty($full_response) && !empty($session_id)) {
8188 + // Prepare RAG context for streaming response
8189 + $rag_context_for_storage = null;
8190 + $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8191 + $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8192 +
8193 + if ($has_rag_data || $has_action_data) {
8194 + $rag_context_for_storage = [];
8195 +
8196 + if ($has_rag_data) {
8197 + $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8198 + $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8199 + $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8200 + $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8201 + $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8202 + }
8203 +
8204 + if ($has_action_data) {
8205 + $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8206 + }
8207 + }
8208 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
8209 + }
8210 +
8211 + return true; // Indicate streaming completed successfully
8212 +
8213 + } catch (Exception $e) {
8214 + //error_log("MxChat DeepSeek streaming exception: " . $e->getMessage());
8215 +
8216 + // Fallback to regular response
8217 + $regular_response = $this->mxchat_generate_response_deepseek(
8218 + $selected_model,
8219 + $deepseek_api_key,
8220 + $conversation_history,
8221 + $relevant_content
8222 + );
8223 +
8224 + // Handle error response from regular function
8225 + if (is_array($regular_response) && isset($regular_response['error'])) {
8226 + if ($testing_data !== null) {
8227 + $regular_response['testing_data'] = $testing_data;
8228 + }
8229 + header('Content-Type: application/json');
8230 + echo json_encode($regular_response);
8231 + return true;
8232 + }
8233 +
8234 + $response_data = [
8235 + 'text' => $regular_response,
8236 + 'html' => '',
8237 + 'session_id' => $session_id
8238 + ];
8239 +
8240 + if ($testing_data !== null) {
8241 + $response_data['testing_data'] = $testing_data;
8242 + //error_log("MxChat Testing: Added testing data to DeepSeek exception fallback");
8243 + }
8244 +
8245 + header('Content-Type: application/json');
8246 + echo json_encode($response_data);
8247 + return true;
3780 8248 }
8249 +}
3781 8250
3782 8251
3783 - // Build the request body
3784 - $body = json_encode([
3785 - 'model' => $selected_model,
3786 - 'messages' => $conversation_history,
3787 - 'temperature' => 0.8,
3788 - 'stream' => false
3789 - ]);
8252 +private function mxchat_generate_response_openrouter($selected_model, $openrouter_api_key, $conversation_history, $relevant_content) {
8253 + try {
8254 + if (!is_array($conversation_history)) {
8255 + $conversation_history = array();
8256 + }
3790 8257
3791 - // Set up the API request
3792 - $args = [
3793 - 'body' => $body,
3794 - 'headers' => [
3795 - 'Content-Type' => 'application/json',
3796 - 'Authorization' => 'Bearer ' . $xai_api_key,
3797 - ],
3798 - 'timeout' => 60,
3799 - 'redirection' => 5,
3800 - 'blocking' => true,
3801 - 'httpversion' => '1.0',
3802 - 'sslverify' => true,
3803 - ];
8258 + $bot_id = $this->get_current_bot_id('');
8259 + $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8260 +
8261 + $formatted_conversation = array();
3804 8262
3805 - // Make the API request
3806 - $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
8263 + $formatted_conversation[] = array(
8264 + 'role' => 'system',
8265 + 'content' => $system_prompt_instructions . " " . $relevant_content
8266 + );
3807 8267
3808 - // Process the response
3809 - if (is_wp_error($response)) {
3810 - return "Sorry, there was an error processing your request.";
3811 - }
8268 + foreach ($conversation_history as $message) {
8269 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
8270 + $role = $message['role'];
3812 8271
3813 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
8272 + if ($role === 'bot' || $role === 'agent') {
8273 + $role = 'assistant';
8274 + }
8275 + if (!in_array($role, ['system', 'assistant', 'user'])) {
8276 + $role = 'user';
8277 + }
3814 8278
3815 - if (isset($response_body['choices'][0]['message']['content'])) {
3816 - return trim($response_body['choices'][0]['message']['content']);
3817 - } else {
3818 - return "Sorry, I couldn't process that request.";
8279 + $formatted_conversation[] = array(
8280 + 'role' => $role,
8281 + 'content' => $message['content']
8282 + );
8283 + }
8284 + }
8285 +
8286 + $body = json_encode([
8287 + 'model' => $selected_model,
8288 + 'messages' => $formatted_conversation,
8289 + 'temperature' => 1,
8290 + ]);
8291 +
8292 + $args = [
8293 + 'body' => $body,
8294 + 'headers' => [
8295 + 'Content-Type' => 'application/json',
8296 + 'Authorization' => 'Bearer ' . $openrouter_api_key,
8297 + 'HTTP-Referer' => home_url(),
8298 + 'X-Title' => get_bloginfo('name'),
8299 + ],
8300 + 'timeout' => 60,
8301 + 'redirection' => 5,
8302 + 'blocking' => true,
8303 + 'httpversion' => '1.0',
8304 + 'sslverify' => true,
8305 + ];
8306 +
8307 + $response = wp_remote_post('https://openrouter.ai/api/v1/chat/completions', $args);
8308 +
8309 + if (is_wp_error($response)) {
8310 + $error_message = $response->get_error_message();
8311 + return [
8312 + 'error' => esc_html__('Connection error when contacting OpenRouter: ', 'mxchat') . esc_html($error_message),
8313 + 'error_code' => 'openrouter_connection_error',
8314 + 'provider' => 'openrouter'
8315 + ];
8316 + }
8317 +
8318 + $status_code = wp_remote_retrieve_response_code($response);
8319 + if ($status_code !== 200) {
8320 + $response_body = wp_remote_retrieve_body($response);
8321 + $decoded_response = json_decode($response_body, true);
8322 +
8323 + $error_message = isset($decoded_response['error']['message'])
8324 + ? $decoded_response['error']['message']
8325 + : 'HTTP Error ' . $status_code;
8326 +
8327 + return [
8328 + 'error' => esc_html__('OpenRouter API error: ', 'mxchat') . esc_html($error_message),
8329 + 'error_code' => 'openrouter_api_error',
8330 + 'provider' => 'openrouter',
8331 + 'status_code' => $status_code
8332 + ];
8333 + }
8334 +
8335 + $response_body = wp_remote_retrieve_body($response);
8336 + $decoded_response = json_decode($response_body, true);
8337 +
8338 + if (isset($decoded_response['choices'][0]['message']['content'])) {
8339 + return trim($decoded_response['choices'][0]['message']['content']);
8340 + } else {
8341 + return [
8342 + 'error' => esc_html__('Unexpected response format from OpenRouter.', 'mxchat'),
8343 + 'error_code' => 'openrouter_response_format_error',
8344 + 'provider' => 'openrouter'
8345 + ];
8346 + }
8347 + } catch (Exception $e) {
8348 + return [
8349 + 'error' => esc_html__('System error when processing OpenRouter request: ', 'mxchat') . esc_html($e->getMessage()),
8350 + 'error_code' => 'openrouter_exception',
8351 + 'provider' => 'openrouter'
8352 + ];
3819 8353 }
3820 8354 }
3821 -
3822 8355 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
3823 - // Get system prompt instructions from options
3824 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3825 -
8356 +
8357 + // Get bot ID from session or request
8358 + $bot_id = $this->get_current_bot_id($session_id);
8359 +
8360 + // Get system prompt instructions using centralized function
8361 + $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8362 +
3826 8363 // Clean and validate conversation history
3827 8364 foreach ($conversation_history as &$message) {
3828 8365 // Convert bot and agent roles to assistant
3829 8366 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
@@ -3918,8 +8455,821 @@
3918 8455 // Log unexpected response format
3919 8456 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
3920 8457 return "Sorry, I received an unexpected response format from the API.";
3921 8458 }
8459 +private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
8460 + try {
8461 + // Ensure conversation_history is an array
8462 + if (!is_array($conversation_history)) {
8463 + $conversation_history = array();
8464 + }
8465 +
8466 + // Get bot ID from session or request
8467 + $bot_id = $this->get_current_bot_id('');
8468 +
8469 + // Get system prompt instructions using centralized function
8470 + $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8471 +
8472 + // Create a new array for the formatted conversation
8473 + $formatted_conversation = array();
8474 +
8475 + // Add system message first
8476 + $formatted_conversation[] = array(
8477 + 'role' => 'system',
8478 + 'content' => $system_prompt_instructions . " " . $relevant_content
8479 + );
8480 +
8481 + // Add the rest of the conversation history
8482 + foreach ($conversation_history as $message) {
8483 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
8484 + $role = $message['role'];
8485 +
8486 + // Convert roles to supported format
8487 + if ($role === 'bot' || $role === 'agent') {
8488 + $role = 'assistant';
8489 + }
8490 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
8491 + $role = 'user';
8492 + }
8493 +
8494 + $formatted_conversation[] = array(
8495 + 'role' => $role,
8496 + 'content' => $message['content']
8497 + );
8498 + }
8499 + }
8500 +
8501 + // Check if this is a GPT-5 model (supports reasoning_effort parameter)
8502 + $is_gpt5_model = (
8503 + strpos($selected_model, 'gpt-5') === 0 ||
8504 + $selected_model === 'gpt-5.2' ||
8505 + $selected_model === 'gpt-5.1-2025-11-13' ||
8506 + $selected_model === 'gpt-5' ||
8507 + $selected_model === 'gpt-5-mini' ||
8508 + $selected_model === 'gpt-5-nano'
8509 + );
8510 +
8511 + // Build request body with optimal settings for fast responses
8512 + $request_body = [
8513 + 'model' => $selected_model,
8514 + 'messages' => $formatted_conversation,
8515 + 'temperature' => 1,
8516 + 'stream' => false
8517 + ];
8518 +
8519 + // Add reasoning_effort only for GPT-5 models that support it
8520 + // These chat models don't support reasoning_effort parameter
8521 + $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');
8522 + if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
8523 + // GPT-5.1 uses 'low' instead of 'minimal'
8524 + if ($selected_model === 'gpt-5.1-2025-11-13') {
8525 + $request_body['reasoning_effort'] = 'low';
8526 + } elseif ($selected_model === 'gpt-5.4') {
8527 + $request_body['reasoning_effort'] = 'none';
8528 + } else {
8529 + $request_body['reasoning_effort'] = 'minimal';
8530 + }
8531 + }
8532 +
8533 + $body = json_encode($request_body);
8534 +
8535 + $args = [
8536 + 'body' => $body,
8537 + 'headers' => [
8538 + 'Content-Type' => 'application/json',
8539 + 'Authorization' => 'Bearer ' . $api_key,
8540 + ],
8541 + 'timeout' => 60,
8542 + 'redirection' => 5,
8543 + 'blocking' => true,
8544 + 'httpversion' => '1.0',
8545 + 'sslverify' => true,
8546 + ];
8547 +
8548 + $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
8549 +
8550 + if (is_wp_error($response)) {
8551 + $error_message = $response->get_error_message();
8552 + return [
8553 + 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
8554 + 'error_code' => 'openai_connection_error',
8555 + 'provider' => 'openai'
8556 + ];
8557 + }
8558 +
8559 + $status_code = wp_remote_retrieve_response_code($response);
8560 + if ($status_code !== 200) {
8561 + $response_body = wp_remote_retrieve_body($response);
8562 + $decoded_response = json_decode($response_body, true);
8563 +
8564 + $error_message = isset($decoded_response['error']['message'])
8565 + ? $decoded_response['error']['message']
8566 + : 'HTTP Error ' . $status_code;
8567 +
8568 + $error_type = isset($decoded_response['error']['type'])
8569 + ? $decoded_response['error']['type']
8570 + : 'unknown';
8571 +
8572 + // Handle specific error types
8573 + switch ($error_type) {
8574 + case 'invalid_request_error':
8575 + if (strpos($error_message, 'API key') !== false) {
8576 + return [
8577 + 'error' => esc_html__('Invalid OpenAI API key. Please check your API key configuration.', 'mxchat'),
8578 + 'error_code' => 'openai_invalid_api_key',
8579 + 'provider' => 'openai'
8580 + ];
8581 + }
8582 + break;
8583 +
8584 + case 'authentication_error':
8585 + return [
8586 + 'error' => esc_html__('Authentication failed with OpenAI. Please check your API key.', 'mxchat'),
8587 + 'error_code' => 'openai_auth_error',
8588 + 'provider' => 'openai'
8589 + ];
8590 +
8591 + case 'rate_limit_exceeded':
8592 + return [
8593 + 'error' => esc_html__('OpenAI rate limit exceeded. Please try again later.', 'mxchat'),
8594 + 'error_code' => 'openai_rate_limit',
8595 + 'provider' => 'openai'
8596 + ];
8597 +
8598 + case 'quota_exceeded':
8599 + return [
8600 + 'error' => esc_html__('OpenAI API quota exceeded. Please check your billing details.', 'mxchat'),
8601 + 'error_code' => 'openai_quota_exceeded',
8602 + 'provider' => 'openai'
8603 + ];
8604 + }
8605 +
8606 + // Generic error fallback
8607 + return [
8608 + 'error' => esc_html__('OpenAI API error: ', 'mxchat') . esc_html($error_message),
8609 + 'error_code' => 'openai_api_error',
8610 + 'provider' => 'openai',
8611 + 'status_code' => $status_code
8612 + ];
8613 + }
8614 +
8615 + $response_body = wp_remote_retrieve_body($response);
8616 + $decoded_response = json_decode($response_body, true);
8617 +
8618 + if (isset($decoded_response['choices'][0]['message']['content'])) {
8619 + return trim($decoded_response['choices'][0]['message']['content']);
8620 + } else {
8621 + return [
8622 + 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
8623 + 'error_code' => 'openai_response_format_error',
8624 + 'provider' => 'openai'
8625 + ];
8626 + }
8627 + } catch (Exception $e) {
8628 + return [
8629 + 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
8630 + 'error_code' => 'openai_exception',
8631 + 'provider' => 'openai'
8632 + ];
8633 + }
8634 +}
8635 +
8636 +private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
8637 + try {
8638 + // Get bot ID from session or request
8639 + $bot_id = $this->get_current_bot_id($session_id);
8640 +
8641 + // Get system prompt instructions using centralized function
8642 + $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8643 +
8644 + // Add system prompt to relevant content
8645 + $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
8646 +
8647 + // Prepend system instructions to the conversation history
8648 + array_unshift($conversation_history, [
8649 + 'role' => 'system',
8650 + 'content' => "Here are your instructions: " . $content_with_instructions
8651 + ]);
8652 +
8653 + // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
8654 + foreach ($conversation_history as &$message) {
8655 + if ($message['role'] === 'bot') {
8656 + $message['role'] = 'assistant';
8657 + } elseif ($message['role'] === 'agent') {
8658 + // Tag the message as coming from a live agent
8659 + $message['role'] = 'assistant';
8660 + if (!isset($message['metadata'])) {
8661 + $message['metadata'] = ['source' => 'live_agent'];
8662 + }
8663 + }
8664 +
8665 + // Ensure all roles are valid
8666 + if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
8667 + $message['role'] = 'user'; // Default to 'user'
8668 + }
8669 + }
8670 +
8671 + // Build the request body
8672 + $body = json_encode([
8673 + 'model' => $selected_model,
8674 + 'messages' => $conversation_history,
8675 + 'temperature' => 0.8,
8676 + 'stream' => false
8677 + ]);
8678 +
8679 + // Set up the API request
8680 + $args = [
8681 + 'body' => $body,
8682 + 'headers' => [
8683 + 'Content-Type' => 'application/json',
8684 + 'Authorization' => 'Bearer ' . $xai_api_key,
8685 + ],
8686 + 'timeout' => 60,
8687 + 'redirection' => 5,
8688 + 'blocking' => true,
8689 + 'httpversion' => '1.0',
8690 + 'sslverify' => true,
8691 + ];
8692 +
8693 + // Make the API request
8694 + $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
8695 +
8696 + // Process the response
8697 + if (is_wp_error($response)) {
8698 + $error_message = $response->get_error_message();
8699 + //error_log('X.AI API Error: ' . $error_message);
8700 + return [
8701 + 'error' => esc_html__('Connection error when contacting X.AI: ', 'mxchat') . esc_html($error_message),
8702 + 'error_code' => 'xai_connection_error',
8703 + 'provider' => 'xai'
8704 + ];
8705 + }
8706 +
8707 + $status_code = wp_remote_retrieve_response_code($response);
8708 + if ($status_code !== 200) {
8709 + $response_body = wp_remote_retrieve_body($response);
8710 + $decoded_response = json_decode($response_body, true);
8711 +
8712 + // Log the full response for debugging
8713 + //error_log('X.AI Error Response: ' . print_r($decoded_response, true));
8714 +
8715 + // Extract error message from X.AI's specific format
8716 + $error_message = '';
8717 +
8718 + // Check for direct error string (as seen in your logs)
8719 + if (isset($decoded_response['error']) && is_string($decoded_response['error'])) {
8720 + $error_message = $decoded_response['error'];
8721 + }
8722 + // Check for nested error object (OpenAI style)
8723 + elseif (isset($decoded_response['error']['message'])) {
8724 + $error_message = $decoded_response['error']['message'];
8725 + }
8726 + // Check for top-level message
8727 + elseif (isset($decoded_response['message'])) {
8728 + $error_message = $decoded_response['message'];
8729 + }
8730 + // Fallback
8731 + else {
8732 + $error_message = 'HTTP Error ' . $status_code;
8733 + }
8734 +
8735 + //error_log('X.AI API HTTP Error: ' . $status_code . ' - ' . $error_message);
8736 +
8737 + // Check for API key errors using string matching
8738 + if (stripos($error_message, 'api key') !== false ||
8739 + stripos($error_message, 'incorrect api key') !== false ||
8740 + stripos($error_message, 'invalid api key') !== false) {
8741 + return [
8742 + 'error' => esc_html__('Invalid X.AI API key. Please check your API key configuration.', 'mxchat'),
8743 + 'error_code' => 'xai_invalid_api_key',
8744 + 'provider' => 'xai'
8745 + ];
8746 + }
8747 +
8748 + // Authentication errors
8749 + if ($status_code === 401 || $status_code === 403 ||
8750 + stripos($error_message, 'auth') !== false) {
8751 + return [
8752 + 'error' => esc_html__('Authentication failed with X.AI. Please check your API key.', 'mxchat'),
8753 + 'error_code' => 'xai_auth_error',
8754 + 'provider' => 'xai'
8755 + ];
8756 + }
8757 +
8758 + // Model errors
8759 + if (stripos($error_message, 'model') !== false) {
8760 + return [
8761 + 'error' => esc_html__('Invalid model specified for X.AI. Please check your model configuration.', 'mxchat'),
8762 + 'error_code' => 'xai_invalid_model',
8763 + 'provider' => 'xai'
8764 + ];
8765 + }
8766 +
8767 + // Rate limit errors
8768 + if ($status_code === 429 ||
8769 + stripos($error_message, 'rate') !== false ||
8770 + stripos($error_message, 'limit') !== false) {
8771 + return [
8772 + 'error' => esc_html__('X.AI rate limit exceeded. Please try again later.', 'mxchat'),
8773 + 'error_code' => 'xai_rate_limit',
8774 + 'provider' => 'xai'
8775 + ];
8776 + }
8777 +
8778 + // Quota errors
8779 + if (stripos($error_message, 'quota') !== false ||
8780 + stripos($error_message, 'billing') !== false) {
8781 + return [
8782 + 'error' => esc_html__('X.AI API quota exceeded. Please check your billing details.', 'mxchat'),
8783 + 'error_code' => 'xai_quota_exceeded',
8784 + 'provider' => 'xai'
8785 + ];
8786 + }
8787 +
8788 + // Server errors
8789 + if ($status_code >= 500) {
8790 + return [
8791 + 'error' => esc_html__('X.AI service is currently unavailable. Please try again later.', 'mxchat'),
8792 + 'error_code' => 'xai_service_unavailable',
8793 + 'provider' => 'xai'
8794 + ];
8795 + }
8796 +
8797 + // Generic error fallback with the actual error message
8798 + return [
8799 + 'error' => esc_html__('X.AI API error: ', 'mxchat') . esc_html($error_message),
8800 + 'error_code' => 'xai_api_error',
8801 + 'provider' => 'xai',
8802 + 'status_code' => $status_code
8803 + ];
8804 + }
8805 +
8806 + $response_body = wp_remote_retrieve_body($response);
8807 + $decoded_response = json_decode($response_body, true);
8808 +
8809 + if (isset($decoded_response['choices'][0]['message']['content'])) {
8810 + return trim($decoded_response['choices'][0]['message']['content']);
8811 + } else {
8812 + //error_log('X.AI API Response Format Error: ' . print_r($decoded_response, true));
8813 + return [
8814 + 'error' => esc_html__('Unexpected response format from X.AI.', 'mxchat'),
8815 + 'error_code' => 'xai_response_format_error',
8816 + 'provider' => 'xai'
8817 + ];
8818 + }
8819 +} catch (Exception $e) {
8820 + //error_log('X.AI Exception: ' . $e->getMessage());
8821 + return [
8822 + 'error' => esc_html__('System error when processing X.AI request: ', 'mxchat') . esc_html($e->getMessage()),
8823 + 'error_code' => 'xai_exception',
8824 + 'provider' => 'xai'
8825 + ];
8826 +}
8827 +
8828 +
8829 +}
8830 +private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
8831 + try {
8832 + // Ensure conversation_history is an array
8833 + if (!is_array($conversation_history)) {
8834 + $conversation_history = array();
8835 + }
8836 +
8837 + // Get bot ID from session or request
8838 + $bot_id = $this->get_current_bot_id($session_id);
8839 +
8840 + // Get system prompt instructions using centralized function
8841 + $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8842 +
8843 + // Create a new array for the formatted conversation
8844 + $formatted_conversation = array();
8845 +
8846 + // Add system message first
8847 + $formatted_conversation[] = array(
8848 + 'role' => 'system',
8849 + 'content' => $system_prompt_instructions . " " . $relevant_content
8850 + );
8851 +
8852 + // Add the rest of the conversation history
8853 + foreach ($conversation_history as $message) {
8854 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
8855 + $role = $message['role'];
8856 +
8857 + // Convert roles to supported format
8858 + if ($role === 'bot' || $role === 'agent') {
8859 + $role = 'assistant';
8860 + }
8861 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
8862 + $role = 'user';
8863 + }
8864 +
8865 + $formatted_conversation[] = array(
8866 + 'role' => $role,
8867 + 'content' => $message['content']
8868 + );
8869 + }
8870 + }
8871 +
8872 + $body = json_encode([
8873 + 'model' => $selected_model,
8874 + 'messages' => $formatted_conversation,
8875 + 'temperature' => 0.8,
8876 + 'stream' => false
8877 + ]);
8878 +
8879 + $args = [
8880 + 'body' => $body,
8881 + 'headers' => [
8882 + 'Content-Type' => 'application/json',
8883 + 'Authorization' => 'Bearer ' . $deepseek_api_key,
8884 + ],
8885 + 'timeout' => 60,
8886 + 'redirection' => 5,
8887 + 'blocking' => true,
8888 + 'httpversion' => '1.0',
8889 + 'sslverify' => true,
8890 + ];
8891 +
8892 + $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
8893 +
8894 + if (is_wp_error($response)) {
8895 + $error_message = $response->get_error_message();
8896 + //error_log('DeepSeek API Error: ' . $error_message);
8897 + return [
8898 + 'error' => esc_html__('Connection error when contacting DeepSeek: ', 'mxchat') . esc_html($error_message),
8899 + 'error_code' => 'deepseek_connection_error',
8900 + 'provider' => 'deepseek'
8901 + ];
8902 + }
8903 +
8904 + $status_code = wp_remote_retrieve_response_code($response);
8905 + if ($status_code !== 200) {
8906 + $response_body = wp_remote_retrieve_body($response);
8907 + $decoded_response = json_decode($response_body, true);
8908 +
8909 + $error_message = isset($decoded_response['error']['message'])
8910 + ? $decoded_response['error']['message']
8911 + : 'HTTP Error ' . $status_code;
8912 +
8913 + $error_type = isset($decoded_response['error']['type'])
8914 + ? $decoded_response['error']['type']
8915 + : 'unknown';
8916 +
8917 + //error_log('DeepSeek API HTTP Error: ' . $status_code . ' - ' . $error_message);
8918 +
8919 + // Handle specific error types
8920 + switch ($status_code) {
8921 + case 401:
8922 + return [
8923 + 'error' => esc_html__('Authentication failed with DeepSeek. Please check your API key.', 'mxchat'),
8924 + 'error_code' => 'deepseek_auth_error',
8925 + 'provider' => 'deepseek'
8926 + ];
8927 +
8928 + case 400:
8929 + if (strpos($error_message, 'API key') !== false) {
8930 + return [
8931 + 'error' => esc_html__('Invalid DeepSeek API key. Please check your API key configuration.', 'mxchat'),
8932 + 'error_code' => 'deepseek_invalid_api_key',
8933 + 'provider' => 'deepseek'
8934 + ];
8935 + }
8936 + break;
8937 +
8938 + case 429:
8939 + if (strpos($error_message, 'quota') !== false) {
8940 + return [
8941 + 'error' => esc_html__('DeepSeek API quota exceeded. Please check your billing details.', 'mxchat'),
8942 + 'error_code' => 'deepseek_quota_exceeded',
8943 + 'provider' => 'deepseek'
8944 + ];
8945 + } else {
8946 + return [
8947 + 'error' => esc_html__('DeepSeek rate limit exceeded. Please try again later.', 'mxchat'),
8948 + 'error_code' => 'deepseek_rate_limit',
8949 + 'provider' => 'deepseek'
8950 + ];
8951 + }
8952 +
8953 + case 500:
8954 + case 502:
8955 + case 503:
8956 + case 504:
8957 + return [
8958 + 'error' => esc_html__('DeepSeek service is currently unavailable. Please try again later.', 'mxchat'),
8959 + 'error_code' => 'deepseek_service_unavailable',
8960 + 'provider' => 'deepseek'
8961 + ];
8962 + }
8963 +
8964 + // Generic error fallback
8965 + return [
8966 + 'error' => esc_html__('DeepSeek API error: ', 'mxchat') . esc_html($error_message),
8967 + 'error_code' => 'deepseek_api_error',
8968 + 'provider' => 'deepseek',
8969 + 'status_code' => $status_code
8970 + ];
8971 + }
8972 +
8973 + $response_body = wp_remote_retrieve_body($response);
8974 + $decoded_response = json_decode($response_body, true);
8975 +
8976 + if (isset($decoded_response['choices'][0]['message']['content'])) {
8977 + return trim($decoded_response['choices'][0]['message']['content']);
8978 + } else {
8979 + //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
8980 + return [
8981 + 'error' => esc_html__('Unexpected response format from DeepSeek.', 'mxchat'),
8982 + 'error_code' => 'deepseek_response_format_error',
8983 + 'provider' => 'deepseek'
8984 + ];
8985 + }
8986 + } catch (Exception $e) {
8987 + //error_log('DeepSeek Exception: ' . $e->getMessage());
8988 + return [
8989 + 'error' => esc_html__('System error when processing DeepSeek request: ', 'mxchat') . esc_html($e->getMessage()),
8990 + 'error_code' => 'deepseek_exception',
8991 + 'provider' => 'deepseek'
8992 + ];
8993 + }
8994 +}
8995 +private function mxchat_generate_response_gemini($selected_model, $gemini_api_key, $conversation_history, $relevant_content) {
8996 + // Get bot ID from session or request
8997 + $bot_id = $this->get_current_bot_id($session_id);
8998 +
8999 + // Get system prompt instructions using centralized function
9000 + $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
9001 +
9002 + // Add system prompt to relevant content
9003 + $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
9004 +
9005 + // Format messages for Gemini API
9006 + $formatted_messages = [];
9007 +
9008 + // Add system message as the first user message with role prefix
9009 + // Note: Gemini doesn't have a dedicated system role, so we use a prefixed user message
9010 + $formatted_messages[] = [
9011 + 'role' => 'user',
9012 + 'parts' => [
9013 + ['text' => "[System Instructions] " . $content_with_instructions]
9014 + ]
9015 + ];
9016 +
9017 + // Add model response to acknowledge system instructions
9018 + $formatted_messages[] = [
9019 + 'role' => 'model',
9020 + 'parts' => [
9021 + ['text' => "I understand and will follow these instructions."]
9022 + ]
9023 + ];
9024 +
9025 + // Process the rest of the conversation history
9026 + $current_role = null;
9027 + $current_parts = [];
9028 +
9029 + foreach ($conversation_history as $message) {
9030 + // Skip the first system message as we already handled it
9031 + if ($message['role'] === 'system') {
9032 + continue;
9033 + }
9034 +
9035 + // Map roles to Gemini format
9036 + $gemini_role = '';
9037 + if ($message['role'] === 'user') {
9038 + $gemini_role = 'user';
9039 + } else if (in_array($message['role'], ['assistant', 'bot', 'agent'])) {
9040 + $gemini_role = 'model';
9041 + } else {
9042 + // Skip unsupported roles
9043 + continue;
9044 + }
9045 +
9046 + // If we have a new role, add the previous message
9047 + if ($current_role !== null && $current_role !== $gemini_role && !empty($current_parts)) {
9048 + $formatted_messages[] = [
9049 + 'role' => $current_role,
9050 + 'parts' => $current_parts
9051 + ];
9052 + $current_parts = [];
9053 + }
9054 +
9055 + // Set current role and add text to parts
9056 + $current_role = $gemini_role;
9057 + $current_parts[] = ['text' => $message['content']];
9058 + }
9059 +
9060 + // Add the last message if there's content
9061 + if ($current_role !== null && !empty($current_parts)) {
9062 + $formatted_messages[] = [
9063 + 'role' => $current_role,
9064 + 'parts' => $current_parts
9065 + ];
9066 + }
9067 +
9068 + // Build the request body
9069 + $body = json_encode([
9070 + 'contents' => $formatted_messages,
9071 + 'generationConfig' => [
9072 + 'temperature' => 0.7,
9073 + 'topP' => 0.95,
9074 + 'topK' => 40,
9075 + 'maxOutputTokens' => 8192,
9076 + ],
9077 + 'safetySettings' => [
9078 + [
9079 + 'category' => 'HARM_CATEGORY_HARASSMENT',
9080 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
9081 + ],
9082 + [
9083 + 'category' => 'HARM_CATEGORY_HATE_SPEECH',
9084 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
9085 + ],
9086 + [
9087 + 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
9088 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
9089 + ],
9090 + [
9091 + 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT',
9092 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
9093 + ]
9094 + ]
9095 + ]);
9096 +
9097 + // Prepare the API endpoint
9098 + // Use v1beta for preview models (Gemini 3, experimental), v1 for stable models
9099 + $api_version = (strpos($selected_model, 'preview') !== false || strpos($selected_model, 'exp') !== false) ? 'v1beta' : 'v1';
9100 + $api_endpoint = 'https://generativelanguage.googleapis.com/' . $api_version . '/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
9101 +
9102 + // Set up the API request
9103 + $args = [
9104 + 'body' => $body,
9105 + 'headers' => [
9106 + 'Content-Type' => 'application/json',
9107 + ],
9108 + 'timeout' => 60,
9109 + 'redirection' => 5,
9110 + 'blocking' => true,
9111 + 'httpversion' => '1.0',
9112 + 'sslverify' => true,
9113 + ];
9114 +
9115 + // Make the API request
9116 + $response = wp_remote_post($api_endpoint, $args);
9117 +
9118 + // Process the response
9119 + if (is_wp_error($response)) {
9120 + return "Sorry, there was an error processing your request: " . $response->get_error_message();
9121 + }
9122 +
9123 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
9124 +
9125 + // Handle potential errors in the response
9126 + if (isset($response_body['error'])) {
9127 + //error_log('Gemini API Error: ' . json_encode($response_body['error']));
9128 + return "Sorry, there was an error with the Gemini API: " .
9129 + (isset($response_body['error']['message']) ? $response_body['error']['message'] : 'Unknown error');
9130 + }
9131 +
9132 + // Extract the response text
9133 + if (isset($response_body['candidates'][0]['content']['parts'][0]['text'])) {
9134 + return trim($response_body['candidates'][0]['content']['parts'][0]['text']);
9135 + } else {
9136 + //error_log('Unexpected Gemini API response format: ' . json_encode($response_body));
9137 + return "Sorry, I couldn't process that request. The response format was unexpected.";
9138 + }
9139 +}
9140 +
9141 +
9142 +public function test_streaming_request() {
9143 + $options = get_option('mxchat_options', []);
9144 + $model = $options['model'] ?? 'gpt-5.1-chat-latest';
9145 +
9146 + // Detect provider from model prefix
9147 + $provider = strtolower(explode('-', $model)[0]);
9148 +
9149 + $sample_prompt = 'Hello! Can you stream this response back to me?';
9150 + $messages = [['role' => 'user', 'content' => $sample_prompt]];
9151 + $headers = [];
9152 + $body = [];
9153 + $url = '';
9154 + $api_key = '';
9155 +
9156 + switch ($provider) {
9157 + case 'gpt':
9158 + case 'o1':
9159 + $api_key = $options['api_key'] ?? '';
9160 + if (empty($api_key)) return '❌ Missing API key for OpenAI';
9161 + $url = 'https://api.openai.com/v1/chat/completions';
9162 + $headers = [
9163 + 'Content-Type: application/json',
9164 + 'Authorization: Bearer ' . $api_key
9165 + ];
9166 + $body = [
9167 + 'model' => $model,
9168 + 'messages' => $messages,
9169 + 'stream' => true
9170 + ];
9171 + break;
9172 +
9173 + case 'claude':
9174 + $api_key = $options['claude_api_key'] ?? '';
9175 + if (empty($api_key)) return '❌ Missing API key for Claude';
9176 + $url = 'https://api.anthropic.com/v1/messages';
9177 + $headers = [
9178 + 'Content-Type: application/json',
9179 + 'x-api-key: ' . $api_key,
9180 + 'anthropic-version: 2023-06-01'
9181 + ];
9182 + $body = [
9183 + 'model' => $model,
9184 + 'messages' => $messages,
9185 + 'max_tokens' => 100,
9186 + 'stream' => true
9187 + ];
9188 + break;
9189 +
9190 + case 'grok':
9191 + $api_key = $options['xai_api_key'] ?? '';
9192 + if (empty($api_key)) return '❌ Missing API key for X.AI';
9193 + $url = 'https://api.x.ai/v1/chat/completions';
9194 + $headers = [
9195 + 'Content-Type: application/json',
9196 + 'Authorization: Bearer ' . $api_key
9197 + ];
9198 + $body = [
9199 + 'model' => $model,
9200 + 'messages' => $messages,
9201 + 'stream' => true
9202 + ];
9203 + break;
9204 +
9205 + case 'deepseek':
9206 + if (empty($deepseek_api_key)) {
9207 + $error_response = [
9208 + 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
9209 + 'error_code' => 'missing_deepseek_api_key'
9210 + ];
9211 + if ($testing_data !== null) {
9212 + $error_response['testing_data'] = $testing_data;
9213 + }
9214 + return $error_response;
9215 + }
9216 + if ($streaming) {
9217 + return $this->mxchat_generate_response_deepseek_stream(
9218 + $selected_model,
9219 + $deepseek_api_key,
9220 + $conversation_history,
9221 + $relevant_content,
9222 + $session_id,
9223 + $testing_data // Pass testing data
9224 + );
9225 + } else {
9226 + $response = $this->mxchat_generate_response_deepseek(
9227 + $selected_model,
9228 + $deepseek_api_key,
9229 + $conversation_history,
9230 + $relevant_content
9231 + );
9232 + }
9233 + break;
9234 +
9235 + case 'gemini':
9236 + $api_key = $options['gemini_api_key'] ?? '';
9237 + if (empty($api_key)) return '❌ Missing API key for Gemini';
9238 + $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':streamGenerateContent?key=' . $api_key;
9239 + $headers = ['Content-Type: application/json'];
9240 + $body = [
9241 + 'contents' => [['role' => 'user', 'parts' => [['text' => $sample_prompt]]]],
9242 + 'generationConfig' => ['temperature' => 0.7]
9243 + ];
9244 + break;
9245 +
9246 + default:
9247 + return '❌ Unsupported provider: ' . $provider;
9248 + }
9249 +
9250 + // Do the actual streaming test
9251 + $ch = curl_init($url);
9252 + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
9253 + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
9254 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
9255 + curl_setopt($ch, CURLOPT_TIMEOUT, 15);
9256 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
9257 +
9258 + $response = curl_exec($ch);
9259 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
9260 + $error = curl_error($ch);
9261 + curl_close($ch);
9262 +
9263 + if ($error) return "❌ cURL error: $error";
9264 + if ($http_code !== 200) {
9265 + $error_message = json_decode($response, true)['error']['message'] ?? 'Unknown';
9266 + return "❌ HTTP $http_code: $error_message";
9267 + }
9268 +
9269 + return true;
9270 +}
9271 +
3922 9272 public function mxchat_dismiss_pre_chat_message() {
3923 9273 // Get and sanitize the user identifier
3924 9274 $user_id = $this->mxchat_get_user_identifier();
3925 9275 $user_id = sanitize_key($user_id);
@@ -3973,38 +9323,74 @@
3973 9323
3974 9324 return $dotProduct / ($normA * $normB);
3975 9325 }
3976 9326
9327 +
3977 9328 public function mxchat_enqueue_scripts_styles() {
3978 - // Define version numbers for the styles and scripts
3979 - $chat_style_version = '2.0.3'; // Replace with your actual version
3980 - $chat_script_version = '2.0.3'; // Replace with your actual version
9329 + // Fetch options from the database first to check loading strategy
9330 + $this->options = get_option('mxchat_options');
9331 + $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
3981 9332
3982 - // Enqueue the script
3983 - wp_enqueue_script(
3984 - 'mxchat-chat-js',
3985 - plugin_dir_url(__FILE__) . '../js/chat-script.js',
3986 - array('jquery'),
3987 - $chat_script_version,
3988 - true
3989 - );
3990 -
3991 - // Enqueue the CSS
9333 + // Always enqueue CSS immediately
3992 9334 wp_enqueue_style(
3993 9335 'mxchat-chat-css',
3994 9336 plugin_dir_url(__FILE__) . '../css/chat-style.css',
3995 9337 array(),
3996 - $chat_style_version
9338 + MXCHAT_VERSION
3997 9339 );
3998 9340
3999 - // Fetch options from the database
4000 - $this->options = get_option('mxchat_options');
9341 + // Protect MxChat CSS from LiteSpeed UCSS/CCSS stripping via data-no-optimize attribute
9342 + add_filter('style_loader_tag', function($tag, $handle) {
9343 + if ($handle === 'mxchat-chat-css' || strpos($handle, 'mxchat') !== false) {
9344 + $tag = str_replace("rel='stylesheet'", "rel='stylesheet' data-no-optimize='1'", $tag);
9345 + $tag = str_replace('rel="stylesheet"', 'rel="stylesheet" data-no-optimize="1"', $tag);
9346 + }
9347 + return $tag;
9348 + }, 10, 2);
9349 +
9350 + // Handle script loading based on strategy
9351 + if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
9352 + // Enqueue the script normally
9353 + wp_enqueue_script(
9354 + 'mxchat-chat-js',
9355 + plugin_dir_url(__FILE__) . '../js/chat-script.js',
9356 + array('jquery'),
9357 + MXCHAT_VERSION,
9358 + true
9359 + );
9360 +
9361 + // Add defer attribute if strategy is 'defer'
9362 + if ($loading_strategy === 'defer') {
9363 + wp_script_add_data('mxchat-chat-js', 'strategy', 'defer');
9364 + }
9365 + } else {
9366 + // For delay or interaction-based loading, we'll use a custom loader
9367 + // Don't enqueue the main script - we'll load it dynamically
9368 + add_action('wp_footer', array($this, 'mxchat_output_delayed_script_loader'), 99);
9369 + }
9370 +
9371 + // Protect MxChat JS from LiteSpeed optimization stripping via data-no-optimize attribute
9372 + add_filter('script_loader_tag', function($tag, $handle) {
9373 + if ($handle === 'mxchat-chat-js' || strpos($handle, 'mxchat') !== false) {
9374 + $tag = str_replace('<script ', '<script data-no-optimize="1" ', $tag);
9375 + }
9376 + return $tag;
9377 + }, 10, 2);
4001 9378 $prompts_options = get_option('mxchat_prompts_options', array());
4002 9379
9380 + // Check if AI theme is active - if so, skip inline colors in JavaScript
9381 + $theme_options = get_option('mxchat_theme_options', array());
9382 + $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
9383 + $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
9384 + $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
9385 +
4003 9386 // Prepare settings for JavaScript
4004 9387 $style_settings = array(
4005 9388 'ajax_url' => admin_url('admin-ajax.php'),
4006 9389 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
9390 + 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest',
9391 + 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
9392 + 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
4007 9393 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
4008 9394 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
4009 9395 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
4010 9396 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
@@ -4018,10 +9404,71 @@
4018 9404 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
4019 9405 'icon_color' => $this->options['icon_color'] ?? '#fff',
4020 9406 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
4021 9407 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
4022 - 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off', // Example for consistency
9408 + 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
9409 + 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
9410 + 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
9411 + 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
9412 + 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
9413 + 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
9414 + 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
9415 + 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
9416 + 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off', // FIXED
9417 + 'initial_email_state' => null, // Also fixed this undefined variable
9418 + 'skip_email_check' => true,
9419 + 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
9420 + 'skip_inline_colors' => $skip_inline_colors,
9421 + 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array()
9422 + );
4023 9423
9424 + // For normal/defer loading, use wp_localize_script
9425 + // For delayed loading, we store settings in a transient to be output inline
9426 + if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
9427 + wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
9428 + } else {
9429 + // Store settings for the delayed loader to use
9430 + set_transient('mxchat_delayed_settings_' . get_current_user_id(), $style_settings, 60);
9431 + }
9432 +}
9433 +
9434 +/**
9435 + * Output the delayed script loader for performance optimization
9436 + */
9437 +public function mxchat_output_delayed_script_loader() {
9438 + $this->options = get_option('mxchat_options');
9439 + $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
9440 + $script_url = plugin_dir_url(__FILE__) . '../js/chat-script.js?ver=' . MXCHAT_VERSION;
9441 +
9442 + // Get the stored settings
9443 + $prompts_options = get_option('mxchat_prompts_options', array());
9444 + $theme_options = get_option('mxchat_theme_options', array());
9445 + $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
9446 + $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
9447 + $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
9448 +
9449 + $style_settings = array(
9450 + 'ajax_url' => admin_url('admin-ajax.php'),
9451 + 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
9452 + 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest',
9453 + 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
9454 + 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
9455 + 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
9456 + 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
9457 + 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
9458 + 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
9459 + 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
9460 + 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
9461 + 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
9462 + 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
9463 + 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
9464 + 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
9465 + 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
9466 + 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
9467 + 'icon_color' => $this->options['icon_color'] ?? '#fff',
9468 + 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
9469 + 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
9470 + 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
4024 9471 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
4025 9472 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
4026 9473 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
4027 9474 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
@@ -4026,76 +9473,1202 @@
4026 9473 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
4027 9474 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
4028 9475 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
4029 9476 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
4030 -
4031 9477 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
4032 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
9478 + 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off',
9479 + 'initial_email_state' => null,
9480 + 'skip_email_check' => true,
9481 + 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
9482 + 'skip_inline_colors' => $skip_inline_colors,
9483 + 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array()
4033 9484 );
4034 9485
4035 - // Pass the settings to the script
4036 - wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
9486 + // Determine delay time based on strategy
9487 + $delay_ms = 0;
9488 + switch ($loading_strategy) {
9489 + case 'delay_1s':
9490 + $delay_ms = 1000;
9491 + break;
9492 + case 'delay_3s':
9493 + $delay_ms = 3000;
9494 + break;
9495 + case 'delay_5s':
9496 + $delay_ms = 5000;
9497 + break;
9498 + }
9499 +
9500 + ?>
9501 + <script type="text/javascript">
9502 + (function() {
9503 + var mxchatLoaded = false;
9504 + var mxchatChat = <?php echo wp_json_encode($style_settings); ?>;
9505 + window.mxchatChat = mxchatChat;
9506 +
9507 + function loadMxChatScript() {
9508 + if (mxchatLoaded) return;
9509 + mxchatLoaded = true;
9510 +
9511 + function appendChatScript() {
9512 + var script = document.createElement('script');
9513 + script.src = <?php echo wp_json_encode($script_url); ?>;
9514 + script.type = 'text/javascript';
9515 + document.body.appendChild(script);
9516 + }
9517 +
9518 + if (typeof jQuery !== 'undefined') {
9519 + appendChatScript();
9520 + } else {
9521 + var jq = document.createElement('script');
9522 + jq.src = <?php echo wp_json_encode(includes_url('js/jquery/jquery.min.js')); ?>;
9523 + jq.onload = appendChatScript;
9524 + document.body.appendChild(jq);
9525 + }
9526 + }
9527 +
9528 + <?php if ($loading_strategy === 'on_interaction'): ?>
9529 + // Load on user interaction
9530 + var events = ['scroll', 'mousemove', 'touchstart', 'keydown', 'click'];
9531 + events.forEach(function(evt) {
9532 + window.addEventListener(evt, loadMxChatScript, {once: true, passive: true});
9533 + });
9534 + // Fallback: load after 8 seconds if no interaction
9535 + setTimeout(loadMxChatScript, 8000);
9536 + <?php else: ?>
9537 + // Load after specified delay
9538 + setTimeout(loadMxChatScript, <?php echo intval($delay_ms); ?>);
9539 + <?php endif; ?>
9540 + })();
9541 + </script>
9542 + <?php
4037 9543 }
4038 9544
9545 +/**
9546 + * Setup the cron jobs for rate limits with guard against multiple calls
9547 + */
9548 +public function setup_rate_limit_cron_jobs() {
9549 + // Add a guard to prevent multiple rapid calls
9550 + $last_setup = get_transient('mxchat_cron_setup_guard');
9551 + if ($last_setup && (time() - $last_setup) < 60) {
9552 + // Don't run again if we ran less than 60 seconds ago
9553 + return;
9554 + }
9555 +
9556 + // Set the guard
9557 + set_transient('mxchat_cron_setup_guard', time(), 300); // 5 minutes
9558 +
9559 + try {
9560 + // First, check if WordPress cron is disabled
9561 + if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
9562 + //error_log('MxChat: WordPress cron is disabled (DISABLE_WP_CRON = true), using fallback system');
9563 + $this->setup_fallback_rate_limit_system();
9564 + return;
9565 + }
9566 +
9567 + // Check if cron is already scheduled - if so, don't mess with it
9568 + if (wp_next_scheduled('mxchat_reset_rate_limits')) {
9569 + //error_log('MxChat: Rate limit cron already scheduled, skipping setup');
9570 + return;
9571 + }
9572 +
9573 + // Clear any orphaned hooks (but don't loop indefinitely)
9574 + $hooks_to_clear = [
9575 + 'mxchat_reset_rate_limits',
9576 + 'mxchat_reset_hourly_rate_limits',
9577 + 'mxchat_reset_daily_rate_limits',
9578 + 'mxchat_reset_weekly_rate_limits',
9579 + 'mxchat_reset_monthly_rate_limits'
9580 + ];
9581 +
9582 + foreach ($hooks_to_clear as $hook) {
9583 + // Only clear a maximum of 3 instances to prevent infinite loops
9584 + $cleared = 0;
9585 + while (wp_next_scheduled($hook) && $cleared < 3) {
9586 + wp_clear_scheduled_hook($hook);
9587 + $cleared++;
9588 + }
9589 + }
9590 +
9591 + // Small delay after clearing
9592 + usleep(100000); // 0.1 seconds
9593 +
9594 + // Try to schedule the event
9595 + $initial_time = time() + 300; // Start in 5 minutes
9596 + $result = wp_schedule_event($initial_time, 'hourly', 'mxchat_reset_rate_limits');
9597 +
9598 + if ($result === false) {
9599 + //error_log('MxChat: Failed to schedule cron, using fallback system');
9600 + $this->setup_fallback_rate_limit_system();
9601 + } else {
9602 + //error_log('MxChat: Successfully scheduled rate limit reset cron');
9603 + }
9604 +
9605 + } catch (Exception $e) {
9606 + //error_log('MxChat: Cron setup exception: ' . $e->getMessage());
9607 + $this->setup_fallback_rate_limit_system();
9608 + }
9609 +}
4039 9610
9611 +/**
9612 + * Try alternative cron scheduling methods
9613 + */
9614 +private function try_alternative_cron_scheduling($initial_time) {
9615 + try {
9616 + // Method 1: Try with current time instead of future time
9617 + $result1 = wp_schedule_event(time(), 'hourly', 'mxchat_reset_rate_limits');
9618 + if ($result1 !== false) {
9619 + //error_log('MxChat: Alternative method 1 (current time) succeeded');
9620 + return true;
9621 + }
9622 +
9623 + // Method 2: Try with a different interval
9624 + $result2 = wp_schedule_event($initial_time, 'daily', 'mxchat_reset_rate_limits');
9625 + if ($result2 !== false) {
9626 + //error_log('MxChat: Alternative method 2 (daily interval) succeeded');
9627 + return true;
9628 + }
9629 +
9630 + // Method 3: Try wp_schedule_single_event first, then recurring
9631 + $result3 = wp_schedule_single_event($initial_time, 'mxchat_reset_rate_limits');
9632 + if ($result3 !== false) {
9633 + //error_log('MxChat: Alternative method 3 (single event) succeeded');
9634 + // Schedule the next one manually in the handler
9635 + return true;
9636 + }
9637 +
9638 + return false;
9639 +
9640 + } catch (Exception $e) {
9641 + //error_log('MxChat: Alternative cron scheduling exception: ' . $e->getMessage());
9642 + return false;
9643 + }
9644 +}
9645 +
9646 +/**
9647 + * Enhanced fallback rate limit system
9648 + */
9649 +private function setup_fallback_rate_limit_system() {
9650 + // Set a flag to use database-based rate limit cleanup
9651 + update_option('mxchat_use_fallback_rate_limits', true);
9652 +
9653 + // Schedule a one-time check to happen on the next plugin load
9654 + update_option('mxchat_next_rate_limit_check', time() + 3600);
9655 +
9656 + // Also set up a more frequent fallback check (every 4 hours)
9657 + update_option('mxchat_fallback_check_interval', 4 * 3600);
9658 +
9659 + //error_log('MxChat: Fallback rate limit system activated');
9660 +}
9661 +
9662 +/**
9663 + * Enhanced fallback check method
9664 + */
9665 +public function check_fallback_rate_limits() {
9666 + $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
9667 +
9668 + if (!$use_fallback) {
9669 + return; // Regular cron is working
9670 + }
9671 +
9672 + $next_check = get_option('mxchat_next_rate_limit_check', 0);
9673 + $check_interval = get_option('mxchat_fallback_check_interval', 3600);
9674 +
9675 + if (time() >= $next_check) {
9676 + //error_log('MxChat: Running fallback rate limit cleanup');
9677 + $this->mxchat_reset_rate_limits();
9678 +
9679 + // Schedule next check
9680 + update_option('mxchat_next_rate_limit_check', time() + $check_interval);
9681 + }
9682 +}
9683 +/**
9684 + * Enhanced rate limit check that includes fallback cleanup and bot-specific rate limits
9685 + */
9686 +public function check_rate_limit() {
9687 + // Check if we need to run fallback cleanup
9688 + $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
9689 + $next_check = get_option('mxchat_next_rate_limit_check', 0);
9690 +
9691 + if ($use_fallback && time() >= $next_check) {
9692 + $this->mxchat_reset_rate_limits();
9693 + update_option('mxchat_next_rate_limit_check', time() + 3600); // Next hour
9694 + }
9695 +
9696 + // Get bot ID from current request context
9697 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
9698 +
9699 + // Get bot-specific options (includes rate limits if overridden)
9700 + $bot_options = $this->get_bot_options($bot_id);
9701 + $current_options = !empty($bot_options) ? $bot_options : $this->options;
9702 +
9703 + // Use bot-specific rate limits if available, otherwise fall back to default
9704 + $rate_limits_source = isset($current_options['rate_limits']) ? $current_options['rate_limits'] : get_option('mxchat_options', [])['rate_limits'] ?? [];
9705 +
9706 + // Determine user role or if logged out
9707 + if (is_user_logged_in()) {
9708 + $user = wp_get_current_user();
9709 + $user_id = $user->ID;
9710 +
9711 + // Get the user's primary role using reset() to safely get the first element
9712 + $user_roles = $user->roles;
9713 +
9714 + // Safely get the first role regardless of array key structure
9715 + if (!empty($user_roles) && is_array($user_roles)) {
9716 + $role = reset($user_roles); // This safely gets the first element regardless of key
9717 + } else {
9718 + $role = 'subscriber'; // Default to subscriber if no role found
9719 + }
9720 + } else {
9721 + $role = 'logged_out';
9722 + // Use IP address for non-logged-in users
9723 + $user_id = $this->get_client_ip();
9724 + }
9725 +
9726 + // Check if rate limits are configured for this role
9727 + if (!isset($rate_limits_source[$role])) {
9728 + return true; // No limit set for this role
9729 + }
9730 +
9731 + $limit = $rate_limits_source[$role]['limit'];
9732 +
9733 + // If unlimited, return true immediately
9734 + if ($limit === 'unlimited') {
9735 + return true;
9736 + }
9737 +
9738 + // Get the option name for this user/role with safer naming (include bot_id for bot-specific limits)
9739 + $safe_role = preg_replace('/[^a-zA-Z0-9_]/', '_', $role);
9740 + $safe_user_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $user_id);
9741 + $safe_bot_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $bot_id);
9742 +
9743 + // Include bot_id in option name so each bot has separate rate limits
9744 + $option_name = 'mxchat_chat_limit_' . $safe_bot_id . '_' . $safe_role . '_' . $safe_user_id;
9745 +
9746 + // Get the counter data
9747 + $limit_data = get_option($option_name, ['count' => 0, 'timestamp' => time()]);
9748 +
9749 + // If first request or counter reset needed, set the initial timestamp
9750 + if ($limit_data['count'] === 0) {
9751 + $limit_data['timestamp'] = time();
9752 + update_option($option_name, $limit_data);
9753 + }
9754 +
9755 + // Get the timeframe
9756 + $timeframe = isset($rate_limits_source[$role]['timeframe']) ?
9757 + $rate_limits_source[$role]['timeframe'] : 'daily';
9758 +
9759 + // Check if the counter needs to be reset based on timeframe
9760 + $current_time = time();
9761 + $timestamp = $limit_data['timestamp'];
9762 + $should_reset = false;
9763 +
9764 + switch ($timeframe) {
9765 + case 'hourly':
9766 + $should_reset = ($current_time - $timestamp) >= 3600; // 1 hour
9767 + break;
9768 + case 'daily':
9769 + $should_reset = ($current_time - $timestamp) >= 86400; // 24 hours
9770 + break;
9771 + case 'weekly':
9772 + $should_reset = ($current_time - $timestamp) >= 604800; // 7 days
9773 + break;
9774 + case 'monthly':
9775 + $should_reset = ($current_time - $timestamp) >= 2592000; // 30 days
9776 + break;
9777 + }
9778 +
9779 + // Reset the counter if the timeframe has passed
9780 + if ($should_reset) {
9781 + $limit_data = ['count' => 0, 'timestamp' => $current_time];
9782 + update_option($option_name, $limit_data);
9783 + }
9784 +
9785 + // Check if user has exceeded their limit
9786 + if ($limit_data['count'] >= intval($limit)) {
9787 + // Get the custom message for this role
9788 + $message = !empty($rate_limits_source[$role]['message'])
9789 + ? $rate_limits_source[$role]['message']
9790 + : __('Rate limit exceeded. Please try again later.', 'mxchat');
9791 +
9792 + // Add timeframe information to the message if placeholders exist
9793 + $timeframe_label = '';
9794 + switch ($timeframe) {
9795 + case 'hourly':
9796 + $timeframe_label = __('hour', 'mxchat');
9797 + break;
9798 + case 'daily':
9799 + $timeframe_label = __('day', 'mxchat');
9800 + break;
9801 + case 'weekly':
9802 + $timeframe_label = __('week', 'mxchat');
9803 + break;
9804 + case 'monthly':
9805 + $timeframe_label = __('month', 'mxchat');
9806 + break;
9807 + }
9808 +
9809 + // Replace placeholders in the message
9810 + $message = str_replace(
9811 + ['{limit}', '{count}', '{remaining}', '{timeframe}'],
9812 + [intval($limit), $limit_data['count'], max(0, intval($limit) - $limit_data['count']), $timeframe_label],
9813 + $message
9814 + );
9815 +
9816 + // Process HTML links in the message
9817 + $message = $this->process_rate_limit_message_html($message);
9818 +
9819 + // Return error with the processed message
9820 + return [
9821 + 'error' => true,
9822 + 'message' => $message
9823 + ];
9824 + }
9825 +
9826 + // Increment the counter
9827 + $limit_data['count']++;
9828 + update_option($option_name, $limit_data);
9829 +
9830 + return true;
9831 +}
9832 +
9833 +/**
9834 + * Enhanced rate limit reset with better error handling
9835 + */
4040 9836 public function mxchat_reset_rate_limits() {
9837 + try {
4041 9838 global $wpdb;
9839 + $all_options = get_option('mxchat_options', []);
9840 + $current_time = time();
9841 +
9842 + // Get rate limit options with a safer query and limit
9843 + $option_names = $wpdb->get_col(
9844 + $wpdb->prepare(
9845 + "SELECT option_name FROM {$wpdb->options}
9846 + WHERE option_name LIKE %s
9847 + LIMIT 1000",
9848 + 'mxchat_chat_limit_%'
9849 + )
9850 + );
9851 +
9852 + if (empty($option_names)) {
9853 + return;
9854 + }
9855 +
9856 + $processed_count = 0;
9857 + $max_processing_time = 30; // Maximum 30 seconds
9858 + $start_time = time();
9859 +
9860 + foreach ($option_names as $option_name) {
9861 + // Check processing time limit
9862 + if ((time() - $start_time) > $max_processing_time) {
9863 + //error_log('MxChat: Rate limit reset timeout after processing ' . $processed_count . ' entries');
9864 + break;
9865 + }
9866 +
9867 + // Parse the option name more safely
9868 + if (!preg_match('/^mxchat_chat_limit_(.+)_(.+)$/', $option_name, $matches)) {
9869 + continue;
9870 + }
9871 +
9872 + $role_and_user = $matches[1] . '_' . $matches[2];
9873 + $parts = explode('_', $role_and_user);
9874 +
9875 + if (count($parts) < 2) {
9876 + continue;
9877 + }
9878 +
9879 + // Extract role (everything except the last part which is user ID)
9880 + $user_id_part = array_pop($parts);
9881 + $role = implode('_', $parts);
9882 +
9883 + // Skip if role doesn't exist in our settings
9884 + if (!isset($all_options['rate_limits'][$role])) {
9885 + // Clean up orphaned entries
9886 + delete_option($option_name);
9887 + continue;
9888 + }
9889 +
9890 + $timeframe = $all_options['rate_limits'][$role]['timeframe'] ?? 'daily';
9891 + $limit_data = get_option($option_name);
9892 +
9893 + if (!$limit_data || !is_array($limit_data) || !isset($limit_data['timestamp'])) {
9894 + // Clean up invalid entries
9895 + delete_option($option_name);
9896 + continue;
9897 + }
9898 +
9899 + $timestamp = $limit_data['timestamp'];
9900 + $should_reset = false;
9901 +
9902 + // Determine if we should reset based on the timeframe
9903 + switch ($timeframe) {
9904 + case 'hourly':
9905 + $should_reset = ($current_time - $timestamp) >= 3600;
9906 + break;
9907 + case 'daily':
9908 + $should_reset = ($current_time - $timestamp) >= 86400;
9909 + break;
9910 + case 'weekly':
9911 + $should_reset = ($current_time - $timestamp) >= 604800;
9912 + break;
9913 + case 'monthly':
9914 + $should_reset = ($current_time - $timestamp) >= 2592000;
9915 + break;
9916 + }
9917 +
9918 + // Reset the counter if the timeframe has passed
9919 + if ($should_reset) {
9920 + delete_option($option_name);
9921 + wp_cache_delete($option_name, 'options');
9922 + $processed_count++;
9923 + }
9924 + }
9925 +
9926 + // Clean up any orphaned cache entries
9927 + wp_cache_delete('mxchat_all_chat_limits', 'options');
9928 +
9929 + //error_log("MxChat: Rate limit reset completed. Processed {$processed_count} entries.");
9930 +
9931 + } catch (Exception $e) {
9932 + //error_log('MxChat: Rate limit reset error: ' . $e->getMessage());
9933 + }
9934 +}
4042 9935
4043 - // Define a cache key pattern for rate limits
4044 - $cache_key_pattern = 'mxchat_chat_limit_%';
4045 9936
4046 - // Retrieve all option names matching the pattern
4047 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
4048 - $option_names = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
9937 +/**
9938 + * Process HTML links in rate limit messages
9939 + *
9940 + * @param string $message The rate limit message
9941 + * @return string The processed message with safe HTML links
9942 + */
9943 +private function process_rate_limit_message_html($message) {
9944 + // Return original message if empty
9945 + if (empty($message)) {
9946 + return $message;
9947 + }
9948 +
9949 + // First, convert markdown links to HTML
9950 + $message = $this->convert_markdown_links($message);
9951 +
9952 + // Then, auto-convert any remaining plain URLs to links
9953 + $message = $this->auto_link_urls($message);
9954 +
9955 + // Allow basic HTML tags for links and formatting
9956 + $allowed_tags = [
9957 + 'a' => [
9958 + 'href' => true,
9959 + 'target' => true,
9960 + 'rel' => true,
9961 + 'title' => true,
9962 + 'class' => true
9963 + ],
9964 + 'strong' => [],
9965 + 'em' => [],
9966 + 'br' => [],
9967 + 'b' => [],
9968 + 'i' => [],
9969 + 'span' => ['class' => true]
9970 + ];
9971 +
9972 + // Sanitize but allow the specified HTML tags
9973 + $processed_message = wp_kses($message, $allowed_tags);
9974 +
9975 + // If wp_kses stripped everything, return the original message as plain text
9976 + if (empty($processed_message) && !empty($message)) {
9977 + // Strip all HTML and return plain text as fallback
9978 + return wp_strip_all_tags($message);
9979 + }
9980 +
9981 + return $processed_message;
9982 +}
4049 9983
4050 - // db call ok; no-cache ok
4051 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- db call ok
4052 - $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
9984 +/**
9985 + * Convert markdown links to HTML
9986 + *
9987 + * @param string $text The text to process
9988 + * @return string The text with markdown links converted to HTML
9989 + */
9990 +private function convert_markdown_links($text) {
9991 + // Return original text if empty
9992 + if (empty($text)) {
9993 + return $text;
9994 + }
9995 +
9996 + // Pattern to match markdown links: [text](url)
9997 + $pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
9998 +
9999 + $processed_text = preg_replace_callback($pattern, function($matches) {
10000 + $link_text = $matches[1];
10001 + $url = $matches[2];
10002 +
10003 + // Clean up any trailing punctuation from the URL
10004 + $url = rtrim($url, '.,;:!?');
10005 +
10006 + // Sanitize the link text and URL
10007 + $safe_text = esc_html($link_text);
10008 + $safe_url = esc_url($url);
10009 +
10010 + // Create the HTML link
10011 + return '<a href="' . $safe_url . '" target="_blank" rel="noopener noreferrer">' . $safe_text . '</a>';
10012 + }, $text);
10013 +
10014 + // If preg_replace_callback failed, return original text
10015 + if ($processed_text === null) {
10016 + return $text;
10017 + }
10018 +
10019 + return $processed_text;
10020 +}
4053 10021
4054 - // Clear the relevant cache entries
4055 - foreach ($option_names as $option_name) {
4056 - wp_cache_delete($option_name, 'options');
4057 - }
10022 +/**
10023 + * Auto-convert plain URLs to clickable links
10024 + *
10025 + * @param string $text The text to process
10026 + * @return string The text with URLs converted to links
10027 + */
10028 +private function auto_link_urls($text) {
10029 + // Return original text if empty
10030 + if (empty($text)) {
10031 + return $text;
10032 + }
10033 +
10034 + // Simple pattern that avoids complex lookbehinds
10035 + // This will match URLs that are not already inside href attributes or markdown links
10036 + $pattern = '/(?<!href=["\'])(?<!\]\()https?:\/\/[^\s<>"\')\]]+/i';
10037 +
10038 + $processed_text = preg_replace_callback($pattern, function($matches) {
10039 + $url = $matches[0];
10040 + // Clean up any trailing punctuation that might have been captured
10041 + $url = rtrim($url, '.,;:!?');
10042 +
10043 + // Add target="_blank" and rel="noopener noreferrer" for security
10044 + return '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($url) . '</a>';
10045 + }, $text);
10046 +
10047 + // If preg_replace_callback failed, return original text
10048 + if ($processed_text === null) {
10049 + return $text;
10050 + }
10051 +
10052 + return $processed_text;
10053 +}
4058 10054
4059 - // Optionally, clear a general cache if you have one
4060 - wp_cache_delete('mxchat_all_chat_limits', 'options');
10055 +
10056 +// Helper function to get client IP address
10057 +private function get_client_ip() {
10058 + // Check for shared internet/ISP IP
10059 + if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
10060 + return sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
4061 10061 }
10062 +
10063 + // Check for IPs passing through proxies
10064 + if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
10065 + // Use the first value in the comma-separated list
10066 + $forwarded_for = explode(',', sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']));
10067 + return trim($forwarded_for[0]);
10068 + }
10069 +
10070 + if (!empty($_SERVER['REMOTE_ADDR'])) {
10071 + return sanitize_text_field($_SERVER['REMOTE_ADDR']);
10072 + }
10073 +
10074 + // Fallback
10075 + return 'unknown';
10076 +}
4062 10077
4063 -private function mxchat_fetch_woocommerce_products() {
4064 - // Ensure WooCommerce is active
4065 - if (!class_exists('WooCommerce')) {
4066 - return [];
10078 +/**
10079 + * AJAX handler to get system information for testing panel
10080 + */
10081 +/**
10082 + * AJAX handler to get system information for testing panel
10083 + */
10084 +public function mxchat_get_system_info() {
10085 + // Verify nonce for security
10086 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
10087 + wp_send_json_error(['message' => 'Invalid nonce']);
10088 + return;
4067 10089 }
10090 +
10091 + // Only allow admin users
10092 + if (!current_user_can('administrator')) {
10093 + wp_send_json_error(['message' => 'Unauthorized']);
10094 + return;
10095 + }
10096 +
10097 + // Get system prompt from options
10098 + $system_prompt = isset($this->options['system_prompt_instructions'])
10099 + ? $this->options['system_prompt_instructions']
10100 + : 'No system prompt configured';
10101 +
10102 + // Get selected model
10103 + $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest';
10104 +
10105 + // Check if OpenRouter is being used
10106 + $is_openrouter = ($selected_model === 'openrouter');
10107 + $openrouter_model = '';
10108 +
10109 + if ($is_openrouter) {
10110 + // Get the actual OpenRouter model that's selected
10111 + $openrouter_model = isset($this->options['openrouter_selected_model'])
10112 + ? $this->options['openrouter_selected_model']
10113 + : 'No OpenRouter model selected';
10114 +
10115 + // Update selected_model display to show both
10116 + $selected_model = 'OpenRouter: ' . $openrouter_model;
10117 + }
10118 +
10119 + // Get API key status (just check if they exist, don't expose the keys)
10120 + $api_status = [];
10121 + $api_status['openai'] = !empty($this->options['api_key']);
10122 + $api_status['claude'] = !empty($this->options['claude_api_key']);
10123 + $api_status['gemini'] = !empty($this->options['gemini_api_key']);
10124 + $api_status['xai'] = !empty($this->options['xai_api_key']);
10125 + $api_status['deepseek'] = !empty($this->options['deepseek_api_key']);
10126 + $api_status['openrouter'] = !empty($this->options['openrouter_api_key']);
10127 +
10128 + wp_send_json_success([
10129 + 'system_prompt' => $system_prompt,
10130 + 'selected_model' => $selected_model,
10131 + 'is_openrouter' => $is_openrouter,
10132 + 'openrouter_model' => $openrouter_model,
10133 + 'api_status' => $api_status
10134 + ]);
10135 +}
4068 10136
4069 - $args = array(
4070 - 'post_type' => 'product',
4071 - 'post_status' => 'publish',
4072 - 'posts_per_page' => -1,
10137 +/**
10138 + * AJAX handler to get similarity threshold
10139 + */
10140 +public function mxchat_get_similarity_threshold() {
10141 + // Verify nonce for security
10142 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
10143 + wp_send_json_error(['message' => 'Invalid nonce']);
10144 + return;
10145 + }
10146 +
10147 + // Only allow admin users
10148 + if (!current_user_can('administrator')) {
10149 + wp_send_json_error(['message' => 'Unauthorized']);
10150 + return;
10151 + }
10152 +
10153 + // Get similarity threshold from main options (default 35%)
10154 + $similarity_threshold = isset($this->options['similarity_threshold'])
10155 + ? ((int) $this->options['similarity_threshold']) / 100
10156 + : 0.35;
10157 +
10158 + wp_send_json_success([
10159 + 'threshold' => $similarity_threshold,
10160 + 'threshold_percentage' => ($similarity_threshold * 100) . '%'
10161 + ]);
10162 +}
10163 +
10164 +/**
10165 + * AJAX handler to get knowledge base status
10166 + */
10167 +public function mxchat_get_kb_status() {
10168 + // Verify nonce for security
10169 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
10170 + wp_send_json_error(['message' => 'Invalid nonce']);
10171 + return;
10172 + }
10173 +
10174 + // Only allow admin users
10175 + if (!current_user_can('administrator')) {
10176 + wp_send_json_error(['message' => 'Unauthorized']);
10177 + return;
10178 + }
10179 +
10180 + // Check OpenAI Vector Store first (takes priority)
10181 + $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
10182 + $use_vectorstore = (isset($vectorstore_options['mxchat_use_openai_vectorstore']) && $vectorstore_options['mxchat_use_openai_vectorstore'] === '1');
10183 +
10184 + if ($use_vectorstore) {
10185 + $vectorstore_ids = $vectorstore_options['mxchat_openai_vectorstore_ids'] ?? '';
10186 + $id_count = !empty($vectorstore_ids) ? count(array_filter(array_map('trim', explode(',', $vectorstore_ids)))) : 0;
10187 +
10188 + $kb_info = [
10189 + 'type' => 'OpenAI Vector Store',
10190 + 'status' => 'Active',
10191 + 'documents' => $id_count > 0 ? $id_count . ' vector store' . ($id_count > 1 ? 's' : '') . ' configured' : 'No vector stores configured'
10192 + ];
10193 +
10194 + wp_send_json_success($kb_info);
10195 + return;
10196 + }
10197 +
10198 + // Check Pinecone vs WordPress
10199 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
10200 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
10201 +
10202 + $kb_info = [
10203 + 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
10204 + 'status' => 'Active'
10205 + ];
10206 +
10207 + // Get document count
10208 + if ($use_pinecone) {
10209 + $kb_info['documents'] = 'Connected to Pinecone';
10210 + $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
10211 + } else {
10212 + // Count documents in WordPress database
10213 + global $wpdb;
10214 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
10215 + $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
10216 + $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
10217 + }
10218 +
10219 + wp_send_json_success($kb_info);
10220 +}
10221 +
10222 +/**
10223 + * AJAX handler to start a completely fresh session (NEW - replaces old clear session)
10224 + */
10225 +public function mxchat_start_fresh_session() {
10226 + // Verify nonce for security
10227 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
10228 + wp_send_json_error(['message' => 'Invalid nonce']);
10229 + return;
10230 + }
10231 +
10232 + // Only allow admin users
10233 + if (!current_user_can('administrator')) {
10234 + wp_send_json_error(['message' => 'Unauthorized']);
10235 + return;
10236 + }
10237 +
10238 + $old_session_id = isset($_POST['old_session_id']) ? sanitize_text_field($_POST['old_session_id']) : '';
10239 + $new_session_id = isset($_POST['new_session_id']) ? sanitize_text_field($_POST['new_session_id']) : '';
10240 +
10241 + if (empty($old_session_id)) {
10242 + wp_send_json_error(['message' => 'Old session ID required']);
10243 + return;
10244 + }
10245 +
10246 + // If no new session ID provided, generate one
10247 + if (empty($new_session_id)) {
10248 + $new_session_id = 'mxchat_chat_' . substr(md5(uniqid()), 0, 9);
10249 + }
10250 +
10251 + // Clear ALL data associated with the old session
10252 + $this->clear_complete_session_data($old_session_id);
10253 +
10254 + // Initialize the new session
10255 + $this->initialize_fresh_session($new_session_id);
10256 +
10257 + wp_send_json_success([
10258 + 'message' => 'Fresh session started successfully',
10259 + 'new_session_id' => $new_session_id,
10260 + 'old_session_id' => $old_session_id
10261 + ]);
10262 +}
10263 +
10264 +/**
10265 + * Clear ALL data associated with a session (ENHANCED)
10266 + */
10267 +private function clear_complete_session_data($session_id) {
10268 + // Clear chat history
10269 + delete_option("mxchat_history_{$session_id}");
10270 +
10271 + // Clear chat mode
10272 + delete_option("mxchat_mode_{$session_id}");
10273 +
10274 + // Clear any PDF/Word transients
10275 + $this->clear_pdf_transients($session_id);
10276 + if (method_exists($this, 'clear_word_transients')) {
10277 + $this->clear_word_transients($session_id);
10278 + }
10279 +
10280 + // Clear agent-related data
10281 + delete_option("mxchat_channel_{$session_id}");
10282 + delete_option("mxchat_agent_name_{$session_id}");
10283 + delete_option("mxchat_email_{$session_id}");
10284 +
10285 + // Clear any recommendation flow state
10286 + delete_option("mxchat_sr_flow_state_{$session_id}");
10287 +
10288 + // Clear any cached embeddings or context
10289 + delete_transient("mxchat_context_{$session_id}");
10290 + delete_transient("mxchat_last_query_{$session_id}");
10291 +
10292 + // Clear any testing data
10293 + delete_transient("mxchat_testing_data_{$session_id}");
10294 +
10295 + // Clear any rate limiting data for this session
10296 + delete_transient("mxchat_rate_limit_{$session_id}");
10297 +
10298 + // Clear any other session-specific transients
10299 + delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
10300 + delete_transient("mxchat_include_pdf_in_context_{$session_id}");
10301 + delete_transient("mxchat_include_word_in_context_{$session_id}");
10302 +
10303 + // Clear form addon state (pending forms and submitted forms)
10304 + delete_option("mxchat_pending_form_{$session_id}");
10305 + delete_option("mxchat_submitted_forms_{$session_id}");
10306 +
10307 + //error_log("MxChat: Cleared all data for session: {$session_id}");
10308 +}
10309 +
10310 +/**
10311 + * Initialize a fresh session with default data
10312 + */
10313 +private function initialize_fresh_session($session_id) {
10314 + // Set default chat mode
10315 + update_option("mxchat_mode_{$session_id}", 'ai');
10316 +
10317 + //error_log("MxChat: Initialized fresh session: {$session_id}");
10318 +}
10319 +
10320 +/**
10321 + * Helper method to clear Word document transients (if you have Word support)
10322 + */
10323 +private function clear_word_transients($session_id) {
10324 + delete_transient('mxchat_word_url_' . $session_id);
10325 + delete_transient('mxchat_word_filename_' . $session_id);
10326 + delete_transient('mxchat_word_embeddings_' . $session_id);
10327 + delete_transient('mxchat_include_word_in_context_' . $session_id);
10328 +}
10329 +
10330 +/**
10331 + * Simplified testing data capture method (CLEANED UP)
10332 + */
10333 +private function capture_testing_data($user_embedding, $message, $session_id) {
10334 + // Only capture for admin users
10335 + if (!current_user_can('administrator')) {
10336 + return null;
10337 + }
10338 +
10339 + $testing_data = [
10340 + 'query' => $message,
10341 + 'timestamp' => time(),
10342 + 'top_matches' => [],
10343 + 'action_matches' => [] // Add action matches
10344 + ];
10345 +
10346 + // Get similarity threshold
10347 + $similarity_threshold = isset($this->options['similarity_threshold'])
10348 + ? ((int) $this->options['similarity_threshold']) / 100
10349 + : 0.35;
10350 +
10351 + $testing_data['similarity_threshold'] = $similarity_threshold;
10352 +
10353 + // Use the real similarity analysis if available
10354 + if ($this->last_similarity_analysis !== null) {
10355 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
10356 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
10357 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
10358 + } else {
10359 + // Fallback: determine knowledge base type
10360 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
10361 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
10362 +
10363 + $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
10364 + }
10365 +
10366 + // Include action analysis if available
10367 + if (isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
10368 + $testing_data['action_matches'] = $this->last_action_analysis;
10369 +
10370 + // Clear it after capturing to avoid stale data
10371 + $this->last_action_analysis = null;
10372 + }
10373 +
10374 + return $testing_data;
10375 +}
10376 +
10377 +
10378 +/**
10379 + * Track URL clicks from chatbot responses
10380 + */
10381 +public function mxchat_track_url_click() {
10382 + // Verify nonce for security
10383 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
10384 + wp_send_json_error(['message' => 'Invalid nonce']);
10385 + wp_die();
10386 + }
10387 +
10388 + $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
10389 + $clicked_url = isset($_POST['url']) ? esc_url_raw($_POST['url']) : '';
10390 + $message_context = isset($_POST['message_context']) ? sanitize_textarea_field($_POST['message_context']) : '';
10391 +
10392 + if (empty($session_id) || empty($clicked_url)) {
10393 + wp_send_json_error(['message' => 'Missing required data']);
10394 + wp_die();
10395 + }
10396 +
10397 + global $wpdb;
10398 + $table_name = $wpdb->prefix . 'mxchat_url_clicks';
10399 +
10400 + // Insert click tracking record
10401 + $wpdb->insert(
10402 + $table_name,
10403 + [
10404 + 'session_id' => $session_id,
10405 + 'clicked_url' => $clicked_url,
10406 + 'message_context' => $message_context,
10407 + 'click_timestamp' => current_time('mysql', 1),
10408 + 'user_ip' => $_SERVER['REMOTE_ADDR'],
10409 + 'user_agent' => $_SERVER['HTTP_USER_AGENT']
10410 + ]
4073 10411 );
10412 +
10413 + wp_send_json_success(['message' => 'Click tracked']);
10414 + wp_die();
10415 +}
4074 10416
4075 - $products = get_posts($args);
4076 - $product_data = [];
10417 +/**
10418 + * Get URL click analytics for a session
10419 + */
10420 +public function mxchat_get_url_clicks($session_id) {
10421 + global $wpdb;
10422 + $table_name = $wpdb->prefix . 'mxchat_url_clicks';
10423 +
10424 + $clicks = $wpdb->get_results($wpdb->prepare(
10425 + "SELECT * FROM $table_name WHERE session_id = %s ORDER BY click_timestamp ASC",
10426 + $session_id
10427 + ));
10428 +
10429 + return $clicks;
10430 +}
10431 +/**
10432 + * Track the originating page where chat was started
10433 + */
10434 +public function mxchat_track_originating_page() {
10435 + // Verify nonce
10436 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
10437 + wp_send_json_error(['message' => 'Invalid nonce']);
10438 + wp_die();
10439 + }
10440 +
10441 + $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
10442 + $page_url = isset($_POST['page_url']) ? esc_url_raw($_POST['page_url']) : '';
10443 + $page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
10444 +
10445 + if (empty($session_id)) {
10446 + wp_send_json_error(['message' => 'Missing session ID']);
10447 + wp_die();
10448 + }
10449 +
10450 + global $wpdb;
10451 + $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
10452 +
10453 + // Check if we've already tracked for this session
10454 + $existing = $wpdb->get_var($wpdb->prepare(
10455 + "SELECT COUNT(*) FROM $table_name
10456 + WHERE session_id = %s
10457 + AND originating_page_url IS NOT NULL",
10458 + $session_id
10459 + ));
10460 +
10461 + if ($existing > 0) {
10462 + wp_send_json_success(['message' => 'Already tracked']);
10463 + wp_die();
10464 + }
10465 +
10466 + // Update the first message in this session with originating page info
10467 + $wpdb->query($wpdb->prepare(
10468 + "UPDATE $table_name
10469 + SET originating_page_url = %s,
10470 + originating_page_title = %s
10471 + WHERE session_id = %s
10472 + ORDER BY timestamp ASC
10473 + LIMIT 1",
10474 + $page_url,
10475 + $page_title,
10476 + $session_id
10477 + ));
10478 +
10479 + wp_send_json_success(['message' => 'Originating page tracked']);
10480 + wp_die();
10481 +}
4077 10482
4078 - foreach ($products as $product) {
4079 - $product_id = $product->ID;
4080 - $product_obj = wc_get_product($product_id);
10483 +/**
10484 + * Validate and clean URLs from AI response
10485 + * Removes any URLs that aren't in the knowledge base
10486 + *
10487 + * @param string $response_text The AI-generated response
10488 + * @param array $valid_urls Array of URLs from the knowledge base
10489 + * @return string Cleaned response with invalid URLs removed/flagged
10490 + */
10491 +private function validate_and_clean_urls($response_text, $valid_urls) {
10492 + // DEBUG: Log what we're working with
10493 + //error_log("=== MxChat URL Validation Debug ===");
10494 + //error_log("Valid URLs count: " . count($valid_urls));
10495 + //error_log("Valid URLs: " . print_r($valid_urls, true));
10496 + //error_log("Response text length: " . strlen($response_text));
10497 + //error_log("Response text preview: " . substr($response_text, 0, 500));
10498 +
10499 + // If no valid URLs provided or empty response, return as-is
10500 + if (empty($valid_urls) || empty($response_text)) {
10501 + //error_log("Validation skipped - empty valid_urls or response");
10502 + return $response_text;
10503 + }
10504 +
10505 + // Extract all URLs from the AI response
10506 + // This regex matches http:// and https:// URLs
10507 + preg_match_all(
10508 + '#\bhttps?://[^\s<>"\')\]]+#i',
10509 + $response_text,
10510 + $matches
10511 + );
10512 +
10513 + // If no URLs found in response, return as-is
10514 + if (empty($matches[0])) {
10515 + //error_log("No URLs found in response");
10516 + return $response_text;
10517 + }
10518 +
10519 + $found_urls = $matches[0];
10520 + $cleaned_response = $response_text;
10521 + $removed_count = 0;
10522 +
10523 + // Normalize valid URLs for comparison (remove trailing slashes, fragments, etc.)
10524 + $normalized_valid_urls = array_map(function($url) {
10525 + // Remove trailing slash
10526 + $url = rtrim($url, '/');
10527 + // Remove URL fragments (#section)
10528 + $url = preg_replace('/#.*$/', '', $url);
10529 + // Remove trailing punctuation that might have been captured
10530 + $url = rtrim($url, '.,;:!?');
10531 + return $url;
10532 + }, $valid_urls);
10533 +
10534 + //error_log("Normalized valid URLs: " . print_r($normalized_valid_urls, true));
10535 +
10536 + foreach ($found_urls as $found_url) {
10537 + // Clean up the found URL (remove trailing punctuation that might have been captured)
10538 + $clean_found_url = rtrim($found_url, '.,;:!?)');
10539 +
10540 + // DEBUG: Log each URL being checked
10541 + //error_log("Checking found URL: " . $found_url);
10542 +
10543 + // Normalize for comparison
10544 + $normalized_found = rtrim($clean_found_url, '/');
10545 + $normalized_found = preg_replace('/#.*$/', '', $normalized_found);
10546 +
10547 + //error_log("Normalized found URL: " . $normalized_found);
10548 +
10549 + // Check if this URL exists in our valid URLs list
10550 + $is_valid = false;
10551 +
10552 + //error_log("Starting validation checks for: " . $normalized_found);
10553 +
10554 + // First, try exact match
10555 + if (in_array($normalized_found, $normalized_valid_urls)) {
10556 + $is_valid = true;
10557 + //error_log("EXACT MATCH FOUND");
10558 + } else {
10559 + //error_log("No exact match, checking variations...");
10560 + // If no exact match, check if it's a variation (with query params, etc.)
10561 + foreach ($normalized_valid_urls as $valid_url) {
10562 + //error_log(" Comparing against valid URL: " . $valid_url);
10563 +
10564 + // Check if the found URL starts with a valid URL (handles query params)
10565 + if (strpos($normalized_found, $valid_url) === 0) {
10566 + // Check what comes after the valid URL
10567 + $remainder = substr($normalized_found, strlen($valid_url));
10568 +
10569 + // Only valid if:
10570 + // 1. Exact match (remainder is empty)
10571 + // 2. Query params (starts with ?)
10572 + // 3. Fragment (starts with #)
10573 + if (empty($remainder) || $remainder[0] === '?' || $remainder[0] === '#') {
10574 + $is_valid = true;
10575 + //error_log(" MATCH: Found URL is valid variation of base URL");
10576 + break;
10577 + } else {
10578 + //error_log(" NOT A MATCH: Found URL extends path beyond valid URL (remainder: " . $remainder . ")");
10579 + }
10580 + }
10581 + // Also check the reverse (in case valid URL has query params)
10582 + if (strpos($valid_url, $normalized_found) === 0) {
10583 + $is_valid = true;
10584 + //error_log(" MATCH: Valid URL starts with found URL");
10585 + break;
10586 + }
10587 + }
10588 +
10589 + if (!$is_valid) {
10590 + //error_log("NO MATCH FOUND - URL should be removed");
10591 + }
10592 + }
10593 +
10594 + // If URL is not valid, remove it from the response
10595 + if (!$is_valid) {
10596 + // Log the removal for debugging
10597 + //error_log("MxChat: Removed hallucinated URL: " . $found_url);
10598 + //error_log("MxChat: Valid URLs were: " . implode(', ', array_slice($normalized_valid_urls, 0, 5)));
10599 +
10600 + $removed_count++;
10601 +
10602 + // Check if URL is part of a markdown link: [text](url)
10603 + $markdown_pattern = '/\[([^\]]+)\]\(' . preg_quote($found_url, '/') . '\)/';
10604 + if (preg_match($markdown_pattern, $cleaned_response)) {
10605 + //error_log("Found markdown link, removing but keeping text");
10606 + // Remove the markdown link but keep the text
10607 + $cleaned_response = preg_replace($markdown_pattern, '$1', $cleaned_response);
10608 + }
10609 + // Check if URL is part of an HTML link: <a href="url">text</a>
10610 + else if (preg_match('/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>(.*?)<\/a>/i', $cleaned_response, $link_match)) {
10611 + //error_log("Found HTML link, removing but keeping text");
10612 + // Remove the HTML link but keep the text
10613 + $link_text = $link_match[1];
10614 + $cleaned_response = preg_replace(
10615 + '/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>.*?<\/a>/i',
10616 + $link_text,
10617 + $cleaned_response
10618 + );
10619 + }
10620 + // Otherwise just remove the bare URL
10621 + else {
10622 + //error_log("Removing bare URL");
10623 + $cleaned_response = str_replace($found_url, '', $cleaned_response);
10624 + }
10625 + }
10626 + }
10627 +
10628 + // Log summary if any URLs were removed
10629 + if ($removed_count > 0) {
10630 + //error_log("MxChat: URL Validation Summary - Removed {$removed_count} hallucinated URL(s)");
10631 + } else {
10632 + //error_log("MxChat: URL Validation Summary - No URLs removed, all were valid");
10633 + }
10634 +
10635 + // Clean up any double spaces or awkward punctuation left behind
10636 + // IMPORTANT: Only collapse horizontal whitespace (spaces/tabs), preserve newlines for markdown formatting
10637 + $cleaned_response = preg_replace('/[^\S\n]+/', ' ', $cleaned_response); // Collapse spaces/tabs but NOT newlines
10638 + $cleaned_response = preg_replace('/[^\S\n]+([.,;:!?])/', '$1', $cleaned_response); // Same for punctuation cleanup
10639 +
10640 + //error_log("Final cleaned response: " . $cleaned_response);
10641 +
10642 + return trim($cleaned_response);
10643 +}
4081 10644
4082 - $product_data[] = array(
4083 - 'id' => $product_id,
4084 - 'name' => $product_obj->get_name(),
4085 - 'description' => $product_obj->get_description(),
4086 - 'short_description' => $product_obj->get_short_description(),
4087 - 'url' => get_permalink($product_id),
4088 - 'price' => $product_obj->get_regular_price(),
4089 - 'sale_price' => $product_obj->get_sale_price(),
4090 - 'stock_status' => $product_obj->get_stock_status(),
4091 - 'sku' => $product_obj->get_sku(),
4092 - 'in_stock' => $product_obj->is_in_stock(),
4093 - 'total_sales' => $product_obj->get_total_sales(),
4094 - );
10645 +/**
10646 + * AJAX handler to get current chat mode for a session
10647 + */
10648 +public function mxchat_get_current_chat_mode() {
10649 + // Verify nonce for security
10650 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
10651 + wp_send_json_error(['message' => 'Invalid nonce']);
10652 + wp_die();
4095 10653 }
10654 +
10655 + $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
10656 +
10657 + if (empty($session_id)) {
10658 + wp_send_json_error(['message' => 'Session ID missing']);
10659 + wp_die();
10660 + }
10661 +
10662 + // Get the current chat mode for this session
10663 + $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
10664 +
10665 + wp_send_json_success([
10666 + 'chat_mode' => $chat_mode
10667 + ]);
10668 + wp_die();
10669 +}
4096 10670
4097 - return $product_data;
4098 -}
10671 +
4099 10672
4100 10673 }
4101 10674 ?>