PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.5.4
MxChat – AI Chatbot & Content Generation for WordPress v2.5.4
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 +5996 -1383 2.0.52.5.4 View file →
@@ -9,50 +9,50 @@
9 9 private $chat_count;
10 10 private $fallbackResponse;
11 11 private $productCardHtml;
12 12 private $word_handler;
13 + private $last_similarity_analysis = null;
14 + private $current_valid_urls = [];
13 15
16 +/**
17 + * Class constructor
18 + */
14 19 public function __construct() {
15 20 $this->options = get_option('mxchat_options');
16 21 $this->prompts_options = get_option('mxchat_prompts_options', array());
17 -
18 22 $this->chat_count = get_option('mxchat_chat_count', 0);
19 23 $this->word_handler = new MXChat_Word_Handler($this->options);
20 -
24 +
25 + // Add all action hooks
21 26 add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
22 27 add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
23 28 add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
24 -
25 29 add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
26 30 add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
31 +
27 32 // Add the AJAX actions for checking if the pre-chat message was dismissed
28 33 add_action('wp_ajax_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
29 34 add_action('wp_ajax_nopriv_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
30 -
31 35 add_action('wp_ajax_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
32 36 add_action('wp_ajax_nopriv_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
33 -
34 37 add_action('wp_ajax_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
35 38 add_action('wp_ajax_nopriv_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
36 -
37 - if (!wp_next_scheduled('mxchat_reset_rate_limits')) {
38 - wp_schedule_event(time(), 'daily', 'mxchat_reset_rate_limits');
39 - }
40 -
39 +
41 40 // Add REST API routes registration
42 41 add_action('rest_api_init', array($this, 'register_routes'));
43 -
44 42 add_action('wp_ajax_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
45 43 add_action('wp_ajax_nopriv_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
46 -
44 +
45 + // Rate limit action - notice we removed the old schedule setup
47 46 add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
48 -
47 +
48 + // File upload and handling actions
49 49 add_action('wp_ajax_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
50 50 add_action('wp_ajax_nopriv_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
51 51 add_action('wp_ajax_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
52 52 add_action('wp_ajax_nopriv_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
53 -
54 - // Add these with your other add_action hooks
53 +
54 + // Word document handling actions
55 55 add_action('wp_ajax_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
56 56 add_action('wp_ajax_nopriv_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
57 57 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
58 58 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
@@ -57,15 +57,47 @@
57 57 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
58 58 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
59 59 add_action('wp_ajax_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
60 60 add_action('wp_ajax_nopriv_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
61 -
61 +
62 + // Email handling actions
62 63 add_action('wp_ajax_nopriv_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
63 64 add_action('wp_ajax_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
64 65 add_action('wp_ajax_nopriv_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
65 66 add_action('wp_ajax_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
67 +
68 + add_action('wp_ajax_mxchat_stream_chat', array($this, 'mxchat_handle_chat_request'));
69 + add_action('wp_ajax_nopriv_mxchat_stream_chat', array($this, 'mxchat_handle_chat_request'));
70 +
71 + // Testing panel AJAX actions
72 + add_action('wp_ajax_mxchat_get_system_info', array($this, 'mxchat_get_system_info'));
73 + add_action('wp_ajax_mxchat_get_similarity_threshold', array($this, 'mxchat_get_similarity_threshold'));
74 + add_action('wp_ajax_mxchat_get_kb_status', array($this, 'mxchat_get_kb_status'));
75 + add_action('wp_ajax_mxchat_start_fresh_session', array($this, 'mxchat_start_fresh_session'));
76 + // Add to your existing constructor, in the section with other AJAX actions:
77 + add_action('wp_ajax_mxchat_track_url_click', array($this, 'mxchat_track_url_click'));
78 + add_action('wp_ajax_nopriv_mxchat_track_url_click', array($this, 'mxchat_track_url_click'));
79 + add_action('wp_ajax_mxchat_track_originating_page', array($this, 'mxchat_track_originating_page'));
80 + add_action('wp_ajax_nopriv_mxchat_track_originating_page', array($this, 'mxchat_track_originating_page'));
81 + // Add chat mode checking actions
82 + add_action('wp_ajax_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
83 + add_action('wp_ajax_nopriv_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
84 +
85 + add_filter('mxchat_check_actions_only', array($this, 'check_actions_for_addons'), 10, 4);
86 +
87 +
66 88 }
67 89
90 +// In your core plugin's check_actions_for_addons method:
91 +public function check_actions_for_addons($default, $message, $user_id, $session_id) {
92 + //error_log('MxChat Core: check_actions_for_addons called with message: ' . $message);
93 +
94 + $result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
95 +
96 + //error_log('MxChat Core: Intent check result = ' . ($result === false ? 'false' : 'true'));
97 +
98 + return $result;
99 +}
68 100
69 101 private function mxchat_increment_chat_count() {
70 102 $chat_count = get_option('mxchat_chat_count', 0);
71 103 $chat_count++;
@@ -78,8 +110,26 @@
78 110 wp_die();
79 111 }
80 112
81 113 $session_id = sanitize_text_field($_POST['session_id']);
114 +
115 + // SECURITY FIX: Verify session ownership before retrieving data
116 + $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
117 +
118 + // Check if this session has an owner recorded
119 + $session_owner = get_option("mxchat_session_owner_{$session_id}");
120 +
121 + // If session has an owner and it doesn't match current user, deny access
122 + if ($session_owner && $session_owner !== $current_user_identifier) {
123 + wp_send_json_error(['message' => esc_html__('Unauthorized access.', 'mxchat')]);
124 + wp_die();
125 + }
126 +
127 + // If no owner is set yet, claim ownership (for legacy sessions)
128 + if (!$session_owner) {
129 + update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
130 + }
131 +
82 132 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history
83 133 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai'); // Get current chat mode
84 134
85 135 if (empty($history)) {
@@ -96,24 +146,8 @@
96 146 'chat_mode' => $chat_mode
97 147 ]);
98 148 wp_die();
99 149 }
100 -private function mxchat_fetch_conversation_history_for_ajax($session_id) {
101 - $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history based on session ID
102 - $formatted_history = [];
103 -
104 - // Format the history to align with the expected structure for OpenAI
105 - foreach ($history as $entry) {
106 - $formatted_history[] = [
107 - 'role' => $entry['role'], // Ensure this matches 'user' or 'assistant'
108 - 'content' => $entry['content']
109 - ];
110 - }
111 -
112 - return $formatted_history;
113 -}
114 -
115 -
116 150 private function mxchat_fetch_conversation_history_for_ai($session_id) {
117 151 $history = get_option("mxchat_history_{$session_id}", []);
118 152 $formatted_history = [];
119 153
@@ -150,9 +184,9 @@
150 184 if (!$has_code && $clean_content !== strip_tags($entry['content'])) {
151 185 continue;
152 186 }
153 187
154 - // More accurate token estimation (1 token ≈ 4 characters)
188 + // More accurate token estimation (1 token ≈ 4 characters)
155 189 $token_estimate = ceil(mb_strlen($clean_content, 'UTF-8') / 4);
156 190
157 191 // Check token budget with the new estimate
158 192 if (($current_token_count + $token_estimate + $reserved_tokens) > $max_tokens) {
@@ -206,8 +240,14 @@
206 240 'methods' => 'POST',
207 241 'callback' => [$this, 'handle_slack_interaction'],
208 242 'permission_callback' => [$this, 'verify_slack_request'],
209 243 ]);
244 +
245 + register_rest_route('mxchat/v1', '/slack-messages', [
246 + 'methods' => 'POST',
247 + 'callback' => [$this, 'handle_slack_messages'],
248 + 'permission_callback' => [$this, 'verify_slack_request'],
249 + ]);
210 250
211 251 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
212 252 }
213 253
@@ -295,20 +335,45 @@
295 335
296 336
297 337
298 338
299 -private function mxchat_save_chat_message($session_id, $role, $message) {
339 +private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null) {
300 340 global $wpdb;
301 -
302 341 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
303 342 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
304 -
343 +
344 + // Check if this is the first message in a new session (before any other database operations)
345 + $is_new_session = false;
346 + if ($role === 'user') { // Only check for user messages, not bot responses
347 + $existing_messages = $wpdb->get_var($wpdb->prepare(
348 + "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
349 + $session_id
350 + ));
351 + $is_new_session = ($existing_messages == 0);
352 +
353 + // Log for debugging
354 + if ($is_new_session) {
355 + //error_log("[DEBUG] This is a NEW session - first message");
356 + }
357 + }
358 +
359 + // SECURITY FIX: Set session ownership for new sessions
360 + if ($is_new_session && $role === 'user') {
361 + $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
362 + $session_owner_key = "mxchat_session_owner_{$session_id}";
363 +
364 + // Only set ownership if not already set
365 + if (!get_option($session_owner_key)) {
366 + update_option($session_owner_key, $current_user_identifier, 'no');
367 + //error_log("[DEBUG] Set session ownership for {$session_id} to {$current_user_identifier}");
368 + }
369 + }
370 +
305 371 // 1) Extract agent name if present
306 372 $agent_name = '';
307 373 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
308 374 $agent_name = $matches[1];
309 375 $message = str_replace("Agent: $agent_name - ", '', $message);
310 -
311 376 $session_meta_key = "mxchat_agent_name_{$session_id}";
312 377 if (empty(get_option($session_meta_key))) {
313 378 update_option($session_meta_key, $agent_name);
314 379 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
@@ -313,42 +378,57 @@
313 378 update_option($session_meta_key, $agent_name);
314 379 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
315 380 }
316 381 }
317 -
382 +
318 383 // 2) Generate unique message_id
319 384 $message_id = uniqid();
320 385 //error_log("[DEBUG] mxchat_save_chat_message -> Generated message_id: {$message_id}");
321 -
386 +
322 387 // 3) Determine user_id
323 388 $user_id = is_user_logged_in() ? get_current_user_id() : 0;
324 -
389 +
325 390 // 4) Determine user_identifier
326 391 $user_identifier = $agent_name
327 392 ? $agent_name
328 393 : MxChat_User::mxchat_get_user_identifier();
329 -
394 +
330 395 // 5) Determine displayed_name
331 396 $user_email = MxChat_User::mxchat_get_user_email();
332 397 $displayed_name = $agent_name ? $agent_name : ($user_email ?: $user_identifier);
333 -
398 +
334 399 // 6) Check for a saved email in wp_options
335 400 $email_option_key = "mxchat_email_{$session_id}";
336 401 $saved_email = get_option($email_option_key);
337 402 //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}");
403 +
404 + // Check for a saved name in wp_options
405 + $name_option_key = "mxchat_name_{$session_id}";
406 + $saved_name = get_option($name_option_key);
407 + //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for name_option_key: {$name_option_key}, found: {$saved_name}");
408 +
409 + // If found, update DB user_email and user_name
410 + if ($saved_email || $saved_name) {
411 + $update_data = [];
412 + if ($saved_email) {
413 + $update_data['user_email'] = $saved_email;
414 + }
415 + if ($saved_name) {
416 + $update_data['user_name'] = $saved_name;
417 + }
418 +
419 + if (!empty($update_data)) {
420 + $update_res = $wpdb->update(
421 + $table_name,
422 + $update_data,
423 + ['session_id' => $session_id],
424 + array_fill(0, count($update_data), '%s'),
425 + ['%s']
426 + );
427 + //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email/user_name update for session_id {$session_id}. update_res: {$update_res}");
428 + }
349 429 }
350 -
430 +
351 431 // 7) Save to session history in wp_options
352 432 $history_key = "mxchat_history_{$session_id}";
353 433 $history = get_option($history_key, []);
354 434 $history[] = [
@@ -357,34 +437,150 @@
357 437 'content' => $message,
358 438 'timestamp' => round(microtime(true) * 1000),
359 439 'agent_name' => $displayed_name,
360 440 ];
361 - update_option($history_key, $history);
441 + update_option($history_key, $history, 'no');
362 442 //error_log("[DEBUG] mxchat_save_chat_message -> Updated session history in option: {$history_key}");
363 -
443 +
364 444 // 8) Save the message to DB (INSERT)
365 445 $insert_data = [
366 446 'user_id' => $user_id,
367 447 'user_identifier'=> $user_identifier,
368 448 'user_email' => $saved_email ?: $user_email,
449 + 'user_name' => $saved_name ?: '', // Add name to insert data
369 450 'session_id' => $session_id,
370 451 'role' => $role,
371 452 'message' => $message,
372 453 'timestamp' => current_time('mysql', 1),
373 454 ];
455 +
456 + // IMPROVED: Handle originating page data
457 + $columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
458 +
459 + if ($columns_exist) {
460 + if ($is_new_session && $role === 'user') {
461 + // For the first user message, set originating page data
462 +
463 + // First check if we have it from the parameter
464 + if ($originating_page && !empty($originating_page['url'])) {
465 + $insert_data['originating_page_url'] = $originating_page['url'];
466 + $insert_data['originating_page_title'] = $originating_page['title'] ?? '';
467 +
468 + //error_log("[DEBUG] Setting originating page from parameter: " . $originating_page['url']);
469 + }
470 + // Otherwise check if it's stored in the instance property
471 + else if (isset($this->pending_originating_page) && !empty($this->pending_originating_page['url'])) {
472 + $insert_data['originating_page_url'] = $this->pending_originating_page['url'];
473 + $insert_data['originating_page_title'] = $this->pending_originating_page['title'] ?? '';
474 +
475 + //error_log("[DEBUG] Setting originating page from pending_originating_page: " . $this->pending_originating_page['url']);
476 +
477 + // Clear after using
478 + unset($this->pending_originating_page);
479 + }
480 + // Fallback to HTTP_REFERER if nothing else is available
481 + else if (isset($_SERVER['HTTP_REFERER'])) {
482 + $referer_url = esc_url_raw($_SERVER['HTTP_REFERER']);
483 + $insert_data['originating_page_url'] = $referer_url;
484 +
485 + // Generate title from URL
486 + $parsed_url = parse_url($referer_url);
487 + $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
488 +
489 + if (empty($path) || $path === 'index.php' || $path === 'index.html') {
490 + $insert_data['originating_page_title'] = 'Homepage';
491 + } else {
492 + $title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
493 + $insert_data['originating_page_title'] = ucwords(trim($title));
494 + }
495 +
496 + //error_log("[DEBUG] Setting originating page from HTTP_REFERER: " . $referer_url);
497 + }
498 +
499 + // Store for this session so all messages have the same originating page
500 + if (!empty($insert_data['originating_page_url'])) {
501 + update_option("mxchat_originating_page_{$session_id}", [
502 + 'url' => $insert_data['originating_page_url'],
503 + 'title' => $insert_data['originating_page_title']
504 + ], 'no');
505 + }
506 + } else {
507 + // For subsequent messages in the session, use the stored originating page
508 + $stored_originating = get_option("mxchat_originating_page_{$session_id}");
509 + if ($stored_originating && !empty($stored_originating['url'])) {
510 + $insert_data['originating_page_url'] = $stored_originating['url'];
511 + $insert_data['originating_page_title'] = $stored_originating['title'] ?? '';
512 + }
513 + }
514 + }
515 +
374 516 $wpdb->insert($table_name, $insert_data);
375 517 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
376 -
518 +
519 + // 9) Send notification email if this is the first user message in a new session
520 + if ($wpdb->insert_id && $is_new_session && $role === 'user') {
521 + $this->send_new_chat_notification($session_id, array(
522 + 'identifier' => $user_identifier,
523 + 'email' => $saved_email ?: $user_email,
524 + 'ip' => $_SERVER['REMOTE_ADDR']
525 + ));
526 + }
527 +
377 528 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
378 529 return $message_id;
379 530 }
380 531
532 +private function send_new_chat_notification($session_id, $user_info = array()) {
533 + $options = get_option('mxchat_transcripts_options');
534 +
535 + // Check if notifications are enabled
536 + if (empty($options['mxchat_enable_notifications'])) {
537 + return false;
538 + }
539 +
540 + // Get notification email
541 + $to = !empty($options['mxchat_notification_email']) ?
542 + $options['mxchat_notification_email'] :
543 + get_option('admin_email');
544 +
545 + if (!is_email($to)) {
546 + return false;
547 + }
548 +
549 + // Prepare email content
550 + $subject = sprintf('[%s] New Chat Session Started', get_bloginfo('name'));
551 +
552 + $user_identifier = isset($user_info['identifier']) ? $user_info['identifier'] : 'Guest';
553 + $user_email = isset($user_info['email']) ? $user_info['email'] : 'Not provided';
554 + $user_ip = isset($user_info['ip']) ? $user_info['ip'] : $_SERVER['REMOTE_ADDR'];
555 +
556 + $message = sprintf(
557 + "A new chat session has started on your website.\n\n" .
558 + "Session ID: %s\n" .
559 + "User: %s\n" .
560 + "Email: %s\n" .
561 + "IP Address: %s\n" .
562 + "Time: %s\n\n" .
563 + "View transcripts: %s",
564 + $session_id,
565 + $user_identifier,
566 + $user_email,
567 + $user_ip,
568 + current_time('mysql'),
569 + admin_url('admin.php?page=mxchat-transcripts')
570 + );
571 +
572 + // Send email
573 + return wp_mail($to, $subject, $message);
574 +}
575 +
381 576 public function mxchat_handle_save_email_and_response() {
382 577 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
578 + //error_log('DEBUG: POST data: ' . print_r($_POST, true));
383 579
384 580 // Validate nonce
385 581 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'));
582 + //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
387 583 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
388 584 wp_die();
389 585 }
390 586
@@ -389,10 +585,11 @@
389 585 }
390 586
391 587 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
392 588 $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
589 + $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
393 590
394 - //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}");
591 + //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}, name: {$name}");
395 592
396 593 if (empty($session_id) || empty($email)) {
397 594 //error_log("[ERROR] Missing session_id or email: session_id={$session_id}, email={$email}");
398 595 wp_send_json_error(['message' => esc_html__('Session ID or email is missing.', 'mxchat')]);
@@ -398,13 +595,31 @@
398 595 wp_send_json_error(['message' => esc_html__('Session ID or email is missing.', 'mxchat')]);
399 596 wp_die();
400 597 }
401 598
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}");
599 + // Validate name if provided (check if name field is enabled and name is required)
600 + $options = get_option('mxchat_options', []);
601 + $name_field_enabled = isset($options['enable_name_field']) &&
602 + ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
603 +
604 + if ($name_field_enabled && (empty($name) || strlen(trim($name)) < 2 || strlen(trim($name)) > 100)) {
605 + //error_log("[ERROR] Invalid name: {$name} (enabled: {$name_field_enabled})");
606 + wp_send_json_error(['message' => esc_html__('Name must be between 2 and 100 characters.', 'mxchat')]);
607 + wp_die();
608 + }
406 609
610 + // 1) Always store email in wp_options
611 + $email_option_key = "mxchat_email_{$session_id}";
612 + update_option($email_option_key, $email);
613 + //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$email_option_key} => {$email}");
614 +
615 + // Store name in wp_options if provided
616 + if (!empty($name)) {
617 + $name_option_key = "mxchat_name_{$session_id}";
618 + update_option($name_option_key, $name);
619 + //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$name_option_key} => {$name}");
620 + }
621 +
407 622 // 2) (Optional) Also store in DB if a row already exists
408 623 global $wpdb;
409 624 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
410 625
@@ -414,21 +629,30 @@
414 629
415 630 //error_log("[DEBUG] handle_save_email_and_response -> session_count for {$session_id}: {$session_count} (SQL: {$sql})");
416 631
417 632 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 - );
633 + // Update both user_email and user_name if row(s) exist
634 + if (!empty($name)) {
635 + $update_sql = $wpdb->prepare(
636 + "UPDATE {$table_name} SET user_email = %s, user_name = %s WHERE session_id = %s",
637 + $email,
638 + $name,
639 + $session_id
640 + );
641 + } else {
642 + $update_sql = $wpdb->prepare(
643 + "UPDATE {$table_name} SET user_email = %s WHERE session_id = %s",
644 + $email,
645 + $session_id
646 + );
647 + }
424 648 $wpdb->query($update_sql);
425 649 //error_log("[DEBUG] handle_save_email_and_response -> DB updated: {$update_sql}");
426 650 } else {
427 - //error_log("[INFO] handle_save_email_and_response -> No DB entry for {$session_id}, so email is only in wp_options.");
651 + //error_log("[INFO] handle_save_email_and_response -> No DB entry for {$session_id}, so email/name is only in wp_options.");
428 652 }
429 653
430 - // Provide success response
654 + // Provide success response (same as original)
431 655 $bot_message = __('Thanks for providing your email! You can continue chatting now.', 'mxchat');
432 656 //error_log("[DEBUG] handle_save_email_and_response -> success, returning bot_message: {$bot_message}");
433 657 wp_send_json_success(['message' => $bot_message]);
434 658 wp_die();
@@ -451,113 +675,93 @@
451 675 // Check if the user is logged in
452 676 if (is_user_logged_in()) {
453 677 $current_user = wp_get_current_user();
454 678 //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]);
679 +
680 + // Get user's display name for logged in users
681 + $user_name = !empty($current_user->display_name) ? $current_user->display_name :
682 + (!empty($current_user->first_name) ? $current_user->first_name : '');
683 +
684 + $response_data = ['logged_in' => true, 'email' => $current_user->user_email];
685 + if (!empty($user_name)) {
686 + $response_data['name'] = $user_name;
687 + }
688 +
689 + wp_send_json_success($response_data);
456 690 }
457 691
458 - $option_key = "mxchat_email_{$session_id}";
459 - $stored_email = get_option($option_key, '');
692 + // Check if name field is required
693 + $options = get_option('mxchat_options', []);
694 + $name_field_enabled = isset($options['enable_name_field']) &&
695 + ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
460 696
461 - //error_log("[DEBUG] mxchat_check_email_provided -> Checking option: {$option_key}, found: {$stored_email}");
697 + $email_option_key = "mxchat_email_{$session_id}";
698 + $stored_email = get_option($email_option_key, '');
699 +
700 + // Check for stored name
701 + $name_option_key = "mxchat_name_{$session_id}";
702 + $stored_name = get_option($name_option_key, '');
462 703
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 -}
704 + //error_log("[DEBUG] mxchat_check_email_provided -> Checking email option: {$email_option_key}, found: {$stored_email}");
705 + //error_log("[DEBUG] mxchat_check_email_provided -> Checking name option: {$name_option_key}, found: {$stored_name}, required: " . ($name_field_enabled ? 'yes' : 'no'));
471 706
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';
707 + // Check if we have email and name (if name is required)
708 + $has_required_info = !empty($stored_email);
709 +
710 + if ($name_field_enabled) {
711 + $has_required_info = $has_required_info && !empty($stored_name);
480 712 }
481 713
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);
714 + if ($has_required_info) {
715 + //error_log("[DEBUG] mxchat_check_email_provided -> Required info found, returning success");
716 +
717 + $response_data = ['email' => $stored_email];
718 + if (!empty($stored_name)) {
719 + $response_data['name'] = $stored_name;
507 720 }
721 +
722 + wp_send_json_success($response_data);
723 + } else {
724 + //error_log("[DEBUG] mxchat_check_email_provided -> Required info missing, returning error");
725 + wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
508 726 }
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 727 }
514 728
729 +public function mxchat_handle_chat_request() {
730 + global $wpdb;
515 731
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 - }
732 + // Debug: Log incoming bot_id
733 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
734 + error_log("=== MXCHAT DEBUG: Starting chat request ===");
735 + error_log("MXCHAT DEBUG: Bot ID received: " . $bot_id);
736 +
737 + // Get bot-specific options
738 + $bot_options = $this->get_bot_options($bot_id);
739 + $current_options = !empty($bot_options) ? $bot_options : $this->options;
522 740
523 - $session_id = sanitize_text_field($_POST['session_id']);
524 - $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
525 -
526 - // Get chat history
527 - $history = get_option("mxchat_history_{$session_id}", []);
528 -
529 - if (empty($history)) {
530 - wp_send_json_success([
531 - 'hasNewMessages' => false,
532 - 'new_messages' => []
533 - ]);
534 - wp_die();
741 + // Check if this is a streaming request
742 + $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat' &&
743 + isset($current_options['enable_streaming_toggle']) && $current_options['enable_streaming_toggle'] === 'on';
744 +
745 + // Set streaming headers if needed
746 + if ($is_streaming) {
747 + // Disable output buffering
748 + while (ob_get_level()) {
749 + ob_end_flush(); // Changed from ob_end_clean()
750 + }
751 +
752 + // Set headers for SSE
753 + header('Content-Type: text/event-stream');
754 + header('Cache-Control: no-cache');
755 + header('Connection: keep-alive');
756 + header('X-Accel-Buffering: no');
757 +
758 + // Add these new lines:
759 + ob_implicit_flush(true);
760 + flush();
535 761 }
536 762
537 - // Filter new messages
538 - $new_messages = array_filter($history, function($message) use ($last_seen_id) {
539 - return isset($message['id']) && $message['id'] > $last_seen_id;
540 - });
541 -
542 - // Sort by ID to ensure proper order
543 - usort($new_messages, function($a, $b) {
544 - return $a['id'] <=> $b['id'];
545 - });
546 -
547 - wp_send_json_success([
548 - 'hasNewMessages' => !empty($new_messages),
549 - 'new_messages' => array_values($new_messages),
550 - 'latestMessageId' => end($new_messages)['id'] ?? $last_seen_id
551 - ]);
552 - wp_die();
553 -}
554 -
555 -public function mxchat_handle_chat_request() {
556 - global $wpdb;
557 -
558 -
559 - // Check if MX Chat Moderation is active
763 + // Check if MX Chat Moderation is active
560 764 if (class_exists('MX_Chat_Moderation')) {
561 765 // Get user email and IP
562 766 $user_email = '';
563 767 $user_ip = $_SERVER['REMOTE_ADDR'];
@@ -591,14 +795,12 @@
591 795 wp_die();
592 796 }
593 797 }
594 798
595 -
596 - // Reset fallback response at the start of each request
597 799 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
598 800 $this->productCardHtml = '';
599 801
600 - // Get the actual WordPress user ID if logged in
802 + // Get the actual WordPress user ID if logged in
601 803 $is_logged_in = is_user_logged_in();
602 804 if ($is_logged_in) {
603 805 $user_id = get_current_user_id(); // This will get the actual WordPress user ID
604 806 } else {
@@ -608,65 +810,24 @@
608 810
609 811 // Get and sanitize the user identifier
610 812 $user_id = sanitize_key($user_id);
611 813
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'));
814 + // Check rate limit using new settings structure
815 + $rate_limit_result = $this->check_rate_limit();
615 816
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);
817 + if ($rate_limit_result !== true) {
818 + wp_send_json([
819 + 'success' => false,
820 + 'message' => $rate_limit_result['message'],
821 + 'status' => 'rate_limit_exceeded'
822 + ]);
823 + wp_die();
661 824 }
662 825
663 826 // Rest of your existing code...
664 827 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
665 - //error_log("Session ID: $session_id");
666 828
667 829 if (empty($session_id)) {
668 - //error_log("Error: Session ID is missing.");
669 830 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
670 831 wp_die();
671 832 }
672 833
@@ -671,116 +832,296 @@
671 832 }
672 833
673 834 // Validate and sanitize the incoming message
674 835 if (empty($_POST['message'])) {
675 - //error_log("Error: No message received.");
676 836 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
677 837 wp_die();
678 838 }
839 +
840 +
841 + // Track originating page for first message in session
842 + $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
679 843
844 + // Check if originating page columns exist
845 + $columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
680 846
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 -];
847 + if ($columns_exist) {
848 + // Check if this session already has messages
849 + $message_count = $wpdb->get_var($wpdb->prepare(
850 + "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
851 + $session_id
852 + ));
853 +
854 + // If this is the first message in the session
855 + if ($message_count == 0) {
856 + // Get originating page from JavaScript (preferred) or HTTP_REFERER (fallback)
857 + $originating_url = '';
858 + $originating_title = '';
859 +
860 + // Try to get from POST data first (sent by JavaScript)
861 + if (isset($_POST['current_page_url'])) {
862 + $originating_url = esc_url_raw($_POST['current_page_url']);
863 + $originating_title = isset($_POST['current_page_title'])
864 + ? sanitize_text_field($_POST['current_page_title'])
865 + : '';
866 + }
867 + // Fallback to HTTP_REFERER if not provided by JavaScript
868 + else if (isset($_SERVER['HTTP_REFERER'])) {
869 + $originating_url = esc_url_raw($_SERVER['HTTP_REFERER']);
870 + }
871 +
872 + // Generate title if we have URL but no title
873 + if ($originating_url && empty($originating_title)) {
874 + $parsed_url = parse_url($originating_url);
875 + $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
876 +
877 + if (empty($path) || $path === 'index.php' || $path === 'index.html') {
878 + $originating_title = 'Homepage';
879 + } else {
880 + // Clean up the path to make a readable title
881 + $originating_title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
882 + $originating_title = ucwords(trim($originating_title));
883 + }
884 + }
885 +
886 + // Store for later use when saving the message
887 + $this->pending_originating_page = [
888 + 'url' => $originating_url,
889 + 'title' => $originating_title
890 + ];
891 + }
892 + }
893 +
894 +
688 895
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']);
896 + // Get page context if provided
897 + $page_context = null;
898 + if (isset($_POST['page_context']) && !empty($_POST['page_context'])) {
899 + $page_context_raw = stripslashes($_POST['page_context']);
900 + $page_context = json_decode($page_context_raw, true);
901 +
902 + // Validate page context structure
903 + if (is_array($page_context) &&
904 + isset($page_context['url']) &&
905 + isset($page_context['title']) &&
906 + isset($page_context['content'])) {
907 +
908 + // Sanitize page context
909 + $page_context['url'] = esc_url_raw($page_context['url']);
910 + $page_context['title'] = sanitize_text_field($page_context['title']);
911 + $page_context['content'] = wp_kses_post($page_context['content']);
912 + } else {
913 + $page_context = null;
914 + }
915 + }
693 916
694 -// Then apply sanitization
695 -$message = wp_kses($message, $allowed_tags);
917 + // Modify the message sanitization to preserve PHP tags in code blocks
918 + $allowed_tags = [
919 + 'pre' => [],
920 + 'code' => ['class' => true],
921 + 'span' => ['class' => true],
922 + 'div' => ['class' => true],
923 + ];
696 924
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);
925 + // First preserve code blocks
926 + $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
927 + return htmlspecialchars_decode($matches[0]);
928 + }, $_POST['message']);
701 929
702 -$message = trim($message);
930 + // Then apply sanitization
931 + $message = wp_kses($message, $allowed_tags);
703 932
704 -// Preserve code blocks from markdown conversion
705 -$message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
933 + // Preserve code blocks from markdown conversion
934 + $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
935 + $message = apply_filters('mxchat_filter_message', $message, 'prompt', $session_id);
706 936
707 - // Save the user's message
708 - $this->mxchat_save_chat_message($session_id, 'user', $message);
937 + // ===== SIMPLIFIED TESTING PANEL INITIALIZATION =====
938 + // Always initialize testing data for admins (no toggle needed)
939 + $testing_data = null;
940 + if (current_user_can('administrator')) {
941 + // For vision messages, use the original user message for the query display
942 + $query_for_testing = $message;
943 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
944 + $query_for_testing = sanitize_textarea_field($_POST['original_user_message']);
945 + }
946 +
947 + $testing_data = [
948 + 'query' => $query_for_testing,
949 + 'timestamp' => time(),
950 + 'top_matches' => [],
951 + 'action_matches' => [], // Initialize action matches array
952 + 'page_context' => $page_context, // Include page context in testing data
953 + 'is_vision' => isset($_POST['vision_processed']) && $_POST['vision_processed'],
954 + 'bot_id' => $bot_id // Include bot ID in testing data
955 + ];
956 +
957 + // Get similarity threshold from bot options or default options
958 + $similarity_threshold = isset($current_options['similarity_threshold'])
959 + ? ((int) $current_options['similarity_threshold']) / 100
960 + : 0.35;
961 +
962 + $testing_data['similarity_threshold'] = $similarity_threshold;
963 +
964 + // Determine knowledge base type using bot-specific config
965 + $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
966 + $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
967 + $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
968 + }
969 + // ===== END SIMPLIFIED TESTING INITIALIZATION =====
709 970
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);
971 + // Add debug before and after:
972 + //error_log('MxChat Core: About to call mxchat_pre_process_message filter with message: ' . $message);
973 + $pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
974 + //error_log('MxChat Core: Filter returned: ' . (is_array($pre_processed_result) ? 'array' : $pre_processed_result));
714 975
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 976
719 - wp_send_json([
720 - 'success' => true,
721 - 'status' => 'email_captured',
722 - 'message' => $response_message
723 - ]);
724 - wp_die();
725 - }
977 + // If the pre-processing returned a result (not the original message), use it directly
978 + if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
979 + // Save the AI response
980 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
981 +
982 + // Save HTML content if provided
983 + if (!empty($pre_processed_result['html'])) {
984 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
985 + }
986 +
987 + // Add testing data if admin
988 + $response_data = [
989 + 'text' => $pre_processed_result['text'],
990 + 'html' => $pre_processed_result['html'] ?? '',
991 + 'session_id' => $session_id
992 + ];
993 +
994 + if ($testing_data !== null) {
995 + $response_data['testing_data'] = $testing_data;
996 + }
997 +
998 + wp_send_json($response_data);
999 + wp_die();
1000 + }
726 1001
727 - $intent_info = '';
1002 + // Save the user's message - handle vision processed messages differently
1003 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
1004 + // For vision messages, save the original user message with image indicator
1005 + $original_message = sanitize_textarea_field($_POST['original_user_message']);
1006 + if (isset($_POST['vision_images_count']) && $_POST['vision_images_count'] > 0) {
1007 + $image_count = intval($_POST['vision_images_count']);
1008 + $original_message .= " [{$image_count} image(s)]";
1009 + }
1010 + $this->mxchat_save_chat_message($session_id, 'user', $original_message);
1011 + } else {
1012 + // Regular message - save as normal
1013 + $this->mxchat_save_chat_message($session_id, 'user', $message);
1014 + }
728 1015
729 - // Check chat mode
730 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
731 - //error_log("Chat Mode: $chat_mode");
1016 +
1017 + if (is_email($message)) {
1018 + // Add the email to Loops
1019 + $this->add_email_to_loops($message);
1020 +
1021 + // Get the user's success message instruction using current_options
1022 + $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
1023 +
1024 + // Set instruction for AI using the user's success message
1025 + $this->current_action_instruction = $user_success_message;
1026 +
1027 + // Clear the email capture transient since we got the email
1028 + delete_transient('mxchat_email_capture_' . $user_id);
1029 + }
1030 +
1031 + // Check if we're in an email capture flow but user hasn't provided email yet
1032 + elseif (get_transient('mxchat_email_capture_' . $user_id)) {
1033 + // Check if the message contains an email (not the whole message being an email)
1034 + if (preg_match('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', $message, $matches)) {
1035 + $extracted_email = $matches[0];
1036 +
1037 + // Add the extracted email to Loops
1038 + $this->add_email_to_loops($extracted_email);
1039 +
1040 + // Get the user's success message instruction using current_options
1041 + $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
1042 +
1043 + // Set instruction for AI using the user's success message
1044 + $this->current_action_instruction = $user_success_message;
1045 +
1046 + // Clear the email capture transient since we got the email
1047 + delete_transient('mxchat_email_capture_' . $user_id);
1048 + }
1049 + // If no email found but we're in capture mode, remind them
1050 + else {
1051 + // Get the original instruction to remind them using current_options
1052 + $original_instruction = $current_options['triggered_phrase_response'] ?? __("Please provide your email address.", 'mxchat');
1053 + $this->current_action_instruction = $original_instruction;
1054 + }
1055 + }
732 1056
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);
1057 + $intent_info = '';
737 1058
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");
1059 + // Check chat mode
1060 + $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
741 1061
742 - // Update chat mode first
743 - update_option("mxchat_mode_{$session_id}", 'ai');
1062 + // Handle agent mode
1063 + // Handle agent mode
1064 + if ($chat_mode === 'agent') {
1065 + // First, check for switch intent before doing anything else
1066 + $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
744 1067
745 - // Clear any existing PDF context to start fresh
746 - $this->clear_pdf_transients($session_id);
1068 + // Capture action analysis for testing panel after intent check
1069 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1070 + $testing_data['action_matches'] = $this->last_action_analysis;
1071 + }
1072 +
1073 + // Around line 506, in the agent mode handling section:
1074 + if ($intent_matched && !empty($this->fallbackResponse['text'])) {
1075 + // Update chat mode first
1076 + update_option("mxchat_mode_{$session_id}", 'ai');
1077 +
1078 + // Clear any existing PDF context to start fresh
1079 + $this->clear_pdf_transients($session_id);
1080 +
1081 + // Prepare clean switch response with explicit chat_mode
1082 + $response_data = [
1083 + 'text' => $this->fallbackResponse['text'],
1084 + 'html' => $this->fallbackResponse['html'] ?? '',
1085 + 'session_id' => $session_id,
1086 + 'chat_mode' => 'ai' // EXPLICITLY SET THIS
1087 + ];
1088 +
1089 + if ($testing_data !== null) {
1090 + $response_data['testing_data'] = $testing_data;
1091 + }
1092 +
1093 + // Save the mode switch message
1094 + $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
1095 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1096 +
1097 + // Send response and exit
1098 + wp_send_json($response_data);
1099 + wp_die();
1100 + } elseif (!$intent_matched) {
1101 + // No intent matched, handle live agent message
1102 + try {
1103 + $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
747 1104
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 - ];
1105 + $agent_response = [
1106 + 'status' => 'waiting_for_agent',
1107 + 'message' => esc_html__('Message sent to live agent.', 'mxchat')
1108 + ];
1109 +
1110 + if ($testing_data !== null) {
1111 + $agent_response['testing_data'] = $testing_data;
1112 + }
755 1113
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'));
1114 + wp_send_json_success($agent_response);
1115 + } catch (\Exception $e) {
1116 + wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
1117 + }
1118 + wp_die();
776 1119 }
777 - wp_die();
778 1120 }
779 - }
780 1121
781 1122 // Step 1: Check for new PDF URL in the message
782 - if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1123 + if (!isset($_POST['vision_processed']) && preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
783 1124 $new_pdf_url = $matches[0];
784 1125
785 1126 // Check if this is likely a PDF-related request
786 1127 $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
@@ -802,15 +1143,15 @@
802 1143
803 1144 // Clear previous PDF transients
804 1145 $this->clear_pdf_transients($session_id);
805 1146
806 - // Process new PDF
807 - $max_pages = $this->options['pdf_max_pages'] ?? 69;
1147 + // Process new PDF using current_options
1148 + $max_pages = $current_options['pdf_max_pages'] ?? 69;
808 1149 $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
809 1150
810 1151 if ($embeddings === 'too_many_pages') {
811 1152 $error_text = sprintf(
812 - $this->options['pdf_intent_error_text'] ??
1153 + $current_options['pdf_intent_error_text'] ??
813 1154 esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
814 1155 $max_pages
815 1156 );
816 1157 $this->fallbackResponse['text'] = $error_text;
@@ -815,15 +1156,13 @@
815 1156 );
816 1157 $this->fallbackResponse['text'] = $error_text;
817 1158 } elseif ($embeddings) {
818 1159 // Store new PDF information
819 - // Create a more meaningful filename from URL
820 1160 $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
821 1161
822 - // If the filename is generic (like results_download.php), create a more descriptive one
1162 + // If the filename is generic, create a more descriptive one
823 1163 if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
824 1164 strpos($pdf_filename, '.php') !== false) {
825 - // Create a timestamp-based name
826 1165 $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
827 1166 }
828 1167
829 1168 set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
@@ -830,77 +1169,238 @@
830 1169 set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
831 1170 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
832 1171 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
833 1172
834 - $success_text = $this->options['pdf_intent_success_text'] ??
1173 + $success_text = $current_options['pdf_intent_success_text'] ??
835 1174 esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
836 1175
837 - // Return success with filename for UI update
838 - wp_send_json([
1176 + $pdf_response = [
839 1177 'success' => true,
840 1178 'message' => $success_text,
841 1179 'data' => [
842 1180 'filename' => $pdf_filename
843 1181 ]
844 - ]);
1182 + ];
1183 +
1184 + if ($testing_data !== null) {
1185 + $pdf_response['testing_data'] = $testing_data;
1186 + }
1187 +
1188 + wp_send_json($pdf_response);
845 1189 wp_die();
846 1190 } else {
847 - $error_text = $this->options['pdf_intent_error_text'] ??
1191 + $error_text = $current_options['pdf_intent_error_text'] ??
848 1192 esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
849 1193 $this->fallbackResponse['text'] = $error_text;
850 1194 }
851 1195
852 - wp_send_json([
1196 + $pdf_error_response = [
853 1197 'success' => false,
854 1198 'message' => $this->fallbackResponse['text']
855 - ]);
1199 + ];
1200 +
1201 + if ($testing_data !== null) {
1202 + $pdf_error_response['testing_data'] = $testing_data;
1203 + }
1204 +
1205 + wp_send_json($pdf_error_response);
856 1206 wp_die();
857 1207 }
858 1208 }
859 1209 }
860 1210
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 1211
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 - }
1212 + // Step 2: Detect intent and handle intent-based responses
1213 + $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
877 1214
878 - // If no intent matched or product not found, proceed with AI response
879 - //error_log("No matching intent or fallback. Generating AI response.");
1215 + // Capture action analysis for testing panel after intent check
1216 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1217 + $testing_data['action_matches'] = $this->last_action_analysis;
1218 + }
880 1219
881 - // Step 4: Generate AI response
882 - $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
883 - $this->mxchat_increment_chat_count();
1220 + // Step 3: Handle the intent result appropriately
1221 + if ($intent_result !== false) {
1222 + // Intent was matched - ALWAYS send as JSON response, never streaming
1223 +
1224 + if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
1225 + // Intent returned a direct response array
1226 + $response_data = [
1227 + 'text' => $intent_result['text'] ?? '',
1228 + 'html' => $intent_result['html'] ?? '',
1229 + 'session_id' => $session_id
1230 + ];
884 1231
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 - }
1232 + // IMPORTANT: Include chat_mode if present (for WhatsApp, Slack, etc.)
1233 + if (isset($intent_result['chat_mode'])) {
1234 + $response_data['chat_mode'] = $intent_result['chat_mode'];
1235 + }
892 1236
893 - // Build context with both knowledge base and PDF content if available
894 - $context_content = "User asked: '{$message}'\n\n";
1237 + if ($testing_data !== null) {
1238 + $response_data['testing_data'] = $testing_data;
1239 + }
1240 +
1241 + // Clear streaming headers if they were set
1242 + if ($is_streaming) {
1243 + header_remove('Content-Type');
1244 + header_remove('Cache-Control');
1245 + header_remove('Connection');
1246 + header_remove('X-Accel-Buffering');
1247 + header('Content-Type: application/json');
1248 + }
1249 +
1250 + wp_send_json($response_data);
1251 + wp_die();
1252 + } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1253 + // Intent returned true and set fallbackResponse
1254 +
1255 + // SAVE TO TRANSCRIPT FIRST
1256 + if (!empty($this->fallbackResponse['text'])) {
1257 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1258 + }
1259 + if (!empty($this->fallbackResponse['html'])) {
1260 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1261 + }
1262 +
1263 + $response_data = [
1264 + 'text' => $this->fallbackResponse['text'] ?? '',
1265 + 'html' => $this->fallbackResponse['html'] ?? '',
1266 + 'session_id' => $session_id
1267 + ];
1268 +
1269 + if (isset($this->fallbackResponse['chat_mode'])) {
1270 + $response_data['chat_mode'] = $this->fallbackResponse['chat_mode'];
1271 + }
1272 +
1273 + if ($testing_data !== null) {
1274 + $response_data['testing_data'] = $testing_data;
1275 + }
1276 +
1277 + // Clear streaming headers if they were set
1278 + if ($is_streaming) {
1279 + header_remove('Content-Type');
1280 + header_remove('Cache-Control');
1281 + header_remove('Connection');
1282 + header_remove('X-Accel-Buffering');
1283 + header('Content-Type: application/json');
1284 + }
1285 +
1286 + wp_send_json($response_data);
1287 + wp_die();
1288 + }
1289 + }
895 1290
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 - }
1291 + // If we get here, no intent matched OR the intent didn't provide a usable response
1292 +
1293 + // Step 4: Generate AI response
1294 + $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
1295 + $this->mxchat_increment_chat_count();
1296 +
1297 + // Generate embedding for the user's query - USE BOT-SPECIFIC API KEY
1298 + $api_key = $current_options['api_key'] ?? $this->options['api_key'];
1299 + $user_message_embedding = $this->mxchat_generate_embedding($message, $api_key);
1300 +
1301 + // Check if the embedding generation returned an error
1302 + if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1303 + $error_message = $user_message_embedding['error'];
1304 + $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
1305 +
1306 + wp_send_json_error([
1307 + 'error_message' => $error_message,
1308 + 'error_code' => $error_code
1309 + ]);
1310 + wp_die();
1311 + }
1312 +
1313 + // Check if the embedding is valid
1314 + if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1315 + wp_send_json_error([
1316 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1317 + 'error_code' => 'invalid_embedding'
1318 + ]);
1319 + wp_die();
1320 + }
901 1321
1322 + // Build context with both knowledge base and PDF content if available
1323 + $context_content = "User asked: '{$message}'\n\n";
1324 +
1325 + // Add action instruction if present (add this right after the above line)
1326 + if (!empty($this->current_action_instruction)) {
1327 + $context_content .= "===== SPECIAL INSTRUCTION =====\n";
1328 + $context_content .= "IMPORTANT: " . $this->current_action_instruction . "\n";
1329 + $context_content .= "Respond naturally and conversationally while following this instruction.\n";
1330 + $context_content .= "===== END SPECIAL INSTRUCTION =====\n\n";
1331 +
1332 + // Clear the instruction after using it
1333 + $this->current_action_instruction = null;
1334 + }
902 1335
1336 +
1337 + // Add page context if available and contextual awareness is enabled using current_options
1338 + if ($page_context && isset($current_options['contextual_awareness_toggle']) && $current_options['contextual_awareness_toggle'] === 'on') {
1339 + $context_content .= "===== CURRENT PAGE CONTEXT =====\n";
1340 + $context_content .= "Page URL: " . $page_context['url'] . "\n";
1341 + $context_content .= "Page Title: " . $page_context['title'] . "\n";
1342 + $context_content .= "Page Content: " . $page_context['content'] . "\n";
1343 + $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1344 + }
1345 +
1346 + // Get relevant content from knowledge base - PASS BOT_ID
1347 + $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding, $bot_id);
1348 +
1349 + // NEW: Also extract URLs from system instructions
1350 + $system_instructions = $this->get_system_instructions($bot_id);
1351 + if (!empty($system_instructions)) {
1352 + preg_match_all(
1353 + '#\bhttps?://[^\s<>"\']+#i',
1354 + $system_instructions,
1355 + $system_instruction_urls
1356 + );
1357 +
1358 + if (!empty($system_instruction_urls[0])) {
1359 + // Merge with existing valid URLs
1360 + $this->current_valid_urls = array_merge(
1361 + $this->current_valid_urls,
1362 + $system_instruction_urls[0]
1363 + );
1364 + // Remove duplicates
1365 + $this->current_valid_urls = array_unique($this->current_valid_urls);
1366 +
1367 + error_log("Added " . count($system_instruction_urls[0]) . " URLs from system instructions");
1368 + }
1369 + }
1370 +
1371 +// ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1372 +if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1373 + // Update testing data with the REAL similarity analysis
1374 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1375 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1376 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1377 +}
1378 +// ===== END SIMILARITY DATA CAPTURE =====
1379 +
1380 +// NEW: Add valid URLs to testing data for admin panel display (AFTER similarity data)
1381 +if ($testing_data !== null && !empty($this->current_valid_urls)) {
1382 + $testing_data['approved_urls'] = array_values($this->current_valid_urls);
1383 + error_log("Added " . count($this->current_valid_urls) . " approved URLs to testing data");
1384 +}
1385 +
1386 + if (!empty($relevant_content)) {
1387 + $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1388 + } else {
1389 + $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
1390 + }
1391 +
1392 + // NEW: Add approved URLs list to context for AI
1393 + if (!empty($this->current_valid_urls)) {
1394 + $context_content .= "===== APPROVED URLS FOR CITATIONS =====\n";
1395 + $context_content .= "You may ONLY use these exact URLs in your response:\n";
1396 + foreach ($this->current_valid_urls as $url) {
1397 + $context_content .= "- " . $url . "\n";
1398 + }
1399 + $context_content .= "\nCRITICAL: Do NOT create, modify, extend, or invent any other URLs. ";
1400 + $context_content .= "===== END APPROVED URLS =====\n\n";
1401 + }
1402 +
903 1403 // Check for and include PDF content
904 1404 $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
905 1405 $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
906 1406 $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
@@ -928,76 +1428,200 @@
928 1428 }
929 1429 $context_content .= "\n";
930 1430 }
931 1431 }
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 - );
1432 +
1433 + $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
941 1434
942 - $this->mxchat_save_chat_message($session_id, 'bot', $response);
1435 + // Extract model from current options for bot-specific model support
1436 + $selected_model = isset($current_options['model']) ? $current_options['model'] : 'gpt-4o';
1437 +
1438 + $response = $this->mxchat_generate_response(
1439 + $context_content,
1440 + $current_options['api_key'] ?? $this->options['api_key'],
1441 + $current_options['xai_api_key'] ?? $this->options['xai_api_key'],
1442 + $current_options['claude_api_key'] ?? $this->options['claude_api_key'],
1443 + $current_options['deepseek_api_key'] ?? $this->options['deepseek_api_key'],
1444 + $current_options['gemini_api_key'] ?? $this->options['gemini_api_key'],
1445 + $current_options['openrouter_api_key'] ?? $this->options['openrouter_api_key'],
1446 + $conversation_history,
1447 + $is_streaming,
1448 + $session_id,
1449 + $testing_data,
1450 + $selected_model
1451 + );
1452 +
1453 + // Handle streaming vs non-streaming responses
1454 + if ($is_streaming) {
1455 + // Check if streaming actually happened or if it fell back to regular response
1456 + if ($response === true) {
1457 + wp_die();
1458 + }
1459 + // If we get here, streaming fell back to regular response, continue
1460 + }
1461 +
1462 + // Check if the response is an error array
1463 + if (is_array($response) && isset($response['error'])) {
1464 + wp_send_json_error([
1465 + 'error_message' => $response['error'],
1466 + 'error_code' => $response['error_code'] ?? 'api_error'
1467 + ]);
1468 + wp_die();
1469 + }
1470 +
1471 + // DEBUG: Check what we have
1472 + error_log("=== BEFORE URL VALIDATION ===");
1473 + error_log("current_valid_urls is empty? " . (empty($this->current_valid_urls) ? 'YES' : 'NO'));
1474 + error_log("current_valid_urls count: " . count($this->current_valid_urls));
1475 + error_log("current_valid_urls content: " . print_r($this->current_valid_urls, true));
1476 +
1477 + // If we get here, the response is valid text - now validate URLs
1478 + if (!empty($this->current_valid_urls)) {
1479 + error_log("CALLING validate_and_clean_urls");
1480 + $response = $this->validate_and_clean_urls($response, $this->current_valid_urls);
1481 + } else {
1482 + error_log("SKIPPING validation - current_valid_urls is empty");
1483 + }
1484 + // ===== END URL VALIDATION =====
1485 +
1486 + // Save the cleaned response
1487 + $this->mxchat_save_chat_message($session_id, 'bot', $response);
943 1488
944 - // Step 5: Save additional content if available
945 - if (!empty($this->productCardHtml)) {
946 - $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
1489 + // Step 5: Save additional content if available
1490 + if (!empty($this->productCardHtml)) {
1491 + $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
1492 + }
1493 +
1494 + if (!empty($this->fallbackResponse['html'])) {
1495 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1496 + }
1497 +
1498 + // Step 6: Return the response
1499 + $response_data = [
1500 + 'text' => $response,
1501 + 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1502 + 'session_id' => $session_id
1503 + ];
1504 +
1505 + // Always add testing data for admins (no toggle needed)
1506 + if ($testing_data !== null) {
1507 + $response_data['testing_data'] = $testing_data;
1508 + }
1509 +
1510 + wp_send_json($response_data);
1511 + wp_die();
1512 +}
1513 +
1514 +/**
1515 + * Get bot-specific options for multi-bot functionality
1516 + * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
1517 + */
1518 +// Also debug the bot options retrieval
1519 +private function get_bot_options($bot_id = 'default') {
1520 + error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
1521 +
1522 + if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1523 + error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
1524 + return array();
947 1525 }
1526 +
1527 + $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
1528 +
1529 + if (!empty($bot_options)) {
1530 + error_log("MXCHAT DEBUG: Got bot-specific options from filter");
1531 + if (isset($bot_options['similarity_threshold'])) {
1532 + error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
1533 + }
1534 + }
1535 +
1536 + return is_array($bot_options) ? $bot_options : array();
1537 +}
948 1538
949 - if (!empty($this->fallbackResponse['html'])) {
950 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1539 +/**
1540 + * Get bot-specific Pinecone configuration
1541 + * Used in the knowledge retrieval functions
1542 + */
1543 +// Also add debugging to your get_bot_pinecone_config function
1544 +private function get_bot_pinecone_config($bot_id = 'default') {
1545 + error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
1546 +
1547 + // If default bot or multi-bot add-on not active, use default Pinecone config
1548 + if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1549 + error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
1550 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
1551 + $config = array(
1552 + 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
1553 + 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
1554 + 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
1555 + 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
1556 + );
1557 + error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
1558 + return $config;
951 1559 }
1560 +
1561 + error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
1562 +
1563 + // Hook for multi-bot add-on to provide bot-specific Pinecone config
1564 + $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
1565 +
1566 + if (!empty($bot_pinecone_config)) {
1567 + error_log("MXCHAT DEBUG: Got bot-specific config from filter");
1568 + error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
1569 + error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
1570 + error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
1571 + } else {
1572 + error_log("MXCHAT DEBUG: Filter returned empty config!");
1573 + }
1574 +
1575 + return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
1576 +}
952 1577
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 - ];
959 1578
960 - wp_send_json($response_data);
961 - wp_die();
962 -}
963 -
964 -// New function to check intents and invoke the callback function
1579 +// Updated function to check intents and invoke the callback function
965 1580 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
966 1581 global $wpdb;
967 1582 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
968 1583
969 - //error_log('🔍 MXCHAT DEBUG: Intent Check Started ==================');
970 - //error_log("🔍 MXCHAT DEBUG: Message: '$message'");
971 - //error_log("🔍 MXCHAT DEBUG: Chat Mode: $chat_mode");
1584 + // Get the current bot_id
1585 + $current_bot_id = $this->get_current_bot_id($session_id);
972 1586
973 1587 // Generate the user embedding
974 - //error_log('🔄 MXCHAT DEBUG: Generating user embedding');
975 1588 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
976 - if (!is_array($user_embedding)) {
977 - //error_log('❌ MXCHAT DEBUG: Failed to generate user embedding');
978 - return false;
1589 +
1590 + // Check if embedding generation returned an error
1591 + if (is_array($user_embedding) && isset($user_embedding['error'])) {
1592 + $error_message = $user_embedding['error'];
1593 + $error_code = $user_embedding['error_code'] ?? 'embedding_error';
1594 +
1595 + wp_send_json_error([
1596 + 'error_message' => $error_message,
1597 + 'error_code' => $error_code
1598 + ]);
1599 + wp_die();
979 1600 }
980 - //error_log('✅ MXCHAT DEBUG: User embedding generated successfully');
981 1601
1602 + // Check if embedding is valid
1603 + if (!is_array($user_embedding) || empty($user_embedding)) {
1604 + wp_send_json_error([
1605 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1606 + 'error_code' => 'invalid_embedding'
1607 + ]);
1608 + wp_die();
1609 + }
1610 +
982 1611 // Fetch intents from the database
983 1612 $table_name = $wpdb->prefix . 'mxchat_intents';
984 1613 if ($chat_mode === 'agent') {
985 - //error_log('🔍 MXCHAT DEBUG: Agent mode - fetching only chatbot switch intent');
986 1614 $query = $wpdb->prepare(
987 - "SELECT * FROM $table_name WHERE callback_function = %s",
1615 + "SELECT * FROM $table_name WHERE callback_function = %s AND (enabled = 1 OR enabled IS NULL)",
988 1616 'mxchat_handle_switch_to_chatbot_intent'
989 1617 );
990 1618 $intents = $wpdb->get_results($query);
991 1619 } else {
992 - //error_log('🔍 MXCHAT DEBUG: AI mode - fetching all intents');
993 - $intents = $wpdb->get_results("SELECT * FROM $table_name");
1620 + $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
994 1621 }
995 1622
996 - //error_log('🔍 MXCHAT DEBUG: Found ' . count($intents) . ' intents to check');
997 -
998 1623 if (empty($intents)) {
999 - //error_log('❌ MXCHAT DEBUG: No intents found in database');
1000 1624 return false;
1001 1625 }
1002 1626
1003 1627 $highest_similarity = -INF;
@@ -1002,12 +1626,23 @@
1002 1626
1003 1627 $highest_similarity = -INF;
1004 1628 $matched_intent = null;
1005 1629
1006 - //error_log('📊 MXCHAT DEBUG: Intent Similarity Scores ==================');
1630 + // Array to store action analysis for testing panel
1631 + $action_analysis = [];
1632 +
1007 1633 foreach ($intents as $intent) {
1008 - //error_log("🔄 MXCHAT DEBUG: Checking intent: '{$intent->intent_label}' (callback: {$intent->callback_function})");
1634 + // Additional check for enabled state
1635 + $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
1636 + if (!$is_enabled) {
1637 + continue;
1638 + }
1009 1639
1640 + // Check if this action is enabled for the current bot
1641 + if (!$this->is_action_enabled_for_bot($intent, $current_bot_id)) {
1642 + continue;
1643 + }
1644 +
1010 1645 $intent_embedding_serialized = $intent->embedding_vector;
1011 1646 $intent_embedding = $intent_embedding_serialized
1012 1647 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
1013 1648 : null;
@@ -1012,9 +1647,8 @@
1012 1647 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
1013 1648 : null;
1014 1649
1015 1650 if (!is_array($intent_embedding)) {
1016 - //error_log("❌ MXCHAT DEBUG: Invalid embedding for intent: {$intent->intent_label}");
1017 1651 continue;
1018 1652 }
1019 1653
1020 1654 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
@@ -1019,38 +1653,61 @@
1019 1653
1020 1654 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
1021 1655 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
1022 1656
1657 + // Store action analysis data for testing panel
1658 + $action_analysis[] = [
1659 + 'intent_label' => $intent->intent_label,
1660 + 'callback_function' => $intent->callback_function,
1661 + 'similarity' => round($similarity, 4),
1662 + 'similarity_percentage' => round($similarity * 100, 2),
1663 + 'threshold' => $intent_threshold,
1664 + 'threshold_percentage' => round($intent_threshold * 100, 2),
1665 + 'above_threshold' => $similarity >= $intent_threshold,
1666 + 'triggered' => false // Will be updated below if this intent is triggered
1667 + ];
1023 1668
1024 1669 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
1025 1670 $highest_similarity = $similarity;
1026 1671 $matched_intent = $intent;
1027 - //error_log("✅ MXCHAT DEBUG: New best match: '{$intent->intent_label}' with similarity {$similarity}");
1028 1672 }
1029 1673 }
1030 - //error_log('📊 MXCHAT DEBUG: End Intent Scores ==================');
1031 1674
1675 + // Mark the triggered action if any
1032 1676 if ($matched_intent) {
1033 - //error_log("🎯 MXCHAT DEBUG: Final Intent Match: '{$matched_intent->intent_label}'");
1034 - //error_log("🔄 MXCHAT DEBUG: Invoking callback: {$matched_intent->callback_function}");
1035 -
1677 + foreach ($action_analysis as &$action) {
1678 + if ($action['intent_label'] === $matched_intent->intent_label) {
1679 + $action['triggered'] = true;
1680 + break;
1681 + }
1682 + }
1683 + }
1684 +
1685 + // Sort actions by similarity (highest first) and store for testing panel
1686 + usort($action_analysis, function($a, $b) {
1687 + return $b['similarity'] <=> $a['similarity'];
1688 + });
1689 +
1690 + // Store action analysis for testing panel capture
1691 + $this->last_action_analysis = $action_analysis;
1692 +
1693 + // Around line 715 in your mxchat_check_intent_and_invoke_callback function
1694 + if ($matched_intent) {
1036 1695 // If the callback is a method on this instance (core callback), call it directly
1037 1696 if (method_exists($this, $matched_intent->callback_function)) {
1038 - //error_log('🔍 MXCHAT DEBUG: Using direct method call for core callback');
1039 1697 $callback_result = call_user_func(
1040 - [$this, $matched_intent->callback_function],
1041 - $message,
1042 - $user_id,
1043 - $session_id,
1044 - $matched_intent,
1045 - $user_context // Add user context
1046 - );
1698 + [$this, $matched_intent->callback_function],
1699 + $message,
1700 + $user_id,
1701 + $session_id,
1702 + $matched_intent,
1703 + $user_context ?? null
1704 + );
1047 1705 } else {
1048 - //error_log('🔍 MXCHAT DEBUG: Using apply_filters for add-on callback');
1049 1706 // Otherwise, use apply_filters for add-on callbacks
1050 1707 $callback_result = apply_filters(
1051 1708 $matched_intent->callback_function,
1052 - false, // default return value
1709 + false,
1053 1710 $message,
1054 1711 $user_id,
1055 1712 $session_id,
1056 1713 $matched_intent
@@ -1056,23 +1713,43 @@
1056 1713 $matched_intent
1057 1714 );
1058 1715 }
1059 1716
1060 - //error_log('🔍 MXCHAT DEBUG: Callback result type: ' . gettype($callback_result));
1717 + // Handle the callback result properly
1061 1718 if ($callback_result !== false) {
1062 - //error_log('✅ MXCHAT DEBUG: Intent handled successfully');
1063 - $this->fallbackResponse = $callback_result;
1064 - return true;
1719 + // If callback returned an array with chat_mode, use it directly
1720 + if (is_array($callback_result) && isset($callback_result['chat_mode'])) {
1721 + $this->fallbackResponse = $callback_result;
1722 + return $callback_result; // Return the full array
1723 + } else {
1724 + $this->fallbackResponse = $callback_result;
1725 + return true;
1726 + }
1065 1727 }
1066 - //error_log('❌ MXCHAT DEBUG: Callback returned false');
1067 - } else {
1068 - //error_log('❌ MXCHAT DEBUG: No matching intent found');
1069 1728 }
1070 1729
1071 - //error_log('🔍 MXCHAT DEBUG: Intent Check Completed ==================');
1072 1730 return false;
1073 1731 }
1074 1732
1733 +/**
1734 + * Check if an action is enabled for a specific bot
1735 + */
1736 +private function is_action_enabled_for_bot($intent, $bot_id) {
1737 + // If enabled_bots column doesn't exist or is null, assume it's enabled for all bots (backward compatibility)
1738 + if (!isset($intent->enabled_bots) || empty($intent->enabled_bots)) {
1739 + return true;
1740 + }
1741 +
1742 + $enabled_bots = json_decode($intent->enabled_bots, true);
1743 +
1744 + // If JSON decode fails or returns empty array, assume enabled for all (backward compatibility)
1745 + if (!is_array($enabled_bots) || empty($enabled_bots)) {
1746 + return true;
1747 + }
1748 +
1749 + // Check if the current bot is in the enabled bots list
1750 + return in_array($bot_id, $enabled_bots);
1751 +}
1075 1752
1076 1753 // Helper function to clear PDF and Word document related transients
1077 1754 private function clear_pdf_transients($session_id) {
1078 1755 // PDF transients
@@ -1092,61 +1769,76 @@
1092 1769
1093 1770
1094 1771 //verified good
1095 1772 public function mxchat_handle_email_capture($message, $user_id, $session_id) {
1096 - // Log the message safely
1097 - //error_log("Triggered email capture intent for message: " . sanitize_text_field($message));
1098 -
1099 - // Initiate email capture flow
1100 - $response = esc_html($this->options['triggered_phrase_response'] ?? esc_html__("Would you like to join our mailing list? Please provide your email below.", 'mxchat'));
1101 -
1773 + // Get the user's original instruction/message
1774 + $user_instruction = esc_html($this->options['triggered_phrase_response'] ?? esc_html__("Please provide your email address.", 'mxchat'));
1775 +
1776 + // Set instruction for AI - just pass along what the user wanted to say
1777 + $this->current_action_instruction = $user_instruction;
1778 +
1779 + // Set the transient to track email capture flow
1102 1780 set_transient('mxchat_email_capture_' . $user_id, true, 5 * MINUTE_IN_SECONDS);
1103 - $this->mxchat_save_chat_message($session_id, 'bot', $response);
1104 -
1105 - // Respond to the user
1106 - wp_send_json(['message' => $response]);
1107 - wp_die();
1781 +
1782 + // Return false to let the AI generate the response
1783 + return false;
1108 1784 }
1109 1785
1110 -//very good
1111 1786 public function mxchat_generate_image($message, $user_id, $session_id) {
1787 + //error_log("Starting image generation for message: " . $message);
1788 +
1112 1789 // Prepare a prompt for DALL-E
1113 1790 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
1114 -
1791 +
1115 1792 // Use the existing OpenAI API key
1116 1793 $openai_api_key = sanitize_text_field($this->options['api_key']);
1117 -
1794 +
1118 1795 // Call DALL-E to generate an image
1119 1796 $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1120 -
1797 +
1121 1798 // Check if the response contains an image URL
1122 1799 if (isset($image_response['imageUrl'])) {
1123 1800 $image_url = esc_url_raw($image_response['imageUrl']);
1124 -
1801 +
1125 1802 // Construct the HTML with a CSS class instead of inline styles
1126 1803 $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
1804 + $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1805 +
1806 + // Save the bot message with both text and HTML
1807 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1808 + $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
1809 +
1810 + // Set the fallback response for the chat handler
1811 + $this->fallbackResponse = [
1812 + 'text' => $response_text,
1813 + 'html' => $response_html,
1814 + 'images' => [$image_url]
1815 + ];
1816 +
1817 + // For debugging/verification - Use json_encode to verify what's being set
1818 + //error_log("Image generation successful - fallbackResponse set: " . json_encode($this->fallbackResponse));
1127 1819
1128 - $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1820 + // Return the response directly instead of relying on the property
1821 + return $this->fallbackResponse;
1129 1822 } else {
1130 1823 $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
1131 - $response_html = '';
1824 +
1825 + // Save the error message
1826 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1827 +
1828 + // Set the fallback response for the chat handler
1829 + $this->fallbackResponse = [
1830 + 'text' => $response_text,
1831 + 'html' => '',
1832 + 'images' => []
1833 + ];
1834 +
1132 1835 //error_log("DALL-E image generation error: " . esc_html($image_response['error'] ?? 'Unknown error.'));
1836 + //error_log("Error fallbackResponse set: " . json_encode($this->fallbackResponse));
1837 +
1838 + // Return the response directly instead of relying on the property
1839 + return $this->fallbackResponse;
1133 1840 }
1134 -
1135 - // Save both text and HTML responses
1136 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text . "\n" . $response_html);
1137 -
1138 - // Prepare the response data
1139 - $response_data = [
1140 - 'message' => $response_text,
1141 - 'html' => $response_html,
1142 - 'image_url' => $image_url ?? '',
1143 - ];
1144 -
1145 - // Send the JSON response
1146 - header('Content-Type: application/json; charset=' . get_option('blog_charset'));
1147 - echo json_encode($response_data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
1148 - wp_die();
1149 1841 }
1150 1842 private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
1151 1843 $api_url = 'https://api.openai.com/v1/images/generations';
1152 1844 $body = json_encode([
@@ -1185,44 +1877,43 @@
1185 1877
1186 1878 /**
1187 1879 * Handle web search requests.
1188 1880 *
1189 - * Sends the refined search query to the Brave Search API and displays neatly formatted,
1190 - * styled search results. Results are cached for performance.
1881 + * Sends the refined search query to the Brave Search API and uses the
1882 + * results to generate a conversational response with the AI model.
1191 1883 *
1192 1884 * @since 1.0.0
1193 1885 * @param string $message The user's search query.
1194 1886 * @param string $user_id The user identifier.
1195 1887 * @param string $session_id The current session ID.
1196 - * @return void
1888 + * @return array Response array containing text with embedded HTML links
1197 1889 */
1198 -public function mxchat_handle_search_request( $message, $user_id, $session_id ) {
1890 +public function mxchat_handle_search_request($message, $user_id, $session_id) {
1199 1891 // Step 1: Interpret and refine the search query
1200 - $refined_search_query = $this->mxchat_interpret_search_query( $message );
1201 -
1202 - if ( empty( $refined_search_query ) ) {
1203 - $this->fallbackResponse = array(
1204 - 'text' => esc_html__( 'I apologize, but could you please rephrase your search request?', 'mxchat' ),
1892 + $refined_search_query = $this->mxchat_interpret_search_query($message);
1893 + if (empty($refined_search_query)) {
1894 + return array(
1895 + 'text' => esc_html__('I apologize, but could you please rephrase your search request?', 'mxchat'),
1896 + 'html' => ''
1205 1897 );
1206 - return;
1207 1898 }
1208 -
1899 +
1209 1900 // Retrieve and validate API settings
1210 - $options = get_option( 'mxchat_options' );
1211 - $api_key = isset( $options['brave_api_key'] ) ? sanitize_text_field( $options['brave_api_key'] ) : '';
1212 - $results_count = isset( $options['brave_results_count'] ) ? absint( $options['brave_results_count'] ) : 5;
1213 -
1214 - if ( empty( $api_key ) ) {
1215 - $this->fallbackResponse = array(
1216 - 'text' => esc_html__( 'Search functionality is temporarily unavailable. Please try again later.', 'mxchat' ),
1901 + $options = get_option('mxchat_options');
1902 + $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1903 + $results_count = isset($options['brave_results_count']) ? absint($options['brave_results_count']) : 5;
1904 +
1905 + if (empty($api_key)) {
1906 + return array(
1907 + 'text' => esc_html__('Search functionality is temporarily unavailable. Please try again later.', 'mxchat'),
1908 + 'html' => ''
1217 1909 );
1218 - return;
1219 1910 }
1220 -
1911 +
1221 1912 // Build the API request URL
1222 1913 $api_url = add_query_arg(
1223 1914 array(
1224 - 'q' => rawurlencode( $refined_search_query ),
1915 + 'q' => rawurlencode($refined_search_query),
1225 1916 'count' => $results_count,
1226 1917 'text_decorations' => 'true',
1227 1918 'rich_data' => 'true',
1228 1919 ),
@@ -1227,16 +1918,16 @@
1227 1918 'rich_data' => 'true',
1228 1919 ),
1229 1920 'https://api.search.brave.com/res/v1/web/search'
1230 1921 );
1231 -
1922 +
1232 1923 // Attempt to retrieve cached results first
1233 - $transient_key = 'mxchat_search_' . md5( $refined_search_query );
1234 - $results = get_transient( $transient_key );
1235 -
1236 - if ( false === $results ) {
1237 - // Fetch new results from the Brave Search API
1238 - $response = wp_remote_get(
1924 + $transient_key = 'mxchat_search_' . md5($refined_search_query);
1925 + $results = get_transient($transient_key);
1926 +
1927 + if (false === $results) {
1928 + // SECURITY FIX: Changed to wp_safe_remote_get
1929 + $response = wp_safe_remote_get(
1239 1930 $api_url,
1240 1931 array(
1241 1932 'headers' => array(
1242 1933 'Accept' => 'application/json',
@@ -1245,162 +1936,98 @@
1245 1936 ),
1246 1937 'timeout' => 10,
1247 1938 )
1248 1939 );
1249 -
1250 - if ( is_wp_error( $response ) ) {
1251 - $this->fallbackResponse = array(
1252 - 'text' => esc_html__( 'I encountered an error while searching. Please try again.', 'mxchat' ),
1940 +
1941 + if (is_wp_error($response)) {
1942 + return array(
1943 + 'text' => esc_html__('I encountered an error while searching. Please try again.', 'mxchat'),
1944 + 'html' => ''
1253 1945 );
1254 - return;
1255 1946 }
1256 -
1257 - $results = json_decode( wp_remote_retrieve_body( $response ), true );
1258 -
1259 - if ( json_last_error() !== JSON_ERROR_NONE ) {
1260 - $this->fallbackResponse = array(
1261 - 'text' => esc_html__( 'I received an invalid response from the search service.', 'mxchat' ),
1947 +
1948 + $results = json_decode(wp_remote_retrieve_body($response), true);
1949 +
1950 + if (json_last_error() !== JSON_ERROR_NONE) {
1951 + return array(
1952 + 'text' => esc_html__('I received an invalid response from the search service.', 'mxchat'),
1953 + 'html' => ''
1262 1954 );
1263 - return;
1264 1955 }
1265 -
1956 +
1266 1957 // Cache results for one hour
1267 - set_transient( $transient_key, $results, HOUR_IN_SECONDS );
1958 + set_transient($transient_key, $results, HOUR_IN_SECONDS);
1268 1959 }
1269 -
1270 - // Process and display results
1271 - if ( ! empty( $results['web']['results'] ) && is_array( $results['web']['results'] ) ) {
1272 - $html = $this->generate_search_results_html( $results['web']['results'], $refined_search_query );
1273 -
1274 - // Only return HTML (no large text summary)
1275 - $this->fallbackResponse = array(
1276 - 'html' => $html,
1960 +
1961 + // Process results
1962 + if (!empty($results['web']['results']) && is_array($results['web']['results'])) {
1963 + // Create a more straightforward summary with HTML links
1964 + $search_results_text = '';
1965 +
1966 + // Add a simple intro
1967 + $search_results_text .= sprintf(
1968 + esc_html__("Here's what I found about '%s':", 'mxchat'),
1969 + esc_html($refined_search_query)
1277 1970 );
1278 -
1971 +
1972 + // Add the top results with HTML links
1973 + foreach (array_slice($results['web']['results'], 0, 5) as $result) {
1974 + $title = isset($result['title']) ? wp_strip_all_tags($result['title']) : '';
1975 + $url = isset($result['url']) ? esc_url($result['url']) : '';
1976 + $description = isset($result['description']) ? wp_strip_all_tags($result['description']) : '';
1977 +
1978 + // Add a line break after the intro
1979 + $search_results_text .= '<br><br>';
1980 +
1981 + // Add title as a link
1982 + $search_results_text .= sprintf(
1983 + '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a><br>',
1984 + $url,
1985 + $title
1986 + );
1987 +
1988 + // Add a condensed description
1989 + $search_results_text .= sprintf("%s", $description);
1990 + }
1991 +
1279 1992 // Save to chat history
1280 - $this->mxchat_save_chat_message( $session_id, 'bot', $html );
1993 + $this->mxchat_save_chat_message($session_id, 'bot', $search_results_text);
1994 +
1995 + // Return the formatted text with embedded HTML links
1996 + return array(
1997 + 'text' => $search_results_text,
1998 + 'html' => ''
1999 + );
1281 2000 } else {
1282 - $this->fallbackResponse = array(
2001 + return array(
1283 2002 'text' => sprintf(
1284 - esc_html__( 'I couldn\'t find any relevant results for "%s". Would you like to try different search terms?', 'mxchat' ),
1285 - esc_html( $refined_search_query )
2003 + esc_html__('I searched for "%s" but couldn\'t find any relevant results. Would you like to try different search terms?', 'mxchat'),
2004 + esc_html($refined_search_query)
1286 2005 ),
2006 + 'html' => ''
1287 2007 );
1288 2008 }
1289 2009 }
1290 2010
1291 -
2011 +//very good
1292 2012 /**
1293 - * Format search results into a natural text summary.
2013 + * Handle image search requests from the chatbot
1294 2014 *
1295 - * @since 1.0.0
1296 - * @param array $results The search results from the API.
1297 - * @param string $query The original search query.
1298 - * @return string The text summary of the top results.
2015 + * @param string $message The user's search query
2016 + * @param int $user_id The user's ID
2017 + * @param string $session_id The chat session ID
2018 + * @return array Response array with text and HTML content
1299 2019 */
1300 -private function format_search_results( $results, $query ) {
1301 - $summary = sprintf(
1302 - esc_html__( 'Here are the most relevant results for "%s":', 'mxchat' ),
1303 - esc_html( $query )
1304 - ) . "\n\n";
1305 -
1306 - $max_results = min( count( $results ), 3 );
1307 - for ( $i = 0; $i < $max_results; $i++ ) {
1308 - $result = $results[ $i ];
1309 - $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1310 - $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1311 -
1312 - // Append title and description to the summary
1313 - $summary .= sprintf(
1314 - "%s\n%s\n\n",
1315 - esc_html( $title ),
1316 - esc_html( $description )
1317 - );
1318 - }
1319 -
1320 - return $summary;
1321 -}
1322 -
1323 -/**
1324 - * Generate HTML markup for search results.
1325 - *
1326 - * @since 1.0.0
1327 - * @param array $results The search results from the API.
1328 - * @param string $query The user-refined query.
1329 - * @return string The HTML markup for displaying the results.
1330 - */
1331 -private function generate_search_results_html( $results, $query ) {
1332 - ob_start();
1333 - ?>
1334 - <div class="mxchat-search-results">
1335 - <?php foreach ( $results as $result ) :
1336 - $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1337 - $url = isset( $result['url'] ) ? esc_url( $result['url'] ) : '#';
1338 - $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1339 - $favicon = isset( $result['meta_url']['favicon'] ) ? esc_url( $result['meta_url']['favicon'] ) : '';
1340 - $thumbnail = isset( $result['thumbnail']['src'] ) ? esc_url( $result['thumbnail']['src'] ) : '';
1341 - $domain = parse_url( $url, PHP_URL_HOST );
1342 - ?>
1343 - <div class="mxchat-search-item">
1344 - <div class="mxchat-search-header">
1345 - <?php if ( $favicon ) : ?>
1346 - <img
1347 - src="<?php echo esc_url( $favicon ); ?>"
1348 - class="mxchat-site-icon"
1349 - alt="<?php echo esc_attr__( 'Site icon', 'mxchat' ); ?>"
1350 - width="16"
1351 - height="16"
1352 - />
1353 - <?php endif; ?>
1354 - <div class="mxchat-site-url"><?php echo esc_html( $domain ); ?></div>
1355 - </div>
1356 -
1357 - <div class="mxchat-search-content">
1358 - <h3 class="mxchat-search-title">
1359 - <a href="<?php echo esc_url( $url ); ?>"
1360 - target="_blank"
1361 - rel="noopener noreferrer"
1362 - >
1363 - <?php echo esc_html( $title ); ?>
1364 - </a>
1365 - </h3>
1366 -
1367 - <?php if ( $thumbnail ) : ?>
1368 - <div class="mxchat-search-thumbnail">
1369 - <img
1370 - src="<?php echo esc_url( $thumbnail ); ?>"
1371 - alt="<?php echo esc_attr__( 'Thumbnail image', 'mxchat' ); ?>"
1372 - loading="lazy"
1373 - />
1374 - </div>
1375 - <?php endif; ?>
1376 -
1377 - <div class="mxchat-search-description">
1378 - <?php echo esc_html( $description ); ?>
1379 - </div>
1380 - </div>
1381 - </div>
1382 - <?php endforeach; ?>
1383 - </div>
1384 - <?php
1385 - return ob_get_clean();
1386 -}
1387 -
1388 -
1389 -//very good
1390 2020 public function mxchat_handle_image_search_request($message, $user_id, $session_id) {
1391 -
1392 - // Step 1: Interpret the search query for better results
2021 + // Step 1: Interpret the search query using the user's selected AI model
1393 2022 $refined_search_query = $this->mxchat_interpret_search_query($message);
1394 2023
1395 -
1396 2024 // If no query was interpreted, return a fallback message
1397 2025 if (empty($refined_search_query)) {
1398 - $this->fallbackResponse = [
2026 + return array(
1399 2027 'text' => __("I'm sorry, I couldn't interpret your search query. Please specify what you'd like to see images of.", 'mxchat'),
1400 2028 'html' => "",
1401 - ];
1402 - return;
2029 + );
1403 2030 }
1404 2031
1405 2032 // Brave API URL
1406 2033 $api_url = 'https://api.search.brave.com/res/v1/images/search';
@@ -1409,19 +2036,12 @@
1409 2036 $options = get_option('mxchat_options');
1410 2037 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1411 2038
1412 2039 if (empty($api_key)) {
1413 -/*
1414 - if (defined('WP_DEBUG') && WP_DEBUG) {
1415 - error_log("Brave API key is missing.");
1416 - }
1417 -*/
1418 -
1419 - $this->fallbackResponse = [
2040 + return array(
1420 2041 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'),
1421 2042 'html' => "",
1422 - ];
1423 - return;
2043 + );
1424 2044 }
1425 2045
1426 2046 $image_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1427 2047 $safe_search = isset($options['brave_safe_search']) ? sanitize_text_field($options['brave_safe_search']) : 'strict';
@@ -1432,16 +2052,8 @@
1432 2052 'count' => $image_count,
1433 2053 'safesearch' => $safe_search,
1434 2054 ], $api_url);
1435 2055
1436 -/*
1437 - // Log the final API URL for the search
1438 - if (defined('WP_DEBUG') && WP_DEBUG) {
1439 - error_log("Final API URL for Brave Image Search: " . esc_url_raw($api_url));
1440 - }
1441 -*/
1442 -
1443 -
1444 2056 // Implement caching
1445 2057 $transient_key = 'mxchat_image_search_' . md5($refined_search_query);
1446 2058 $body = get_transient($transient_key);
1447 2059
@@ -1454,22 +2066,16 @@
1454 2066 ],
1455 2067 'timeout' => 10,
1456 2068 ];
1457 2069
1458 - $response = wp_remote_get($api_url, $args);
2070 + // SECURITY FIX: Changed to wp_safe_remote_get
2071 + $response = wp_safe_remote_get($api_url, $args);
1459 2072
1460 2073 if (is_wp_error($response)) {
1461 -/*
1462 - if (defined('WP_DEBUG') && WP_DEBUG) {
1463 - error_log("Brave Image API request failed: " . $response->get_error_message());
1464 - }
1465 -*/
1466 -
1467 - $this->fallbackResponse = [
2074 + return array(
1468 2075 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1469 2076 'html' => "",
1470 - ];
1471 - return;
2077 + );
1472 2078 }
1473 2079
1474 2080 $body = json_decode(wp_remote_retrieve_body($response), true);
1475 2081 set_transient($transient_key, $body, HOUR_IN_SECONDS);
@@ -1477,10 +2083,16 @@
1477 2083
1478 2084 // Process the API response
1479 2085 if (isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) {
1480 2086 $html_output = '<div class="mxchat-image-gallery">';
1481 -
1482 - foreach ($body['results'] as $image) {
2087 +
2088 + // Get the configured image count (1-6)
2089 + $display_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
2090 + $display_count = min($display_count, count($body['results'])); // Make sure we don't exceed available images
2091 +
2092 + // Use only the requested number of images
2093 + for ($i = 0; $i < $display_count; $i++) {
2094 + $image = $body['results'][$i];
1483 2095 $image_url = isset($image['url']) ? esc_url($image['url']) : '';
1484 2096 $thumbnail_url = isset($image['thumbnail']['src']) ? esc_url($image['thumbnail']['src']) : '';
1485 2097 $title = isset($image['title']) ? esc_html($image['title']) : esc_html__('Image', 'mxchat');
1486 2098
@@ -1494,47 +2106,95 @@
1494 2106 }
1495 2107
1496 2108 $html_output .= '</div>';
1497 2109
1498 - $this->fallbackResponse = [
1499 - 'text' => "",
1500 - 'html' => $html_output,
1501 - ];
1502 -
1503 - // Save response in chat history
2110 + // Create response text
2111 + $response_text = sprintf(__("Here are some images of %s:", 'mxchat'), $refined_search_query);
2112 +
2113 + // Save both response text and HTML to chat history
2114 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1504 2115 $this->mxchat_save_chat_message($session_id, 'bot', $html_output);
1505 2116
2117 + // Return the combined response
2118 + return array(
2119 + 'text' => $response_text,
2120 + 'html' => $html_output,
2121 + );
1506 2122 } else {
1507 -/*
1508 - if (defined('WP_DEBUG') && WP_DEBUG) {
1509 - error_log("Brave Image API response did not contain expected data structure or was empty: " . print_r($body, true));
1510 - }
1511 -*/
1512 -
1513 - $this->fallbackResponse = [
1514 - 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
2123 + $response_text = __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat');
2124 +
2125 + // Save the error message to chat history
2126 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2127 +
2128 + return array(
2129 + 'text' => $response_text,
1515 2130 'html' => "",
1516 - ];
2131 + );
1517 2132 }
1518 2133 }
2134 +
2135 +/**
2136 + * Interpret the search query using the user's selected AI model
2137 + *
2138 + * @param string $user_query The original query from the user
2139 + * @return string The refined search query
2140 + */
1519 2141 public function mxchat_interpret_search_query($user_query) {
1520 2142 $system_prompt = esc_html__("Interpret the user's request to provide only the essential keywords or phrases for image searching. Remove conversational language, politeness, or extra context. Return a concise search query that doesn't lose any of the original meaning.", 'mxchat');
1521 -
1522 - // Retrieve OpenAI API key using 'api_key' as the option key
1523 - $api_key = isset($this->options['api_key']) ? sanitize_text_field($this->options['api_key']) : sanitize_text_field(get_option('mxchat_options')['api_key']);
1524 -
1525 - /*
1526 - // Log the API key check, without exposing the key
1527 - if (defined('WP_DEBUG') && WP_DEBUG) {
1528 - error_log("Retrieved OpenAI API Key: " . ($api_key ? "Present" : "Missing"));
2143 +
2144 + // Get options and determine the selected model
2145 + $options = $this->options ?? get_option('mxchat_options');
2146 + $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
2147 +
2148 + // Extract model prefix to determine the provider
2149 + $model_parts = explode('-', $selected_model);
2150 + $provider = strtolower($model_parts[0]);
2151 +
2152 + // Determine which API key to use based on the provider
2153 + switch ($provider) {
2154 + case 'gemini':
2155 + $api_key = isset($options['gemini_api_key']) ? sanitize_text_field($options['gemini_api_key']) : '';
2156 + if (empty($api_key)) {
2157 + return sanitize_text_field($user_query); // Default to original query if API key missing
2158 + }
2159 + return $this->interpret_query_with_gemini($user_query, $system_prompt, $api_key, $selected_model);
2160 +
2161 + case 'claude':
2162 + $api_key = isset($options['claude_api_key']) ? sanitize_text_field($options['claude_api_key']) : '';
2163 + if (empty($api_key)) {
2164 + return sanitize_text_field($user_query);
2165 + }
2166 + return $this->interpret_query_with_claude($user_query, $system_prompt, $api_key, $selected_model);
2167 +
2168 + case 'grok':
2169 + $api_key = isset($options['xai_api_key']) ? sanitize_text_field($options['xai_api_key']) : '';
2170 + if (empty($api_key)) {
2171 + return sanitize_text_field($user_query);
2172 + }
2173 + return $this->interpret_query_with_xai($user_query, $system_prompt, $api_key, $selected_model);
2174 +
2175 + case 'deepseek':
2176 + $api_key = isset($options['deepseek_api_key']) ? sanitize_text_field($options['deepseek_api_key']) : '';
2177 + if (empty($api_key)) {
2178 + return sanitize_text_field($user_query);
2179 + }
2180 + return $this->interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $selected_model);
2181 +
2182 + case 'gpt':
2183 + default:
2184 + // Default to OpenAI for custom models or unrecognized prefixes
2185 + $api_key = isset($options['api_key']) ? sanitize_text_field($options['api_key']) : '';
2186 + if (empty($api_key)) {
2187 + return sanitize_text_field($user_query);
2188 + }
2189 + return $this->interpret_query_with_openai($user_query, $system_prompt, $api_key, $selected_model);
1529 2190 }
1530 - */
2191 +}
1531 2192
1532 - if (empty($api_key)) {
1533 - //error_log("OpenAI API key is missing.");
1534 - return sanitize_text_field($user_query); // Default to the original query if API key is missing
1535 - }
1536 -
2193 +/**
2194 + * Interpret query using OpenAI models
2195 + */
2196 +private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-4o') {
1537 2197 $url = 'https://api.openai.com/v1/chat/completions';
1538 2198 $args = [
1539 2199 'headers' => [
1540 2200 'Authorization' => 'Bearer ' . $api_key,
@@ -1540,9 +2200,9 @@
1540 2200 'Authorization' => 'Bearer ' . $api_key,
1541 2201 'Content-Type' => 'application/json',
1542 2202 ],
1543 2203 'body' => wp_json_encode([
1544 - 'model' => 'gpt-3.5-turbo',
2204 + 'model' => $model,
1545 2205 'messages' => [
1546 2206 ['role' => 'system', 'content' => $system_prompt],
1547 2207 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1548 2208 ],
@@ -1549,166 +2209,178 @@
1549 2209 'temperature' => 0.2,
1550 2210 'max_tokens' => 20,
1551 2211 ]),
1552 2212 'method' => 'POST',
2213 + 'timeout' => 15,
1553 2214 ];
1554 2215
1555 2216 $response = wp_remote_post($url, $args);
1556 -
1557 2217 if (is_wp_error($response)) {
1558 - //error_log("OpenAI request failed: " . $response->get_error_message());
1559 - return sanitize_text_field($user_query); // Fallback to the original query if there's an error
2218 + return sanitize_text_field($user_query);
1560 2219 }
1561 2220
1562 2221 $body = json_decode(wp_remote_retrieve_body($response), true);
2222 + return isset($body['choices'][0]['message']['content'])
2223 + ? sanitize_text_field(trim($body['choices'][0]['message']['content']))
2224 + : sanitize_text_field($user_query);
2225 +}
1563 2226
1564 - // Check for a valid response and sanitize output
1565 - if (isset($body['choices'][0]['message']['content'])) {
1566 - $interpreted_query = sanitize_text_field(trim($body['choices'][0]['message']['content']));
2227 +/**
2228 + * Interpret query using Claude models
2229 + */
2230 +private function interpret_query_with_claude($user_query, $system_prompt, $api_key, $model) {
2231 + $url = 'https://api.anthropic.com/v1/messages';
2232 +
2233 + $args = [
2234 + 'headers' => [
2235 + 'Content-Type' => 'application/json',
2236 + 'x-api-key' => $api_key,
2237 + 'anthropic-version' => '2023-06-01',
2238 + ],
2239 + 'body' => wp_json_encode([
2240 + 'model' => $model,
2241 + 'system' => $system_prompt,
2242 + 'messages' => [
2243 + ['role' => 'user', 'content' => sanitize_text_field($user_query)]
2244 + ],
2245 + 'max_tokens' => 20,
2246 + 'temperature' => 0.2,
2247 + ]),
2248 + 'method' => 'POST',
2249 + 'timeout' => 15,
2250 + ];
1567 2251
1568 - /*
1569 - // Log the interpreted query for debugging
1570 - if (defined('WP_DEBUG') && WP_DEBUG) {
1571 - error_log("Interpreted search query: " . $interpreted_query);
1572 - }
1573 - */
2252 + $response = wp_remote_post($url, $args);
2253 + if (is_wp_error($response)) {
2254 + return sanitize_text_field($user_query);
2255 + }
1574 2256
1575 - return $interpreted_query;
1576 - } else {
1577 - //error_log("Unexpected API response format: " . print_r($body, true));
1578 - return sanitize_text_field($user_query);
2257 + $body = json_decode(wp_remote_retrieve_body($response), true);
2258 + if (!empty($body['content'][0]['text'])) {
2259 + return sanitize_text_field(trim($body['content'][0]['text']));
1579 2260 }
2261 +
2262 + return sanitize_text_field($user_query);
1580 2263 }
1581 2264
1582 -
1583 -
1584 -private function find_product_in_message($message) {
1585 - global $wpdb;
1586 -
1587 - // Get embedding for the search query
1588 - $query_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1589 - if (!is_array($query_embedding)) {
1590 - return null;
2265 +/**
2266 + * Interpret query using Gemini models
2267 + */
2268 +private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
2269 + // Strip "gemini-" prefix for the API
2270 + $model_version = str_replace('gemini-', '', $model);
2271 +
2272 + $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
2273 +
2274 + $args = [
2275 + 'headers' => [
2276 + 'Content-Type' => 'application/json',
2277 + ],
2278 + 'body' => wp_json_encode([
2279 + 'contents' => [
2280 + [
2281 + 'role' => 'user',
2282 + 'parts' => [
2283 + ['text' => $system_prompt . "\n\nQuery: " . sanitize_text_field($user_query)]
2284 + ]
2285 + ]
2286 + ],
2287 + 'generationConfig' => [
2288 + 'temperature' => 0.2,
2289 + 'maxOutputTokens' => 20,
2290 + ],
2291 + ]),
2292 + 'method' => 'POST',
2293 + 'timeout' => 15,
2294 + ];
2295 +
2296 + $response = wp_remote_post($url, $args);
2297 + if (is_wp_error($response)) {
2298 + return sanitize_text_field($user_query);
1591 2299 }
1592 -
1593 - // Get relevant content as string
1594 - $relevant_content = $this->mxchat_find_relevant_products($query_embedding);
1595 - if (empty($relevant_content)) {
1596 - // Return null to indicate no results and set fallback response
1597 - $this->fallbackResponse['text'] = esc_html__("I couldn't find any relevant products based on your query. Could you please be more specific about the product you're looking for?", 'mxchat');
1598 - return null;
2300 +
2301 + $body = json_decode(wp_remote_retrieve_body($response), true);
2302 + if (!empty($body['candidates'][0]['content']['parts'][0]['text'])) {
2303 + return sanitize_text_field(trim($body['candidates'][0]['content']['parts'][0]['text']));
1599 2304 }
2305 +
2306 + return sanitize_text_field($user_query);
2307 +}
1600 2308
1601 - // Extract product URLs from the content
1602 - preg_match_all('/https?:\/\/[^\s<>"\']+?\/product\/[^\s<>"\']+/', $relevant_content, $matches);
1603 -
1604 - if (!empty($matches[0])) {
1605 - // Try each URL found
1606 - foreach ($matches[0] as $url) {
1607 - // Clean the URL
1608 - $url = rtrim($url, '/."\']');
1609 -
1610 - // Get the product slug
1611 - $path = parse_url($url, PHP_URL_PATH);
1612 - $slug = basename(rtrim($path, '/'));
1613 -
1614 - // Find product by slug
1615 - $args = array(
1616 - 'post_type' => 'product',
1617 - 'post_status' => 'publish',
1618 - 'name' => $slug,
1619 - 'posts_per_page' => 1
1620 - );
1621 -
1622 - $products = get_posts($args);
1623 -
1624 - if (!empty($products)) {
1625 - $product_id = $products[0]->ID;
1626 - $product = wc_get_product($product_id);
1627 -
1628 - if ($product && $product->is_purchasable()) {
1629 - return $product_id;
1630 - }
1631 - }
1632 - }
2309 +/**
2310 + * Interpret query using X.AI (Grok) models
2311 + */
2312 +private function interpret_query_with_xai($user_query, $system_prompt, $api_key, $model) {
2313 + $url = 'https://api.xai.com/v1/chat/completions';
2314 +
2315 + $args = [
2316 + 'headers' => [
2317 + 'Content-Type' => 'application/json',
2318 + 'Authorization' => 'Bearer ' . $api_key,
2319 + ],
2320 + 'body' => wp_json_encode([
2321 + 'model' => $model,
2322 + 'messages' => [
2323 + ['role' => 'system', 'content' => $system_prompt],
2324 + ['role' => 'user', 'content' => sanitize_text_field($user_query)],
2325 + ],
2326 + 'temperature' => 0.2,
2327 + 'max_tokens' => 20,
2328 + ]),
2329 + 'method' => 'POST',
2330 + 'timeout' => 15,
2331 + ];
2332 +
2333 + $response = wp_remote_post($url, $args);
2334 + if (is_wp_error($response)) {
2335 + return sanitize_text_field($user_query);
1633 2336 }
1634 -
1635 - // Fallback: Look for product names in the content
1636 - $products = wc_get_products([
1637 - 'status' => 'publish',
1638 - 'limit' => -1,
1639 - 'return' => 'all'
1640 - ]);
1641 -
1642 - foreach ($products as $product) {
1643 - $name = $product->get_name();
1644 - if (stripos($relevant_content, $name) !== false) {
1645 - if ($product->is_purchasable()) {
1646 - return $product->get_id();
1647 - }
1648 - }
2337 +
2338 + $body = json_decode(wp_remote_retrieve_body($response), true);
2339 + if (isset($body['choices'][0]['message']['content'])) {
2340 + return sanitize_text_field(trim($body['choices'][0]['message']['content']));
1649 2341 }
1650 -
1651 - // If no product is found after all checks, set the fallback response
1652 - $this->fallbackResponse['text'] = esc_html__("I couldn't find any relevant products based on your query. Try to be more specific", 'mxchat');
1653 - return null;
2342 +
2343 + return sanitize_text_field($user_query);
1654 2344 }
1655 2345
1656 -// New method to handle intent responses
1657 -private function generate_intent_response($context_content, $session_id) {
1658 - // Convert the context array to a structured string for the AI
1659 - $context_string = $this->format_intent_context($context_content);
1660 -
1661 - // Generate AI response using the context
1662 - $response = $this->mxchat_generate_response(
1663 - $context_string,
1664 - $this->options['api_key'],
1665 - $this->options['xai_api_key'],
1666 - $this->options['claude_api_key'],
1667 - $this->options['deepseek_api_key'],
1668 - $this->mxchat_fetch_conversation_history_for_ai($session_id)
1669 - );
1670 -
1671 - $this->fallbackResponse['text'] = $response;
1672 - return true;
1673 -}
1674 -// Helper method to format intent context
1675 -private function format_intent_context($context) {
1676 - $context_string = esc_html__("INTENT CONTEXT:\n", 'mxchat');
1677 -
1678 - switch ($context['intent']) {
1679 - case 'add_to_cart':
1680 - if ($context['status'] === 'success') {
1681 - $context_string .= esc_html__("Action: Successfully added product to cart\n", 'mxchat');
1682 - $context_string .= sprintf(esc_html__("Product: %s\n", 'mxchat'), $context['product']['name']);
1683 - $context_string .= esc_html__("Available actions: ", 'mxchat') . implode(', ', $context['available_actions']) . "\n";
1684 - $context_string .= sprintf(esc_html__("Cart URL: %s\n", 'mxchat'), $context['cart_url']);
1685 - $context_string .= esc_html__("\nPlease inform the user of the successful addition and their available options.", 'mxchat');
1686 - } else {
1687 - $context_string .= esc_html__("Action: Failed to add product to cart\n", 'mxchat');
1688 - $context_string .= sprintf(esc_html__("Reason: %s\n", 'mxchat'), $context['reason']);
1689 - switch ($context['reason']) {
1690 - case 'woocommerce_not_available':
1691 - $context_string .= esc_html__("\nPlease inform the user that shopping features are not available.", 'mxchat');
1692 - break;
1693 - case 'no_product_context':
1694 - $context_string .= esc_html__("\nPlease ask the user to specify which product they want to add.", 'mxchat');
1695 - break;
1696 - case 'product_not_found':
1697 - $context_string .= esc_html__("\nPlease inform the user that the product couldn't be found and ask them to try again.", 'mxchat');
1698 - break;
1699 - case 'add_to_cart_failed':
1700 - $context_string .= esc_html__("\nPlease apologize to the user and suggest they try again or ask for assistance.", 'mxchat');
1701 - break;
1702 - }
1703 - }
1704 - break;
2346 +/**
2347 + * Interpret query using DeepSeek models
2348 + */
2349 +private function interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $model) {
2350 + $url = 'https://api.deepseek.com/v1/chat/completions';
2351 +
2352 + $args = [
2353 + 'headers' => [
2354 + 'Content-Type' => 'application/json',
2355 + 'Authorization' => 'Bearer ' . $api_key,
2356 + ],
2357 + 'body' => wp_json_encode([
2358 + 'model' => $model,
2359 + 'messages' => [
2360 + ['role' => 'system', 'content' => $system_prompt],
2361 + ['role' => 'user', 'content' => sanitize_text_field($user_query)],
2362 + ],
2363 + 'temperature' => 0.2,
2364 + 'max_tokens' => 20,
2365 + ]),
2366 + 'method' => 'POST',
2367 + 'timeout' => 15,
2368 + ];
2369 +
2370 + $response = wp_remote_post($url, $args);
2371 + if (is_wp_error($response)) {
2372 + return sanitize_text_field($user_query);
1705 2373 }
1706 -
1707 - return $context_string;
2374 +
2375 + $body = json_decode(wp_remote_retrieve_body($response), true);
2376 + if (isset($body['choices'][0]['message']['content'])) {
2377 + return sanitize_text_field(trim($body['choices'][0]['message']['content']));
2378 + }
2379 +
2380 + return sanitize_text_field($user_query);
1708 2381 }
1709 2382
1710 -
1711 2383 //very good
1712 2384 private function add_email_to_loops($email) {
1713 2385 // Sanitize the email
1714 2386 $email = sanitize_email($email);
@@ -1792,95 +2464,203 @@
1792 2464
1793 2465 // Default to proceeding with conversation if no specific PDF action is needed
1794 2466 $this->fallbackResponse['text'] = '';
1795 2467 }
2468 +
2469 +
2470 +/**
2471 + * Enhanced fetch_and_split_pdf_pages with SSRF protection
2472 + */
1796 2473 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
2474 + // CLEAR DEBUG LOGGING
2475 + //error_log("=== MXCHAT PDF PROCESSING START ===");
2476 + //error_log("PDF Source: " . $pdf_source);
2477 + //error_log("Max Pages: " . $max_pages);
2478 + //error_log("Session ID: " . ($this->session_id ?? 'not set'));
2479 +
2480 + // Check if Advanced Claude Toolbar is available and enabled
2481 + $claude_available = function_exists('mxchatACT_is_advanced_claude_enabled');
2482 + $claude_enabled = $claude_available ? mxchatACT_is_advanced_claude_enabled() : false;
2483 +
2484 + //error_log("Claude Function Available: " . ($claude_available ? 'YES' : 'NO'));
2485 + //error_log("Claude Enabled: " . ($claude_enabled ? 'YES' : 'NO'));
2486 +
2487 + if ($claude_available && $claude_enabled) {
2488 + //error_log("🚀 ATTEMPTING CLAUDE PROCESSING...");
2489 +
2490 + // Attempt Claude processing first
2491 + $claude_result = apply_filters('mxchat_process_pdf_advanced', false, $pdf_source, $max_pages, $this->session_id);
2492 +
2493 + if ($claude_result !== false && is_array($claude_result) && !empty($claude_result)) {
2494 + //error_log("✅ CLAUDE PROCESSING SUCCESSFUL!");
2495 + //error_log("Claude returned " . count($claude_result) . " processed pages");
2496 +
2497 + // Log first page details for verification
2498 + if (isset($claude_result[0])) {
2499 + $first_page = $claude_result[0];
2500 + //error_log("First page enhanced: " . (isset($first_page['enhanced']) && $first_page['enhanced'] ? 'YES' : 'NO'));
2501 + //error_log("Processing method: " . ($first_page['processing_method'] ?? 'not set'));
2502 + //error_log("First page text preview: " . substr($first_page['text'] ?? '', 0, 100) . "...");
2503 + }
2504 +
2505 + //error_log("=== MXCHAT PDF PROCESSING END (CLAUDE) ===");
2506 + return $claude_result;
2507 + } else {
2508 + //error_log("❌ CLAUDE PROCESSING FAILED or returned invalid result");
2509 + //error_log("Claude result type: " . gettype($claude_result));
2510 + if (is_array($claude_result)) {
2511 + //error_log("Claude result count: " . count($claude_result));
2512 + }
2513 + }
2514 + }
2515 +
2516 + // Fallback to basic processing
2517 + //error_log("🔄 FALLING BACK TO BASIC PDF PROCESSING...");
2518 +
1797 2519 $upload_dir = wp_upload_dir();
1798 2520 $temp_file = null;
1799 -
2521 +
1800 2522 try {
1801 - // Handle URL vs local file
2523 + // Your existing basic processing code here...
2524 + // (I'll include the key parts with debug logging)
2525 +
1802 2526 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
1803 - // Validate and download the file from URL
1804 - $temp_file = wp_tempnam($pdf_source); // Safe temporary file name
1805 - $response = wp_remote_get($pdf_source, ['timeout' => 60]);
1806 -
2527 + //error_log("Downloading PDF from URL...");
2528 +
2529 + // SECURITY FIX: Validate URL before processing
2530 + if (!$this->mxchat_is_safe_pdf_url($pdf_source)) {
2531 + //error_log("❌ SECURITY: Blocked unsafe PDF URL");
2532 + return false;
2533 + }
2534 +
2535 + $temp_file = wp_tempnam($pdf_source);
2536 +
2537 + // SECURITY FIX: Changed from wp_remote_get to wp_safe_remote_get
2538 + $response = wp_safe_remote_get($pdf_source, [
2539 + 'timeout' => 60,
2540 + 'headers' => ['User-Agent' => 'MxChat PDF Processor']
2541 + ]);
2542 +
1807 2543 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1808 - //error_log(esc_html__("Failed to download PDF. Error: ", 'mxchat') . print_r($response, true));
2544 + $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code($response);
2545 + //error_log("❌ BASIC PROCESSING: Failed to download PDF: " . $error_message);
1809 2546 return false;
1810 2547 }
1811 -
2548 +
1812 2549 file_put_contents($temp_file, wp_remote_retrieve_body($response));
1813 -
1814 - // Validate that the downloaded file is a PDF
1815 - $mime_type = mime_content_type($temp_file);
1816 - if ($mime_type !== 'application/pdf') {
1817 - //error_log(esc_html__("Invalid MIME type detected for PDF: ", 'mxchat') . $mime_type);
1818 - unlink($temp_file);
1819 - return false;
1820 - }
2550 + //error_log("✅ PDF downloaded successfully");
1821 2551 } else {
1822 - // For local files, use the provided path directly
1823 2552 $temp_file = $pdf_source;
2553 + //error_log("Using local PDF file: " . $temp_file);
1824 2554 }
1825 -
1826 - // Parse and process the PDF
2555 +
2556 + // Parse PDF
2557 + //error_log("Parsing PDF with basic parser...");
1827 2558 $parser = new \Smalot\PdfParser\Parser();
1828 2559 $pdf = $parser->parseFile($temp_file);
1829 2560 $pages = $pdf->getPages();
1830 -
2561 +
2562 + //error_log("PDF contains " . count($pages) . " pages");
2563 +
1831 2564 if (count($pages) > $max_pages) {
1832 - //error_log(esc_html__("PDF exceeds the maximum allowed pages: ", 'mxchat') . count($pages));
1833 - if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
2565 + //error_log("❌ BASIC PROCESSING: Too many pages (" . count($pages) . " > " . $max_pages . ")");
2566 + if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
1834 2567 unlink($temp_file);
1835 2568 }
1836 - return esc_html__('too_many_pages', 'mxchat');
2569 + return 'too_many_pages';
1837 2570 }
1838 -
2571 +
1839 2572 $embeddings = [];
2573 + $processed_pages = 0;
2574 +
1840 2575 foreach ($pages as $page_number => $page) {
1841 2576 $text = $page->getText();
1842 -
1843 - // Ensure text is non-empty before generating embeddings
2577 +
1844 2578 if (empty(trim($text))) {
1845 - //error_log(esc_html__("Skipping empty page: ", 'mxchat') . ($page_number + 1));
2579 + //error_log("Skipping empty page: " . ($page_number + 1));
1846 2580 continue;
1847 2581 }
1848 -
2582 +
2583 + $text = $this->mxchat_clean_text($text);
2584 +
1849 2585 $embedding = $this->mxchat_generate_embedding(
1850 - esc_html__("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
2586 + __("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
1851 2587 $this->options['api_key']
1852 2588 );
1853 -
2589 +
1854 2590 if ($embedding) {
1855 2591 $embeddings[] = [
1856 2592 'page_number' => $page_number + 1,
1857 2593 'embedding' => $embedding,
1858 2594 'text' => $text,
2595 + 'enhanced' => false, // CLEARLY MARK AS BASIC
2596 + 'processing_method' => 'basic_pdf_parser'
1859 2597 ];
1860 - } else {
1861 - //error_log(esc_html__("Failed to generate embedding for page ", 'mxchat') . ($page_number + 1));
2598 + $processed_pages++;
1862 2599 }
1863 2600 }
1864 -
1865 - // Clean up downloaded file if it was from URL
1866 - if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
2601 +
2602 + //error_log("✅ BASIC PROCESSING COMPLETE: " . $processed_pages . " pages processed");
2603 +
2604 + // Cleanup
2605 + if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
1867 2606 unlink($temp_file);
1868 2607 }
1869 -
2608 +
2609 + //error_log("=== MXCHAT PDF PROCESSING END (BASIC) ===");
1870 2610 return $embeddings;
1871 -
2611 +
1872 2612 } catch (\Exception $e) {
1873 - // error_log(esc_html__("Error parsing or processing PDF: ", 'mxchat') . $e->getMessage());
1874 -
1875 - // Cleanup in case of exception
2613 + //error_log("❌ BASIC PROCESSING ERROR: " . $e->getMessage());
1876 2614 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
1877 2615 unlink($temp_file);
1878 2616 }
2617 + //error_log("=== MXCHAT PDF PROCESSING END (ERROR) ===");
2618 + return false;
2619 + }
2620 +}
1879 2621
2622 +
2623 +/**
2624 + * Validate PDF URL for security
2625 + * Prevents SSRF attacks by blocking dangerous URLs
2626 + */
2627 +
2628 +private function mxchat_is_safe_pdf_url($url) {
2629 + // Use WordPress core function for comprehensive validation
2630 + // This blocks localhost, private IPs, and reserved IP ranges
2631 + $validated_url = wp_http_validate_url($url);
2632 +
2633 + if ($validated_url === false) {
1880 2634 return false;
1881 2635 }
2636 +
2637 + // Additional check: only allow HTTP/HTTPS schemes
2638 + $parsed = parse_url($url);
2639 + if (!isset($parsed['scheme']) || !in_array($parsed['scheme'], ['http', 'https'], true)) {
2640 + return false;
2641 + }
2642 +
2643 + return true;
1882 2644 }
2645 +
2646 +
2647 +private function mxchat_clean_text($text) {
2648 + // Remove excessive whitespace
2649 + $text = preg_replace('/\s+/', ' ', $text);
2650 +
2651 + // Remove control characters except newlines and tabs
2652 + $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text);
2653 +
2654 + // Normalize line endings
2655 + $text = str_replace(["\r\n", "\r"], "\n", $text);
2656 +
2657 + // Trim whitespace
2658 + $text = trim($text);
2659 +
2660 + return $text;
2661 +}
2662 +
1883 2663 private function find_relevant_pdf_pages($query_embedding, $embeddings) {
1884 2664 //error_log(esc_html__("find_relevant_pdf_pages called.", 'mxchat'));
1885 2665
1886 2666 $most_relevant = null;
@@ -1903,9 +2683,10 @@
1903 2683 }
1904 2684
1905 2685 return [];
1906 2686 }
1907 -// Add this to your class
2687 +
2688 +
1908 2689 public function handle_pdf_upload() {
1909 2690 check_ajax_referer('mxchat_chat_nonce', 'nonce');
1910 2691
1911 2692 if (!isset($_FILES['pdf_file']) || !isset($_POST['session_id'])) {
@@ -1912,12 +2693,30 @@
1912 2693 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
1913 2694 return;
1914 2695 }
1915 2696
2697 + // SECURITY FIX: Check if PDF uploads are enabled in settings
2698 + $options = get_option('mxchat_options', array());
2699 + $show_pdf_button = isset($options['show_pdf_upload_button']) ? $options['show_pdf_upload_button'] : 'on';
2700 +
2701 + if ($show_pdf_button !== 'on') {
2702 + wp_send_json_error(esc_html__('PDF uploads are currently disabled.', 'mxchat'));
2703 + return;
2704 + }
2705 +
1916 2706 $file = $_FILES['pdf_file'];
1917 2707 $session_id = sanitize_text_field($_POST['session_id']);
1918 2708 $original_filename = sanitize_text_field($file['name']);
1919 2709
2710 + // SECURITY FIX: Verify session ownership before allowing upload
2711 + $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
2712 + $session_owner = get_option("mxchat_session_owner_{$session_id}");
2713 +
2714 + if ($session_owner && $session_owner !== $current_user_identifier) {
2715 + wp_send_json_error(esc_html__('Unauthorized access.', 'mxchat'));
2716 + return;
2717 + }
2718 +
1920 2719 $file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
1921 2720 if ($file_type['type'] !== 'application/pdf') {
1922 2721 wp_send_json_error(esc_html__('Invalid file type. Only PDF files are allowed.', 'mxchat'));
1923 2722 return;
@@ -1923,9 +2722,12 @@
1923 2722 return;
1924 2723 }
1925 2724
1926 2725 $upload_dir = wp_upload_dir();
1927 - $pdf_filename = 'mxchat_' . $session_id . '_' . time() . '.pdf';
2726 +
2727 + // SECURITY FIX: Generate random filename without exposing session_id
2728 + $random_string = wp_generate_password(20, false, false); // 20 char alphanumeric string
2729 + $pdf_filename = 'mxchat_' . $random_string . '_' . time() . '.pdf';
1928 2730 $pdf_path = $upload_dir['path'] . '/' . $pdf_filename;
1929 2731
1930 2732 if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
1931 2733 wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
@@ -1956,8 +2758,9 @@
1956 2758 return;
1957 2759 }
1958 2760
1959 2761 if (!empty($embeddings)) {
2762 + // Store the mapping between session and the random filename
1960 2763 set_transient('mxchat_pdf_url_' . $session_id, $pdf_path, HOUR_IN_SECONDS);
1961 2764 set_transient('mxchat_pdf_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
1962 2765 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
1963 2766 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
@@ -2001,10 +2804,8 @@
2001 2804 wp_die();
2002 2805 }
2003 2806
2004 2807
2005 -
2006 -
2007 2808 function mxchat_fetch_new_messages() {
2008 2809 $session_id = sanitize_text_field($_POST['session_id']);
2009 2810 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
2010 2811 $persistence_enabled = $_POST['persistence_enabled'] === 'true';
@@ -2017,14 +2818,31 @@
2017 2818 }
2018 2819
2019 2820 $history = get_option("mxchat_history_{$session_id}", []);
2020 2821
2822 + error_log("MxChat WhatsApp DEBUG: Fetch new messages for session {$session_id}");
2823 + error_log("MxChat WhatsApp DEBUG: last_seen_id = " . var_export($last_seen_id, true));
2824 + error_log("MxChat WhatsApp DEBUG: History count = " . count($history));
2825 + error_log("MxChat WhatsApp DEBUG: Full history = " . print_r($history, true));
2826 +
2021 2827 $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
2828 + error_log("MxChat WhatsApp DEBUG: Checking message - ID: " . ($message['id'] ?? 'NO_ID') . ", Role: " . ($message['role'] ?? 'NO_ROLE'));
2829 +
2022 2830 // If persistence is enabled, show all new messages
2023 2831 if ($persistence_enabled) {
2024 - return !empty($message['id']) &&
2025 - strcmp($message['id'], $last_seen_id) > 0 &&
2026 - $message['role'] === 'agent';
2832 + $has_id = !empty($message['id']);
2833 + $is_agent = $message['role'] === 'agent';
2834 +
2835 + // If last_seen_id is empty, 'NaN', or invalid, show all agent messages
2836 + if (empty($last_seen_id) || $last_seen_id === 'NaN' || $last_seen_id === 'undefined') {
2837 + $is_newer = true;
2838 + } else {
2839 + $is_newer = strcmp($message['id'] ?? '', $last_seen_id) > 0;
2840 + }
2841 +
2842 + error_log("MxChat WhatsApp DEBUG: has_id={$has_id}, is_newer={$is_newer}, is_agent={$is_agent}");
2843 +
2844 + return $has_id && $is_newer && $is_agent;
2027 2845 }
2028 2846
2029 2847 // If persistence is disabled, only show messages after initial timestamp
2030 2848 return !empty($message['id']) &&
@@ -2031,9 +2849,9 @@
2031 2849 $message['role'] === 'agent' &&
2032 2850 $message['timestamp'] > $initial_timestamp;
2033 2851 });
2034 2852
2035 - //error_log(esc_html__("New agent messages fetched for session $session_id. Last seen ID: $last_seen_id", 'mxchat'));
2853 + error_log("MxChat WhatsApp DEBUG: Filtered messages count = " . count($new_messages));
2036 2854
2037 2855 wp_send_json_success([
2038 2856 'new_messages' => array_values($new_messages)
2039 2857 ]);
@@ -2038,10 +2856,8 @@
2038 2856 'new_messages' => array_values($new_messages)
2039 2857 ]);
2040 2858 wp_die();
2041 2859 }
2042 -
2043 -
2044 2860 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
2045 2861 // First check if live agents are available
2046 2862 $live_agent_available = $this->options['live_agent_status'] ?? 'off';
2047 2863 if ($live_agent_available !== 'on') {
@@ -2060,18 +2876,101 @@
2060 2876 ]);
2061 2877 wp_die();
2062 2878 }
2063 2879
2064 - $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2065 - if (empty($slack_webhook_url)) {
2880 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2881 +
2882 + if (empty($slack_bot_token)) {
2066 2883 return false;
2067 2884 }
2068 2885
2069 - // Get recent chat history (last 5 messages)
2886 + // Check if channel already exists for this session
2887 + $channel_id = get_option("mxchat_channel_{$session_id}", '');
2888 +
2889 + if (empty($channel_id)) {
2890 + // Create new channel with session ID as name
2891 + $channel_name = $this->generate_channel_name($session_id);
2892 +
2893 + //error_log("Attempting to create channel: $channel_name");
2894 +
2895 + $response = wp_remote_post('https://slack.com/api/conversations.create', [
2896 + 'headers' => [
2897 + 'Content-Type' => 'application/json',
2898 + 'Authorization' => 'Bearer ' . $slack_bot_token
2899 + ],
2900 + 'body' => json_encode([
2901 + 'name' => $channel_name,
2902 + 'is_private' => false // Public channel - anyone in workspace can join
2903 + ])
2904 + ]);
2905 +
2906 + if (!is_wp_error($response)) {
2907 + $response_body = wp_remote_retrieve_body($response);
2908 + $response_data = json_decode($response_body, true);
2909 +
2910 + //error_log("Channel creation response: " . $response_body);
2911 +
2912 + if (isset($response_data['ok']) && $response_data['ok']) {
2913 + $channel_id = $response_data['channel']['id'];
2914 + $actual_channel_name = $response_data['channel']['name'] ?? 'unknown';
2915 + //error_log("Channel created successfully: ID=$channel_id, Name=$actual_channel_name");
2916 + update_option("mxchat_channel_{$session_id}", $channel_id);
2917 +
2918 + // Auto-invite agents to the channel
2919 + $agent_user_ids = $this->options['live_agent_user_ids'] ?? '';
2920 +
2921 + if (!empty($agent_user_ids)) {
2922 + // Parse user IDs (one per line)
2923 + $user_ids = array_filter(array_map('trim', explode("\n", $agent_user_ids)));
2924 +
2925 + foreach ($user_ids as $user_id_to_invite) {
2926 + //error_log("Inviting user to channel: $user_id_to_invite");
2927 +
2928 + $invite_response = wp_remote_post('https://slack.com/api/conversations.invite', [
2929 + 'headers' => [
2930 + 'Content-Type' => 'application/json',
2931 + 'Authorization' => 'Bearer ' . $slack_bot_token
2932 + ],
2933 + 'body' => json_encode([
2934 + 'channel' => $channel_id,
2935 + 'users' => $user_id_to_invite
2936 + ])
2937 + ]);
2938 +
2939 + if (!is_wp_error($invite_response)) {
2940 + $invite_body = wp_remote_retrieve_body($invite_response);
2941 + $invite_data = json_decode($invite_body, true);
2942 + //error_log("Invite response for $user_id_to_invite: " . $invite_body);
2943 +
2944 + if (isset($invite_data['ok']) && $invite_data['ok']) {
2945 + //error_log("Successfully invited user $user_id_to_invite to channel");
2946 + } else {
2947 + //error_log("Failed to invite user $user_id_to_invite: " . ($invite_data['error'] ?? 'Unknown error'));
2948 + }
2949 + } else {
2950 + //error_log("WP Error inviting user $user_id_to_invite: " . $invite_response->get_error_message());
2951 + }
2952 + }
2953 + } else {
2954 + //error_log("No agent user IDs configured for auto-invite");
2955 + }
2956 + } else {
2957 + //error_log("Channel creation failed: " . ($response_data['error'] ?? 'Unknown error'));
2958 + }
2959 + } else {
2960 + //error_log("WP Error creating channel: " . $response->get_error_message());
2961 + }
2962 +
2963 + if (empty($channel_id)) {
2964 + return false; // Failed to create channel
2965 + }
2966 + }
2967 +
2968 + // Get recent chat history
2070 2969 $history = get_option("mxchat_history_{$session_id}", []);
2071 - $recent_history = array_slice($history, -5); // Get last 5 messages
2970 + $recent_history = array_slice($history, -5);
2072 2971
2073 - // Format conversation history
2972 + // Format conversation context
2074 2973 $conversation_context = "";
2075 2974 if (!empty($recent_history)) {
2076 2975 $conversation_context = "*Recent Conversation:*\n";
2077 2976 foreach ($recent_history as $hist_message) {
@@ -2082,83 +2981,32 @@
2082 2981 }
2083 2982
2084 2983 update_option("mxchat_mode_{$session_id}", 'agent');
2085 2984
2086 - $webhook_data = [
2087 - 'blocks' => [
2088 - [
2089 - 'type' => 'header',
2090 - 'text' => [
2091 - 'type' => 'plain_text',
2092 - 'text' => '🔔 New Live Agent Request',
2093 - 'emoji' => true
2094 - ]
2095 - ],
2096 - [
2097 - 'type' => 'section',
2098 - 'fields' => [
2099 - [
2100 - 'type' => 'mrkdwn',
2101 - 'text' => sprintf('*User ID:*\n`%s`', $user_id)
2102 - ],
2103 - [
2104 - 'type' => 'mrkdwn',
2105 - 'text' => sprintf('*Session ID:*\n`%s`', $session_id)
2106 - ]
2107 - ]
2108 - ]
2109 - ]
2110 - ];
2111 -
2112 - // Add conversation history if exists
2985 + // Send message to channel
2986 + $channel_message = "🔔 *New Live Agent Request*\n\n";
2987 + $channel_message .= "*Session ID:* `{$session_id}`\n";
2988 + $channel_message .= "*User ID:* `{$user_id}`\n\n";
2989 +
2113 2990 if (!empty($conversation_context)) {
2114 - $webhook_data['blocks'][] = [
2115 - 'type' => 'section',
2116 - 'text' => [
2117 - 'type' => 'mrkdwn',
2118 - 'text' => $conversation_context
2119 - ]
2120 - ];
2991 + $channel_message .= $conversation_context;
2121 2992 }
2993 +
2994 + $channel_message .= "*Current Message:*\n{$message}\n\n";
2995 + $channel_message .= "_Reply directly in this channel - all messages will go to the user_";
2122 2996
2123 - // Add the current message
2124 - $webhook_data['blocks'][] = [
2125 - 'type' => 'section',
2126 - 'text' => [
2127 - 'type' => 'mrkdwn',
2128 - 'text' => sprintf('*Current Message:*\n%s', $message)
2129 - ]
2130 - ];
2131 -
2132 - // Add the reply button
2133 - $webhook_data['blocks'][] = [
2134 - 'type' => 'actions',
2135 - 'elements' => [
2136 - [
2137 - 'type' => 'button',
2138 - 'text' => [
2139 - 'type' => 'plain_text',
2140 - 'text' => '✍️ Reply',
2141 - 'emoji' => true
2142 - ],
2143 - 'value' => $session_id,
2144 - 'action_id' => 'reply_to_user',
2145 - 'style' => 'primary'
2146 - ]
2147 - ]
2148 - ];
2149 -
2150 - $response = wp_remote_post($slack_webhook_url, [
2151 - 'body' => json_encode($webhook_data),
2997 + wp_remote_post('https://slack.com/api/chat.postMessage', [
2152 2998 'headers' => [
2153 2999 'Content-Type' => 'application/json',
3000 + 'Authorization' => 'Bearer ' . $slack_bot_token
2154 3001 ],
3002 + 'body' => json_encode([
3003 + 'channel' => $channel_id,
3004 + 'text' => $channel_message,
3005 + 'mrkdwn' => true
3006 + ])
2155 3007 ]);
2156 3008
2157 - if (is_wp_error($response)) {
2158 - return false;
2159 - }
2160 -
2161 3009 $success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
2162 3010 $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
2163 3011
2164 3012 $this->fallbackResponse = [
@@ -2177,79 +3025,145 @@
2177 3025 'fallbackResponse' => $this->fallbackResponse
2178 3026 ]);
2179 3027 wp_die();
2180 3028 }
3029 +
3030 +private function generate_channel_name($session_id) {
3031 + $email = null;
3032 + $name = null;
3033 +
3034 + // 1. First priority: Check if user is logged in and get their info
3035 + if (is_user_logged_in()) {
3036 + $current_user = wp_get_current_user();
3037 + if (!empty($current_user->user_email)) {
3038 + $email = $current_user->user_email;
3039 + //error_log("[DEBUG] Using logged-in user email for channel: {$email}");
3040 + }
3041 + if (!empty($current_user->display_name)) {
3042 + $name = $current_user->display_name;
3043 + //error_log("[DEBUG] Using logged-in user name for channel: {$name}");
3044 + }
3045 + }
3046 +
3047 + // 2. Second priority: Check for saved email/name from "require email to chat" option
3048 + if (empty($email)) {
3049 + $email_option_key = "mxchat_email_{$session_id}";
3050 + $saved_email = get_option($email_option_key);
3051 + if (!empty($saved_email)) {
3052 + $email = $saved_email;
3053 + //error_log("[DEBUG] Using saved email from session for channel: {$email}");
3054 + }
3055 + }
3056 +
3057 + if (empty($name)) {
3058 + $name_option_key = "mxchat_name_{$session_id}";
3059 + $saved_name = get_option($name_option_key);
3060 + if (!empty($saved_name)) {
3061 + $name = $saved_name;
3062 + //error_log("[DEBUG] Using saved name from session for channel: {$name}");
3063 + }
3064 + }
3065 +
3066 + // 3. Third priority: Check existing chat transcript for email/name
3067 + if (empty($email) || empty($name)) {
3068 + global $wpdb;
3069 + $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
3070 + $existing_data = $wpdb->get_row($wpdb->prepare(
3071 + "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",
3072 + $session_id
3073 + ));
3074 +
3075 + if ($existing_data) {
3076 + if (empty($email) && !empty($existing_data->user_email)) {
3077 + $email = $existing_data->user_email;
3078 + //error_log("[DEBUG] Using email from chat transcript for channel: {$email}");
3079 + }
3080 + if (empty($name) && !empty($existing_data->user_name)) {
3081 + $name = $existing_data->user_name;
3082 + //error_log("[DEBUG] Using name from chat transcript for channel: {$name}");
3083 + }
3084 + }
3085 + }
3086 +
3087 + // 4. Generate channel name based on priority: Name > Email > Session ID
3088 + $channel_name = '';
3089 +
3090 + if (!empty($name)) {
3091 + // Convert name to valid Slack channel name
3092 + $base_name = strtolower(trim($name));
3093 + // Replace spaces and invalid characters
3094 + $base_name = preg_replace('/[^a-z0-9\s]/', '', $base_name);
3095 + $base_name = preg_replace('/\s+/', '-', $base_name);
3096 + $base_name = trim($base_name, '-');
3097 +
3098 + // Get last 4 characters of session ID for uniqueness
3099 + $session_suffix = substr($session_id, -4);
3100 + $channel_name = 'chat-' . $base_name . '-' . strtolower($session_suffix);
3101 +
3102 + // Slack channel names have a 21 character limit
3103 + if (strlen($channel_name) > 21) {
3104 + // Calculate available space for name (21 - 'chat-' - '-' - session_suffix)
3105 + $available_space = 21 - 5 - 1 - strlen($session_suffix); // 'chat-' = 5, '-' = 1
3106 + $truncated_name = substr($base_name, 0, $available_space);
3107 + $truncated_name = rtrim($truncated_name, '-'); // Remove trailing hyphen
3108 + $channel_name = 'chat-' . $truncated_name . '-' . strtolower($session_suffix);
3109 + }
3110 +
3111 + //error_log("[DEBUG] Using name for channel: {$channel_name} (from name: {$name})");
3112 +
3113 + } elseif (!empty($email)) {
3114 + // Convert email to valid Slack channel name (your existing logic)
3115 + $channel_name = 'chat-' . strtolower(str_replace(['@', '.', '+', '_'], ['-at-', '-', '-plus-', '-'], $email));
3116 + // Remove any remaining invalid characters
3117 + $channel_name = preg_replace('/[^a-z0-9\-]/', '', $channel_name);
3118 + // Ensure it doesn't end with a hyphen
3119 + $channel_name = rtrim($channel_name, '-');
3120 + // Slack channel names have a 21 character limit, so truncate if needed
3121 + if (strlen($channel_name) > 21) {
3122 + $channel_name = substr($channel_name, 0, 21);
3123 + $channel_name = rtrim($channel_name, '-'); // Remove trailing hyphen if truncation created one
3124 + }
3125 +
3126 + //error_log("[DEBUG] Using email for channel: {$channel_name} (from email: {$email})");
3127 +
3128 + } else {
3129 + // Fallback to session ID if no name or email found
3130 + $channel_name = 'chat-' . strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $session_id));
3131 + //error_log("[DEBUG] No name or email found, using session ID for channel: {$channel_name}");
3132 + }
3133 +
3134 + // Final validation - ensure channel name meets Slack requirements
3135 + if (strlen($channel_name) > 21) {
3136 + $channel_name = substr($channel_name, 0, 21);
3137 + $channel_name = rtrim($channel_name, '-');
3138 + }
3139 +
3140 + //error_log("[DEBUG] Generated channel name: {$channel_name}");
3141 + return $channel_name;
3142 +}
2181 3143 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
2182 - $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
3144 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
3145 + $channel_id = get_option("mxchat_channel_{$session_id}", '');
2183 3146
2184 - if (empty($slack_webhook_url)) {
2185 - //error_log(esc_html__('Slack Webhook URL is not configured.', 'mxchat'));
3147 + if (empty($slack_bot_token) || empty($channel_id)) {
2186 3148 return false;
2187 3149 }
2188 3150
2189 - $webhook_data = [
2190 - 'blocks' => [
2191 - [
2192 - 'type' => 'header',
2193 - 'text' => [
2194 - 'type' => 'plain_text',
2195 - 'text' => esc_html__('📩 New Chat Message', 'mxchat'),
2196 - 'emoji' => true
2197 - ]
2198 - ],
2199 - [
2200 - 'type' => 'section',
2201 - 'fields' => [
2202 - [
2203 - 'type' => 'mrkdwn',
2204 - 'text' => sprintf(esc_html__('*User ID:*\n`%s`', 'mxchat'), $user_id)
2205 - ],
2206 - [
2207 - 'type' => 'mrkdwn',
2208 - 'text' => sprintf(esc_html__('*Session ID:*\n`%s`', 'mxchat'), $session_id)
2209 - ]
2210 - ]
2211 - ],
2212 - [
2213 - 'type' => 'section',
2214 - 'text' => [
2215 - 'type' => 'mrkdwn',
2216 - 'text' => sprintf(esc_html__('*Message:*\n%s', 'mxchat'), $message)
2217 - ]
2218 - ],
2219 - [
2220 - 'type' => 'actions',
2221 - 'elements' => [
2222 - [
2223 - 'type' => 'button',
2224 - 'text' => [
2225 - 'type' => 'plain_text',
2226 - 'text' => esc_html__('✍️ Reply', 'mxchat'),
2227 - 'emoji' => true
2228 - ],
2229 - 'value' => $session_id,
2230 - 'action_id' => 'reply_to_user',
2231 - 'style' => 'primary'
2232 - ]
2233 - ]
2234 - ]
2235 - ]
2236 - ];
3151 + $user_message = "💬 *User:* {$message}";
2237 3152
2238 - $response = wp_remote_post($slack_webhook_url, [
2239 - 'body' => json_encode($webhook_data),
3153 + $response = wp_remote_post('https://slack.com/api/chat.postMessage', [
2240 3154 'headers' => [
2241 3155 'Content-Type' => 'application/json',
3156 + 'Authorization' => 'Bearer ' . $slack_bot_token
2242 3157 ],
3158 + 'body' => json_encode([
3159 + 'channel' => $channel_id,
3160 + 'text' => $user_message,
3161 + 'mrkdwn' => true
3162 + ])
2243 3163 ]);
2244 3164
2245 - if (is_wp_error($response)) {
2246 - //error_log(esc_html__('Error sending message to Slack: ', 'mxchat') . $response->get_error_message());
2247 - return false;
2248 - }
2249 -
2250 - //error_log(esc_html__('Message sent to Slack successfully.', 'mxchat'));
2251 - return true;
3165 + return !is_wp_error($response);
2252 3166 }
2253 3167 public function handle_slack_interaction(WP_REST_Request $request) {
2254 3168 //error_log('Received Slack interaction');
2255 3169
@@ -2337,17 +3251,16 @@
2337 3251
2338 3252 // Default acknowledgment
2339 3253 return new WP_REST_Response(['ok' => true]);
2340 3254 }
2341 -
2342 3255 public function mxchat_handle_agent_response(WP_REST_Request $request) {
2343 3256 //error_log('Received agent response request');
2344 3257 //error_log('Request data: ' . print_r($request->get_params(), true));
2345 - // error_log('Raw body: ' . file_get_contents('php://input'));
3258 + // //error_log('Raw body: ' . file_get_contents('php://input'));
2346 3259
2347 3260 // Get the data from Slack's slash command format
2348 3261 $command_text = $request->get_param('text');
2349 - // error_log('Command text: ' . $command_text);
3262 + // //error_log('Command text: ' . $command_text);
2350 3263
2351 3264 if (empty($command_text)) {
2352 3265 //error_log(esc_html__('Agent response error: No command text received', 'mxchat'));
2353 3266 return new WP_REST_Response([
@@ -2372,9 +3285,9 @@
2372 3285 // Save the message
2373 3286 $message_id = $this->mxchat_save_chat_message($session_id, 'agent', $message);
2374 3287
2375 3288 if (!$message_id) {
2376 - // error_log('Failed to save agent message');
3289 + // //error_log('Failed to save agent message');
2377 3290 return new WP_REST_Response([
2378 3291 'error' => esc_html__('Failed to save message', 'mxchat')
2379 3292 ], 500);
2380 3293 }
@@ -2384,29 +3297,141 @@
2384 3297 'response_type' => 'in_channel',
2385 3298 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
2386 3299 ], 200);
2387 3300 }
2388 -
2389 -
2390 3301 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
2391 - //error_log(esc_html__("Switching back to chatbot mode via intent.", 'mxchat'));
2392 -
2393 - // Just update mode to AI
3302 + // Update mode to AI
2394 3303 update_option("mxchat_mode_{$session_id}", 'ai');
3304 +
3305 + // Clear any existing PDF context to start fresh
3306 + $this->clear_pdf_transients($session_id);
3307 +
3308 + // Set the response with explicit chat_mode
3309 + $this->fallbackResponse = [
3310 + 'text' => esc_html__('You are now chatting with the AI chatbot.', 'mxchat'),
3311 + 'html' => '',
3312 + 'images' => [],
3313 + 'chat_mode' => 'ai' // Ensure this is set
3314 + ];
3315 +
3316 + // Return the complete response array instead of just true
3317 + return $this->fallbackResponse;
3318 +}
2395 3319
2396 - // Initialize states
2397 - $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
2398 - $this->productCardHtml = '';
2399 -
2400 - // Set the response message
2401 - $this->fallbackResponse['text'] = esc_html__('You are now chatting with the AI chatbot.', 'mxchat');
2402 -
2403 - return true; // Intent was handled
3320 +public function handle_slack_messages(WP_REST_Request $request) {
3321 + // Log the incoming request for debugging
3322 + //error_log('Slack events request received: ' . $request->get_body());
3323 +
3324 + $body = $request->get_body();
3325 + $data = json_decode($body, true);
3326 +
3327 + // Handle Slack URL verification
3328 + if (isset($data['type']) && $data['type'] === 'url_verification') {
3329 + //error_log('Slack URL verification challenge: ' . $data['challenge']);
3330 + return new WP_REST_Response($data['challenge'], 200, ['Content-Type' => 'text/plain']);
3331 + }
3332 +
3333 + // IMPORTANT: Handle Slack's event deduplication
3334 + if (isset($data['event_id'])) {
3335 + $event_id = $data['event_id'];
3336 + $processed_events = get_transient('mxchat_slack_events') ?: [];
3337 +
3338 + // Check if we've already processed this event
3339 + if (in_array($event_id, $processed_events)) {
3340 + //error_log("Duplicate event detected: $event_id");
3341 + return new WP_REST_Response(['ok' => true]);
3342 + }
3343 +
3344 + // Add this event to processed list
3345 + $processed_events[] = $event_id;
3346 + // Keep only last 100 events to prevent memory issues
3347 + if (count($processed_events) > 100) {
3348 + $processed_events = array_slice($processed_events, -100);
3349 + }
3350 + // Store for 1 hour
3351 + set_transient('mxchat_slack_events', $processed_events, HOUR_IN_SECONDS);
3352 + }
3353 +
3354 + // Handle message events
3355 + if (isset($data['event']) && $data['event']['type'] === 'message') {
3356 + $event = $data['event'];
3357 +
3358 + // Skip bot messages and messages with subtypes (like bot_message)
3359 + if (isset($event['bot_id']) || isset($event['subtype'])) {
3360 + return new WP_REST_Response(['ok' => true]);
3361 + }
3362 +
3363 + // Additional check: Skip if this is a threaded reply to our confirmation
3364 + if (isset($event['thread_ts']) && $event['thread_ts'] !== $event['ts']) {
3365 + return new WP_REST_Response(['ok' => true]);
3366 + }
3367 +
3368 + $channel_id = $event['channel'];
3369 + $message_text = $event['text'] ?? '';
3370 + $message_ts = $event['ts'] ?? '';
3371 +
3372 + // Find session ID by looking for matching channel
3373 + global $wpdb;
3374 + $session_option = $wpdb->get_var(
3375 + $wpdb->prepare(
3376 + "SELECT option_name FROM {$wpdb->options}
3377 + WHERE option_name LIKE 'mxchat_channel_%'
3378 + AND option_value = %s",
3379 + $channel_id
3380 + )
3381 + );
3382 +
3383 + if ($session_option) {
3384 + $session_id = str_replace('mxchat_channel_', '', $session_option);
3385 +
3386 + // Create a unique key for this specific message
3387 + $message_key = md5($session_id . $message_ts . $message_text);
3388 + $processed_messages = get_transient('mxchat_processed_messages_' . $session_id) ?: [];
3389 +
3390 + // Check if we've already processed this exact message
3391 + if (in_array($message_key, $processed_messages)) {
3392 + //error_log("Duplicate message detected for session $session_id");
3393 + return new WP_REST_Response(['ok' => true]);
3394 + }
3395 +
3396 + // Add to processed messages
3397 + $processed_messages[] = $message_key;
3398 + // Keep only last 50 messages per session
3399 + if (count($processed_messages) > 50) {
3400 + $processed_messages = array_slice($processed_messages, -50);
3401 + }
3402 + set_transient('mxchat_processed_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
3403 +
3404 + // Save the agent message
3405 + $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
3406 +
3407 + // Send confirmation back to Slack (only once)
3408 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
3409 + if (!empty($slack_bot_token)) {
3410 + // Use a transient to prevent duplicate confirmations
3411 + $confirm_key = 'mxchat_confirm_' . $message_key;
3412 + if (!get_transient($confirm_key)) {
3413 + wp_remote_post('https://slack.com/api/chat.postMessage', [
3414 + 'headers' => [
3415 + 'Content-Type' => 'application/json',
3416 + 'Authorization' => 'Bearer ' . $slack_bot_token
3417 + ],
3418 + 'body' => json_encode([
3419 + 'channel' => $channel_id,
3420 + 'text' => "✅ _Message sent to user_",
3421 + 'thread_ts' => $event['ts'] // Reply in thread
3422 + ])
3423 + ]);
3424 + // Set transient to prevent duplicate confirmations
3425 + set_transient($confirm_key, true, 300); // 5 minutes
3426 + }
3427 + }
3428 + }
3429 + }
3430 +
3431 + return new WP_REST_Response(['ok' => true]);
2404 3432 }
2405 3433
2406 -
2407 -
2408 -
2409 3434 // For the word upload handler
2410 3435 public function mxchat_handle_word_upload() {
2411 3436 // Delegate to word handler
2412 3437 $this->word_handler->mxchat_handle_word_upload();
@@ -2429,21 +3454,102 @@
2429 3454 return MxChat_User::mxchat_get_user_identifier();
2430 3455 }
2431 3456
2432 3457 private function mxchat_generate_embedding($text, $api_key) {
2433 - $endpoint = 'https://api.openai.com/v1/embeddings';
2434 -
2435 - $body = wp_json_encode([
2436 - 'input' => $text,
2437 - 'model' => 'text-embedding-ada-002'
2438 - ]);
2439 -
3458 + try {
3459 + // Get options and selected model
3460 + $options = get_option('mxchat_options');
3461 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3462 +
3463 + // Determine endpoint and API key based on model
3464 + if (strpos($selected_model, 'voyage') === 0) {
3465 + $endpoint = 'https://api.voyageai.com/v1/embeddings';
3466 + $api_key = $options['voyage_api_key'] ?? '';
3467 +
3468 + // Check if Voyage API key is missing
3469 + if (empty($api_key)) {
3470 + //error_log('Voyage API key is missing');
3471 + return [
3472 + 'error' => esc_html__('Voyage AI API key is not configured', 'mxchat'),
3473 + 'error_code' => 'missing_voyage_api_key'
3474 + ];
3475 + }
3476 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3477 + $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
3478 + $api_key = $options['gemini_api_key'] ?? '';
3479 +
3480 + // Check if Gemini API key is missing
3481 + if (empty($api_key)) {
3482 + //error_log('Gemini API key is missing');
3483 + return [
3484 + 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
3485 + 'error_code' => 'missing_gemini_api_key'
3486 + ];
3487 + }
3488 + } else {
3489 + $endpoint = 'https://api.openai.com/v1/embeddings';
3490 + // Use the passed API key for OpenAI
3491 +
3492 + // Check if OpenAI API key is missing
3493 + if (empty($api_key)) {
3494 + //error_log('OpenAI API key is missing');
3495 + return [
3496 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
3497 + 'error_code' => 'missing_openai_api_key'
3498 + ];
3499 + }
3500 + }
3501 +
3502 + // Check if text is empty
3503 + if (empty($text)) {
3504 + //error_log('Empty text provided for embedding generation');
3505 + return [
3506 + 'error' => esc_html__('No text provided for embedding generation', 'mxchat'),
3507 + 'error_code' => 'empty_embedding_text'
3508 + ];
3509 + }
3510 +
3511 + // Prepare request body based on provider
3512 + if (strpos($selected_model, 'gemini-embedding') === 0) {
3513 + // Gemini API format
3514 + $request_body = [
3515 + 'model' => 'models/' . $selected_model,
3516 + 'content' => [
3517 + 'parts' => [
3518 + ['text' => $text]
3519 + ]
3520 + ],
3521 + 'outputDimensionality' => 1536
3522 + ];
3523 +
3524 + // Prepare headers for Gemini (API key as query parameter)
3525 + $endpoint .= '?key=' . $api_key;
3526 + $headers = [
3527 + 'Content-Type' => 'application/json'
3528 + ];
3529 + } else {
3530 + // OpenAI/Voyage API format
3531 + $request_body = [
3532 + 'input' => $text,
3533 + 'model' => $selected_model
3534 + ];
3535 +
3536 + // Add output_dimension for voyage-3-large
3537 + if ($selected_model === 'voyage-3-large') {
3538 + $request_body['output_dimension'] = 2048;
3539 + }
3540 +
3541 + // Prepare headers for OpenAI/Voyage
3542 + $headers = [
3543 + 'Content-Type' => 'application/json',
3544 + 'Authorization' => 'Bearer ' . $api_key
3545 + ];
3546 + }
3547 +
3548 + // Prepare request arguments
2440 3549 $args = [
2441 - 'body' => $body,
2442 - 'headers' => [
2443 - 'Content-Type' => 'application/json',
2444 - 'Authorization' => 'Bearer ' . $api_key,
2445 - ],
3550 + 'body' => wp_json_encode($request_body),
3551 + 'headers' => $headers,
2446 3552 'timeout' => 60,
2447 3553 'redirection' => 5,
2448 3554 'blocking' => true,
2449 3555 'httpversion' => '1.0',
@@ -2448,63 +3554,180 @@
2448 3554 'blocking' => true,
2449 3555 'httpversion' => '1.0',
2450 3556 'sslverify' => true,
2451 3557 ];
2452 -
3558 +
3559 + // Make the request
2453 3560 $response = wp_remote_post($endpoint, $args);
2454 -
3561 +
3562 + // Handle WordPress errors
2455 3563 if (is_wp_error($response)) {
2456 - return null;
3564 + $error_message = $response->get_error_message();
3565 + //error_log('Embedding Generation Error: ' . $error_message);
3566 + return [
3567 + 'error' => esc_html__('Connection error when generating embeddings: ', 'mxchat') . esc_html($error_message),
3568 + 'error_code' => 'embedding_connection_error'
3569 + ];
2457 3570 }
2458 -
3571 +
3572 + // Check HTTP status code
3573 + $status_code = wp_remote_retrieve_response_code($response);
3574 + if ($status_code !== 200) {
3575 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
3576 +
3577 + $error_message = isset($response_body['error']['message'])
3578 + ? $response_body['error']['message']
3579 + : 'HTTP Error ' . $status_code;
3580 +
3581 + $error_type = isset($response_body['error']['type'])
3582 + ? $response_body['error']['type']
3583 + : 'unknown';
3584 +
3585 + //error_log('Embedding API HTTP Error: ' . $status_code . ' - ' . $error_message);
3586 +
3587 + // Handle specific error types
3588 + switch ($error_type) {
3589 + case 'invalid_request_error':
3590 + if (strpos($error_message, 'API key') !== false) {
3591 + return [
3592 + 'error' => esc_html__('Invalid API key for embedding generation. Please check your API key configuration.', 'mxchat'),
3593 + 'error_code' => 'embedding_invalid_api_key'
3594 + ];
3595 + }
3596 + break;
3597 +
3598 + case 'authentication_error':
3599 + return [
3600 + 'error' => esc_html__('Authentication failed for embedding generation. Please check your API key.', 'mxchat'),
3601 + 'error_code' => 'embedding_auth_error'
3602 + ];
3603 +
3604 + case 'rate_limit_exceeded':
3605 + return [
3606 + 'error' => esc_html__('Rate limit exceeded for embedding generation. Please try again later.', 'mxchat'),
3607 + 'error_code' => 'embedding_rate_limit'
3608 + ];
3609 +
3610 + case 'quota_exceeded':
3611 + return [
3612 + 'error' => esc_html__('API quota exceeded for embedding generation. Please check your billing details.', 'mxchat'),
3613 + 'error_code' => 'embedding_quota_exceeded'
3614 + ];
3615 + }
3616 +
3617 + // Generic error fallback
3618 + return [
3619 + 'error' => esc_html__('Embedding API error - check embedding API key.: ', 'mxchat') . esc_html($error_message),
3620 + 'error_code' => 'embedding_api_error',
3621 + 'status_code' => $status_code
3622 + ];
3623 + }
3624 +
2459 3625 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2460 -
2461 - if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
2462 - return $response_body['data'][0]['embedding'];
3626 +
3627 + // Handle different response formats based on provider
3628 + if (strpos($selected_model, 'gemini-embedding') === 0) {
3629 + // Gemini API response format
3630 + if (isset($response_body['embedding']['values']) && is_array($response_body['embedding']['values'])) {
3631 + return $response_body['embedding']['values'];
3632 + } else {
3633 + //error_log('Invalid Gemini embedding response: ' . wp_json_encode($response_body));
3634 + return [
3635 + 'error' => esc_html__('Received invalid embedding data from the Gemini API.', 'mxchat'),
3636 + 'error_code' => 'invalid_gemini_embedding_response'
3637 + ];
3638 + }
2463 3639 } else {
2464 - return null;
3640 + // OpenAI/Voyage API response format
3641 + if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
3642 + return $response_body['data'][0]['embedding'];
3643 + } else {
3644 + //error_log('Invalid embedding response: ' . wp_json_encode($response_body));
3645 + return [
3646 + 'error' => esc_html__('Received invalid embedding data from the API.', 'mxchat'),
3647 + 'error_code' => 'invalid_embedding_response'
3648 + ];
3649 + }
2465 3650 }
3651 + } catch (Exception $e) {
3652 + //error_log('Embedding Exception: ' . $e->getMessage());
3653 + return [
3654 + 'error' => esc_html__('System error when generating embeddings: ', 'mxchat') . esc_html($e->getMessage()),
3655 + 'error_code' => 'embedding_exception'
3656 + ];
2466 3657 }
3658 +}
2467 3659
2468 3660
2469 -private function mxchat_find_relevant_content($user_embedding) {
2470 - //error_log('MXChat Vector Search: Starting content search...');
3661 +private function mxchat_find_relevant_content($user_embedding, $bot_id = 'default') {
3662 + error_log("MXCHAT DEBUG: find_relevant_content called with bot_id: " . $bot_id);
3663 +
3664 + // Get bot-specific Pinecone configuration
3665 + $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
3666 +
3667 + // Debug: Log the Pinecone configuration
3668 + error_log("MXCHAT DEBUG: Pinecone config for bot '$bot_id':");
3669 + error_log(" - use_pinecone: " . ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false'));
3670 + error_log(" - api_key: " . (empty($bot_pinecone_config['api_key']) ? 'EMPTY' : 'SET (hidden)'));
3671 + error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'NOT SET'));
3672 + error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'NOT SET'));
3673 +
3674 + // Determine whether to use Pinecone based on bot configuration
3675 + $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
3676 +
3677 + error_log("MXCHAT DEBUG: Using " . ($use_pinecone ? "Pinecone" : "WordPress Database") . " for knowledge retrieval");
2471 3678
2472 - // Retrieve the add-on settings from the database.
2473 - $addon_options = get_option('mxchat_pinecone_addon_options', array());
2474 -
2475 - // Determine whether Pinecone is enabled.
2476 - // We expect the sanitized setting to be a string '1' if enabled, otherwise '0'.
2477 - $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
2478 -
2479 - //error_log('Pinecone enabled flag: ' . $use_pinecone);
2480 -
2481 - if ($use_pinecone === 1) {
2482 - //error_log('MXChat Vector Search: Using Pinecone database');
2483 - return $this->find_relevant_content_pinecone($user_embedding);
3679 + if ($use_pinecone) {
3680 + return $this->find_relevant_content_pinecone($user_embedding, $bot_id, $bot_pinecone_config);
2484 3681 } else {
2485 - //error_log('MXChat Vector Search: Using WordPress database');
2486 - return $this->find_relevant_content_wordpress($user_embedding);
3682 + return $this->find_relevant_content_wordpress($user_embedding, $bot_id);
2487 3683 }
2488 3684 }
2489 3685
2490 -private function find_relevant_content_wordpress($user_embedding) {
3686 +private function find_relevant_content_wordpress($user_embedding, $bot_id = 'default') {
2491 3687 global $wpdb;
2492 3688 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2493 - $cache_key = 'mxchat_system_prompt_embeddings';
3689 + $cache_key = 'mxchat_system_prompt_embeddings_' . $bot_id;
2494 3690 $batch_size = 500;
2495 3691
3692 + // Initialize similarity analysis storage
3693 + $this->last_similarity_analysis = [
3694 + 'knowledge_base_type' => 'WordPress Database',
3695 + 'bot_id' => $bot_id,
3696 + 'top_matches' => [],
3697 + 'threshold_used' => 0,
3698 + 'total_checked' => 0
3699 + ];
3700 +
3701 + // NEW: Initialize valid URLs array
3702 + $valid_urls = [];
3703 +
3704 + // Get bot-specific options for similarity threshold
3705 + $bot_options = $this->get_bot_options($bot_id);
3706 + $current_options = !empty($bot_options) ? $bot_options : $this->options;
3707 +
2496 3708 // Retrieve embeddings from cache or database
2497 3709 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
2498 3710 if ($embeddings === false) {
3711 + // Cache miss - load embeddings from database WITH CONTENT and ROLE RESTRICTION for testing
2499 3712 $embeddings = [];
2500 3713 $offset = 0;
2501 3714
2502 - // Load in batches and build cache
2503 3715 do {
3716 + // Add bot_id filter if not default and if bot_metadata column exists
3717 + $bot_filter = '';
3718 + if ($bot_id !== 'default') {
3719 + // Check if bot_metadata column exists
3720 + $column_exists = $wpdb->get_var("SHOW COLUMNS FROM {$system_prompt_table} LIKE 'bot_metadata'");
3721 + if ($column_exists) {
3722 + $bot_filter = $wpdb->prepare(" AND (bot_metadata = %s OR bot_metadata IS NULL OR bot_metadata = '')", $bot_id);
3723 + }
3724 + }
3725 +
2504 3726 $query = $wpdb->prepare(
2505 - "SELECT id, embedding_vector
3727 + "SELECT id, embedding_vector, article_content, source_url, role_restriction
2506 3728 FROM {$system_prompt_table}
3729 + WHERE 1=1 {$bot_filter}
2507 3730 LIMIT %d OFFSET %d",
2508 3731 $batch_size,
2509 3732 $offset
2510 3733 );
@@ -2515,62 +3738,153 @@
2515 3738 }
2516 3739
2517 3740 $embeddings = array_merge($embeddings, $batch);
2518 3741 $offset += $batch_size;
2519 -
2520 - // Free memory
2521 3742 unset($batch);
2522 -
2523 3743 } while (true);
2524 3744
2525 3745 if (empty($embeddings)) {
2526 - return ''; // Return an empty string if no embeddings found
3746 + // Store empty array for valid URLs since no content found
3747 + $this->current_valid_urls = [];
3748 + return '';
2527 3749 }
3750 +
3751 + // Cache embeddings for future use (but note: this now includes content and role restrictions)
2528 3752 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
2529 3753 }
2530 3754
2531 - // Initialize array to store relevant results with similarity scores
3755 + // Get knowledge manager instance for role checking
3756 + $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
3757 +
3758 + // Get base similarity threshold from bot options or default options
3759 + $similarity_threshold = isset($current_options['similarity_threshold'])
3760 + ? ((int) $current_options['similarity_threshold']) / 100
3761 + : 0.35;
3762 +
3763 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3764 +
3765 + // Calculate similarities and build results array
3766 + $all_similarities = [];
2532 3767 $relevant_results = [];
2533 - // Iterate through embeddings to calculate similarity
3768 +
2534 3769 foreach ($embeddings as $embedding) {
2535 3770 $database_embedding = $embedding->embedding_vector
2536 3771 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
2537 3772 : null;
3773 +
2538 3774 if (is_array($database_embedding) && is_array($user_embedding)) {
2539 3775 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
2540 - $relevant_results[] = [
2541 - 'id' => $embedding->id,
2542 - 'similarity' => $similarity
3776 +
3777 + // Check role access
3778 + $role_restriction = $embedding->role_restriction ?? 'public';
3779 + $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
3780 +
3781 + // Store ALL similarities for testing (top 10)
3782 + $source_display = '';
3783 + if (!empty($embedding->source_url) && $embedding->source_url !== '#') {
3784 + $source_display = $embedding->source_url;
3785 + } else {
3786 + $content_preview = strip_tags($embedding->article_content ?? '');
3787 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3788 + $source_display = substr(trim($content_preview), 0, 50) . '...';
3789 + }
3790 +
3791 + $all_similarities[] = [
3792 + 'document_id' => $embedding->id,
3793 + 'similarity' => $similarity,
3794 + 'similarity_percentage' => round($similarity * 100, 2),
3795 + 'above_threshold' => $similarity >= $similarity_threshold,
3796 + 'source_display' => $source_display,
3797 + 'content_preview' => substr(strip_tags($embedding->article_content ?? ''), 0, 100) . '...',
3798 + 'used_for_context' => false, // Initialize as false, we'll update this later
3799 + 'role_restriction' => $role_restriction,
3800 + 'has_access' => $has_access,
3801 + 'filtered_out' => !$has_access
2543 3802 ];
3803 +
3804 + // Only consider results above threshold AND with access for actual content retrieval
3805 + if ($similarity >= $similarity_threshold && $has_access) {
3806 + $relevant_results[] = [
3807 + 'id' => $embedding->id,
3808 + 'similarity' => $similarity
3809 + ];
3810 + }
2544 3811 }
2545 - // Free memory
3812 +
2546 3813 unset($database_embedding);
2547 3814 }
2548 3815
2549 - // Retrieve the similarity threshold
2550 - $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
2551 -
2552 - // Filter and sort relevant results by similarity
2553 - $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
2554 - return $result['similarity'] >= $similarity_threshold;
3816 + // Sort ALL similarities for testing display (highest first)
3817 + usort($all_similarities, function ($a, $b) {
3818 + return $b['similarity'] <=> $a['similarity'];
2555 3819 });
3820 +
3821 + // Sort relevant results by similarity (highest first)
2556 3822 usort($relevant_results, function ($a, $b) {
2557 3823 return $b['similarity'] <=> $a['similarity'];
2558 3824 });
2559 -
2560 - // Limit to the top 5 results
2561 - $top_results = array_slice($relevant_results, 0, 5);
2562 -
2563 - // Initialize the final content
3825 +
3826 + // Get top 5 results for actual content (standard approach)
3827 + $top_results = array_slice($relevant_results, 0, 3);
3828 +
3829 + // NOW mark which documents are actually used for context
3830 + $used_document_ids = [];
3831 + foreach ($top_results as $result) {
3832 + $used_document_ids[] = $result['id'];
3833 + }
3834 +
3835 + // Update the all_similarities array to mark which were actually used
3836 + foreach ($all_similarities as &$similarity_item) {
3837 + $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
3838 + }
3839 +
3840 + // Store top 10 for testing panel (now with correct used_for_context flags and role info)
3841 + $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
3842 + $this->last_similarity_analysis['total_checked'] = count($embeddings);
3843 +
3844 + // Initialize final content
2564 3845 $content = '';
2565 -
2566 - // Fetch and combine content for the top results
2567 - foreach ($top_results as $result) {
3846 +
3847 + // Track document IDs to avoid duplicates
3848 + $added_document_ids = [];
3849 +
3850 + // Fetch and format content for each selected result
3851 + foreach ($top_results as $index => $result) {
3852 + if (in_array($result['id'], $added_document_ids)) {
3853 + continue;
3854 + }
3855 +
2568 3856 $chunk_content = $this->fetch_content_with_product_links($result['id']);
2569 - // Check if the content is PDF-related and add surrounding pages
3857 + $added_document_ids[] = $result['id'];
3858 +
3859 + // NEW: Extract source_url from database for this result
3860 + $source_url = $wpdb->get_var($wpdb->prepare(
3861 + "SELECT source_url FROM {$system_prompt_table} WHERE id = %d",
3862 + $result['id']
3863 + ));
3864 +
3865 + // NEW: Add source_url to valid URLs list if it exists and is not empty/placeholder
3866 + if (!empty($source_url) && $source_url !== '#') {
3867 + $valid_urls[] = $source_url;
3868 + }
3869 +
3870 + // NEW: Extract any URLs from the article content itself
3871 + preg_match_all(
3872 + '#\bhttps?://[^\s<>"\']+#i',
3873 + $chunk_content,
3874 + $content_urls
3875 + );
3876 + if (!empty($content_urls[0])) {
3877 + $valid_urls = array_merge($valid_urls, $content_urls[0]);
3878 + }
3879 +
3880 + $content .= "## Reference " . ($index + 1) . " ##\n";
3881 + $content .= $chunk_content . "\n\n";
3882 +
3883 + // PDF surrounding pages logic (unchanged)
2570 3884 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
2571 3885 $surrounding_content = $wpdb->get_results($wpdb->prepare(
2572 - "SELECT article_content FROM {$system_prompt_table}
3886 + "SELECT id, article_content, role_restriction FROM {$system_prompt_table}
2573 3887 WHERE id IN (
2574 3888 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
2575 3889 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
2576 3890 )",
@@ -2576,52 +3890,146 @@
2576 3890 )",
2577 3891 $result['id'],
2578 3892 $result['id']
2579 3893 ));
2580 - // Add previous content if it exists
3894 +
3895 + // Check role access for surrounding content too
2581 3896 if (!empty($surrounding_content[0])) {
2582 - $content .= $surrounding_content[0]->article_content . "\n\n";
3897 + $surrounding_role = $surrounding_content[0]->role_restriction ?? 'public';
3898 + if ($knowledge_manager->mxchat_user_has_content_access($surrounding_role)) {
3899 + // NEW: Extract URLs from surrounding content too
3900 + preg_match_all(
3901 + '#\bhttps?://[^\s<>"\']+#i',
3902 + $surrounding_content[0]->article_content,
3903 + $surrounding_urls
3904 + );
3905 + if (!empty($surrounding_urls[0])) {
3906 + $valid_urls = array_merge($valid_urls, $surrounding_urls[0]);
3907 + }
3908 +
3909 + $content .= "## Related Content ##\n";
3910 + $content .= $surrounding_content[0]->article_content . "\n\n";
3911 + $added_document_ids[] = $surrounding_content[0]->id;
3912 + }
2583 3913 }
2584 - // Add the main chunk content
2585 - $content .= $chunk_content . "\n\n";
2586 - // Add next content if it exists
3914 +
2587 3915 if (!empty($surrounding_content[1])) {
2588 - $content .= $surrounding_content[1]->article_content . "\n\n";
3916 + $surrounding_role = $surrounding_content[1]->role_restriction ?? 'public';
3917 + if ($knowledge_manager->mxchat_user_has_content_access($surrounding_role)) {
3918 + // NEW: Extract URLs from surrounding content too
3919 + preg_match_all(
3920 + '#\bhttps?://[^\s<>"\']+#i',
3921 + $surrounding_content[1]->article_content,
3922 + $surrounding_urls
3923 + );
3924 + if (!empty($surrounding_urls[0])) {
3925 + $valid_urls = array_merge($valid_urls, $surrounding_urls[0]);
3926 + }
3927 +
3928 + $content .= "## Related Content ##\n";
3929 + $content .= $surrounding_content[1]->article_content . "\n\n";
3930 + $added_document_ids[] = $surrounding_content[1]->id;
3931 + }
2589 3932 }
2590 - } else {
2591 - // For non-PDF content, add directly
2592 - $content .= $chunk_content . "\n\n";
2593 3933 }
2594 3934 }
3935 +
3936 + // NEW: Store unique valid URLs for validation
3937 + $this->current_valid_urls = array_unique($valid_urls);
3938 +
3939 + // Add response guidelines
3940 + if (empty($top_results)) {
3941 + $content = "No reference information was found for this query.\n\n";
3942 + } else {
3943 + $content .= "\n## Response Guidelines ##\n" .
3944 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3945 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
3946 + "If you don't have specific information or are uncertain about any details, it's always " .
3947 + "better to honestly say you don't know rather than making up or guessing at answers. " .
3948 + "When information is incomplete, let them know you are unsure.\n\n" .
3949 + "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
3950 + "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about.";
3951 + }
2595 3952
2596 3953 return trim($content);
2597 3954 }
2598 -/**
2599 - * Find relevant content in Pinecone vector database
2600 - */
2601 -private function find_relevant_content_pinecone($user_embedding) {
2602 - $options = get_option('mxchat_pinecone_addon_options', array());
2603 - $api_key = $options['mxchat_pinecone_api_key'] ?? '';
2604 - $host = $options['mxchat_pinecone_host'] ?? '';
2605 3955
3956 +private function find_relevant_content_pinecone($user_embedding, $bot_id = 'default', $bot_config = null) {
3957 + global $wpdb;
3958 +
3959 + error_log("MXCHAT DEBUG: find_relevant_content_pinecone called");
3960 + error_log(" - bot_id: " . $bot_id);
3961 + error_log(" - user_embedding is array: " . (is_array($user_embedding) ? 'yes' : 'no'));
3962 + error_log(" - user_embedding count: " . (is_array($user_embedding) ? count($user_embedding) : 'N/A'));
3963 +
3964 + // Use bot-specific config or fall back to default
3965 + if ($bot_config === null) {
3966 + $bot_config = $this->get_bot_pinecone_config($bot_id);
3967 + }
3968 +
3969 + $api_key = $bot_config['api_key'] ?? '';
3970 + $host = $bot_config['host'] ?? '';
3971 + $namespace = $bot_config['namespace'] ?? '';
3972 +
3973 + error_log("MXCHAT DEBUG: Pinecone query parameters:");
3974 + error_log(" - API Key: " . (empty($api_key) ? 'EMPTY - ERROR!' : 'Present (length: ' . strlen($api_key) . ')'));
3975 + error_log(" - Host: " . (empty($host) ? 'EMPTY - ERROR!' : $host));
3976 + error_log(" - Namespace: " . (empty($namespace) ? 'EMPTY (will use default)' : $namespace));
3977 +
3978 + // Initialize similarity analysis storage
3979 + $this->last_similarity_analysis = [
3980 + 'knowledge_base_type' => 'Pinecone',
3981 + 'bot_id' => $bot_id,
3982 + 'namespace' => $namespace,
3983 + 'top_matches' => [],
3984 + 'threshold_used' => 0,
3985 + 'total_checked' => 0
3986 + ];
3987 +
3988 + // NEW: Initialize valid URLs array
3989 + $valid_urls = [];
3990 +
2606 3991 if (empty($host) || empty($api_key)) {
2607 - //error_log('Pinecone credentials not properly configured');
3992 + error_log("MXCHAT DEBUG ERROR: Missing Pinecone host or API key!");
3993 + error_log(" - Host empty: " . (empty($host) ? 'YES' : 'NO'));
3994 + error_log(" - API key empty: " . (empty($api_key) ? 'YES' : 'NO'));
3995 + // Store empty array for valid URLs since we can't proceed
3996 + $this->current_valid_urls = [];
2608 3997 return '';
2609 3998 }
2610 -
2611 - // Get similarity threshold from WordPress settings
2612 - $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
2613 -
3999 +
4000 + // Get knowledge manager instance for role checking
4001 + $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
4002 +
4003 + // Get the similarity threshold from the bot options or main options
4004 + $bot_options = $this->get_bot_options($bot_id);
4005 + $current_options = !empty($bot_options) ? $bot_options : get_option('mxchat_options', []);
4006 +
4007 + $similarity_threshold = isset($current_options['similarity_threshold'])
4008 + ? ((int) $current_options['similarity_threshold']) / 100
4009 + : 0.35;
4010 +
4011 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
4012 +
2614 4013 // Prepare the query request for Pinecone
2615 4014 $api_endpoint = "https://{$host}/query";
2616 -
4015 +
2617 4016 $request_body = array(
2618 4017 'vector' => $user_embedding,
2619 - 'topK' => 5,
4018 + 'topK' => 20, // Request more to get good testing data
2620 4019 'includeMetadata' => true,
2621 4020 'includeValues' => true
2622 4021 );
2623 -
4022 +
4023 + // Add namespace if specified for this bot
4024 + if (!empty($namespace)) {
4025 + $request_body['namespace'] = $namespace;
4026 + }
4027 +
4028 + error_log("MXCHAT DEBUG: About to call Pinecone API");
4029 + error_log(" - Endpoint: " . $api_endpoint);
4030 + error_log(" - Namespace in request: " . (!empty($namespace) ? $namespace : 'NOT SET'));
4031 +
2624 4032 $response = wp_remote_post($api_endpoint, array(
2625 4033 'headers' => array(
2626 4034 'Api-Key' => $api_key,
2627 4035 'accept' => 'application/json',
@@ -2629,45 +4037,220 @@
2629 4037 ),
2630 4038 'body' => wp_json_encode($request_body),
2631 4039 'timeout' => 30
2632 4040 ));
2633 -
4041 +
2634 4042 if (is_wp_error($response)) {
2635 - //error_log('Pinecone query error: ' . $response->get_error_message());
4043 + error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
4044 + // Store empty array for valid URLs
4045 + $this->current_valid_urls = [];
2636 4046 return '';
2637 4047 }
2638 -
4048 +
2639 4049 $response_code = wp_remote_retrieve_response_code($response);
4050 + error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
4051 +
2640 4052 if ($response_code !== 200) {
2641 - //error_log('Pinecone API error: ' . wp_remote_retrieve_body($response));
4053 + $response_body = wp_remote_retrieve_body($response);
4054 + error_log("MXCHAT DEBUG ERROR: Pinecone API error response: " . substr($response_body, 0, 500));
4055 + // Store empty array for valid URLs
4056 + $this->current_valid_urls = [];
2642 4057 return '';
2643 4058 }
2644 -
2645 - $results = json_decode(wp_remote_retrieve_body($response), true);
4059 +
4060 + // ADD DETAILED DEBUG SECTION HERE
4061 + $response_body = wp_remote_retrieve_body($response);
4062 + error_log("MXCHAT DEBUG: Raw Pinecone response length: " . strlen($response_body));
4063 +
4064 + $results = json_decode($response_body, true);
4065 +
4066 + if (json_last_error() !== JSON_ERROR_NONE) {
4067 + error_log("MXCHAT DEBUG ERROR: JSON decode error: " . json_last_error_msg());
4068 + error_log("MXCHAT DEBUG: First 500 chars of response: " . substr($response_body, 0, 500));
4069 + // Store empty array for valid URLs
4070 + $this->current_valid_urls = [];
4071 + return '';
4072 + }
4073 +
4074 + error_log("MXCHAT DEBUG: Pinecone response structure:");
4075 + error_log(" - Has 'matches' key: " . (isset($results['matches']) ? 'yes' : 'no'));
4076 + error_log(" - Has 'namespace' key: " . (isset($results['namespace']) ? 'yes (' . $results['namespace'] . ')' : 'no'));
4077 +
2646 4078 if (empty($results['matches'])) {
4079 + error_log("MXCHAT DEBUG: No matches found in Pinecone response");
4080 + error_log("MXCHAT DEBUG: Response keys: " . implode(', ', array_keys($results)));
4081 + // Store empty array for valid URLs
4082 + $this->current_valid_urls = [];
2647 4083 return '';
2648 4084 }
2649 -
4085 +
4086 + error_log("MXCHAT DEBUG: Found " . count($results['matches']) . " matches in Pinecone");
4087 +
4088 + // Log first match details for debugging
4089 + if (!empty($results['matches'][0])) {
4090 + $first_match = $results['matches'][0];
4091 + error_log("MXCHAT DEBUG: First match details:");
4092 + error_log(" - Score: " . ($first_match['score'] ?? 'no score'));
4093 + error_log(" - Has metadata: " . (isset($first_match['metadata']) ? 'yes' : 'no'));
4094 + if (isset($first_match['metadata'])) {
4095 + error_log(" - Metadata keys: " . implode(', ', array_keys($first_match['metadata'])));
4096 + }
4097 + }
4098 +
2650 4099 // Initialize the final content
2651 4100 $content = '';
2652 -
2653 - // Process each match
2654 - foreach ($results['matches'] as $match) {
4101 + $matches_used = 0;
4102 + $matches_used_for_context = [];
4103 +
4104 + // Process each match for actual content generation (lazy role checking)
4105 + foreach ($results['matches'] as $index => $match) {
2655 4106 // Skip if similarity is below threshold
2656 4107 if ($match['score'] < $similarity_threshold) {
2657 4108 continue;
2658 4109 }
2659 -
2660 - if (!empty($match['metadata']['text']) && !empty($match['metadata']['source_url'])) {
2661 - // Add content with citation
2662 - $content .= $match['metadata']['text'] . "\n";
2663 - $content .= "Source: " . $match['metadata']['source_url'] . "\n\n";
4110 +
4111 + // Limit to top 3 matches above threshold
4112 + if ($matches_used >= 3) {
4113 + break;
2664 4114 }
4115 +
4116 + if (!empty($match['metadata']['text'])) {
4117 + // LAZY ROLE CHECK: Only check role for content we're actually considering
4118 + $match_id = $match['id'] ?? '';
4119 + $role_restriction = $this->get_single_vector_role($match_id, $match['metadata']);
4120 + $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
4121 +
4122 + // Skip if user doesn't have access
4123 + if (!$has_access) {
4124 + continue;
4125 + }
4126 +
4127 + // User has access - add to content
4128 + $content .= "## Reference " . ($matches_used + 1) . " ##\n";
4129 + $content .= $match['metadata']['text'] . "\n\n";
4130 +
4131 + // NEW: Extract source_url from metadata if it exists
4132 + if (!empty($match['metadata']['source_url']) && $match['metadata']['source_url'] !== '#') {
4133 + $valid_urls[] = $match['metadata']['source_url'];
4134 + $content .= "URL: " . $match['metadata']['source_url'] . "\n\n";
4135 + }
4136 +
4137 + // NEW: Extract any URLs from the text content itself
4138 + preg_match_all(
4139 + '#\bhttps?://[^\s<>"\']+#i',
4140 + $match['metadata']['text'],
4141 + $content_urls
4142 + );
4143 + if (!empty($content_urls[0])) {
4144 + $valid_urls = array_merge($valid_urls, $content_urls[0]);
4145 + }
4146 +
4147 + $matches_used_for_context[] = $match['id'] ?? $index;
4148 + $matches_used++;
4149 + }
2665 4150 }
2666 -
4151 +
4152 + // Process ALL matches for testing data (top 10) - with role checking for testing display
4153 + $all_matches = [];
4154 + foreach ($results['matches'] as $index => $match) {
4155 + if ($index >= 10) break; // Limit to top 10 for testing
4156 +
4157 + $match_id = $match['id'] ?? '';
4158 +
4159 + // Check role access for testing display (use cache if available)
4160 + $role_restriction = $this->get_single_vector_role($match_id, $match['metadata']);
4161 + $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
4162 +
4163 + $source_display = '';
4164 + if (!empty($match['metadata']['source_url'])) {
4165 + $source_display = $match['metadata']['source_url'];
4166 + } else {
4167 + $content_preview = strip_tags($match['metadata']['text'] ?? '');
4168 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
4169 + $source_display = substr(trim($content_preview), 0, 50) . '...';
4170 + }
4171 +
4172 + $match_id_for_display = $match['id'] ?? $index;
4173 +
4174 + $all_matches[] = [
4175 + 'document_id' => $match_id_for_display,
4176 + 'similarity' => $match['score'],
4177 + 'similarity_percentage' => round($match['score'] * 100, 2),
4178 + 'above_threshold' => $match['score'] >= $similarity_threshold,
4179 + 'source_display' => $source_display,
4180 + 'content_preview' => substr(strip_tags($match['metadata']['text'] ?? ''), 0, 100) . '...',
4181 + 'used_for_context' => in_array($match_id_for_display, $matches_used_for_context),
4182 + 'role_restriction' => $role_restriction,
4183 + 'has_access' => $has_access,
4184 + 'filtered_out' => !$has_access
4185 + ];
4186 + }
4187 +
4188 + // Store for testing panel
4189 + $this->last_similarity_analysis['top_matches'] = $all_matches;
4190 + $this->last_similarity_analysis['total_checked'] = count($results['matches']);
4191 +
4192 + // NEW: Store unique valid URLs for validation
4193 + $this->current_valid_urls = array_unique($valid_urls);
4194 +
4195 + // Add response guidelines
4196 + if ($matches_used === 0) {
4197 + $content = "No reference information was found for this query.\n\n";
4198 + } else {
4199 + $content .= "\n## Response Guidelines ##\n" .
4200 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
4201 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
4202 + "If you don't have specific information or are uncertain about any details, it's always " .
4203 + "better to honestly say you don't know rather than making up or guessing at answers. " .
4204 + "When information is incomplete, let them know you are unsure.\n\n" .
4205 + "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
4206 + "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about.";
4207 + }
4208 +
2667 4209 return trim($content);
2668 4210 }
2669 4211
4212 +/**
4213 + * Get role restriction for a single vector (with caching)
4214 + */
4215 +private function get_single_vector_role($vector_id, $metadata = array()) {
4216 + global $wpdb;
4217 +
4218 + if (empty($vector_id)) {
4219 + return 'public';
4220 + }
4221 +
4222 + // Check cache first
4223 + $cache_key = 'mxchat_vector_role_' . $vector_id;
4224 + $cached_role = wp_cache_get($cache_key, 'mxchat_vector_roles');
4225 +
4226 + if ($cached_role !== false) {
4227 + return $cached_role;
4228 + }
4229 +
4230 + $role_restriction = 'public';
4231 +
4232 + // First try Pinecone metadata
4233 + if (!empty($metadata['role_restriction'])) {
4234 + $role_restriction = $metadata['role_restriction'];
4235 + } else {
4236 + // Check WordPress table for user-modified roles
4237 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
4238 + $stored_role = $wpdb->get_var($wpdb->prepare(
4239 + "SELECT role_restriction FROM {$roles_table} WHERE vector_id = %s",
4240 + $vector_id
4241 + ));
4242 +
4243 + if ($stored_role) {
4244 + $role_restriction = $stored_role;
4245 + }
4246 + }
4247 +
4248 + // Cache individual role for 1 hour
4249 + wp_cache_set($cache_key, $role_restriction, 'mxchat_vector_roles', 3600);
4250 +
4251 + return $role_restriction;
4252 +}
2670 4253
2671 4254 private function mxchat_find_relevant_products($user_embedding) {
2672 4255 //error_log('MXChat Vector Search: Starting product search...');
2673 4256
@@ -2686,9 +4269,8 @@
2686 4269 //error_log('MXChat Vector Search: Using WordPress database for products');
2687 4270 return $this->find_relevant_products_wordpress($user_embedding);
2688 4271 }
2689 4272 }
2690 -
2691 4273 private function find_relevant_products_wordpress($user_embedding) {
2692 4274 global $wpdb;
2693 4275 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2694 4276 $cache_key = 'mxchat_system_prompt_embeddings';
@@ -2752,9 +4334,9 @@
2752 4334 usort($relevant_results, function ($a, $b) {
2753 4335 return $b['similarity'] <=> $a['similarity'];
2754 4336 });
2755 4337
2756 - $top_results = array_slice($relevant_results, 0, 5);
4338 + $top_results = array_slice($relevant_results, 0, 3);
2757 4339 $content = '';
2758 4340
2759 4341 foreach ($top_results as $result) {
2760 4342 $chunk_content = $this->fetch_content_with_product_links($result['id']);
@@ -2763,9 +4345,9 @@
2763 4345
2764 4346 return trim($content);
2765 4347 }
2766 4348
2767 -// Modified search function with correct filter syntax
4349 +
2768 4350 private function find_relevant_products_pinecone($user_embedding) {
2769 4351 //error_log('Starting Pinecone product search...');
2770 4352
2771 4353 $options = get_option('mxchat_pinecone_addon_options', array());
@@ -2862,321 +4444,1509 @@
2862 4444
2863 4445 return null;
2864 4446 }
2865 4447
2866 -// Function definition
2867 -private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $conversation_history) {
4448 +/**
4449 + * Get system instructions for a specific bot or default
4450 + * Checks for multi-bot add-on and uses bot-specific instructions if available
4451 + */
4452 +private function get_system_instructions($bot_id = 'default') {
4453 + // Check if multi-bot add-on is active
4454 + if (class_exists('MxChat_Multi_Bot_Core_Manager') && $bot_id !== 'default') {
4455 + // Get bot-specific options from multi-bot add-on
4456 + $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
4457 +
4458 + // If bot has custom system instructions, use those
4459 + if (!empty($bot_options['system_prompt_instructions'])) {
4460 + return $bot_options['system_prompt_instructions'];
4461 + }
4462 + }
4463 +
4464 + // Fall back to default system instructions
4465 + return isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4466 +}
4467 +/**
4468 + * Get the current bot ID from session or request context
4469 + */
4470 +private function get_current_bot_id($session_id = '') {
4471 + // First, check if bot_id is passed in the current request
4472 + if (isset($_POST['bot_id']) && !empty($_POST['bot_id'])) {
4473 + return sanitize_key($_POST['bot_id']);
4474 + }
4475 +
4476 + // If not in POST, try to get it from session data
4477 + if (!empty($session_id)) {
4478 + $bot_id = get_option("mxchat_session_bot_{$session_id}", '');
4479 + if (!empty($bot_id)) {
4480 + return $bot_id;
4481 + }
4482 + }
4483 +
4484 + // Fall back to default
4485 + return 'default';
4486 +}
4487 +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-4o') {
2868 4488 try {
2869 4489 if (!$relevant_content) {
2870 - return esc_html__("I'm sorry, I couldn't find relevant information on that topic.", 'mxchat');
4490 + $error_response = [
4491 + 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
4492 + 'error_code' => 'no_relevant_content'
4493 + ];
4494 +
4495 + if ($testing_data !== null) {
4496 + $error_response['testing_data'] = $testing_data;
4497 + }
4498 +
4499 + return $error_response;
2871 4500 }
2872 -
2873 - // Ensure conversation_history is an array
4501 +
2874 4502 if (!is_array($conversation_history)) {
2875 4503 $conversation_history = array();
2876 4504 }
4505 +
4506 + // Check if this is an OpenRouter model
4507 + if ($selected_model === 'openrouter') {
4508 + // Get the actual OpenRouter model from options
4509 + $openrouter_selected_model = $this->options['openrouter_selected_model'] ?? '';
4510 +
4511 + if (empty($openrouter_selected_model)) {
4512 + $error_response = [
4513 + 'error' => esc_html__('No OpenRouter model selected. Please select a model in settings.', 'mxchat'),
4514 + 'error_code' => 'no_openrouter_model_selected'
4515 + ];
4516 + if ($testing_data !== null) {
4517 + $error_response['testing_data'] = $testing_data;
4518 + }
4519 + return $error_response;
4520 + }
4521 +
4522 + if (empty($openrouter_api_key)) {
4523 + $error_response = [
4524 + 'error' => esc_html__('OpenRouter API key is not configured', 'mxchat'),
4525 + 'error_code' => 'missing_openrouter_api_key'
4526 + ];
4527 + if ($testing_data !== null) {
4528 + $error_response['testing_data'] = $testing_data;
4529 + }
4530 + return $error_response;
4531 + }
4532 +
4533 + if ($streaming) {
4534 + return $this->mxchat_generate_response_openrouter_stream(
4535 + $openrouter_selected_model,
4536 + $openrouter_api_key,
4537 + $conversation_history,
4538 + $relevant_content,
4539 + $session_id,
4540 + $testing_data
4541 + );
4542 + } else {
4543 + $response = $this->mxchat_generate_response_openrouter(
4544 + $openrouter_selected_model,
4545 + $openrouter_api_key,
4546 + $conversation_history,
4547 + $relevant_content
4548 + );
4549 + }
4550 +
4551 + if (is_array($response) && isset($response['error'])) {
4552 + if ($testing_data !== null) {
4553 + $response['testing_data'] = $testing_data;
4554 + }
4555 + return $response;
4556 + }
4557 +
4558 + return $response;
4559 + }
2877 4560
2878 - // Get selected model with default fallback
2879 - $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
2880 -
2881 4561 // Extract model prefix to determine the provider
2882 4562 $model_parts = explode('-', $selected_model);
2883 4563 $provider = strtolower($model_parts[0]);
2884 -
4564 +
2885 4565 // Handle model selection based on provider prefix
2886 4566 switch ($provider) {
2887 - case 'claude':
2888 - if (empty($claude_api_key)) {
2889 - throw new Exception(esc_html__('Claude API key is not configured', 'mxchat'));
4567 + case 'gemini':
4568 + if (empty($gemini_api_key)) {
4569 + $error_response = [
4570 + 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
4571 + 'error_code' => 'missing_gemini_api_key'
4572 + ];
4573 + if ($testing_data !== null) {
4574 + $error_response['testing_data'] = $testing_data;
4575 + }
4576 + return $error_response;
2890 4577 }
2891 - return $this->mxchat_generate_response_claude(
4578 + $response = $this->mxchat_generate_response_gemini(
2892 4579 $selected_model,
2893 - $claude_api_key,
4580 + $gemini_api_key,
2894 4581 $conversation_history,
2895 4582 $relevant_content
2896 4583 );
2897 -
4584 + break;
4585 +
4586 + case 'claude':
4587 + if (empty($claude_api_key)) {
4588 + $error_response = [
4589 + 'error' => esc_html__('Claude API key is not configured', 'mxchat'),
4590 + 'error_code' => 'missing_claude_api_key'
4591 + ];
4592 + if ($testing_data !== null) {
4593 + $error_response['testing_data'] = $testing_data;
4594 + }
4595 + return $error_response;
4596 + }
4597 + if ($streaming) {
4598 + return $this->mxchat_generate_response_claude_stream(
4599 + $selected_model,
4600 + $claude_api_key,
4601 + $conversation_history,
4602 + $relevant_content,
4603 + $session_id,
4604 + $testing_data
4605 + );
4606 + } else {
4607 + $response = $this->mxchat_generate_response_claude(
4608 + $selected_model,
4609 + $claude_api_key,
4610 + $conversation_history,
4611 + $relevant_content
4612 + );
4613 + }
4614 + break;
4615 +
2898 4616 case 'grok':
2899 4617 if (empty($xai_api_key)) {
2900 - throw new Exception(esc_html__('X.AI API key is not configured', 'mxchat'));
4618 + $error_response = [
4619 + 'error' => esc_html__('X.AI API key is not configured', 'mxchat'),
4620 + 'error_code' => 'missing_xai_api_key'
4621 + ];
4622 + if ($testing_data !== null) {
4623 + $error_response['testing_data'] = $testing_data;
4624 + }
4625 + return $error_response;
2901 4626 }
2902 - return $this->mxchat_generate_response_xai(
2903 - $selected_model,
2904 - $xai_api_key,
2905 - $conversation_history,
2906 - $relevant_content
2907 - );
2908 -
4627 + if ($streaming) {
4628 + return $this->mxchat_generate_response_xai_stream(
4629 + $selected_model,
4630 + $xai_api_key,
4631 + $conversation_history,
4632 + $relevant_content,
4633 + $session_id,
4634 + $testing_data
4635 + );
4636 + } else {
4637 + $response = $this->mxchat_generate_response_xai(
4638 + $selected_model,
4639 + $xai_api_key,
4640 + $conversation_history,
4641 + $relevant_content
4642 + );
4643 + }
4644 + break;
4645 +
2909 4646 case 'deepseek':
2910 4647 if (empty($deepseek_api_key)) {
2911 - throw new Exception(esc_html__('DeepSeek API key is not configured', 'mxchat'));
4648 + $error_response = [
4649 + 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
4650 + 'error_code' => 'missing_deepseek_api_key'
4651 + ];
4652 + if ($testing_data !== null) {
4653 + $error_response['testing_data'] = $testing_data;
4654 + }
4655 + return $error_response;
2912 4656 }
2913 - return $this->mxchat_generate_response_deepseek(
2914 - $selected_model,
2915 - $deepseek_api_key,
2916 - $conversation_history,
2917 - $relevant_content
2918 - );
2919 -
4657 + if ($streaming) {
4658 + return $this->mxchat_generate_response_deepseek_stream(
4659 + $selected_model,
4660 + $deepseek_api_key,
4661 + $conversation_history,
4662 + $relevant_content,
4663 + $session_id,
4664 + $testing_data
4665 + );
4666 + } else {
4667 + $response = $this->mxchat_generate_response_deepseek(
4668 + $selected_model,
4669 + $deepseek_api_key,
4670 + $conversation_history,
4671 + $relevant_content
4672 + );
4673 + }
4674 + break;
4675 +
2920 4676 case 'gpt':
4677 + case 'o1':
2921 4678 if (empty($api_key)) {
2922 - throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
4679 + $error_response = [
4680 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
4681 + 'error_code' => 'missing_openai_api_key'
4682 + ];
4683 + if ($testing_data !== null) {
4684 + $error_response['testing_data'] = $testing_data;
4685 + }
4686 + return $error_response;
2923 4687 }
2924 - return $this->mxchat_generate_response_openai(
2925 - $selected_model,
2926 - $api_key,
2927 - $conversation_history,
2928 - $relevant_content
2929 - );
2930 -
4688 + if ($streaming) {
4689 + return $this->mxchat_generate_response_openai_stream(
4690 + $selected_model,
4691 + $api_key,
4692 + $conversation_history,
4693 + $relevant_content,
4694 + $session_id,
4695 + $testing_data
4696 + );
4697 + } else {
4698 + $response = $this->mxchat_generate_response_openai(
4699 + $selected_model,
4700 + $api_key,
4701 + $conversation_history,
4702 + $relevant_content
4703 + );
4704 + }
4705 + break;
4706 +
2931 4707 default:
2932 - // Default to OpenAI for custom models or unrecognized prefixes
2933 4708 if (empty($api_key)) {
2934 - throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
4709 + $error_response = [
4710 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
4711 + 'error_code' => 'missing_openai_api_key'
4712 + ];
4713 + if ($testing_data !== null) {
4714 + $error_response['testing_data'] = $testing_data;
4715 + }
4716 + return $error_response;
2935 4717 }
2936 - return $this->mxchat_generate_response_openai(
2937 - $selected_model,
2938 - $api_key,
2939 - $conversation_history,
2940 - $relevant_content
4718 + if ($streaming) {
4719 + return $this->mxchat_generate_response_openai_stream(
4720 + $selected_model,
4721 + $api_key,
4722 + $conversation_history,
4723 + $relevant_content,
4724 + $session_id,
4725 + $testing_data
4726 + );
4727 + } else {
4728 + $response = $this->mxchat_generate_response_openai(
4729 + $selected_model,
4730 + $api_key,
4731 + $conversation_history,
4732 + $relevant_content
4733 + );
4734 + }
4735 + break;
4736 + }
4737 +
4738 + if (is_array($response) && isset($response['error'])) {
4739 + if ($testing_data !== null) {
4740 + $response['testing_data'] = $testing_data;
4741 + }
4742 + return $response;
4743 + }
4744 +
4745 + return $response;
4746 +
4747 + } catch (Exception $e) {
4748 + $error_response = [
4749 + 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
4750 + 'error_code' => 'system_exception',
4751 + 'exception_details' => $e->getMessage()
4752 + ];
4753 +
4754 + if ($testing_data !== null) {
4755 + $error_response['testing_data'] = $testing_data;
4756 + }
4757 +
4758 + return $error_response;
4759 + }
4760 +}
4761 +private function mxchat_generate_response_openrouter_stream($selected_model, $openrouter_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4762 + try {
4763 + $bot_id = $this->get_current_bot_id($session_id);
4764 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
4765 +
4766 + if (!is_array($conversation_history)) {
4767 + $conversation_history = array();
4768 + }
4769 +
4770 + $formatted_conversation = array();
4771 +
4772 + $formatted_conversation[] = array(
4773 + 'role' => 'system',
4774 + 'content' => $system_prompt_instructions . " " . $relevant_content
4775 + );
4776 +
4777 + foreach ($conversation_history as $message) {
4778 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4779 + $role = $message['role'];
4780 + if ($role === 'bot' || $role === 'agent') {
4781 + $role = 'assistant';
4782 + }
4783 + if (!in_array($role, ['system', 'assistant', 'user'])) {
4784 + $role = 'user';
4785 + }
4786 + $formatted_conversation[] = array(
4787 + 'role' => $role,
4788 + 'content' => $message['content']
2941 4789 );
4790 + }
2942 4791 }
4792 +
4793 + if (headers_sent() || !function_exists('curl_init')) {
4794 + $regular_response = $this->mxchat_generate_response_openrouter(
4795 + $selected_model,
4796 + $openrouter_api_key,
4797 + $conversation_history,
4798 + $relevant_content
4799 + );
4800 +
4801 + $response_data = [
4802 + 'text' => $regular_response,
4803 + 'html' => '',
4804 + 'session_id' => $session_id
4805 + ];
4806 +
4807 + if ($testing_data !== null) {
4808 + $response_data['testing_data'] = $testing_data;
4809 + }
4810 +
4811 + header('Content-Type: application/json');
4812 + echo json_encode($response_data);
4813 + return true;
4814 + }
4815 +
4816 + $body = json_encode([
4817 + 'model' => $selected_model,
4818 + 'messages' => $formatted_conversation,
4819 + 'temperature' => 1,
4820 + 'stream' => true
4821 + ]);
4822 +
4823 + $ch = curl_init();
4824 + curl_setopt($ch, CURLOPT_URL, 'https://openrouter.ai/api/v1/chat/completions');
4825 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4826 + curl_setopt($ch, CURLOPT_POST, true);
4827 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4828 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4829 + 'Content-Type: application/json',
4830 + 'Authorization: Bearer ' . $openrouter_api_key,
4831 + 'HTTP-Referer: ' . home_url(),
4832 + 'X-Title: ' . get_bloginfo('name')
4833 + ));
4834 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4835 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
4836 +
4837 + $full_response = '';
4838 + $stream_started = false;
4839 + $buffer = '';
4840 +
4841 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
4842 + if (!$stream_started && $testing_data !== null) {
4843 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4844 + flush();
4845 + $stream_started = true;
4846 + }
4847 +
4848 + $buffer .= $data;
4849 + $lines = explode("\n", $buffer);
4850 + $buffer = array_pop($lines);
4851 +
4852 + foreach ($lines as $line) {
4853 + if (trim($line) === '') {
4854 + continue;
4855 + }
4856 +
4857 + if (strpos($line, 'data: ') !== 0) {
4858 + continue;
4859 + }
4860 +
4861 + $json_str = substr($line, 6);
4862 +
4863 + if (trim($json_str) === '[DONE]') {
4864 + echo "data: [DONE]\n\n";
4865 + flush();
4866 + continue;
4867 + }
4868 +
4869 + $json = json_decode(trim($json_str), true);
4870 + if ($json && isset($json['choices'][0]['delta']['content'])) {
4871 + $content = $json['choices'][0]['delta']['content'];
4872 + $full_response .= $content;
4873 +
4874 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
4875 + flush();
4876 + }
4877 + }
4878 +
4879 + return strlen($data);
4880 + });
4881 +
4882 + $response = curl_exec($ch);
4883 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4884 +
4885 + if (curl_errno($ch) || $http_code !== 200) {
4886 + curl_close($ch);
4887 +
4888 + $regular_response = $this->mxchat_generate_response_openrouter(
4889 + $selected_model,
4890 + $openrouter_api_key,
4891 + $conversation_history,
4892 + $relevant_content
4893 + );
4894 +
4895 + $response_data = [
4896 + 'text' => $regular_response,
4897 + 'html' => '',
4898 + 'session_id' => $session_id
4899 + ];
4900 +
4901 + if ($testing_data !== null) {
4902 + $response_data['testing_data'] = $testing_data;
4903 + }
4904 +
4905 + header('Content-Type: application/json');
4906 + echo json_encode($response_data);
4907 + return true;
4908 + }
4909 +
4910 + curl_close($ch);
4911 +
4912 + if (!empty($full_response) && !empty($session_id)) {
4913 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
4914 + }
4915 +
4916 + return true;
4917 +
2943 4918 } catch (Exception $e) {
2944 - //error_log('MXChat Error: ' . $e->getMessage());
2945 - return sprintf(
2946 - esc_html__('An error occurred: %s', 'mxchat'),
2947 - esc_html($e->getMessage())
4919 + $regular_response = $this->mxchat_generate_response_openrouter(
4920 + $selected_model,
4921 + $openrouter_api_key,
4922 + $conversation_history,
4923 + $relevant_content
2948 4924 );
4925 +
4926 + $response_data = [
4927 + 'text' => $regular_response,
4928 + 'html' => '',
4929 + 'session_id' => $session_id
4930 + ];
4931 +
4932 + if ($testing_data !== null) {
4933 + $response_data['testing_data'] = $testing_data;
4934 + }
4935 +
4936 + header('Content-Type: application/json');
4937 + echo json_encode($response_data);
4938 + return true;
2949 4939 }
2950 4940 }
4941 +private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4942 + try {
4943 + $bot_id = $this->get_current_bot_id($session_id);
4944 +
4945 + // Get system prompt instructions using centralized function
4946 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
4947 +
4948 + // Ensure conversation_history is an array
4949 + if (!is_array($conversation_history)) {
4950 + $conversation_history = array();
4951 + }
2951 4952
4953 + // Format conversation history for OpenAI
4954 + $formatted_conversation = array();
2952 4955
2953 -private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
2954 - // Ensure conversation_history is an array
2955 - if (!is_array($conversation_history)) {
2956 - $conversation_history = array();
2957 - }
4956 + $formatted_conversation[] = array(
4957 + 'role' => 'system',
4958 + 'content' => $system_prompt_instructions . " " . $relevant_content
4959 + );
2958 4960
2959 - // Get system prompt instructions from options
2960 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4961 + foreach ($conversation_history as $message) {
4962 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4963 + $role = $message['role'];
4964 + if ($role === 'bot' || $role === 'agent') {
4965 + $role = 'assistant';
4966 + }
4967 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4968 + $role = 'user';
4969 + }
4970 + $formatted_conversation[] = array(
4971 + 'role' => $role,
4972 + 'content' => $message['content']
4973 + );
4974 + }
4975 + }
2961 4976
2962 - // Create a new array for the formatted conversation
2963 - $formatted_conversation = array();
4977 + // Check if we can actually stream
4978 + if (headers_sent() || !function_exists('curl_init')) {
4979 + // Fallback to regular response with testing data
4980 + $regular_response = $this->mxchat_generate_response_openai(
4981 + $selected_model,
4982 + $api_key,
4983 + $conversation_history,
4984 + $relevant_content
4985 + );
4986 +
4987 + $response_data = [
4988 + 'text' => $regular_response,
4989 + 'html' => '',
4990 + 'session_id' => $session_id
4991 + ];
4992 +
4993 + if ($testing_data !== null) {
4994 + $response_data['testing_data'] = $testing_data;
4995 + }
4996 +
4997 + header('Content-Type: application/json');
4998 + echo json_encode($response_data);
4999 + return true;
5000 + }
2964 5001
2965 - // Add system message first
2966 - $formatted_conversation[] = array(
2967 - 'role' => 'system',
2968 - 'content' => $system_prompt_instructions . " " . $relevant_content
2969 - );
5002 + // Check if this is a GPT-5 model (supports reasoning_effort parameter)
5003 + $is_gpt5_model = (
5004 + strpos($selected_model, 'gpt-5') === 0 ||
5005 + $selected_model === 'gpt-5' ||
5006 + $selected_model === 'gpt-5-mini' ||
5007 + $selected_model === 'gpt-5-nano'
5008 + );
2970 5009
2971 - // Add the rest of the conversation history
2972 - foreach ($conversation_history as $message) {
2973 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
2974 - $role = $message['role'];
5010 + // Build request body with optimal settings for fast streaming
5011 + $request_body = [
5012 + 'model' => $selected_model,
5013 + 'messages' => $formatted_conversation,
5014 + 'temperature' => 1,
5015 + 'stream' => true
5016 + ];
2975 5017
2976 - // Convert roles to supported format
2977 - if ($role === 'bot' || $role === 'agent') {
2978 - $role = 'assistant';
5018 + // Add reasoning_effort only for GPT-5 models
5019 + if ($is_gpt5_model) {
5020 + $request_body['reasoning_effort'] = 'minimal'; // Fastest response for GPT-5
5021 + }
5022 +
5023 + $body = json_encode($request_body);
5024 +
5025 + // Use cURL for streaming support
5026 + $ch = curl_init();
5027 + curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
5028 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
5029 + curl_setopt($ch, CURLOPT_POST, true);
5030 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
5031 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
5032 + 'Content-Type: application/json',
5033 + 'Authorization: Bearer ' . $api_key
5034 + ));
5035 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
5036 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
5037 +
5038 + $full_response = ''; // Accumulate full response for saving
5039 + $stream_started = false;
5040 + $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
5041 +
5042 + // Buffer control for real-time streaming
5043 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
5044 + // Send testing data as the first event if available
5045 + if (!$stream_started && $testing_data !== null) {
5046 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
5047 + flush();
5048 + $stream_started = true;
2979 5049 }
2980 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
2981 - $role = 'user';
5050 +
5051 + // CRITICAL FIX: Append new data to buffer
5052 + $buffer .= $data;
5053 +
5054 + // Process complete lines only
5055 + $lines = explode("\n", $buffer);
5056 +
5057 + // CRITICAL FIX: Keep the last incomplete line in the buffer
5058 + // The last element might be incomplete, so keep it in buffer
5059 + $buffer = array_pop($lines);
5060 +
5061 + foreach ($lines as $line) {
5062 + // Skip empty lines
5063 + if (trim($line) === '') {
5064 + continue;
5065 + }
5066 +
5067 + // Only process lines that start with "data: "
5068 + if (strpos($line, 'data: ') !== 0) {
5069 + continue;
5070 + }
5071 +
5072 + $json_str = substr($line, 6); // Remove 'data: ' prefix
5073 +
5074 + if (trim($json_str) === '[DONE]') {
5075 + echo "data: [DONE]\n\n";
5076 + flush();
5077 + continue;
5078 + }
5079 +
5080 + // Try to decode JSON
5081 + $json = json_decode(trim($json_str), true);
5082 + if ($json && isset($json['choices'][0]['delta']['content'])) {
5083 + $content = $json['choices'][0]['delta']['content'];
5084 + $full_response .= $content; // Accumulate the full response
5085 +
5086 + // Send as SSE format
5087 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
5088 + flush();
5089 + }
2982 5090 }
2983 -
2984 - $formatted_conversation[] = array(
2985 - 'role' => $role,
2986 - 'content' => $message['content']
5091 +
5092 + return strlen($data);
5093 + });
5094 +
5095 + $response = curl_exec($ch);
5096 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
5097 +
5098 + if (curl_errno($ch) || $http_code !== 200) {
5099 + curl_close($ch);
5100 +
5101 + // Fallback to regular response
5102 + $regular_response = $this->mxchat_generate_response_openai(
5103 + $selected_model,
5104 + $api_key,
5105 + $conversation_history,
5106 + $relevant_content
2987 5107 );
5108 +
5109 + $response_data = [
5110 + 'text' => $regular_response,
5111 + 'html' => '',
5112 + 'session_id' => $session_id
5113 + ];
5114 +
5115 + if ($testing_data !== null) {
5116 + $response_data['testing_data'] = $testing_data;
5117 + }
5118 +
5119 + header('Content-Type: application/json');
5120 + echo json_encode($response_data);
5121 + return true;
2988 5122 }
5123 +
5124 + curl_close($ch);
5125 +
5126 + // Save the complete response to maintain chat persistence
5127 + if (!empty($full_response) && !empty($session_id)) {
5128 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
5129 + }
5130 +
5131 + return true; // Indicate streaming completed successfully
5132 +
5133 + } catch (Exception $e) {
5134 + // Fallback to regular response
5135 + $regular_response = $this->mxchat_generate_response_openai(
5136 + $selected_model,
5137 + $api_key,
5138 + $conversation_history,
5139 + $relevant_content
5140 + );
5141 +
5142 + $response_data = [
5143 + 'text' => $regular_response,
5144 + 'html' => '',
5145 + 'session_id' => $session_id
5146 + ];
5147 +
5148 + if ($testing_data !== null) {
5149 + $response_data['testing_data'] = $testing_data;
5150 + }
5151 +
5152 + header('Content-Type: application/json');
5153 + echo json_encode($response_data);
5154 + return true;
2989 5155 }
5156 +}
5157 +private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
5158 + try {
5159 + // Get bot ID from session or request
5160 + $bot_id = $this->get_current_bot_id($session_id);
5161 +
5162 + // Get system prompt instructions using centralized function
5163 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
5164 + // Ensure conversation_history is an array
5165 + if (!is_array($conversation_history)) {
5166 + $conversation_history = array();
5167 + }
2990 5168
2991 - $body = json_encode([
2992 - 'model' => $selected_model,
2993 - 'messages' => $formatted_conversation,
2994 - 'temperature' => 0.8,
2995 - 'stream' => false
2996 - ]);
5169 + // Clean and validate conversation history
5170 + foreach ($conversation_history as &$message) {
5171 + // Convert bot and agent roles to assistant
5172 + if ($message['role'] === 'bot' || $message['role'] === 'agent') {
5173 + $message['role'] = 'assistant';
5174 + }
5175 +
5176 + // Remove unsupported roles - Claude only supports 'assistant' and 'user'
5177 + if (!in_array($message['role'], ['assistant', 'user'])) {
5178 + $message['role'] = 'user';
5179 + }
2997 5180
2998 - $args = [
2999 - 'body' => $body,
3000 - 'headers' => [
3001 - 'Content-Type' => 'application/json',
3002 - 'Authorization' => 'Bearer ' . $deepseek_api_key,
3003 - ],
3004 - 'timeout' => 60,
3005 - 'redirection' => 5,
3006 - 'blocking' => true,
3007 - 'httpversion' => '1.0',
3008 - 'sslverify' => true,
3009 - ];
5181 + // Ensure content field exists
5182 + if (!isset($message['content']) || empty($message['content'])) {
5183 + $message['content'] = '';
5184 + }
3010 5185
3011 - $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
5186 + // Remove any unsupported fields
5187 + $message = array_intersect_key($message, array_flip(['role', 'content']));
5188 + }
3012 5189
3013 - if (is_wp_error($response)) {
3014 - //error_log('DeepSeek API Error: ' . $response->get_error_message());
3015 - return "Sorry, there was an error processing your request.";
3016 - }
5190 + // Add relevant content as the latest user message
5191 + $conversation_history[] = [
5192 + 'role' => 'user',
5193 + 'content' => $relevant_content
5194 + ];
3017 5195
3018 - $response_body = wp_remote_retrieve_body($response);
3019 - $decoded_response = json_decode($response_body, true);
5196 + // Prepare the request body with stream: true
5197 + $body = json_encode([
5198 + 'model' => $selected_model,
5199 + 'messages' => $conversation_history,
5200 + 'max_tokens' => 1000,
5201 + 'temperature' => 0.8,
5202 + 'system' => $system_prompt_instructions,
5203 + 'stream' => true
5204 + ]);
3020 5205
3021 - if (isset($decoded_response['choices'][0]['message']['content'])) {
3022 - return trim($decoded_response['choices'][0]['message']['content']);
3023 - } else {
3024 - //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
3025 - return "Sorry, I couldn't process that request.";
3026 - }
3027 -}
5206 + // Check if we can actually stream (headers not sent, etc.)
5207 + if (headers_sent() || !function_exists('curl_init')) {
5208 + // Fallback to regular response with testing data
5209 + //error_log("MxChat: Streaming not possible, falling back to regular response");
5210 + $regular_response = $this->mxchat_generate_response_claude(
5211 + $selected_model,
5212 + $claude_api_key,
5213 + array_slice($conversation_history, 0, -1), // Remove the added content
5214 + $relevant_content
5215 + );
5216 +
5217 + // Return as JSON with testing data
5218 + $response_data = [
5219 + 'text' => $regular_response,
5220 + 'html' => '',
5221 + 'session_id' => $session_id
5222 + ];
5223 +
5224 + if ($testing_data !== null) {
5225 + $response_data['testing_data'] = $testing_data;
5226 + //error_log("MxChat Testing: Added testing data to Claude fallback response");
5227 + }
5228 +
5229 + // Clear any streaming headers and send JSON
5230 + if (headers_sent() === false) {
5231 + header('Content-Type: application/json');
5232 + }
5233 + echo json_encode($response_data);
5234 + return true; // Indicate we handled the response
5235 + }
3028 5236
3029 -private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
3030 - // Ensure conversation_history is an array
3031 - if (!is_array($conversation_history)) {
3032 - $conversation_history = array();
3033 - }
5237 + // Use cURL for streaming support
5238 + $ch = curl_init();
5239 + curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
5240 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
5241 + curl_setopt($ch, CURLOPT_POST, true);
5242 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
5243 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
5244 + 'Content-Type: application/json',
5245 + 'x-api-key: ' . $claude_api_key,
5246 + 'anthropic-version: 2023-06-01'
5247 + ));
5248 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
5249 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
3034 5250
3035 - // Get system prompt instructions from options
3036 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5251 + $full_response = ''; // Accumulate full response for saving
5252 + $stream_started = false;
5253 + $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
3037 5254
3038 - // Create a new array for the formatted conversation
3039 - $formatted_conversation = array();
5255 + // Buffer control for real-time streaming
5256 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
5257 + // Send testing data as the first event if available
5258 + if (!$stream_started && $testing_data !== null) {
5259 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
5260 + flush();
5261 + $stream_started = true;
5262 + //error_log("MxChat Testing: Sent testing data in Claude stream");
5263 + }
5264 +
5265 + // CRITICAL FIX: Append new data to buffer
5266 + $buffer .= $data;
5267 +
5268 + // Process complete lines only
5269 + $lines = explode("\n", $buffer);
5270 +
5271 + // CRITICAL FIX: Keep the last incomplete line in the buffer
5272 + // The last element might be incomplete, so keep it in buffer
5273 + $buffer = array_pop($lines);
3040 5274
3041 - // Add system message first
3042 - $formatted_conversation[] = array(
3043 - 'role' => 'system',
3044 - 'content' => $system_prompt_instructions . " " . $relevant_content
3045 - );
5275 + foreach ($lines as $line) {
5276 + if (trim($line) === '') {
5277 + continue;
5278 + }
3046 5279
3047 - // Add the rest of the conversation history
3048 - foreach ($conversation_history as $message) {
3049 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3050 - $role = $message['role'];
5280 + // Claude uses event: and data: format
5281 + if (strpos($line, 'event: ') === 0) {
5282 + // Store the event type for the next data line
5283 + continue;
5284 + }
3051 5285
3052 - // Convert roles to supported format
3053 - if ($role === 'bot' || $role === 'agent') {
3054 - $role = 'assistant';
5286 + if (strpos($line, 'data: ') === 0) {
5287 + $json_str = substr($line, 6); // Remove 'data: ' prefix
5288 +
5289 + $json = json_decode(trim($json_str), true);
5290 + if (json_last_error() !== JSON_ERROR_NONE) {
5291 + continue;
5292 + }
5293 +
5294 + // Handle different event types
5295 + if (isset($json['type'])) {
5296 + switch ($json['type']) {
5297 + case 'content_block_delta':
5298 + if (isset($json['delta']['text'])) {
5299 + $content = $json['delta']['text'];
5300 + $full_response .= $content; // Accumulate
5301 + // Send as SSE format compatible with your frontend
5302 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
5303 + flush();
5304 + }
5305 + break;
5306 +
5307 + case 'message_stop':
5308 + echo "data: [DONE]\n\n";
5309 + flush();
5310 + break;
5311 +
5312 + case 'error':
5313 + echo "data: " . json_encode(['error' => $json['error']['message'] ?? 'Unknown error']) . "\n\n";
5314 + flush();
5315 + break;
5316 + }
5317 + }
5318 + }
3055 5319 }
3056 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3057 - $role = 'user';
5320 +
5321 + return strlen($data);
5322 + });
5323 +
5324 + $response = curl_exec($ch);
5325 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
5326 +
5327 + if (curl_errno($ch)) {
5328 + curl_close($ch);
5329 + throw new Exception('cURL Error: ' . curl_error($ch));
5330 + }
5331 +
5332 + curl_close($ch);
5333 +
5334 + if ($http_code !== 200) {
5335 + // Fallback to regular response
5336 + //error_log("MxChat: Claude streaming failed with HTTP $http_code, falling back");
5337 + $regular_response = $this->mxchat_generate_response_claude(
5338 + $selected_model,
5339 + $claude_api_key,
5340 + array_slice($conversation_history, 0, -1), // Remove the added content
5341 + $relevant_content
5342 + );
5343 +
5344 + $response_data = [
5345 + 'text' => $regular_response,
5346 + 'html' => '',
5347 + 'session_id' => $session_id
5348 + ];
5349 +
5350 + if ($testing_data !== null) {
5351 + $response_data['testing_data'] = $testing_data;
5352 + //error_log("MxChat Testing: Added testing data to Claude error fallback");
3058 5353 }
5354 +
5355 + header('Content-Type: application/json');
5356 + echo json_encode($response_data);
5357 + return true;
5358 + }
3059 5359
3060 - $formatted_conversation[] = array(
3061 - 'role' => $role,
3062 - 'content' => $message['content']
3063 - );
5360 + // Save the complete response to maintain chat persistence
5361 + if (!empty($full_response) && !empty($session_id)) {
5362 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
3064 5363 }
5364 +
5365 + return true; // Indicate streaming completed successfully
5366 +
5367 + } catch (Exception $e) {
5368 + //error_log("MxChat Claude streaming exception: " . $e->getMessage());
5369 +
5370 + // Fallback to regular response on exception
5371 + $regular_response = $this->mxchat_generate_response_claude(
5372 + $selected_model,
5373 + $claude_api_key,
5374 + $conversation_history,
5375 + $relevant_content
5376 + );
5377 +
5378 + $response_data = [
5379 + 'text' => $regular_response,
5380 + 'html' => '',
5381 + 'session_id' => $session_id
5382 + ];
5383 +
5384 + if ($testing_data !== null) {
5385 + $response_data['testing_data'] = $testing_data;
5386 + //error_log("MxChat Testing: Added testing data to Claude exception fallback");
5387 + }
5388 +
5389 + header('Content-Type: application/json');
5390 + echo json_encode($response_data);
5391 + return true;
3065 5392 }
5393 +}
5394 +private function mxchat_generate_response_xai_stream($selected_model, $xai_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
5395 + try {
5396 + // Get bot ID from session or request
5397 + $bot_id = $this->get_current_bot_id($session_id);
5398 +
5399 + // Get system prompt instructions using centralized function
5400 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
5401 +
5402 + // Ensure conversation_history is an array
5403 + if (!is_array($conversation_history)) {
5404 + $conversation_history = array();
5405 + }
3066 5406
3067 - $body = json_encode([
3068 - 'model' => $selected_model,
3069 - 'messages' => $formatted_conversation,
3070 - 'temperature' => 0.8,
3071 - 'stream' => false
3072 - ]);
5407 + // Format conversation history for X.AI (same as OpenAI format)
5408 + $formatted_conversation = array();
3073 5409
3074 - $args = [
3075 - 'body' => $body,
3076 - 'headers' => [
3077 - 'Content-Type' => 'application/json',
3078 - 'Authorization' => 'Bearer ' . $api_key,
3079 - ],
3080 - 'timeout' => 60,
3081 - 'redirection' => 5,
3082 - 'blocking' => true,
3083 - 'httpversion' => '1.0',
3084 - 'sslverify' => true,
3085 - ];
5410 + $formatted_conversation[] = array(
5411 + 'role' => 'system',
5412 + 'content' => $system_prompt_instructions . " " . $relevant_content
5413 + );
3086 5414
3087 - $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
5415 + foreach ($conversation_history as $message) {
5416 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
5417 + $role = $message['role'];
5418 + if ($role === 'bot' || $role === 'agent') {
5419 + $role = 'assistant';
5420 + }
5421 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
5422 + $role = 'user';
5423 + }
5424 + $formatted_conversation[] = array(
5425 + 'role' => $role,
5426 + 'content' => $message['content']
5427 + );
5428 + }
5429 + }
3088 5430
3089 - if (is_wp_error($response)) {
3090 - //error_log('OpenAI API Error: ' . $response->get_error_message());
3091 - return "Sorry, there was an error processing your request.";
3092 - }
5431 + // Check if we can actually stream
5432 + if (headers_sent() || !function_exists('curl_init')) {
5433 + // Fallback to regular response with testing data
5434 + //error_log("MxChat: X.AI streaming not possible, falling back to regular response");
5435 + $regular_response = $this->mxchat_generate_response_xai(
5436 + $selected_model,
5437 + $xai_api_key,
5438 + $conversation_history,
5439 + $relevant_content
5440 + );
5441 +
5442 + $response_data = [
5443 + 'text' => $regular_response,
5444 + 'html' => '',
5445 + 'session_id' => $session_id
5446 + ];
5447 +
5448 + if ($testing_data !== null) {
5449 + $response_data['testing_data'] = $testing_data;
5450 + //error_log("MxChat Testing: Added testing data to X.AI fallback response");
5451 + }
5452 +
5453 + header('Content-Type: application/json');
5454 + echo json_encode($response_data);
5455 + return true;
5456 + }
3093 5457
3094 - $response_body = wp_remote_retrieve_body($response);
3095 - $decoded_response = json_decode($response_body, true);
5458 + // Prepare the request body with stream: true
5459 + $body = json_encode([
5460 + 'model' => $selected_model,
5461 + 'messages' => $formatted_conversation,
5462 + 'temperature' => 0.8,
5463 + 'stream' => true
5464 + ]);
3096 5465
3097 - if (isset($decoded_response['choices'][0]['message']['content'])) {
3098 - return trim($decoded_response['choices'][0]['message']['content']);
3099 - } else {
3100 - //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
3101 - return "Sorry, I couldn't process that request.";
5466 + // Use cURL for streaming support
5467 + $ch = curl_init();
5468 + curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
5469 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
5470 + curl_setopt($ch, CURLOPT_POST, true);
5471 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
5472 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
5473 + 'Content-Type: application/json',
5474 + 'Authorization: Bearer ' . $xai_api_key
5475 + ));
5476 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
5477 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
5478 +
5479 + $full_response = ''; // Accumulate full response for saving
5480 + $stream_started = false;
5481 + $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
5482 +
5483 + // Buffer control for real-time streaming
5484 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
5485 + // Send testing data as the first event if available
5486 + if (!$stream_started && $testing_data !== null) {
5487 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
5488 + flush();
5489 + $stream_started = true;
5490 + //error_log("MxChat Testing: Sent testing data in X.AI stream");
5491 + }
5492 +
5493 + // CRITICAL FIX: Append new data to buffer
5494 + $buffer .= $data;
5495 +
5496 + // Process complete lines only
5497 + $lines = explode("\n", $buffer);
5498 +
5499 + // CRITICAL FIX: Keep the last incomplete line in the buffer
5500 + // The last element might be incomplete, so keep it in buffer
5501 + $buffer = array_pop($lines);
5502 +
5503 + foreach ($lines as $line) {
5504 + // Skip empty lines
5505 + if (trim($line) === '') {
5506 + continue;
5507 + }
5508 +
5509 + // Only process lines that start with "data: "
5510 + if (strpos($line, 'data: ') !== 0) {
5511 + continue;
5512 + }
5513 +
5514 + $json_str = substr($line, 6); // Remove 'data: ' prefix
5515 +
5516 + if (trim($json_str) === '[DONE]') {
5517 + echo "data: [DONE]\n\n";
5518 + flush();
5519 + continue;
5520 + }
5521 +
5522 + // Try to decode JSON
5523 + $json = json_decode(trim($json_str), true);
5524 + if ($json && isset($json['choices'][0]['delta']['content'])) {
5525 + $content = $json['choices'][0]['delta']['content'];
5526 + $full_response .= $content; // Accumulate
5527 + // Send as SSE format
5528 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
5529 + flush();
5530 + }
5531 + }
5532 +
5533 + return strlen($data);
5534 + });
5535 +
5536 + $response = curl_exec($ch);
5537 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
5538 +
5539 + if (curl_errno($ch) || $http_code !== 200) {
5540 + curl_close($ch);
5541 +
5542 + // Fallback to regular response
5543 + //error_log("MxChat: X.AI streaming failed, falling back");
5544 + $regular_response = $this->mxchat_generate_response_xai(
5545 + $selected_model,
5546 + $xai_api_key,
5547 + $conversation_history,
5548 + $relevant_content
5549 + );
5550 +
5551 + $response_data = [
5552 + 'text' => $regular_response,
5553 + 'html' => '',
5554 + 'session_id' => $session_id
5555 + ];
5556 +
5557 + if ($testing_data !== null) {
5558 + $response_data['testing_data'] = $testing_data;
5559 + //error_log("MxChat Testing: Added testing data to X.AI error fallback");
5560 + }
5561 +
5562 + header('Content-Type: application/json');
5563 + echo json_encode($response_data);
5564 + return true;
5565 + }
5566 +
5567 + curl_close($ch);
5568 +
5569 + // Save the complete response to maintain chat persistence
5570 + if (!empty($full_response) && !empty($session_id)) {
5571 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
5572 + }
5573 +
5574 + return true; // Indicate streaming completed successfully
5575 +
5576 + } catch (Exception $e) {
5577 + //error_log("MxChat X.AI streaming exception: " . $e->getMessage());
5578 +
5579 + // Fallback to regular response
5580 + $regular_response = $this->mxchat_generate_response_xai(
5581 + $selected_model,
5582 + $xai_api_key,
5583 + $conversation_history,
5584 + $relevant_content
5585 + );
5586 +
5587 + $response_data = [
5588 + 'text' => $regular_response,
5589 + 'html' => '',
5590 + 'session_id' => $session_id
5591 + ];
5592 +
5593 + if ($testing_data !== null) {
5594 + $response_data['testing_data'] = $testing_data;
5595 + //error_log("MxChat Testing: Added testing data to X.AI exception fallback");
5596 + }
5597 +
5598 + header('Content-Type: application/json');
5599 + echo json_encode($response_data);
5600 + return true;
3102 5601 }
3103 5602 }
3104 -private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
3105 - // Get system prompt instructions from options
3106 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5603 +private function mxchat_generate_response_deepseek_stream($selected_model, $deepseek_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
5604 + try {
5605 + // Get bot ID from session or request
5606 + $bot_id = $this->get_current_bot_id($session_id);
5607 +
5608 + // Get system prompt instructions using centralized function
5609 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
5610 +
5611 + // Ensure conversation_history is an array
5612 + if (!is_array($conversation_history)) {
5613 + $conversation_history = array();
5614 + }
3107 5615
3108 - // Add system prompt to relevant content
3109 - $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
5616 + // Format conversation history for DeepSeek
5617 + $formatted_conversation = array();
3110 5618
3111 - // Prepend system instructions to the conversation history
3112 - array_unshift($conversation_history, [
3113 - 'role' => 'system',
3114 - 'content' => "Here are your instructions: " . $content_with_instructions
3115 - ]);
5619 + $formatted_conversation[] = array(
5620 + 'role' => 'system',
5621 + 'content' => $system_prompt_instructions . " " . $relevant_content
5622 + );
3116 5623
3117 - // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3118 - foreach ($conversation_history as &$message) {
3119 - if ($message['role'] === 'bot') {
3120 - $message['role'] = 'assistant';
3121 - } elseif ($message['role'] === 'agent') {
3122 - // Tag the message as coming from a live agent
3123 - $message['role'] = 'assistant';
3124 - if (!isset($message['metadata'])) {
3125 - $message['metadata'] = ['source' => 'live_agent'];
5624 + foreach ($conversation_history as $message) {
5625 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
5626 + $role = $message['role'];
5627 + if ($role === 'bot' || $role === 'agent') {
5628 + $role = 'assistant';
5629 + }
5630 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
5631 + $role = 'user';
5632 + }
5633 + $formatted_conversation[] = array(
5634 + 'role' => $role,
5635 + 'content' => $message['content']
5636 + );
3126 5637 }
3127 5638 }
3128 5639
3129 - // Ensure all roles are valid
3130 - if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3131 - $message['role'] = 'user'; // Default to 'user'
5640 + // Check if we can actually stream
5641 + if (headers_sent() || !function_exists('curl_init')) {
5642 + // Fallback to regular response with testing data
5643 + //error_log("MxChat: DeepSeek streaming not possible, falling back to regular response");
5644 + $regular_response = $this->mxchat_generate_response_deepseek(
5645 + $selected_model,
5646 + $deepseek_api_key,
5647 + $conversation_history,
5648 + $relevant_content
5649 + );
5650 +
5651 + $response_data = [
5652 + 'text' => $regular_response,
5653 + 'html' => '',
5654 + 'session_id' => $session_id
5655 + ];
5656 +
5657 + if ($testing_data !== null) {
5658 + $response_data['testing_data'] = $testing_data;
5659 + //error_log("MxChat Testing: Added testing data to DeepSeek fallback response");
5660 + }
5661 +
5662 + header('Content-Type: application/json');
5663 + echo json_encode($response_data);
5664 + return true;
3132 5665 }
5666 +
5667 + // Prepare the request body with stream: true
5668 + $body = json_encode([
5669 + 'model' => $selected_model,
5670 + 'messages' => $formatted_conversation,
5671 + 'temperature' => 0.8,
5672 + 'stream' => true
5673 + ]);
5674 +
5675 + // Use cURL for streaming support
5676 + $ch = curl_init();
5677 + curl_setopt($ch, CURLOPT_URL, 'https://api.deepseek.com/v1/chat/completions');
5678 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
5679 + curl_setopt($ch, CURLOPT_POST, true);
5680 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
5681 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
5682 + 'Content-Type: application/json',
5683 + 'Authorization: Bearer ' . $deepseek_api_key
5684 + ));
5685 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
5686 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
5687 +
5688 + $full_response = ''; // Accumulate full response for saving
5689 + $stream_started = false;
5690 + $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
5691 +
5692 + // Buffer control for real-time streaming
5693 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
5694 + // Send testing data as the first event if available
5695 + if (!$stream_started && $testing_data !== null) {
5696 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
5697 + flush();
5698 + $stream_started = true;
5699 + //error_log("MxChat Testing: Sent testing data in DeepSeek stream");
5700 + }
5701 +
5702 + // CRITICAL FIX: Append new data to buffer
5703 + $buffer .= $data;
5704 +
5705 + // Process complete lines only
5706 + $lines = explode("\n", $buffer);
5707 +
5708 + // CRITICAL FIX: Keep the last incomplete line in the buffer
5709 + // The last element might be incomplete, so keep it in buffer
5710 + $buffer = array_pop($lines);
5711 +
5712 + foreach ($lines as $line) {
5713 + // Skip empty lines
5714 + if (trim($line) === '') {
5715 + continue;
5716 + }
5717 +
5718 + // Only process lines that start with "data: "
5719 + if (strpos($line, 'data: ') !== 0) {
5720 + continue;
5721 + }
5722 +
5723 + $json_str = substr($line, 6); // Remove 'data: ' prefix
5724 +
5725 + if (trim($json_str) === '[DONE]') {
5726 + echo "data: [DONE]\n\n";
5727 + flush();
5728 + continue;
5729 + }
5730 +
5731 + // Try to decode JSON
5732 + $json = json_decode(trim($json_str), true);
5733 + if ($json && isset($json['choices'][0]['delta']['content'])) {
5734 + $content = $json['choices'][0]['delta']['content'];
5735 + $full_response .= $content; // Accumulate the full response
5736 +
5737 + // Send as SSE format
5738 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
5739 + flush();
5740 + }
5741 + }
5742 +
5743 + return strlen($data);
5744 + });
5745 +
5746 + $response = curl_exec($ch);
5747 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
5748 +
5749 + if (curl_errno($ch) || $http_code !== 200) {
5750 + $curl_error = curl_error($ch);
5751 + curl_close($ch);
5752 +
5753 + // Log the specific error for debugging
5754 + //error_log("MxChat: DeepSeek streaming failed - HTTP: $http_code, cURL: $curl_error");
5755 +
5756 + // Fallback to regular response
5757 + $regular_response = $this->mxchat_generate_response_deepseek(
5758 + $selected_model,
5759 + $deepseek_api_key,
5760 + $conversation_history,
5761 + $relevant_content
5762 + );
5763 +
5764 + // Handle error response from regular function
5765 + if (is_array($regular_response) && isset($regular_response['error'])) {
5766 + if ($testing_data !== null) {
5767 + $regular_response['testing_data'] = $testing_data;
5768 + }
5769 + header('Content-Type: application/json');
5770 + echo json_encode($regular_response);
5771 + return true;
5772 + }
5773 +
5774 + $response_data = [
5775 + 'text' => $regular_response,
5776 + 'html' => '',
5777 + 'session_id' => $session_id
5778 + ];
5779 +
5780 + if ($testing_data !== null) {
5781 + $response_data['testing_data'] = $testing_data;
5782 + //error_log("MxChat Testing: Added testing data to DeepSeek error fallback");
5783 + }
5784 +
5785 + header('Content-Type: application/json');
5786 + echo json_encode($response_data);
5787 + return true;
5788 + }
5789 +
5790 + curl_close($ch);
5791 +
5792 + // Save the complete response to maintain chat persistence
5793 + if (!empty($full_response) && !empty($session_id)) {
5794 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
5795 + }
5796 +
5797 + return true; // Indicate streaming completed successfully
5798 +
5799 + } catch (Exception $e) {
5800 + //error_log("MxChat DeepSeek streaming exception: " . $e->getMessage());
5801 +
5802 + // Fallback to regular response
5803 + $regular_response = $this->mxchat_generate_response_deepseek(
5804 + $selected_model,
5805 + $deepseek_api_key,
5806 + $conversation_history,
5807 + $relevant_content
5808 + );
5809 +
5810 + // Handle error response from regular function
5811 + if (is_array($regular_response) && isset($regular_response['error'])) {
5812 + if ($testing_data !== null) {
5813 + $regular_response['testing_data'] = $testing_data;
5814 + }
5815 + header('Content-Type: application/json');
5816 + echo json_encode($regular_response);
5817 + return true;
5818 + }
5819 +
5820 + $response_data = [
5821 + 'text' => $regular_response,
5822 + 'html' => '',
5823 + 'session_id' => $session_id
5824 + ];
5825 +
5826 + if ($testing_data !== null) {
5827 + $response_data['testing_data'] = $testing_data;
5828 + //error_log("MxChat Testing: Added testing data to DeepSeek exception fallback");
5829 + }
5830 +
5831 + header('Content-Type: application/json');
5832 + echo json_encode($response_data);
5833 + return true;
3133 5834 }
5835 +}
3134 5836
3135 5837
3136 - // Build the request body
3137 - $body = json_encode([
3138 - 'model' => $selected_model,
3139 - 'messages' => $conversation_history,
3140 - 'temperature' => 0.8,
3141 - 'stream' => false
3142 - ]);
5838 +private function mxchat_generate_response_openrouter($selected_model, $openrouter_api_key, $conversation_history, $relevant_content) {
5839 + try {
5840 + if (!is_array($conversation_history)) {
5841 + $conversation_history = array();
5842 + }
3143 5843
3144 - // Set up the API request
3145 - $args = [
3146 - 'body' => $body,
3147 - 'headers' => [
3148 - 'Content-Type' => 'application/json',
3149 - 'Authorization' => 'Bearer ' . $xai_api_key,
3150 - ],
3151 - 'timeout' => 60,
3152 - 'redirection' => 5,
3153 - 'blocking' => true,
3154 - 'httpversion' => '1.0',
3155 - 'sslverify' => true,
3156 - ];
5844 + $bot_id = $this->get_current_bot_id('');
5845 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
5846 +
5847 + $formatted_conversation = array();
3157 5848
3158 - // Make the API request
3159 - $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
5849 + $formatted_conversation[] = array(
5850 + 'role' => 'system',
5851 + 'content' => $system_prompt_instructions . " " . $relevant_content
5852 + );
3160 5853
3161 - // Process the response
3162 - if (is_wp_error($response)) {
3163 - return "Sorry, there was an error processing your request.";
3164 - }
5854 + foreach ($conversation_history as $message) {
5855 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
5856 + $role = $message['role'];
3165 5857
3166 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
5858 + if ($role === 'bot' || $role === 'agent') {
5859 + $role = 'assistant';
5860 + }
5861 + if (!in_array($role, ['system', 'assistant', 'user'])) {
5862 + $role = 'user';
5863 + }
3167 5864
3168 - if (isset($response_body['choices'][0]['message']['content'])) {
3169 - return trim($response_body['choices'][0]['message']['content']);
3170 - } else {
3171 - return "Sorry, I couldn't process that request.";
5865 + $formatted_conversation[] = array(
5866 + 'role' => $role,
5867 + 'content' => $message['content']
5868 + );
5869 + }
5870 + }
5871 +
5872 + $body = json_encode([
5873 + 'model' => $selected_model,
5874 + 'messages' => $formatted_conversation,
5875 + 'temperature' => 1,
5876 + ]);
5877 +
5878 + $args = [
5879 + 'body' => $body,
5880 + 'headers' => [
5881 + 'Content-Type' => 'application/json',
5882 + 'Authorization' => 'Bearer ' . $openrouter_api_key,
5883 + 'HTTP-Referer' => home_url(),
5884 + 'X-Title' => get_bloginfo('name'),
5885 + ],
5886 + 'timeout' => 60,
5887 + 'redirection' => 5,
5888 + 'blocking' => true,
5889 + 'httpversion' => '1.0',
5890 + 'sslverify' => true,
5891 + ];
5892 +
5893 + $response = wp_remote_post('https://openrouter.ai/api/v1/chat/completions', $args);
5894 +
5895 + if (is_wp_error($response)) {
5896 + $error_message = $response->get_error_message();
5897 + return [
5898 + 'error' => esc_html__('Connection error when contacting OpenRouter: ', 'mxchat') . esc_html($error_message),
5899 + 'error_code' => 'openrouter_connection_error',
5900 + 'provider' => 'openrouter'
5901 + ];
5902 + }
5903 +
5904 + $status_code = wp_remote_retrieve_response_code($response);
5905 + if ($status_code !== 200) {
5906 + $response_body = wp_remote_retrieve_body($response);
5907 + $decoded_response = json_decode($response_body, true);
5908 +
5909 + $error_message = isset($decoded_response['error']['message'])
5910 + ? $decoded_response['error']['message']
5911 + : 'HTTP Error ' . $status_code;
5912 +
5913 + return [
5914 + 'error' => esc_html__('OpenRouter API error: ', 'mxchat') . esc_html($error_message),
5915 + 'error_code' => 'openrouter_api_error',
5916 + 'provider' => 'openrouter',
5917 + 'status_code' => $status_code
5918 + ];
5919 + }
5920 +
5921 + $response_body = wp_remote_retrieve_body($response);
5922 + $decoded_response = json_decode($response_body, true);
5923 +
5924 + if (isset($decoded_response['choices'][0]['message']['content'])) {
5925 + return trim($decoded_response['choices'][0]['message']['content']);
5926 + } else {
5927 + return [
5928 + 'error' => esc_html__('Unexpected response format from OpenRouter.', 'mxchat'),
5929 + 'error_code' => 'openrouter_response_format_error',
5930 + 'provider' => 'openrouter'
5931 + ];
5932 + }
5933 + } catch (Exception $e) {
5934 + return [
5935 + 'error' => esc_html__('System error when processing OpenRouter request: ', 'mxchat') . esc_html($e->getMessage()),
5936 + 'error_code' => 'openrouter_exception',
5937 + 'provider' => 'openrouter'
5938 + ];
3172 5939 }
3173 5940 }
3174 -
3175 5941 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
3176 - // Get system prompt instructions from options
3177 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3178 -
5942 +
5943 + // Get bot ID from session or request
5944 + $bot_id = $this->get_current_bot_id($session_id);
5945 +
5946 + // Get system prompt instructions using centralized function
5947 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
5948 +
3179 5949 // Clean and validate conversation history
3180 5950 foreach ($conversation_history as &$message) {
3181 5951 // Convert bot and agent roles to assistant
3182 5952 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
@@ -3271,8 +6041,808 @@
3271 6041 // Log unexpected response format
3272 6042 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
3273 6043 return "Sorry, I received an unexpected response format from the API.";
3274 6044 }
6045 +private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
6046 + try {
6047 + // Ensure conversation_history is an array
6048 + if (!is_array($conversation_history)) {
6049 + $conversation_history = array();
6050 + }
6051 +
6052 + // Get bot ID from session or request
6053 + $bot_id = $this->get_current_bot_id('');
6054 +
6055 + // Get system prompt instructions using centralized function
6056 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
6057 +
6058 + // Create a new array for the formatted conversation
6059 + $formatted_conversation = array();
6060 +
6061 + // Add system message first
6062 + $formatted_conversation[] = array(
6063 + 'role' => 'system',
6064 + 'content' => $system_prompt_instructions . " " . $relevant_content
6065 + );
6066 +
6067 + // Add the rest of the conversation history
6068 + foreach ($conversation_history as $message) {
6069 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
6070 + $role = $message['role'];
6071 +
6072 + // Convert roles to supported format
6073 + if ($role === 'bot' || $role === 'agent') {
6074 + $role = 'assistant';
6075 + }
6076 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
6077 + $role = 'user';
6078 + }
6079 +
6080 + $formatted_conversation[] = array(
6081 + 'role' => $role,
6082 + 'content' => $message['content']
6083 + );
6084 + }
6085 + }
6086 +
6087 + // Check if this is a GPT-5 model (supports reasoning_effort parameter)
6088 + $is_gpt5_model = (
6089 + strpos($selected_model, 'gpt-5') === 0 ||
6090 + $selected_model === 'gpt-5' ||
6091 + $selected_model === 'gpt-5-mini' ||
6092 + $selected_model === 'gpt-5-nano'
6093 + );
6094 +
6095 + // Build request body with optimal settings for fast responses
6096 + $request_body = [
6097 + 'model' => $selected_model,
6098 + 'messages' => $formatted_conversation,
6099 + 'temperature' => 1,
6100 + 'stream' => false
6101 + ];
6102 +
6103 + // Add reasoning_effort only for GPT-5 models
6104 + if ($is_gpt5_model) {
6105 + $request_body['reasoning_effort'] = 'minimal'; // Fastest response for GPT-5
6106 + }
6107 +
6108 + $body = json_encode($request_body);
6109 +
6110 + $args = [
6111 + 'body' => $body,
6112 + 'headers' => [
6113 + 'Content-Type' => 'application/json',
6114 + 'Authorization' => 'Bearer ' . $api_key,
6115 + ],
6116 + 'timeout' => 60,
6117 + 'redirection' => 5,
6118 + 'blocking' => true,
6119 + 'httpversion' => '1.0',
6120 + 'sslverify' => true,
6121 + ];
6122 +
6123 + $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
6124 +
6125 + if (is_wp_error($response)) {
6126 + $error_message = $response->get_error_message();
6127 + return [
6128 + 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
6129 + 'error_code' => 'openai_connection_error',
6130 + 'provider' => 'openai'
6131 + ];
6132 + }
6133 +
6134 + $status_code = wp_remote_retrieve_response_code($response);
6135 + if ($status_code !== 200) {
6136 + $response_body = wp_remote_retrieve_body($response);
6137 + $decoded_response = json_decode($response_body, true);
6138 +
6139 + $error_message = isset($decoded_response['error']['message'])
6140 + ? $decoded_response['error']['message']
6141 + : 'HTTP Error ' . $status_code;
6142 +
6143 + $error_type = isset($decoded_response['error']['type'])
6144 + ? $decoded_response['error']['type']
6145 + : 'unknown';
6146 +
6147 + // Handle specific error types
6148 + switch ($error_type) {
6149 + case 'invalid_request_error':
6150 + if (strpos($error_message, 'API key') !== false) {
6151 + return [
6152 + 'error' => esc_html__('Invalid OpenAI API key. Please check your API key configuration.', 'mxchat'),
6153 + 'error_code' => 'openai_invalid_api_key',
6154 + 'provider' => 'openai'
6155 + ];
6156 + }
6157 + break;
6158 +
6159 + case 'authentication_error':
6160 + return [
6161 + 'error' => esc_html__('Authentication failed with OpenAI. Please check your API key.', 'mxchat'),
6162 + 'error_code' => 'openai_auth_error',
6163 + 'provider' => 'openai'
6164 + ];
6165 +
6166 + case 'rate_limit_exceeded':
6167 + return [
6168 + 'error' => esc_html__('OpenAI rate limit exceeded. Please try again later.', 'mxchat'),
6169 + 'error_code' => 'openai_rate_limit',
6170 + 'provider' => 'openai'
6171 + ];
6172 +
6173 + case 'quota_exceeded':
6174 + return [
6175 + 'error' => esc_html__('OpenAI API quota exceeded. Please check your billing details.', 'mxchat'),
6176 + 'error_code' => 'openai_quota_exceeded',
6177 + 'provider' => 'openai'
6178 + ];
6179 + }
6180 +
6181 + // Generic error fallback
6182 + return [
6183 + 'error' => esc_html__('OpenAI API error: ', 'mxchat') . esc_html($error_message),
6184 + 'error_code' => 'openai_api_error',
6185 + 'provider' => 'openai',
6186 + 'status_code' => $status_code
6187 + ];
6188 + }
6189 +
6190 + $response_body = wp_remote_retrieve_body($response);
6191 + $decoded_response = json_decode($response_body, true);
6192 +
6193 + if (isset($decoded_response['choices'][0]['message']['content'])) {
6194 + return trim($decoded_response['choices'][0]['message']['content']);
6195 + } else {
6196 + return [
6197 + 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
6198 + 'error_code' => 'openai_response_format_error',
6199 + 'provider' => 'openai'
6200 + ];
6201 + }
6202 + } catch (Exception $e) {
6203 + return [
6204 + 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
6205 + 'error_code' => 'openai_exception',
6206 + 'provider' => 'openai'
6207 + ];
6208 + }
6209 +}
6210 +
6211 +private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
6212 + try {
6213 + // Get bot ID from session or request
6214 + $bot_id = $this->get_current_bot_id($session_id);
6215 +
6216 + // Get system prompt instructions using centralized function
6217 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
6218 +
6219 + // Add system prompt to relevant content
6220 + $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
6221 +
6222 + // Prepend system instructions to the conversation history
6223 + array_unshift($conversation_history, [
6224 + 'role' => 'system',
6225 + 'content' => "Here are your instructions: " . $content_with_instructions
6226 + ]);
6227 +
6228 + // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
6229 + foreach ($conversation_history as &$message) {
6230 + if ($message['role'] === 'bot') {
6231 + $message['role'] = 'assistant';
6232 + } elseif ($message['role'] === 'agent') {
6233 + // Tag the message as coming from a live agent
6234 + $message['role'] = 'assistant';
6235 + if (!isset($message['metadata'])) {
6236 + $message['metadata'] = ['source' => 'live_agent'];
6237 + }
6238 + }
6239 +
6240 + // Ensure all roles are valid
6241 + if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
6242 + $message['role'] = 'user'; // Default to 'user'
6243 + }
6244 + }
6245 +
6246 + // Build the request body
6247 + $body = json_encode([
6248 + 'model' => $selected_model,
6249 + 'messages' => $conversation_history,
6250 + 'temperature' => 0.8,
6251 + 'stream' => false
6252 + ]);
6253 +
6254 + // Set up the API request
6255 + $args = [
6256 + 'body' => $body,
6257 + 'headers' => [
6258 + 'Content-Type' => 'application/json',
6259 + 'Authorization' => 'Bearer ' . $xai_api_key,
6260 + ],
6261 + 'timeout' => 60,
6262 + 'redirection' => 5,
6263 + 'blocking' => true,
6264 + 'httpversion' => '1.0',
6265 + 'sslverify' => true,
6266 + ];
6267 +
6268 + // Make the API request
6269 + $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
6270 +
6271 + // Process the response
6272 + if (is_wp_error($response)) {
6273 + $error_message = $response->get_error_message();
6274 + //error_log('X.AI API Error: ' . $error_message);
6275 + return [
6276 + 'error' => esc_html__('Connection error when contacting X.AI: ', 'mxchat') . esc_html($error_message),
6277 + 'error_code' => 'xai_connection_error',
6278 + 'provider' => 'xai'
6279 + ];
6280 + }
6281 +
6282 + $status_code = wp_remote_retrieve_response_code($response);
6283 + if ($status_code !== 200) {
6284 + $response_body = wp_remote_retrieve_body($response);
6285 + $decoded_response = json_decode($response_body, true);
6286 +
6287 + // Log the full response for debugging
6288 + //error_log('X.AI Error Response: ' . print_r($decoded_response, true));
6289 +
6290 + // Extract error message from X.AI's specific format
6291 + $error_message = '';
6292 +
6293 + // Check for direct error string (as seen in your logs)
6294 + if (isset($decoded_response['error']) && is_string($decoded_response['error'])) {
6295 + $error_message = $decoded_response['error'];
6296 + }
6297 + // Check for nested error object (OpenAI style)
6298 + elseif (isset($decoded_response['error']['message'])) {
6299 + $error_message = $decoded_response['error']['message'];
6300 + }
6301 + // Check for top-level message
6302 + elseif (isset($decoded_response['message'])) {
6303 + $error_message = $decoded_response['message'];
6304 + }
6305 + // Fallback
6306 + else {
6307 + $error_message = 'HTTP Error ' . $status_code;
6308 + }
6309 +
6310 + //error_log('X.AI API HTTP Error: ' . $status_code . ' - ' . $error_message);
6311 +
6312 + // Check for API key errors using string matching
6313 + if (stripos($error_message, 'api key') !== false ||
6314 + stripos($error_message, 'incorrect api key') !== false ||
6315 + stripos($error_message, 'invalid api key') !== false) {
6316 + return [
6317 + 'error' => esc_html__('Invalid X.AI API key. Please check your API key configuration.', 'mxchat'),
6318 + 'error_code' => 'xai_invalid_api_key',
6319 + 'provider' => 'xai'
6320 + ];
6321 + }
6322 +
6323 + // Authentication errors
6324 + if ($status_code === 401 || $status_code === 403 ||
6325 + stripos($error_message, 'auth') !== false) {
6326 + return [
6327 + 'error' => esc_html__('Authentication failed with X.AI. Please check your API key.', 'mxchat'),
6328 + 'error_code' => 'xai_auth_error',
6329 + 'provider' => 'xai'
6330 + ];
6331 + }
6332 +
6333 + // Model errors
6334 + if (stripos($error_message, 'model') !== false) {
6335 + return [
6336 + 'error' => esc_html__('Invalid model specified for X.AI. Please check your model configuration.', 'mxchat'),
6337 + 'error_code' => 'xai_invalid_model',
6338 + 'provider' => 'xai'
6339 + ];
6340 + }
6341 +
6342 + // Rate limit errors
6343 + if ($status_code === 429 ||
6344 + stripos($error_message, 'rate') !== false ||
6345 + stripos($error_message, 'limit') !== false) {
6346 + return [
6347 + 'error' => esc_html__('X.AI rate limit exceeded. Please try again later.', 'mxchat'),
6348 + 'error_code' => 'xai_rate_limit',
6349 + 'provider' => 'xai'
6350 + ];
6351 + }
6352 +
6353 + // Quota errors
6354 + if (stripos($error_message, 'quota') !== false ||
6355 + stripos($error_message, 'billing') !== false) {
6356 + return [
6357 + 'error' => esc_html__('X.AI API quota exceeded. Please check your billing details.', 'mxchat'),
6358 + 'error_code' => 'xai_quota_exceeded',
6359 + 'provider' => 'xai'
6360 + ];
6361 + }
6362 +
6363 + // Server errors
6364 + if ($status_code >= 500) {
6365 + return [
6366 + 'error' => esc_html__('X.AI service is currently unavailable. Please try again later.', 'mxchat'),
6367 + 'error_code' => 'xai_service_unavailable',
6368 + 'provider' => 'xai'
6369 + ];
6370 + }
6371 +
6372 + // Generic error fallback with the actual error message
6373 + return [
6374 + 'error' => esc_html__('X.AI API error: ', 'mxchat') . esc_html($error_message),
6375 + 'error_code' => 'xai_api_error',
6376 + 'provider' => 'xai',
6377 + 'status_code' => $status_code
6378 + ];
6379 + }
6380 +
6381 + $response_body = wp_remote_retrieve_body($response);
6382 + $decoded_response = json_decode($response_body, true);
6383 +
6384 + if (isset($decoded_response['choices'][0]['message']['content'])) {
6385 + return trim($decoded_response['choices'][0]['message']['content']);
6386 + } else {
6387 + //error_log('X.AI API Response Format Error: ' . print_r($decoded_response, true));
6388 + return [
6389 + 'error' => esc_html__('Unexpected response format from X.AI.', 'mxchat'),
6390 + 'error_code' => 'xai_response_format_error',
6391 + 'provider' => 'xai'
6392 + ];
6393 + }
6394 +} catch (Exception $e) {
6395 + //error_log('X.AI Exception: ' . $e->getMessage());
6396 + return [
6397 + 'error' => esc_html__('System error when processing X.AI request: ', 'mxchat') . esc_html($e->getMessage()),
6398 + 'error_code' => 'xai_exception',
6399 + 'provider' => 'xai'
6400 + ];
6401 +}
6402 +
6403 +
6404 +}
6405 +private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
6406 + try {
6407 + // Ensure conversation_history is an array
6408 + if (!is_array($conversation_history)) {
6409 + $conversation_history = array();
6410 + }
6411 +
6412 + // Get bot ID from session or request
6413 + $bot_id = $this->get_current_bot_id($session_id);
6414 +
6415 + // Get system prompt instructions using centralized function
6416 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
6417 +
6418 + // Create a new array for the formatted conversation
6419 + $formatted_conversation = array();
6420 +
6421 + // Add system message first
6422 + $formatted_conversation[] = array(
6423 + 'role' => 'system',
6424 + 'content' => $system_prompt_instructions . " " . $relevant_content
6425 + );
6426 +
6427 + // Add the rest of the conversation history
6428 + foreach ($conversation_history as $message) {
6429 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
6430 + $role = $message['role'];
6431 +
6432 + // Convert roles to supported format
6433 + if ($role === 'bot' || $role === 'agent') {
6434 + $role = 'assistant';
6435 + }
6436 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
6437 + $role = 'user';
6438 + }
6439 +
6440 + $formatted_conversation[] = array(
6441 + 'role' => $role,
6442 + 'content' => $message['content']
6443 + );
6444 + }
6445 + }
6446 +
6447 + $body = json_encode([
6448 + 'model' => $selected_model,
6449 + 'messages' => $formatted_conversation,
6450 + 'temperature' => 0.8,
6451 + 'stream' => false
6452 + ]);
6453 +
6454 + $args = [
6455 + 'body' => $body,
6456 + 'headers' => [
6457 + 'Content-Type' => 'application/json',
6458 + 'Authorization' => 'Bearer ' . $deepseek_api_key,
6459 + ],
6460 + 'timeout' => 60,
6461 + 'redirection' => 5,
6462 + 'blocking' => true,
6463 + 'httpversion' => '1.0',
6464 + 'sslverify' => true,
6465 + ];
6466 +
6467 + $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
6468 +
6469 + if (is_wp_error($response)) {
6470 + $error_message = $response->get_error_message();
6471 + //error_log('DeepSeek API Error: ' . $error_message);
6472 + return [
6473 + 'error' => esc_html__('Connection error when contacting DeepSeek: ', 'mxchat') . esc_html($error_message),
6474 + 'error_code' => 'deepseek_connection_error',
6475 + 'provider' => 'deepseek'
6476 + ];
6477 + }
6478 +
6479 + $status_code = wp_remote_retrieve_response_code($response);
6480 + if ($status_code !== 200) {
6481 + $response_body = wp_remote_retrieve_body($response);
6482 + $decoded_response = json_decode($response_body, true);
6483 +
6484 + $error_message = isset($decoded_response['error']['message'])
6485 + ? $decoded_response['error']['message']
6486 + : 'HTTP Error ' . $status_code;
6487 +
6488 + $error_type = isset($decoded_response['error']['type'])
6489 + ? $decoded_response['error']['type']
6490 + : 'unknown';
6491 +
6492 + //error_log('DeepSeek API HTTP Error: ' . $status_code . ' - ' . $error_message);
6493 +
6494 + // Handle specific error types
6495 + switch ($status_code) {
6496 + case 401:
6497 + return [
6498 + 'error' => esc_html__('Authentication failed with DeepSeek. Please check your API key.', 'mxchat'),
6499 + 'error_code' => 'deepseek_auth_error',
6500 + 'provider' => 'deepseek'
6501 + ];
6502 +
6503 + case 400:
6504 + if (strpos($error_message, 'API key') !== false) {
6505 + return [
6506 + 'error' => esc_html__('Invalid DeepSeek API key. Please check your API key configuration.', 'mxchat'),
6507 + 'error_code' => 'deepseek_invalid_api_key',
6508 + 'provider' => 'deepseek'
6509 + ];
6510 + }
6511 + break;
6512 +
6513 + case 429:
6514 + if (strpos($error_message, 'quota') !== false) {
6515 + return [
6516 + 'error' => esc_html__('DeepSeek API quota exceeded. Please check your billing details.', 'mxchat'),
6517 + 'error_code' => 'deepseek_quota_exceeded',
6518 + 'provider' => 'deepseek'
6519 + ];
6520 + } else {
6521 + return [
6522 + 'error' => esc_html__('DeepSeek rate limit exceeded. Please try again later.', 'mxchat'),
6523 + 'error_code' => 'deepseek_rate_limit',
6524 + 'provider' => 'deepseek'
6525 + ];
6526 + }
6527 +
6528 + case 500:
6529 + case 502:
6530 + case 503:
6531 + case 504:
6532 + return [
6533 + 'error' => esc_html__('DeepSeek service is currently unavailable. Please try again later.', 'mxchat'),
6534 + 'error_code' => 'deepseek_service_unavailable',
6535 + 'provider' => 'deepseek'
6536 + ];
6537 + }
6538 +
6539 + // Generic error fallback
6540 + return [
6541 + 'error' => esc_html__('DeepSeek API error: ', 'mxchat') . esc_html($error_message),
6542 + 'error_code' => 'deepseek_api_error',
6543 + 'provider' => 'deepseek',
6544 + 'status_code' => $status_code
6545 + ];
6546 + }
6547 +
6548 + $response_body = wp_remote_retrieve_body($response);
6549 + $decoded_response = json_decode($response_body, true);
6550 +
6551 + if (isset($decoded_response['choices'][0]['message']['content'])) {
6552 + return trim($decoded_response['choices'][0]['message']['content']);
6553 + } else {
6554 + //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
6555 + return [
6556 + 'error' => esc_html__('Unexpected response format from DeepSeek.', 'mxchat'),
6557 + 'error_code' => 'deepseek_response_format_error',
6558 + 'provider' => 'deepseek'
6559 + ];
6560 + }
6561 + } catch (Exception $e) {
6562 + //error_log('DeepSeek Exception: ' . $e->getMessage());
6563 + return [
6564 + 'error' => esc_html__('System error when processing DeepSeek request: ', 'mxchat') . esc_html($e->getMessage()),
6565 + 'error_code' => 'deepseek_exception',
6566 + 'provider' => 'deepseek'
6567 + ];
6568 + }
6569 +}
6570 +private function mxchat_generate_response_gemini($selected_model, $gemini_api_key, $conversation_history, $relevant_content) {
6571 + // Get bot ID from session or request
6572 + $bot_id = $this->get_current_bot_id($session_id);
6573 +
6574 + // Get system prompt instructions using centralized function
6575 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
6576 +
6577 + // Add system prompt to relevant content
6578 + $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
6579 +
6580 + // Format messages for Gemini API
6581 + $formatted_messages = [];
6582 +
6583 + // Add system message as the first user message with role prefix
6584 + // Note: Gemini doesn't have a dedicated system role, so we use a prefixed user message
6585 + $formatted_messages[] = [
6586 + 'role' => 'user',
6587 + 'parts' => [
6588 + ['text' => "[System Instructions] " . $content_with_instructions]
6589 + ]
6590 + ];
6591 +
6592 + // Add model response to acknowledge system instructions
6593 + $formatted_messages[] = [
6594 + 'role' => 'model',
6595 + 'parts' => [
6596 + ['text' => "I understand and will follow these instructions."]
6597 + ]
6598 + ];
6599 +
6600 + // Process the rest of the conversation history
6601 + $current_role = null;
6602 + $current_parts = [];
6603 +
6604 + foreach ($conversation_history as $message) {
6605 + // Skip the first system message as we already handled it
6606 + if ($message['role'] === 'system') {
6607 + continue;
6608 + }
6609 +
6610 + // Map roles to Gemini format
6611 + $gemini_role = '';
6612 + if ($message['role'] === 'user') {
6613 + $gemini_role = 'user';
6614 + } else if (in_array($message['role'], ['assistant', 'bot', 'agent'])) {
6615 + $gemini_role = 'model';
6616 + } else {
6617 + // Skip unsupported roles
6618 + continue;
6619 + }
6620 +
6621 + // If we have a new role, add the previous message
6622 + if ($current_role !== null && $current_role !== $gemini_role && !empty($current_parts)) {
6623 + $formatted_messages[] = [
6624 + 'role' => $current_role,
6625 + 'parts' => $current_parts
6626 + ];
6627 + $current_parts = [];
6628 + }
6629 +
6630 + // Set current role and add text to parts
6631 + $current_role = $gemini_role;
6632 + $current_parts[] = ['text' => $message['content']];
6633 + }
6634 +
6635 + // Add the last message if there's content
6636 + if ($current_role !== null && !empty($current_parts)) {
6637 + $formatted_messages[] = [
6638 + 'role' => $current_role,
6639 + 'parts' => $current_parts
6640 + ];
6641 + }
6642 +
6643 + // Build the request body
6644 + $body = json_encode([
6645 + 'contents' => $formatted_messages,
6646 + 'generationConfig' => [
6647 + 'temperature' => 0.7,
6648 + 'topP' => 0.95,
6649 + 'topK' => 40,
6650 + 'maxOutputTokens' => 8192,
6651 + ],
6652 + 'safetySettings' => [
6653 + [
6654 + 'category' => 'HARM_CATEGORY_HARASSMENT',
6655 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
6656 + ],
6657 + [
6658 + 'category' => 'HARM_CATEGORY_HATE_SPEECH',
6659 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
6660 + ],
6661 + [
6662 + 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
6663 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
6664 + ],
6665 + [
6666 + 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT',
6667 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
6668 + ]
6669 + ]
6670 + ]);
6671 +
6672 + // Prepare the API endpoint
6673 + $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
6674 +
6675 + // Set up the API request
6676 + $args = [
6677 + 'body' => $body,
6678 + 'headers' => [
6679 + 'Content-Type' => 'application/json',
6680 + ],
6681 + 'timeout' => 60,
6682 + 'redirection' => 5,
6683 + 'blocking' => true,
6684 + 'httpversion' => '1.0',
6685 + 'sslverify' => true,
6686 + ];
6687 +
6688 + // Make the API request
6689 + $response = wp_remote_post($api_endpoint, $args);
6690 +
6691 + // Process the response
6692 + if (is_wp_error($response)) {
6693 + return "Sorry, there was an error processing your request: " . $response->get_error_message();
6694 + }
6695 +
6696 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
6697 +
6698 + // Handle potential errors in the response
6699 + if (isset($response_body['error'])) {
6700 + //error_log('Gemini API Error: ' . json_encode($response_body['error']));
6701 + return "Sorry, there was an error with the Gemini API: " .
6702 + (isset($response_body['error']['message']) ? $response_body['error']['message'] : 'Unknown error');
6703 + }
6704 +
6705 + // Extract the response text
6706 + if (isset($response_body['candidates'][0]['content']['parts'][0]['text'])) {
6707 + return trim($response_body['candidates'][0]['content']['parts'][0]['text']);
6708 + } else {
6709 + //error_log('Unexpected Gemini API response format: ' . json_encode($response_body));
6710 + return "Sorry, I couldn't process that request. The response format was unexpected.";
6711 + }
6712 +}
6713 +
6714 +
6715 +public function test_streaming_request() {
6716 + $options = get_option('mxchat_options', []);
6717 + $model = $options['model'] ?? 'gpt-4o';
6718 +
6719 + // Detect provider from model prefix
6720 + $provider = strtolower(explode('-', $model)[0]);
6721 +
6722 + $sample_prompt = 'Hello! Can you stream this response back to me?';
6723 + $messages = [['role' => 'user', 'content' => $sample_prompt]];
6724 + $headers = [];
6725 + $body = [];
6726 + $url = '';
6727 + $api_key = '';
6728 +
6729 + switch ($provider) {
6730 + case 'gpt':
6731 + case 'o1':
6732 + $api_key = $options['api_key'] ?? '';
6733 + if (empty($api_key)) return '❌ Missing API key for OpenAI';
6734 + $url = 'https://api.openai.com/v1/chat/completions';
6735 + $headers = [
6736 + 'Content-Type: application/json',
6737 + 'Authorization: Bearer ' . $api_key
6738 + ];
6739 + $body = [
6740 + 'model' => $model,
6741 + 'messages' => $messages,
6742 + 'stream' => true
6743 + ];
6744 + break;
6745 +
6746 + case 'claude':
6747 + $api_key = $options['claude_api_key'] ?? '';
6748 + if (empty($api_key)) return '❌ Missing API key for Claude';
6749 + $url = 'https://api.anthropic.com/v1/messages';
6750 + $headers = [
6751 + 'Content-Type: application/json',
6752 + 'x-api-key: ' . $api_key,
6753 + 'anthropic-version: 2023-06-01'
6754 + ];
6755 + $body = [
6756 + 'model' => $model,
6757 + 'messages' => $messages,
6758 + 'max_tokens' => 100,
6759 + 'stream' => true
6760 + ];
6761 + break;
6762 +
6763 + case 'grok':
6764 + $api_key = $options['xai_api_key'] ?? '';
6765 + if (empty($api_key)) return '❌ Missing API key for X.AI';
6766 + $url = 'https://api.x.ai/v1/chat/completions';
6767 + $headers = [
6768 + 'Content-Type: application/json',
6769 + 'Authorization: Bearer ' . $api_key
6770 + ];
6771 + $body = [
6772 + 'model' => $model,
6773 + 'messages' => $messages,
6774 + 'stream' => true
6775 + ];
6776 + break;
6777 +
6778 + case 'deepseek':
6779 + if (empty($deepseek_api_key)) {
6780 + $error_response = [
6781 + 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
6782 + 'error_code' => 'missing_deepseek_api_key'
6783 + ];
6784 + if ($testing_data !== null) {
6785 + $error_response['testing_data'] = $testing_data;
6786 + }
6787 + return $error_response;
6788 + }
6789 + if ($streaming) {
6790 + return $this->mxchat_generate_response_deepseek_stream(
6791 + $selected_model,
6792 + $deepseek_api_key,
6793 + $conversation_history,
6794 + $relevant_content,
6795 + $session_id,
6796 + $testing_data // Pass testing data
6797 + );
6798 + } else {
6799 + $response = $this->mxchat_generate_response_deepseek(
6800 + $selected_model,
6801 + $deepseek_api_key,
6802 + $conversation_history,
6803 + $relevant_content
6804 + );
6805 + }
6806 + break;
6807 +
6808 + case 'gemini':
6809 + $api_key = $options['gemini_api_key'] ?? '';
6810 + if (empty($api_key)) return '❌ Missing API key for Gemini';
6811 + $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':streamGenerateContent?key=' . $api_key;
6812 + $headers = ['Content-Type: application/json'];
6813 + $body = [
6814 + 'contents' => [['role' => 'user', 'parts' => [['text' => $sample_prompt]]]],
6815 + 'generationConfig' => ['temperature' => 0.7]
6816 + ];
6817 + break;
6818 +
6819 + default:
6820 + return '❌ Unsupported provider: ' . $provider;
6821 + }
6822 +
6823 + // Do the actual streaming test
6824 + $ch = curl_init($url);
6825 + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
6826 + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
6827 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
6828 + curl_setopt($ch, CURLOPT_TIMEOUT, 15);
6829 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
6830 +
6831 + $response = curl_exec($ch);
6832 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
6833 + $error = curl_error($ch);
6834 + curl_close($ch);
6835 +
6836 + if ($error) return "❌ cURL error: $error";
6837 + if ($http_code !== 200) {
6838 + $error_message = json_decode($response, true)['error']['message'] ?? 'Unknown';
6839 + return "❌ HTTP $http_code: $error_message";
6840 + }
6841 +
6842 + return true;
6843 +}
6844 +
3275 6845 public function mxchat_dismiss_pre_chat_message() {
3276 6846 // Get and sanitize the user identifier
3277 6847 $user_id = $this->mxchat_get_user_identifier();
3278 6848 $user_id = sanitize_key($user_id);
@@ -3326,38 +6896,36 @@
3326 6896
3327 6897 return $dotProduct / ($normA * $normB);
3328 6898 }
3329 6899
6900 +
3330 6901 public function mxchat_enqueue_scripts_styles() {
3331 - // Define version numbers for the styles and scripts
3332 - $chat_style_version = '2.0.5'; // Replace with your actual version
3333 - $chat_script_version = '2.0.5'; // Replace with your actual version
3334 -
3335 6902 // Enqueue the script
3336 6903 wp_enqueue_script(
3337 6904 'mxchat-chat-js',
3338 6905 plugin_dir_url(__FILE__) . '../js/chat-script.js',
3339 6906 array('jquery'),
3340 - $chat_script_version,
6907 + MXCHAT_VERSION,
3341 6908 true
3342 6909 );
3343 -
3344 6910 // Enqueue the CSS
3345 6911 wp_enqueue_style(
3346 6912 'mxchat-chat-css',
3347 6913 plugin_dir_url(__FILE__) . '../css/chat-style.css',
3348 6914 array(),
3349 - $chat_style_version
6915 + MXCHAT_VERSION
3350 6916 );
3351 -
3352 6917 // Fetch options from the database
3353 6918 $this->options = get_option('mxchat_options');
3354 6919 $prompts_options = get_option('mxchat_prompts_options', array());
3355 -
6920 +
3356 6921 // Prepare settings for JavaScript
3357 6922 $style_settings = array(
3358 6923 'ajax_url' => admin_url('admin-ajax.php'),
3359 6924 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
6925 + 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-4o',
6926 + 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
6927 + 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
3360 6928 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
3361 6929 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
3362 6930 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
3363 6931 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
@@ -3371,10 +6939,9 @@
3371 6939 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
3372 6940 'icon_color' => $this->options['icon_color'] ?? '#fff',
3373 6941 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
3374 6942 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
3375 - 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off', // Example for consistency
3376 -
6943 + 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
3377 6944 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
3378 6945 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
3379 6946 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
3380 6947 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
@@ -3379,76 +6946,1122 @@
3379 6946 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
3380 6947 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
3381 6948 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
3382 6949 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
3383 -
3384 6950 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
6951 + 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off', // FIXED
6952 + 'initial_email_state' => null, // Also fixed this undefined variable
6953 + 'skip_email_check' => true,
3385 6954 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
3386 6955 );
3387 -
3388 6956 // Pass the settings to the script
3389 6957 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
3390 6958 }
3391 6959
3392 6960
6961 +/**
6962 + * Setup the cron jobs for rate limits with guard against multiple calls
6963 + */
6964 +public function setup_rate_limit_cron_jobs() {
6965 + // Add a guard to prevent multiple rapid calls
6966 + $last_setup = get_transient('mxchat_cron_setup_guard');
6967 + if ($last_setup && (time() - $last_setup) < 60) {
6968 + // Don't run again if we ran less than 60 seconds ago
6969 + return;
6970 + }
6971 +
6972 + // Set the guard
6973 + set_transient('mxchat_cron_setup_guard', time(), 300); // 5 minutes
6974 +
6975 + try {
6976 + // First, check if WordPress cron is disabled
6977 + if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
6978 + //error_log('MxChat: WordPress cron is disabled (DISABLE_WP_CRON = true), using fallback system');
6979 + $this->setup_fallback_rate_limit_system();
6980 + return;
6981 + }
6982 +
6983 + // Check if cron is already scheduled - if so, don't mess with it
6984 + if (wp_next_scheduled('mxchat_reset_rate_limits')) {
6985 + //error_log('MxChat: Rate limit cron already scheduled, skipping setup');
6986 + return;
6987 + }
6988 +
6989 + // Clear any orphaned hooks (but don't loop indefinitely)
6990 + $hooks_to_clear = [
6991 + 'mxchat_reset_rate_limits',
6992 + 'mxchat_reset_hourly_rate_limits',
6993 + 'mxchat_reset_daily_rate_limits',
6994 + 'mxchat_reset_weekly_rate_limits',
6995 + 'mxchat_reset_monthly_rate_limits'
6996 + ];
6997 +
6998 + foreach ($hooks_to_clear as $hook) {
6999 + // Only clear a maximum of 3 instances to prevent infinite loops
7000 + $cleared = 0;
7001 + while (wp_next_scheduled($hook) && $cleared < 3) {
7002 + wp_clear_scheduled_hook($hook);
7003 + $cleared++;
7004 + }
7005 + }
7006 +
7007 + // Small delay after clearing
7008 + usleep(100000); // 0.1 seconds
7009 +
7010 + // Try to schedule the event
7011 + $initial_time = time() + 300; // Start in 5 minutes
7012 + $result = wp_schedule_event($initial_time, 'hourly', 'mxchat_reset_rate_limits');
7013 +
7014 + if ($result === false) {
7015 + //error_log('MxChat: Failed to schedule cron, using fallback system');
7016 + $this->setup_fallback_rate_limit_system();
7017 + } else {
7018 + //error_log('MxChat: Successfully scheduled rate limit reset cron');
7019 + }
7020 +
7021 + } catch (Exception $e) {
7022 + //error_log('MxChat: Cron setup exception: ' . $e->getMessage());
7023 + $this->setup_fallback_rate_limit_system();
7024 + }
7025 +}
7026 +
7027 +/**
7028 + * Try alternative cron scheduling methods
7029 + */
7030 +private function try_alternative_cron_scheduling($initial_time) {
7031 + try {
7032 + // Method 1: Try with current time instead of future time
7033 + $result1 = wp_schedule_event(time(), 'hourly', 'mxchat_reset_rate_limits');
7034 + if ($result1 !== false) {
7035 + //error_log('MxChat: Alternative method 1 (current time) succeeded');
7036 + return true;
7037 + }
7038 +
7039 + // Method 2: Try with a different interval
7040 + $result2 = wp_schedule_event($initial_time, 'daily', 'mxchat_reset_rate_limits');
7041 + if ($result2 !== false) {
7042 + //error_log('MxChat: Alternative method 2 (daily interval) succeeded');
7043 + return true;
7044 + }
7045 +
7046 + // Method 3: Try wp_schedule_single_event first, then recurring
7047 + $result3 = wp_schedule_single_event($initial_time, 'mxchat_reset_rate_limits');
7048 + if ($result3 !== false) {
7049 + //error_log('MxChat: Alternative method 3 (single event) succeeded');
7050 + // Schedule the next one manually in the handler
7051 + return true;
7052 + }
7053 +
7054 + return false;
7055 +
7056 + } catch (Exception $e) {
7057 + //error_log('MxChat: Alternative cron scheduling exception: ' . $e->getMessage());
7058 + return false;
7059 + }
7060 +}
7061 +
7062 +/**
7063 + * Enhanced fallback rate limit system
7064 + */
7065 +private function setup_fallback_rate_limit_system() {
7066 + // Set a flag to use database-based rate limit cleanup
7067 + update_option('mxchat_use_fallback_rate_limits', true);
7068 +
7069 + // Schedule a one-time check to happen on the next plugin load
7070 + update_option('mxchat_next_rate_limit_check', time() + 3600);
7071 +
7072 + // Also set up a more frequent fallback check (every 4 hours)
7073 + update_option('mxchat_fallback_check_interval', 4 * 3600);
7074 +
7075 + //error_log('MxChat: Fallback rate limit system activated');
7076 +}
7077 +
7078 +/**
7079 + * Enhanced fallback check method
7080 + */
7081 +public function check_fallback_rate_limits() {
7082 + $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
7083 +
7084 + if (!$use_fallback) {
7085 + return; // Regular cron is working
7086 + }
7087 +
7088 + $next_check = get_option('mxchat_next_rate_limit_check', 0);
7089 + $check_interval = get_option('mxchat_fallback_check_interval', 3600);
7090 +
7091 + if (time() >= $next_check) {
7092 + //error_log('MxChat: Running fallback rate limit cleanup');
7093 + $this->mxchat_reset_rate_limits();
7094 +
7095 + // Schedule next check
7096 + update_option('mxchat_next_rate_limit_check', time() + $check_interval);
7097 + }
7098 +}
7099 +/**
7100 + * Enhanced rate limit check that includes fallback cleanup and bot-specific rate limits
7101 + */
7102 +public function check_rate_limit() {
7103 + // Check if we need to run fallback cleanup
7104 + $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
7105 + $next_check = get_option('mxchat_next_rate_limit_check', 0);
7106 +
7107 + if ($use_fallback && time() >= $next_check) {
7108 + $this->mxchat_reset_rate_limits();
7109 + update_option('mxchat_next_rate_limit_check', time() + 3600); // Next hour
7110 + }
7111 +
7112 + // Get bot ID from current request context
7113 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
7114 +
7115 + // Get bot-specific options (includes rate limits if overridden)
7116 + $bot_options = $this->get_bot_options($bot_id);
7117 + $current_options = !empty($bot_options) ? $bot_options : $this->options;
7118 +
7119 + // Use bot-specific rate limits if available, otherwise fall back to default
7120 + $rate_limits_source = isset($current_options['rate_limits']) ? $current_options['rate_limits'] : get_option('mxchat_options', [])['rate_limits'] ?? [];
7121 +
7122 + // Determine user role or if logged out
7123 + if (is_user_logged_in()) {
7124 + $user = wp_get_current_user();
7125 + $user_id = $user->ID;
7126 +
7127 + // Get the user's primary role using reset() to safely get the first element
7128 + $user_roles = $user->roles;
7129 +
7130 + // Safely get the first role regardless of array key structure
7131 + if (!empty($user_roles) && is_array($user_roles)) {
7132 + $role = reset($user_roles); // This safely gets the first element regardless of key
7133 + } else {
7134 + $role = 'subscriber'; // Default to subscriber if no role found
7135 + }
7136 + } else {
7137 + $role = 'logged_out';
7138 + // Use IP address for non-logged-in users
7139 + $user_id = $this->get_client_ip();
7140 + }
7141 +
7142 + // Check if rate limits are configured for this role
7143 + if (!isset($rate_limits_source[$role])) {
7144 + return true; // No limit set for this role
7145 + }
7146 +
7147 + $limit = $rate_limits_source[$role]['limit'];
7148 +
7149 + // If unlimited, return true immediately
7150 + if ($limit === 'unlimited') {
7151 + return true;
7152 + }
7153 +
7154 + // Get the option name for this user/role with safer naming (include bot_id for bot-specific limits)
7155 + $safe_role = preg_replace('/[^a-zA-Z0-9_]/', '_', $role);
7156 + $safe_user_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $user_id);
7157 + $safe_bot_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $bot_id);
7158 +
7159 + // Include bot_id in option name so each bot has separate rate limits
7160 + $option_name = 'mxchat_chat_limit_' . $safe_bot_id . '_' . $safe_role . '_' . $safe_user_id;
7161 +
7162 + // Get the counter data
7163 + $limit_data = get_option($option_name, ['count' => 0, 'timestamp' => time()]);
7164 +
7165 + // If first request or counter reset needed, set the initial timestamp
7166 + if ($limit_data['count'] === 0) {
7167 + $limit_data['timestamp'] = time();
7168 + update_option($option_name, $limit_data);
7169 + }
7170 +
7171 + // Get the timeframe
7172 + $timeframe = isset($rate_limits_source[$role]['timeframe']) ?
7173 + $rate_limits_source[$role]['timeframe'] : 'daily';
7174 +
7175 + // Check if the counter needs to be reset based on timeframe
7176 + $current_time = time();
7177 + $timestamp = $limit_data['timestamp'];
7178 + $should_reset = false;
7179 +
7180 + switch ($timeframe) {
7181 + case 'hourly':
7182 + $should_reset = ($current_time - $timestamp) >= 3600; // 1 hour
7183 + break;
7184 + case 'daily':
7185 + $should_reset = ($current_time - $timestamp) >= 86400; // 24 hours
7186 + break;
7187 + case 'weekly':
7188 + $should_reset = ($current_time - $timestamp) >= 604800; // 7 days
7189 + break;
7190 + case 'monthly':
7191 + $should_reset = ($current_time - $timestamp) >= 2592000; // 30 days
7192 + break;
7193 + }
7194 +
7195 + // Reset the counter if the timeframe has passed
7196 + if ($should_reset) {
7197 + $limit_data = ['count' => 0, 'timestamp' => $current_time];
7198 + update_option($option_name, $limit_data);
7199 + }
7200 +
7201 + // Check if user has exceeded their limit
7202 + if ($limit_data['count'] >= intval($limit)) {
7203 + // Get the custom message for this role
7204 + $message = !empty($rate_limits_source[$role]['message'])
7205 + ? $rate_limits_source[$role]['message']
7206 + : __('Rate limit exceeded. Please try again later.', 'mxchat');
7207 +
7208 + // Add timeframe information to the message if placeholders exist
7209 + $timeframe_label = '';
7210 + switch ($timeframe) {
7211 + case 'hourly':
7212 + $timeframe_label = __('hour', 'mxchat');
7213 + break;
7214 + case 'daily':
7215 + $timeframe_label = __('day', 'mxchat');
7216 + break;
7217 + case 'weekly':
7218 + $timeframe_label = __('week', 'mxchat');
7219 + break;
7220 + case 'monthly':
7221 + $timeframe_label = __('month', 'mxchat');
7222 + break;
7223 + }
7224 +
7225 + // Replace placeholders in the message
7226 + $message = str_replace(
7227 + ['{limit}', '{count}', '{remaining}', '{timeframe}'],
7228 + [intval($limit), $limit_data['count'], max(0, intval($limit) - $limit_data['count']), $timeframe_label],
7229 + $message
7230 + );
7231 +
7232 + // Process HTML links in the message
7233 + $message = $this->process_rate_limit_message_html($message);
7234 +
7235 + // Return error with the processed message
7236 + return [
7237 + 'error' => true,
7238 + 'message' => $message
7239 + ];
7240 + }
7241 +
7242 + // Increment the counter
7243 + $limit_data['count']++;
7244 + update_option($option_name, $limit_data);
7245 +
7246 + return true;
7247 +}
7248 +
7249 +/**
7250 + * Enhanced rate limit reset with better error handling
7251 + */
3393 7252 public function mxchat_reset_rate_limits() {
7253 + try {
3394 7254 global $wpdb;
7255 + $all_options = get_option('mxchat_options', []);
7256 + $current_time = time();
7257 +
7258 + // Get rate limit options with a safer query and limit
7259 + $option_names = $wpdb->get_col(
7260 + $wpdb->prepare(
7261 + "SELECT option_name FROM {$wpdb->options}
7262 + WHERE option_name LIKE %s
7263 + LIMIT 1000",
7264 + 'mxchat_chat_limit_%'
7265 + )
7266 + );
7267 +
7268 + if (empty($option_names)) {
7269 + return;
7270 + }
7271 +
7272 + $processed_count = 0;
7273 + $max_processing_time = 30; // Maximum 30 seconds
7274 + $start_time = time();
7275 +
7276 + foreach ($option_names as $option_name) {
7277 + // Check processing time limit
7278 + if ((time() - $start_time) > $max_processing_time) {
7279 + //error_log('MxChat: Rate limit reset timeout after processing ' . $processed_count . ' entries');
7280 + break;
7281 + }
7282 +
7283 + // Parse the option name more safely
7284 + if (!preg_match('/^mxchat_chat_limit_(.+)_(.+)$/', $option_name, $matches)) {
7285 + continue;
7286 + }
7287 +
7288 + $role_and_user = $matches[1] . '_' . $matches[2];
7289 + $parts = explode('_', $role_and_user);
7290 +
7291 + if (count($parts) < 2) {
7292 + continue;
7293 + }
7294 +
7295 + // Extract role (everything except the last part which is user ID)
7296 + $user_id_part = array_pop($parts);
7297 + $role = implode('_', $parts);
7298 +
7299 + // Skip if role doesn't exist in our settings
7300 + if (!isset($all_options['rate_limits'][$role])) {
7301 + // Clean up orphaned entries
7302 + delete_option($option_name);
7303 + continue;
7304 + }
7305 +
7306 + $timeframe = $all_options['rate_limits'][$role]['timeframe'] ?? 'daily';
7307 + $limit_data = get_option($option_name);
7308 +
7309 + if (!$limit_data || !is_array($limit_data) || !isset($limit_data['timestamp'])) {
7310 + // Clean up invalid entries
7311 + delete_option($option_name);
7312 + continue;
7313 + }
7314 +
7315 + $timestamp = $limit_data['timestamp'];
7316 + $should_reset = false;
7317 +
7318 + // Determine if we should reset based on the timeframe
7319 + switch ($timeframe) {
7320 + case 'hourly':
7321 + $should_reset = ($current_time - $timestamp) >= 3600;
7322 + break;
7323 + case 'daily':
7324 + $should_reset = ($current_time - $timestamp) >= 86400;
7325 + break;
7326 + case 'weekly':
7327 + $should_reset = ($current_time - $timestamp) >= 604800;
7328 + break;
7329 + case 'monthly':
7330 + $should_reset = ($current_time - $timestamp) >= 2592000;
7331 + break;
7332 + }
7333 +
7334 + // Reset the counter if the timeframe has passed
7335 + if ($should_reset) {
7336 + delete_option($option_name);
7337 + wp_cache_delete($option_name, 'options');
7338 + $processed_count++;
7339 + }
7340 + }
7341 +
7342 + // Clean up any orphaned cache entries
7343 + wp_cache_delete('mxchat_all_chat_limits', 'options');
7344 +
7345 + //error_log("MxChat: Rate limit reset completed. Processed {$processed_count} entries.");
7346 +
7347 + } catch (Exception $e) {
7348 + //error_log('MxChat: Rate limit reset error: ' . $e->getMessage());
7349 + }
7350 +}
3395 7351
3396 - // Define a cache key pattern for rate limits
3397 - $cache_key_pattern = 'mxchat_chat_limit_%';
3398 7352
3399 - // Retrieve all option names matching the pattern
3400 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
3401 - $option_names = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
7353 +/**
7354 + * Process HTML links in rate limit messages
7355 + *
7356 + * @param string $message The rate limit message
7357 + * @return string The processed message with safe HTML links
7358 + */
7359 +private function process_rate_limit_message_html($message) {
7360 + // Return original message if empty
7361 + if (empty($message)) {
7362 + return $message;
7363 + }
7364 +
7365 + // First, convert markdown links to HTML
7366 + $message = $this->convert_markdown_links($message);
7367 +
7368 + // Then, auto-convert any remaining plain URLs to links
7369 + $message = $this->auto_link_urls($message);
7370 +
7371 + // Allow basic HTML tags for links and formatting
7372 + $allowed_tags = [
7373 + 'a' => [
7374 + 'href' => true,
7375 + 'target' => true,
7376 + 'rel' => true,
7377 + 'title' => true,
7378 + 'class' => true
7379 + ],
7380 + 'strong' => [],
7381 + 'em' => [],
7382 + 'br' => [],
7383 + 'b' => [],
7384 + 'i' => [],
7385 + 'span' => ['class' => true]
7386 + ];
7387 +
7388 + // Sanitize but allow the specified HTML tags
7389 + $processed_message = wp_kses($message, $allowed_tags);
7390 +
7391 + // If wp_kses stripped everything, return the original message as plain text
7392 + if (empty($processed_message) && !empty($message)) {
7393 + // Strip all HTML and return plain text as fallback
7394 + return wp_strip_all_tags($message);
7395 + }
7396 +
7397 + return $processed_message;
7398 +}
3402 7399
3403 - // db call ok; no-cache ok
3404 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- db call ok
3405 - $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
7400 +/**
7401 + * Convert markdown links to HTML
7402 + *
7403 + * @param string $text The text to process
7404 + * @return string The text with markdown links converted to HTML
7405 + */
7406 +private function convert_markdown_links($text) {
7407 + // Return original text if empty
7408 + if (empty($text)) {
7409 + return $text;
7410 + }
7411 +
7412 + // Pattern to match markdown links: [text](url)
7413 + $pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
7414 +
7415 + $processed_text = preg_replace_callback($pattern, function($matches) {
7416 + $link_text = $matches[1];
7417 + $url = $matches[2];
7418 +
7419 + // Clean up any trailing punctuation from the URL
7420 + $url = rtrim($url, '.,;:!?');
7421 +
7422 + // Sanitize the link text and URL
7423 + $safe_text = esc_html($link_text);
7424 + $safe_url = esc_url($url);
7425 +
7426 + // Create the HTML link
7427 + return '<a href="' . $safe_url . '" target="_blank" rel="noopener noreferrer">' . $safe_text . '</a>';
7428 + }, $text);
7429 +
7430 + // If preg_replace_callback failed, return original text
7431 + if ($processed_text === null) {
7432 + return $text;
7433 + }
7434 +
7435 + return $processed_text;
7436 +}
3406 7437
3407 - // Clear the relevant cache entries
3408 - foreach ($option_names as $option_name) {
3409 - wp_cache_delete($option_name, 'options');
3410 - }
7438 +/**
7439 + * Auto-convert plain URLs to clickable links
7440 + *
7441 + * @param string $text The text to process
7442 + * @return string The text with URLs converted to links
7443 + */
7444 +private function auto_link_urls($text) {
7445 + // Return original text if empty
7446 + if (empty($text)) {
7447 + return $text;
7448 + }
7449 +
7450 + // Simple pattern that avoids complex lookbehinds
7451 + // This will match URLs that are not already inside href attributes or markdown links
7452 + $pattern = '/(?<!href=["\'])(?<!\]\()https?:\/\/[^\s<>"\')\]]+/i';
7453 +
7454 + $processed_text = preg_replace_callback($pattern, function($matches) {
7455 + $url = $matches[0];
7456 + // Clean up any trailing punctuation that might have been captured
7457 + $url = rtrim($url, '.,;:!?');
7458 +
7459 + // Add target="_blank" and rel="noopener noreferrer" for security
7460 + return '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($url) . '</a>';
7461 + }, $text);
7462 +
7463 + // If preg_replace_callback failed, return original text
7464 + if ($processed_text === null) {
7465 + return $text;
7466 + }
7467 +
7468 + return $processed_text;
7469 +}
3411 7470
3412 - // Optionally, clear a general cache if you have one
3413 - wp_cache_delete('mxchat_all_chat_limits', 'options');
7471 +
7472 +// Helper function to get client IP address
7473 +private function get_client_ip() {
7474 + // Check for shared internet/ISP IP
7475 + if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
7476 + return sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
3414 7477 }
7478 +
7479 + // Check for IPs passing through proxies
7480 + if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
7481 + // Use the first value in the comma-separated list
7482 + $forwarded_for = explode(',', sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']));
7483 + return trim($forwarded_for[0]);
7484 + }
7485 +
7486 + if (!empty($_SERVER['REMOTE_ADDR'])) {
7487 + return sanitize_text_field($_SERVER['REMOTE_ADDR']);
7488 + }
7489 +
7490 + // Fallback
7491 + return 'unknown';
7492 +}
3415 7493
3416 -private function mxchat_fetch_woocommerce_products() {
3417 - // Ensure WooCommerce is active
3418 - if (!class_exists('WooCommerce')) {
3419 - return [];
7494 +/**
7495 + * AJAX handler to get system information for testing panel
7496 + */
7497 +/**
7498 + * AJAX handler to get system information for testing panel
7499 + */
7500 +public function mxchat_get_system_info() {
7501 + // Verify nonce for security
7502 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
7503 + wp_send_json_error(['message' => 'Invalid nonce']);
7504 + return;
3420 7505 }
7506 +
7507 + // Only allow admin users
7508 + if (!current_user_can('administrator')) {
7509 + wp_send_json_error(['message' => 'Unauthorized']);
7510 + return;
7511 + }
7512 +
7513 + // Get system prompt from options
7514 + $system_prompt = isset($this->options['system_prompt_instructions'])
7515 + ? $this->options['system_prompt_instructions']
7516 + : 'No system prompt configured';
7517 +
7518 + // Get selected model
7519 + $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
7520 +
7521 + // Check if OpenRouter is being used
7522 + $is_openrouter = ($selected_model === 'openrouter');
7523 + $openrouter_model = '';
7524 +
7525 + if ($is_openrouter) {
7526 + // Get the actual OpenRouter model that's selected
7527 + $openrouter_model = isset($this->options['openrouter_selected_model'])
7528 + ? $this->options['openrouter_selected_model']
7529 + : 'No OpenRouter model selected';
7530 +
7531 + // Update selected_model display to show both
7532 + $selected_model = 'OpenRouter: ' . $openrouter_model;
7533 + }
7534 +
7535 + // Get API key status (just check if they exist, don't expose the keys)
7536 + $api_status = [];
7537 + $api_status['openai'] = !empty($this->options['api_key']);
7538 + $api_status['claude'] = !empty($this->options['claude_api_key']);
7539 + $api_status['gemini'] = !empty($this->options['gemini_api_key']);
7540 + $api_status['xai'] = !empty($this->options['xai_api_key']);
7541 + $api_status['deepseek'] = !empty($this->options['deepseek_api_key']);
7542 + $api_status['openrouter'] = !empty($this->options['openrouter_api_key']);
7543 +
7544 + wp_send_json_success([
7545 + 'system_prompt' => $system_prompt,
7546 + 'selected_model' => $selected_model,
7547 + 'is_openrouter' => $is_openrouter,
7548 + 'openrouter_model' => $openrouter_model,
7549 + 'api_status' => $api_status
7550 + ]);
7551 +}
3421 7552
3422 - $args = array(
3423 - 'post_type' => 'product',
3424 - 'post_status' => 'publish',
3425 - 'posts_per_page' => -1,
7553 +/**
7554 + * AJAX handler to get similarity threshold
7555 + */
7556 +public function mxchat_get_similarity_threshold() {
7557 + // Verify nonce for security
7558 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
7559 + wp_send_json_error(['message' => 'Invalid nonce']);
7560 + return;
7561 + }
7562 +
7563 + // Only allow admin users
7564 + if (!current_user_can('administrator')) {
7565 + wp_send_json_error(['message' => 'Unauthorized']);
7566 + return;
7567 + }
7568 +
7569 + // Get similarity threshold from main options (default 35%)
7570 + $similarity_threshold = isset($this->options['similarity_threshold'])
7571 + ? ((int) $this->options['similarity_threshold']) / 100
7572 + : 0.35;
7573 +
7574 + wp_send_json_success([
7575 + 'threshold' => $similarity_threshold,
7576 + 'threshold_percentage' => ($similarity_threshold * 100) . '%'
7577 + ]);
7578 +}
7579 +
7580 +/**
7581 + * AJAX handler to get knowledge base status
7582 + */
7583 +public function mxchat_get_kb_status() {
7584 + // Verify nonce for security
7585 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
7586 + wp_send_json_error(['message' => 'Invalid nonce']);
7587 + return;
7588 + }
7589 +
7590 + // Only allow admin users
7591 + if (!current_user_can('administrator')) {
7592 + wp_send_json_error(['message' => 'Unauthorized']);
7593 + return;
7594 + }
7595 +
7596 + // Check Pinecone vs WordPress
7597 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
7598 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
7599 +
7600 + $kb_info = [
7601 + 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
7602 + 'status' => 'Active'
7603 + ];
7604 +
7605 + // Get document count
7606 + if ($use_pinecone) {
7607 + $kb_info['documents'] = 'Connected to Pinecone';
7608 + $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
7609 + } else {
7610 + // Count documents in WordPress database
7611 + global $wpdb;
7612 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
7613 + $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
7614 + $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
7615 + }
7616 +
7617 + wp_send_json_success($kb_info);
7618 +}
7619 +
7620 +/**
7621 + * AJAX handler to start a completely fresh session (NEW - replaces old clear session)
7622 + */
7623 +public function mxchat_start_fresh_session() {
7624 + // Verify nonce for security
7625 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
7626 + wp_send_json_error(['message' => 'Invalid nonce']);
7627 + return;
7628 + }
7629 +
7630 + // Only allow admin users
7631 + if (!current_user_can('administrator')) {
7632 + wp_send_json_error(['message' => 'Unauthorized']);
7633 + return;
7634 + }
7635 +
7636 + $old_session_id = isset($_POST['old_session_id']) ? sanitize_text_field($_POST['old_session_id']) : '';
7637 + $new_session_id = isset($_POST['new_session_id']) ? sanitize_text_field($_POST['new_session_id']) : '';
7638 +
7639 + if (empty($old_session_id)) {
7640 + wp_send_json_error(['message' => 'Old session ID required']);
7641 + return;
7642 + }
7643 +
7644 + // If no new session ID provided, generate one
7645 + if (empty($new_session_id)) {
7646 + $new_session_id = 'mxchat_chat_' . substr(md5(uniqid()), 0, 9);
7647 + }
7648 +
7649 + // Clear ALL data associated with the old session
7650 + $this->clear_complete_session_data($old_session_id);
7651 +
7652 + // Initialize the new session
7653 + $this->initialize_fresh_session($new_session_id);
7654 +
7655 + wp_send_json_success([
7656 + 'message' => 'Fresh session started successfully',
7657 + 'new_session_id' => $new_session_id,
7658 + 'old_session_id' => $old_session_id
7659 + ]);
7660 +}
7661 +
7662 +/**
7663 + * Clear ALL data associated with a session (ENHANCED)
7664 + */
7665 +private function clear_complete_session_data($session_id) {
7666 + // Clear chat history
7667 + delete_option("mxchat_history_{$session_id}");
7668 +
7669 + // Clear chat mode
7670 + delete_option("mxchat_mode_{$session_id}");
7671 +
7672 + // Clear any PDF/Word transients
7673 + $this->clear_pdf_transients($session_id);
7674 + if (method_exists($this, 'clear_word_transients')) {
7675 + $this->clear_word_transients($session_id);
7676 + }
7677 +
7678 + // Clear agent-related data
7679 + delete_option("mxchat_channel_{$session_id}");
7680 + delete_option("mxchat_agent_name_{$session_id}");
7681 + delete_option("mxchat_email_{$session_id}");
7682 +
7683 + // Clear any recommendation flow state
7684 + delete_option("mxchat_sr_flow_state_{$session_id}");
7685 +
7686 + // Clear any cached embeddings or context
7687 + delete_transient("mxchat_context_{$session_id}");
7688 + delete_transient("mxchat_last_query_{$session_id}");
7689 +
7690 + // Clear any testing data
7691 + delete_transient("mxchat_testing_data_{$session_id}");
7692 +
7693 + // Clear any rate limiting data for this session
7694 + delete_transient("mxchat_rate_limit_{$session_id}");
7695 +
7696 + // Clear any other session-specific transients
7697 + delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
7698 + delete_transient("mxchat_include_pdf_in_context_{$session_id}");
7699 + delete_transient("mxchat_include_word_in_context_{$session_id}");
7700 +
7701 + //error_log("MxChat: Cleared all data for session: {$session_id}");
7702 +}
7703 +
7704 +/**
7705 + * Initialize a fresh session with default data
7706 + */
7707 +private function initialize_fresh_session($session_id) {
7708 + // Set default chat mode
7709 + update_option("mxchat_mode_{$session_id}", 'ai');
7710 +
7711 + //error_log("MxChat: Initialized fresh session: {$session_id}");
7712 +}
7713 +
7714 +/**
7715 + * Helper method to clear Word document transients (if you have Word support)
7716 + */
7717 +private function clear_word_transients($session_id) {
7718 + delete_transient('mxchat_word_url_' . $session_id);
7719 + delete_transient('mxchat_word_filename_' . $session_id);
7720 + delete_transient('mxchat_word_embeddings_' . $session_id);
7721 + delete_transient('mxchat_include_word_in_context_' . $session_id);
7722 +}
7723 +
7724 +/**
7725 + * Simplified testing data capture method (CLEANED UP)
7726 + */
7727 +private function capture_testing_data($user_embedding, $message, $session_id) {
7728 + // Only capture for admin users
7729 + if (!current_user_can('administrator')) {
7730 + return null;
7731 + }
7732 +
7733 + $testing_data = [
7734 + 'query' => $message,
7735 + 'timestamp' => time(),
7736 + 'top_matches' => [],
7737 + 'action_matches' => [] // Add action matches
7738 + ];
7739 +
7740 + // Get similarity threshold
7741 + $similarity_threshold = isset($this->options['similarity_threshold'])
7742 + ? ((int) $this->options['similarity_threshold']) / 100
7743 + : 0.35;
7744 +
7745 + $testing_data['similarity_threshold'] = $similarity_threshold;
7746 +
7747 + // Use the real similarity analysis if available
7748 + if ($this->last_similarity_analysis !== null) {
7749 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
7750 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
7751 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7752 + } else {
7753 + // Fallback: determine knowledge base type
7754 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
7755 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
7756 +
7757 + $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
7758 + }
7759 +
7760 + // Include action analysis if available
7761 + if (isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
7762 + $testing_data['action_matches'] = $this->last_action_analysis;
7763 +
7764 + // Clear it after capturing to avoid stale data
7765 + $this->last_action_analysis = null;
7766 + }
7767 +
7768 + return $testing_data;
7769 +}
7770 +
7771 +
7772 +/**
7773 + * Track URL clicks from chatbot responses
7774 + */
7775 +public function mxchat_track_url_click() {
7776 + // Verify nonce for security
7777 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
7778 + wp_send_json_error(['message' => 'Invalid nonce']);
7779 + wp_die();
7780 + }
7781 +
7782 + $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
7783 + $clicked_url = isset($_POST['url']) ? esc_url_raw($_POST['url']) : '';
7784 + $message_context = isset($_POST['message_context']) ? sanitize_textarea_field($_POST['message_context']) : '';
7785 +
7786 + if (empty($session_id) || empty($clicked_url)) {
7787 + wp_send_json_error(['message' => 'Missing required data']);
7788 + wp_die();
7789 + }
7790 +
7791 + global $wpdb;
7792 + $table_name = $wpdb->prefix . 'mxchat_url_clicks';
7793 +
7794 + // Insert click tracking record
7795 + $wpdb->insert(
7796 + $table_name,
7797 + [
7798 + 'session_id' => $session_id,
7799 + 'clicked_url' => $clicked_url,
7800 + 'message_context' => $message_context,
7801 + 'click_timestamp' => current_time('mysql', 1),
7802 + 'user_ip' => $_SERVER['REMOTE_ADDR'],
7803 + 'user_agent' => $_SERVER['HTTP_USER_AGENT']
7804 + ]
3426 7805 );
7806 +
7807 + wp_send_json_success(['message' => 'Click tracked']);
7808 + wp_die();
7809 +}
3427 7810
3428 - $products = get_posts($args);
3429 - $product_data = [];
7811 +/**
7812 + * Get URL click analytics for a session
7813 + */
7814 +public function mxchat_get_url_clicks($session_id) {
7815 + global $wpdb;
7816 + $table_name = $wpdb->prefix . 'mxchat_url_clicks';
7817 +
7818 + $clicks = $wpdb->get_results($wpdb->prepare(
7819 + "SELECT * FROM $table_name WHERE session_id = %s ORDER BY click_timestamp ASC",
7820 + $session_id
7821 + ));
7822 +
7823 + return $clicks;
7824 +}
7825 +/**
7826 + * Track the originating page where chat was started
7827 + */
7828 +public function mxchat_track_originating_page() {
7829 + // Verify nonce
7830 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
7831 + wp_send_json_error(['message' => 'Invalid nonce']);
7832 + wp_die();
7833 + }
7834 +
7835 + $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
7836 + $page_url = isset($_POST['page_url']) ? esc_url_raw($_POST['page_url']) : '';
7837 + $page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
7838 +
7839 + if (empty($session_id)) {
7840 + wp_send_json_error(['message' => 'Missing session ID']);
7841 + wp_die();
7842 + }
7843 +
7844 + global $wpdb;
7845 + $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
7846 +
7847 + // Check if we've already tracked for this session
7848 + $existing = $wpdb->get_var($wpdb->prepare(
7849 + "SELECT COUNT(*) FROM $table_name
7850 + WHERE session_id = %s
7851 + AND originating_page_url IS NOT NULL",
7852 + $session_id
7853 + ));
7854 +
7855 + if ($existing > 0) {
7856 + wp_send_json_success(['message' => 'Already tracked']);
7857 + wp_die();
7858 + }
7859 +
7860 + // Update the first message in this session with originating page info
7861 + $wpdb->query($wpdb->prepare(
7862 + "UPDATE $table_name
7863 + SET originating_page_url = %s,
7864 + originating_page_title = %s
7865 + WHERE session_id = %s
7866 + ORDER BY timestamp ASC
7867 + LIMIT 1",
7868 + $page_url,
7869 + $page_title,
7870 + $session_id
7871 + ));
7872 +
7873 + wp_send_json_success(['message' => 'Originating page tracked']);
7874 + wp_die();
7875 +}
3430 7876
3431 - foreach ($products as $product) {
3432 - $product_id = $product->ID;
3433 - $product_obj = wc_get_product($product_id);
7877 +/**
7878 + * Validate and clean URLs from AI response
7879 + * Removes any URLs that aren't in the knowledge base
7880 + *
7881 + * @param string $response_text The AI-generated response
7882 + * @param array $valid_urls Array of URLs from the knowledge base
7883 + * @return string Cleaned response with invalid URLs removed/flagged
7884 + */
7885 +private function validate_and_clean_urls($response_text, $valid_urls) {
7886 + // DEBUG: Log what we're working with
7887 + error_log("=== MxChat URL Validation Debug ===");
7888 + error_log("Valid URLs count: " . count($valid_urls));
7889 + error_log("Valid URLs: " . print_r($valid_urls, true));
7890 + error_log("Response text length: " . strlen($response_text));
7891 + error_log("Response text preview: " . substr($response_text, 0, 500));
7892 +
7893 + // If no valid URLs provided or empty response, return as-is
7894 + if (empty($valid_urls) || empty($response_text)) {
7895 + error_log("Validation skipped - empty valid_urls or response");
7896 + return $response_text;
7897 + }
7898 +
7899 + // Extract all URLs from the AI response
7900 + // This regex matches http:// and https:// URLs
7901 + preg_match_all(
7902 + '#\bhttps?://[^\s<>"\')\]]+#i',
7903 + $response_text,
7904 + $matches
7905 + );
7906 +
7907 + // If no URLs found in response, return as-is
7908 + if (empty($matches[0])) {
7909 + error_log("No URLs found in response");
7910 + return $response_text;
7911 + }
7912 +
7913 + $found_urls = $matches[0];
7914 + $cleaned_response = $response_text;
7915 + $removed_count = 0;
7916 +
7917 + // Normalize valid URLs for comparison (remove trailing slashes, fragments, etc.)
7918 + $normalized_valid_urls = array_map(function($url) {
7919 + // Remove trailing slash
7920 + $url = rtrim($url, '/');
7921 + // Remove URL fragments (#section)
7922 + $url = preg_replace('/#.*$/', '', $url);
7923 + // Remove trailing punctuation that might have been captured
7924 + $url = rtrim($url, '.,;:!?');
7925 + return $url;
7926 + }, $valid_urls);
7927 +
7928 + error_log("Normalized valid URLs: " . print_r($normalized_valid_urls, true));
7929 +
7930 + foreach ($found_urls as $found_url) {
7931 + // Clean up the found URL (remove trailing punctuation that might have been captured)
7932 + $clean_found_url = rtrim($found_url, '.,;:!?)');
7933 +
7934 + // DEBUG: Log each URL being checked
7935 + error_log("Checking found URL: " . $found_url);
7936 +
7937 + // Normalize for comparison
7938 + $normalized_found = rtrim($clean_found_url, '/');
7939 + $normalized_found = preg_replace('/#.*$/', '', $normalized_found);
7940 +
7941 + error_log("Normalized found URL: " . $normalized_found);
7942 +
7943 + // Check if this URL exists in our valid URLs list
7944 + $is_valid = false;
7945 +
7946 + error_log("Starting validation checks for: " . $normalized_found);
7947 +
7948 + // First, try exact match
7949 + if (in_array($normalized_found, $normalized_valid_urls)) {
7950 + $is_valid = true;
7951 + error_log("EXACT MATCH FOUND");
7952 + } else {
7953 + error_log("No exact match, checking variations...");
7954 + // If no exact match, check if it's a variation (with query params, etc.)
7955 + foreach ($normalized_valid_urls as $valid_url) {
7956 + error_log(" Comparing against valid URL: " . $valid_url);
7957 +
7958 + // Check if the found URL starts with a valid URL (handles query params)
7959 + if (strpos($normalized_found, $valid_url) === 0) {
7960 + // Check what comes after the valid URL
7961 + $remainder = substr($normalized_found, strlen($valid_url));
7962 +
7963 + // Only valid if:
7964 + // 1. Exact match (remainder is empty)
7965 + // 2. Query params (starts with ?)
7966 + // 3. Fragment (starts with #)
7967 + if (empty($remainder) || $remainder[0] === '?' || $remainder[0] === '#') {
7968 + $is_valid = true;
7969 + error_log(" MATCH: Found URL is valid variation of base URL");
7970 + break;
7971 + } else {
7972 + error_log(" NOT A MATCH: Found URL extends path beyond valid URL (remainder: " . $remainder . ")");
7973 + }
7974 + }
7975 + // Also check the reverse (in case valid URL has query params)
7976 + if (strpos($valid_url, $normalized_found) === 0) {
7977 + $is_valid = true;
7978 + error_log(" MATCH: Valid URL starts with found URL");
7979 + break;
7980 + }
7981 + }
7982 +
7983 + if (!$is_valid) {
7984 + error_log("NO MATCH FOUND - URL should be removed");
7985 + }
7986 + }
7987 +
7988 + // If URL is not valid, remove it from the response
7989 + if (!$is_valid) {
7990 + // Log the removal for debugging
7991 + error_log("MxChat: Removed hallucinated URL: " . $found_url);
7992 + error_log("MxChat: Valid URLs were: " . implode(', ', array_slice($normalized_valid_urls, 0, 5)));
7993 +
7994 + $removed_count++;
7995 +
7996 + // Check if URL is part of a markdown link: [text](url)
7997 + $markdown_pattern = '/\[([^\]]+)\]\(' . preg_quote($found_url, '/') . '\)/';
7998 + if (preg_match($markdown_pattern, $cleaned_response)) {
7999 + error_log("Found markdown link, removing but keeping text");
8000 + // Remove the markdown link but keep the text
8001 + $cleaned_response = preg_replace($markdown_pattern, '$1', $cleaned_response);
8002 + }
8003 + // Check if URL is part of an HTML link: <a href="url">text</a>
8004 + else if (preg_match('/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>(.*?)<\/a>/i', $cleaned_response, $link_match)) {
8005 + error_log("Found HTML link, removing but keeping text");
8006 + // Remove the HTML link but keep the text
8007 + $link_text = $link_match[1];
8008 + $cleaned_response = preg_replace(
8009 + '/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>.*?<\/a>/i',
8010 + $link_text,
8011 + $cleaned_response
8012 + );
8013 + }
8014 + // Otherwise just remove the bare URL
8015 + else {
8016 + error_log("Removing bare URL");
8017 + $cleaned_response = str_replace($found_url, '', $cleaned_response);
8018 + }
8019 + }
8020 + }
8021 +
8022 + // Log summary if any URLs were removed
8023 + if ($removed_count > 0) {
8024 + error_log("MxChat: URL Validation Summary - Removed {$removed_count} hallucinated URL(s)");
8025 + } else {
8026 + error_log("MxChat: URL Validation Summary - No URLs removed, all were valid");
8027 + }
8028 +
8029 + // Clean up any double spaces or awkward punctuation left behind
8030 + $cleaned_response = preg_replace('/\s+/', ' ', $cleaned_response);
8031 + $cleaned_response = preg_replace('/\s+([.,;:!?])/', '$1', $cleaned_response);
8032 +
8033 + error_log("Final cleaned response: " . $cleaned_response);
8034 +
8035 + return trim($cleaned_response);
8036 +}
3434 8037
3435 - $product_data[] = array(
3436 - 'id' => $product_id,
3437 - 'name' => $product_obj->get_name(),
3438 - 'description' => $product_obj->get_description(),
3439 - 'short_description' => $product_obj->get_short_description(),
3440 - 'url' => get_permalink($product_id),
3441 - 'price' => $product_obj->get_regular_price(),
3442 - 'sale_price' => $product_obj->get_sale_price(),
3443 - 'stock_status' => $product_obj->get_stock_status(),
3444 - 'sku' => $product_obj->get_sku(),
3445 - 'in_stock' => $product_obj->is_in_stock(),
3446 - 'total_sales' => $product_obj->get_total_sales(),
3447 - );
8038 +/**
8039 + * AJAX handler to get current chat mode for a session
8040 + */
8041 +public function mxchat_get_current_chat_mode() {
8042 + // Verify nonce for security
8043 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
8044 + wp_send_json_error(['message' => 'Invalid nonce']);
8045 + wp_die();
3448 8046 }
8047 +
8048 + $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
8049 +
8050 + if (empty($session_id)) {
8051 + wp_send_json_error(['message' => 'Session ID missing']);
8052 + wp_die();
8053 + }
8054 +
8055 + // Get the current chat mode for this session
8056 + $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
8057 +
8058 + wp_send_json_success([
8059 + 'chat_mode' => $chat_mode
8060 + ]);
8061 + wp_die();
8062 +}
3449 8063
3450 - return $product_data;
3451 -}
8064 +
3452 8065
3453 8066 }
3454 8067 ?>