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