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