options = get_option('mxchat_options'); $this->chat_count = get_option('mxchat_chat_count', 0); // Add WooCommerce hooks add_action('wp_insert_post', array($this, 'mxchat_handle_product_change'), 10, 3); // Ensure embeddings are removed when a product is moved to trash or permanently deleted add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete')); add_action('before_delete_post', array($this, 'mxchat_handle_product_delete')); add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles')); add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request')); add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request')); add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message')); add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message')); // Add the AJAX actions for checking if the pre-chat message was dismissed add_action('wp_ajax_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status')); add_action('wp_ajax_nopriv_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status')); add_action('wp_ajax_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']); add_action('wp_ajax_nopriv_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']); add_action('wp_ajax_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']); add_action('wp_ajax_nopriv_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']); if (!wp_next_scheduled('mxchat_reset_rate_limits')) { wp_schedule_event(time(), 'daily', 'mxchat_reset_rate_limits'); } add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits')); } public function mxchat_handle_product_change($post_id, $post, $update) { // Ensure this is a product post type if ($post->post_type !== 'product') { return; } // Only generate embeddings if the product is published if ($post->post_status === 'publish') { // Delay the embedding slightly to ensure all product data is available add_action('shutdown', function() use ($post_id) { $product = wc_get_product($post_id); if ($product && $product->get_price() !== '') { $this->mxchat_store_product_embedding($product); } else { // Optionally, log or handle the case where product data is incomplete // error_log("Product {$post_id} does not have complete data. Embedding not generated."); } }); } } public function mxchat_handle_product_delete($post_id) { if (get_post_type($post_id) !== 'product') { return; } global $wpdb; $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; // Delete the embedding associated with this product $wpdb->delete($table_name, array('source_url' => get_permalink($post_id)), array('%s')); } private function mxchat_store_product_embedding($product) { if (isset($this->options['enable_woocommerce_integration']) && $this->options['enable_woocommerce_integration'] === '1') { $source_url = get_permalink($product->get_id()); $regular_price = $product->get_regular_price(); $sale_price = $product->get_sale_price(); $price = $sale_price ?: $regular_price; $description = $product->get_description() . "\n\n" . "Short Description: " . $product->get_short_description() . "\n" . "Price: " . $regular_price . "\n" . "Sale Price: " . ($sale_price ?: 'N/A') . "\n" . "SKU: " . $product->get_sku(); global $wpdb; $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; // Delete any existing embedding for this product $wpdb->delete($table_name, array('source_url' => $source_url), array('%s')); // Submit the new content and embedding to the database MxChat_Utils::submit_content_to_db($description, $source_url, $this->options['api_key']); } } private function mxchat_increment_chat_count() { $chat_count = get_option('mxchat_chat_count', 0); $chat_count++; update_option('mxchat_chat_count', $chat_count); } function mxchat_fetch_conversation_history() { if (empty($_POST['session_id'])) { wp_send_json_error(['message' => 'Session ID missing.']); wp_die(); } $session_id = sanitize_text_field($_POST['session_id']); $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history if (empty($history)) { wp_send_json_error(['message' => 'No history found.']); wp_die(); } wp_send_json_success(['conversation' => $history]); wp_die(); } private function mxchat_fetch_conversation_history_for_ajax($session_id) { $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history based on session ID $formatted_history = []; // Format the history to align with the expected structure for OpenAI foreach ($history as $entry) { $formatted_history[] = [ 'role' => $entry['role'], // Ensure this matches 'user' or 'assistant' 'content' => $entry['content'] ]; } return $formatted_history; } private function mxchat_fetch_conversation_history_for_ai($session_id) { $history = get_option("mxchat_history_{$session_id}", []); $formatted_history = []; foreach ($history as $entry) { // Skip messages containing HTML if ($entry['content'] !== strip_tags($entry['content'])) { continue; } $formatted_history[] = [ 'role' => $entry['role'], 'content' => $entry['content'] ]; } return $formatted_history; } private function mxchat_save_chat_message($session_id, $role, $message) { global $wpdb; $table_name = $wpdb->prefix . 'mxchat_chat_transcripts'; $user_id = is_user_logged_in() ? get_current_user_id() : 0; $user_identifier = MxChat_User::mxchat_get_user_identifier(); $user_email = MxChat_User::mxchat_get_user_email(); $history = get_option("mxchat_history_{$session_id}", []); $history[] = ['role' => $role, 'content' => $message]; update_option("mxchat_history_{$session_id}", $history); $wpdb->insert($table_name, [ 'user_id' => $user_id, 'user_identifier' => $user_identifier, 'user_email' => $user_email, 'session_id' => $session_id, 'role' => $role, 'message' => $message, 'timestamp' => current_time('mysql', 1) ]); } public function mxchat_handle_chat_request() { global $wpdb; // Get and sanitize the user identifier $user_id = $this->mxchat_get_user_identifier(); $user_id = sanitize_key($user_id); // Setup rate limiting $rate_limit_transient_key = 'mxchat_chat_limit_' . $user_id; $chat_count = get_transient($rate_limit_transient_key) ?: 0; // Retrieve the session ID from the client's POST data $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : ''; if (empty($session_id)) { wp_send_json_error('Session ID is missing.'); wp_die(); } // Determine the rate limit based on login status $rate_limit_option = is_user_logged_in() ? ($this->options['rate_limit_logged_in'] ?? '100') : ($this->options['rate_limit_logged_out'] ?? '10'); if ($rate_limit_option !== 'unlimited' && $chat_count >= intval($rate_limit_option)) { $rate_limit_message = $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.'; wp_send_json_error(['message' => $rate_limit_message]); wp_die(); } // Increment chat count only if limit is not 'unlimited' if ($rate_limit_option !== 'unlimited') { set_transient($rate_limit_transient_key, $chat_count + 1, DAY_IN_SECONDS); } // Validate and sanitize the incoming message if (empty($_POST['message'])) { wp_send_json_error('No message received.'); wp_die(); } $message = wp_strip_all_tags($_POST['message'], false); $message = trim($message); $this->mxchat_save_chat_message($session_id, 'user', $message); // Initialize response variables $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []]; $this->productCardHtml = ''; $intent_info = ''; // Step 1: Check for new PDF URL in the message if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) { $new_pdf_url = $matches[0]; // Validate HTTPS if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') { $existing_pdf_url = get_transient('mxchat_pdf_url_' . $session_id); // If a new URL is detected, reset all PDF-related transients if ($existing_pdf_url !== $new_pdf_url) { delete_transient('mxchat_pdf_url_' . $session_id); delete_transient('mxchat_pdf_embeddings_' . $session_id); delete_transient('mxchat_include_pdf_in_context_' . $session_id); // Store the new PDF URL set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS); // Get the maximum number of pages allowed from admin settings $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69; // Default to 69 // Fetch and process the new PDF $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages); // Pass both arguments if ($embeddings === 'too_many_pages') { // Handle PDF exceeding page limit $error_text = sprintf( $this->options['pdf_intent_error_text'] ?? "The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", $max_pages ); $this->fallbackResponse['text'] = $error_text; delete_transient('mxchat_pdf_url_' . $session_id); } elseif ($embeddings) { // Handle successful processing set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS); set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS); $success_text = $this->options['pdf_intent_success_text'] ?? "I've processed the new PDF. What questions do you have about it?"; $this->fallbackResponse['text'] = $success_text; } else { // Handle general processing error $error_text = $this->options['pdf_intent_error_text'] ?? "Sorry, I couldn't process the new PDF. Please ensure it's a valid file."; $this->fallbackResponse['text'] = $error_text; delete_transient('mxchat_pdf_url_' . $session_id); } wp_send_json(['message' => $this->fallbackResponse['text']]); wp_die(); } } } // Step 2: Detect intent and handle intent-based responses $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id); // Step 3: If intent is matched and handled, respond immediately if (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html'])) { $response_data = [ 'text' => $this->fallbackResponse['text'], 'html' => $this->fallbackResponse['html'], 'session_id' => $session_id ]; $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text'] . $this->fallbackResponse['html']); wp_send_json($response_data); wp_die(); } // Step 4: Handle PDF-related content if applicable $include_pdf_in_context = get_transient('mxchat_include_pdf_in_context_' . $session_id); $pdf_vectors = get_transient('mxchat_pdf_embeddings_' . $session_id); // Generate embedding for the user's query $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']); if (!is_array($user_message_embedding)) { wp_send_json_error('Error processing your message.'); wp_die(); } // Prepare context for the response $context_content = "User asked: '{$message}'\n\n"; // Retrieve the order summary if it exists (from the session-specific transient) $orderSummary = get_transient('mxchat_order_summary_' . $session_id); if ($orderSummary) { // Include the order summary in the context for the AI $context_content .= "\n" . $orderSummary . "\n"; // Optionally delete the transient if it should only be used once // delete_transient('mxchat_order_summary_' . $session_id); } // Step 5: Fetch relevant content from the knowledge database $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding); if (!empty($relevant_content)) { $context_content .= "Relevant content from knowledge database:\n" . $relevant_content . "\n\n"; } // Step 6: Check PDF content if available if ($include_pdf_in_context && is_array($pdf_vectors)) { //error_log("Searching relevant PDF pages for query embedding."); $relevant_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_vectors); if (!empty($relevant_pages)) { error_log("Relevant PDF pages found: " . implode(', ', array_map(function($page) { return $page['page_number']; }, $relevant_pages))); foreach ($relevant_pages as $page_data) { $context_content .= "Content from page {$page_data['page_number']}:\n" . $page_data['text'] . "\n\n"; } } else { //error_log("No relevant PDF pages found for query embedding."); } } // Step 7: Generate AI response based on context and conversation history $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id); $this->mxchat_increment_chat_count(); $response = $this->mxchat_generate_response( $context_content, $this->options['api_key'], $this->options['xai_api_key'], $this->options['claude_api_key'], $conversation_history ); $this->mxchat_save_chat_message($session_id, 'bot', $response); // Step 8: Save and include additional content if (!empty($this->productCardHtml)) { $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml); } if (!empty($this->fallbackResponse['html'])) { $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']); } // Step 9: Return the response $response_data = [ 'text' => $response, 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''), 'session_id' => $session_id ]; wp_send_json($response_data); wp_die(); } // New function to check intents and invoke the callback function private function mxchat_check_intent_and_invoke_callback( $message, $user_id, $session_id ) { global $wpdb; // Generate the user embedding $user_embedding = $this->mxchat_generate_embedding( $message, $this->options['api_key'] ); if ( ! is_array( $user_embedding ) ) { //error_log( 'mxchat: Error generating embedding for the message.' ); wp_send_json_error( 'Error generating embedding for the message.' ); return false; } // Fetch all intents from the database $table_name = $wpdb->prefix . 'mxchat_intents'; $intents = $wpdb->get_results( "SELECT * FROM $table_name" ); if ( empty( $intents ) ) { //error_log( 'mxchat: No intents found in the database.' ); return false; // No intents to process } $highest_similarity = -INF; $matched_intent = null; // Calculate similarity for each intent, using each intent's individual threshold if set foreach ( $intents as $intent ) { $intent_embedding_serialized = $intent->embedding_vector; // Securely unserialize the embedding vector $intent_embedding = $intent_embedding_serialized ? unserialize( $intent_embedding_serialized, [ 'allowed_classes' => false ] ) : null; if ( ! is_array( $intent_embedding ) ) { //error_log( "mxchat: Invalid embedding for intent '{$intent->intent_label}'. Skipping." ); continue; // Skip if the embedding is invalid } // Calculate cosine similarity between user message embedding and intent embedding $similarity = $this->mxchat_calculate_cosine_similarity( $user_embedding, $intent_embedding ); //error_log( "mxchat: Similarity for intent '{$intent->intent_label}': {$similarity}" ); // Determine the similarity threshold for this intent $intent_threshold = isset( $intent->similarity_threshold ) ? $intent->similarity_threshold : 0.85; // Default to 0.85 if not set //error_log( "mxchat: Using threshold {$intent_threshold} for intent '{$intent->intent_label}'" ); // Only consider this intent as a potential match if it meets or exceeds the intent's threshold if ( $similarity >= $intent_threshold && $similarity > $highest_similarity ) { $highest_similarity = $similarity; $matched_intent = $intent; } } // Log the highest similarity and matched intent if found if ( $matched_intent ) { //error_log( "mxchat: Highest similarity is {$highest_similarity} for intent '{$matched_intent->intent_label}'" ); } else { //error_log( 'mxchat: No intent matched.' ); } // If a match is found above the threshold, call the associated callback if ( $matched_intent ) { //error_log( "mxchat: Intent '{$matched_intent->intent_label}' matches the threshold. Invoking callback '{$matched_intent->callback_function}'" ); // Ensure the callback function exists before calling it if ( method_exists( $this, $matched_intent->callback_function ) ) { // Call the intent handler function call_user_func( [ $this, $matched_intent->callback_function ], $message, $user_id, $session_id ); return true; // Indicate that an intent was matched and handled } else { //error_log( "mxchat: Callback function '{$matched_intent->callback_function}' not found." ); } } else { //error_log( 'mxchat: No intent matched above its respective threshold.' ); } return false; // No intent was matched above the threshold } //verified good public function mxchat_handle_order_history($message, $user_id, $session_id) { // Check if WooCommerce is active if (!class_exists('WooCommerce')) { $this->fallbackResponse['text'] = "Order functionality is currently unavailable."; return; } // Determine if the user is asking for the "last order" or "all orders" $queryType = (stripos($message, 'last order') !== false || stripos($message, 'recent order') !== false) ? 'last' : 'all'; // Fetch the relevant order details $orderDetails = MxChat_WooCommerce::mxchat_fetch_user_orders_details($queryType); if (empty($orderDetails)) { $this->fallbackResponse['text'] = "No previous orders found."; return; } // Prepare an order summary message $orderSummary = ($queryType === 'last') ? "User's last order details:\n" : "User's recent order history:\n"; foreach ($orderDetails as $order) { $items = implode(', ', $order['items']); $orderSummary .= "Order #{$order['order_id']} placed on {$order['date']}, Status: {$order['status']}, Total: {$order['total']}, Items: {$items}\n"; } // Store the order summary in a transient for the session set_transient('mxchat_order_summary_' . $session_id, $orderSummary, HOUR_IN_SECONDS); } //verified good public function mxchat_handle_product_inquiry( $message, $user_id, $session_id ) { // Attempt to retrieve the product ID from the message $product_id = MxChat_WooCommerce::mxchat_extract_product_id_from_message( $message ); // Check if there's a previously discussed product for follow-up queries if ( ! $product_id ) { $product_id = get_transient( 'mxchat_last_discussed_product_' . $user_id ); } if ( $product_id && class_exists( 'WooCommerce' ) ) { $product = wc_get_product( $product_id ); if ( $product ) { // Escape and prepare product data $product_name = esc_html( $product->get_name() ); $product_price = $product->get_price_html(); // Assuming this is safe HTML $product_image_url = esc_url( wp_get_attachment_url( $product->get_image_id() ) ); $product_url = esc_url( get_permalink( $product_id ) ); $product_id_attr = esc_attr( $product_id ); // Generate the product card HTML using HEREDOC syntax $product_card_html = << {$product_name}

{$product_name}

{$product_price}
HTML; // Save the last discussed product set_transient( 'mxchat_last_discussed_product_' . $user_id, $product_id, HOUR_IN_SECONDS ); // Save the product card HTML to be appended later $this->productCardHtml = $product_card_html; // Do not send a response here; allow execution to continue return; } } // If no product found, set a fallback response $this->fallbackResponse = __( "I couldn't find details for that product. Could you specify the product name?", 'mxchat' ); return; } //verified good public function mxchat_handle_email_capture($message, $user_id, $session_id) { // Log the message safely //error_log("Triggered email capture intent for message: " . sanitize_text_field($message)); // Initiate email capture flow $response = esc_html($this->options['triggered_phrase_response'] ?? "Would you like to join our mailing list? Please provide your email below."); set_transient('mxchat_email_capture_' . $user_id, true, 5 * MINUTE_IN_SECONDS); $this->mxchat_save_chat_message($session_id, 'bot', $response); // Respond to the user wp_send_json(['message' => $response]); wp_die(); } //very good public function mxchat_generate_image($message, $user_id, $session_id) { // Prepare a prompt for DALL-E $prompt = "Create an image of " . sanitize_text_field($message); // Use the existing OpenAI API key $openai_api_key = sanitize_text_field($this->options['api_key']); // Call DALL-E to generate an image $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key); // Check if the response contains an image URL if (isset($image_response['imageUrl'])) { $image_url = esc_url_raw($image_response['imageUrl']); // Construct the HTML with a CSS class instead of inline styles $response_html = 'Generated Image'; $response_text = "Here is the image I generated:"; } else { $response_text = "I'm sorry, but I couldn't generate an image based on your request."; $response_html = ''; //error_log("DALL-E image generation error: " . esc_html($image_response['error'] ?? 'Unknown error.')); } // Save both text and HTML responses $this->mxchat_save_chat_message($session_id, 'bot', $response_text . "\n" . $response_html); // Prepare the response data $response_data = [ 'message' => $response_text, 'html' => $response_html, 'image_url' => $image_url ?? '', ]; // Send the JSON response header('Content-Type: application/json; charset=' . get_option('blog_charset')); echo json_encode($response_data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); wp_die(); } private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) { $api_url = 'https://api.openai.com/v1/images/generations'; $body = json_encode([ 'prompt' => sanitize_text_field($prompt), 'n' => 1, 'size' => '1024x1024', 'model' => sanitize_text_field($model), ]); $args = [ 'body' => $body, 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . sanitize_text_field($api_key), ], 'method' => 'POST', 'timeout' => absint($timeout), ]; $response = wp_remote_post($api_url, $args); if (is_wp_error($response)) { //error_log("DALL-E request failed: " . $response->get_error_message()); return ['error' => "Error generating image: " . $response->get_error_message()]; } $response_body = json_decode(wp_remote_retrieve_body($response), true); if (isset($response_body['data'][0]['url'])) { return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])]; } else { //error_log("DALL-E response error: " . wp_remote_retrieve_body($response)); return ['error' => "Failed to generate image."]; } } //very good public function mxchat_handle_search_request($message, $user_id, $session_id) { // Sanitize user input $search_query = preg_replace('/^search the web for\s+/i', '', $message); $search_query = preg_replace('/^show me the latest news about\s+/i', '', $message); $search_query = preg_replace('/^what\'?s the news in\s+/i', '', $message); $search_query = preg_replace('/^news about\s+/i', '', $message); $search_query = trim(sanitize_text_field($search_query)); if (empty($search_query)) { $this->fallbackResponse = [ 'text' => __("Please provide a valid search query.", 'mxchat'), ]; return; } // Check if the query is likely related to news $is_news_search = preg_match("/\bnews\b/i", $message); // Retrieve settings $options = get_option('mxchat_options'); $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : ''; $news_count = isset($options['brave_news_count']) ? intval($options['brave_news_count']) : 3; $country = isset($options['brave_country']) ? sanitize_text_field($options['brave_country']) : 'us'; $language = isset($options['brave_language']) ? sanitize_text_field($options['brave_language']) : 'en'; if (empty($api_key)) { $this->fallbackResponse = [ 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'), ]; return; } // Set API endpoint and parameters based on search type $api_url = $is_news_search ? 'https://api.search.brave.com/res/v1/news/search' : 'https://api.search.brave.com/res/v1/web/search'; $query_args = [ 'q' => urlencode($search_query), ]; if ($is_news_search) { $query_args['count'] = $news_count; $query_args['country'] = $country; $query_args['search_lang'] = $language; } $api_url = add_query_arg($query_args, $api_url); // Implement caching $transient_key = 'mxchat_search_' . md5($api_url); $body = get_transient($transient_key); if (false === $body) { $args = [ 'headers' => [ 'Accept' => 'application/json', 'Accept-Encoding' => 'gzip', 'Authorization' => 'Bearer ' . $api_key, 'x-subscription-token' => $api_key, ], 'timeout' => 10, ]; $response = wp_remote_get($api_url, $args); if (is_wp_error($response)) { $this->fallbackResponse = [ 'text' => __("I'm sorry, I couldn't retrieve any information based on your request.", 'mxchat'), ]; return; } $body = json_decode(wp_remote_retrieve_body($response), true); set_transient($transient_key, $body, HOUR_IN_SECONDS); } // Process the API response and build a more informative summary $text_summary = ""; if ($is_news_search && isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) { $text_summary = "Here are some recent news articles:\n\n"; foreach ($body['results'] as $news) { $title = isset($news['title']) ? esc_html($news['title']) : __('No Title', 'mxchat'); $url = isset($news['url']) ? esc_url($news['url']) : '#'; $description = isset($news['description']) ? esc_html(strip_tags($news['description'])) : __('No description available.', 'mxchat'); $age = isset($news['age']) ? esc_html($news['age']) : ''; $hostname = isset($news['meta_url']['hostname']) ? esc_html($news['meta_url']['hostname']) : ''; $thumbnail = isset($news['thumbnail']['src']) ? esc_url($news['thumbnail']['src']) : ''; // Build text-only news item summary $text_summary .= "Title: {$title}\nURL: {$url}\nDescription: {$description}\nPublished: {$age}\n"; if (!empty($hostname)) { $text_summary .= "Source: {$hostname}\n"; } if (!empty($thumbnail)) { $text_summary .= "Thumbnail: ![Image]({$thumbnail})\n"; } if (!empty($news['extra_snippets'])) { $extra_snippet_text = implode(" ", $news['extra_snippets']); $text_summary .= "Additional Info: {$extra_snippet_text}\n"; } $text_summary .= "\n"; // Separate entries } } elseif (!$is_news_search && isset($body['web']['results']) && is_array($body['web']['results']) && count($body['web']['results']) > 0) { $text_summary = "Here are some relevant articles based on your query:\n\n"; foreach ($body['web']['results'] as $result) { $title = isset($result['title']) ? esc_html($result['title']) : __('No Title', 'mxchat'); $url = isset($result['url']) ? esc_url($result['url']) : '#'; $description = isset($result['description']) ? esc_html(strip_tags($result['description'])) : __('No description available.', 'mxchat'); $hostname = isset($result['meta_url']['hostname']) ? esc_html($result['meta_url']['hostname']) : ''; $thumbnail = isset($result['thumbnail']['src']) ? esc_url($result['thumbnail']['src']) : ''; // Build text-only web item summary $text_summary .= "Title: {$title}\nURL: {$url}\nDescription: {$description}\n"; if (!empty($hostname)) { $text_summary .= "Source: {$hostname}\n"; } if (!empty($thumbnail)) { $text_summary .= "Thumbnail: ![Image]({$thumbnail})\n"; } if (!empty($result['extra_snippets'])) { $extra_snippet_text = implode(" ", $result['extra_snippets']); $text_summary .= "Additional Info: {$extra_snippet_text}\n"; } $text_summary .= "\n"; // Separate entries } } else { $this->fallbackResponse = [ 'text' => __("I'm sorry, I couldn't retrieve any information based on your request.", 'mxchat'), ]; return; } $this->fallbackResponse = [ 'text' => $text_summary, ]; } //very good public function mxchat_handle_image_search_request($message, $user_id, $session_id) { // Step 1: Interpret the search query for better results $refined_search_query = $this->mxchat_interpret_search_query($message); // If no query was interpreted, return a fallback message if (empty($refined_search_query)) { $this->fallbackResponse = [ 'text' => __("I'm sorry, I couldn't interpret your search query. Please specify what you'd like to see images of.", 'mxchat'), 'html' => "", ]; return; } // Brave API URL $api_url = 'https://api.search.brave.com/res/v1/images/search'; // Retrieve Brave API settings $options = get_option('mxchat_options'); $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : ''; if (empty($api_key)) { /* if (defined('WP_DEBUG') && WP_DEBUG) { error_log("Brave API key is missing."); } */ $this->fallbackResponse = [ 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'), 'html' => "", ]; return; } $image_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4; $safe_search = isset($options['brave_safe_search']) ? sanitize_text_field($options['brave_safe_search']) : 'strict'; // Append query parameters based on settings $api_url = add_query_arg([ 'q' => rawurlencode($refined_search_query), 'count' => $image_count, 'safesearch' => $safe_search, ], $api_url); /* // Log the final API URL for the search if (defined('WP_DEBUG') && WP_DEBUG) { error_log("Final API URL for Brave Image Search: " . esc_url_raw($api_url)); } */ // Implement caching $transient_key = 'mxchat_image_search_' . md5($refined_search_query); $body = get_transient($transient_key); if (false === $body) { $args = [ 'headers' => [ 'Accept' => 'application/json', 'Accept-Encoding' => 'gzip', 'X-Subscription-Token' => $api_key, ], 'timeout' => 10, ]; $response = wp_remote_get($api_url, $args); if (is_wp_error($response)) { /* if (defined('WP_DEBUG') && WP_DEBUG) { error_log("Brave Image API request failed: " . $response->get_error_message()); } */ $this->fallbackResponse = [ 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'), 'html' => "", ]; return; } $body = json_decode(wp_remote_retrieve_body($response), true); set_transient($transient_key, $body, HOUR_IN_SECONDS); } // Process the API response if (isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) { $html_output = ''; $this->fallbackResponse = [ 'text' => "", 'html' => $html_output, ]; // Save response in chat history $this->mxchat_save_chat_message($session_id, 'bot', $html_output); } else { /* if (defined('WP_DEBUG') && WP_DEBUG) { error_log("Brave Image API response did not contain expected data structure or was empty: " . print_r($body, true)); } */ $this->fallbackResponse = [ 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'), 'html' => "", ]; } } public function mxchat_interpret_search_query($user_query) { $system_prompt = "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."; // Retrieve OpenAI API key using 'api_key' as the option key $api_key = isset($this->options['api_key']) ? sanitize_text_field($this->options['api_key']) : sanitize_text_field(get_option('mxchat_options')['api_key']); /* // Log the API key check, without exposing the key if (defined('WP_DEBUG') && WP_DEBUG) { error_log("Retrieved OpenAI API Key: " . ($api_key ? "Present" : "Missing")); } */ if (empty($api_key)) { //error_log("OpenAI API key is missing."); return sanitize_text_field($user_query); // Default to the original query if API key is missing } $url = 'https://api.openai.com/v1/chat/completions'; $args = [ 'headers' => [ 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', ], 'body' => wp_json_encode([ 'model' => 'gpt-3.5-turbo', 'messages' => [ ['role' => 'system', 'content' => $system_prompt], ['role' => 'user', 'content' => sanitize_text_field($user_query)], ], 'temperature' => 0.2, 'max_tokens' => 20, ]), 'method' => 'POST', ]; $response = wp_remote_post($url, $args); if (is_wp_error($response)) { //error_log("OpenAI request failed: " . $response->get_error_message()); return sanitize_text_field($user_query); // Fallback to the original query if there's an error } $body = json_decode(wp_remote_retrieve_body($response), true); // Check for a valid response and sanitize output if (isset($body['choices'][0]['message']['content'])) { $interpreted_query = sanitize_text_field(trim($body['choices'][0]['message']['content'])); /* // Log the interpreted query for debugging if (defined('WP_DEBUG') && WP_DEBUG) { error_log("Interpreted search query: " . $interpreted_query); } */ return $interpreted_query; } else { //error_log("Unexpected API response format: " . print_r($body, true)); return sanitize_text_field($user_query); } } //very good public function mxchat_handle_add_to_cart_intent($message, $user_id, $session_id) { // Check if WooCommerce is active before proceeding if (!class_exists('WooCommerce')) { // error_log("WooCommerce is not active. Add-to-cart intent cannot be processed."); return false; } // Sanitize user ID for transient key $sanitized_user_id = sanitize_key($user_id); // Retrieve the last discussed product ID from the transient $last_product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id); if ($last_product_id) { // Attempt to add the product to the WooCommerce cart $added = WC()->cart->add_to_cart($last_product_id); $product = wc_get_product($last_product_id); if ($added && $product) { // Escape product name for safety $product_name = esc_html($product->get_name()); // Success response if the product is added to the cart $response = "The product '{$product_name}' has been added to your cart. To proceed to checkout, please type 'checkout'."; set_transient('mxchat_checkout_prompt_' . $sanitized_user_id, true, 5 * MINUTE_IN_SECONDS); // Save the response in chat history and send a JSON response $this->mxchat_save_chat_message($session_id, 'bot', $response); wp_send_json(['message' => $response]); wp_die(); } else { // Failure response if the product could not be added $response = "Sorry, I couldn't add the product to your cart. Please try again."; $this->mxchat_save_chat_message($session_id, 'bot', $response); wp_send_json(['message' => $response]); wp_die(); } } else { // Prompt the user to specify the product name if no product was found $response = "I couldn't find the product to add. Please mention the product name again."; $this->mxchat_save_chat_message($session_id, 'bot', $response); wp_send_json(['message' => $response]); wp_die(); } } public function mxchat_handle_checkout_intent($message, $user_id, $session_id) { // Ensure WooCommerce is available and perform checkout actions if (class_exists('WooCommerce')) { // Sanitize user ID for transient key $sanitized_user_id = sanitize_key($user_id); $checkout_prompt = get_transient('mxchat_checkout_prompt_' . $sanitized_user_id); // Check if there is an active checkout prompt and if the cart has items if ($checkout_prompt && WC()->cart->get_cart_contents_count() > 0) { // Get and escape the checkout URL $checkout_url = wc_get_checkout_url(); $checkout_url = esc_url_raw($checkout_url); $response = "Great! Redirecting you to the checkout page..."; // Clear the checkout prompt transient to avoid duplicate responses delete_transient('mxchat_checkout_prompt_' . $sanitized_user_id); // Save and send the response with the checkout URL for redirection $this->mxchat_save_chat_message($session_id, 'bot', $response); wp_send_json(['message' => $response, 'redirect_url' => $checkout_url]); wp_die(); } else { // If no active checkout prompt or empty cart, prompt user to add items first $response = "It seems like there is no active checkout prompt or no items in your cart. Please add a product to the cart first."; $this->mxchat_save_chat_message($session_id, 'bot', $response); wp_send_json(['message' => $response]); wp_die(); } } else { // WooCommerce is not available, send an error message $response = "WooCommerce is not active on this site, so checkout is not possible."; $this->mxchat_save_chat_message($session_id, 'bot', $response); wp_send_json(['message' => $response]); wp_die(); } } //very good private function add_email_to_loops($email) { // Sanitize the email $email = sanitize_email($email); // Retrieve and sanitize options $api_key = isset($this->options['loops_api_key']) ? sanitize_text_field($this->options['loops_api_key']) : ''; $mailing_list_id = isset($this->options['loops_mailing_list']) ? sanitize_text_field($this->options['loops_mailing_list']) : ''; // Check for missing API key or mailing list ID if (empty($api_key) || empty($mailing_list_id)) { //error_log('Loops API key or mailing list ID is missing.'); return; } $data = array( 'email' => $email, 'subscribed' => true, 'source' => 'MxChat AI Chatbot', 'mailingLists' => array($mailing_list_id => true), ); $url = 'https://app.loops.so/api/v1/contacts/create'; $args = array( 'body' => wp_json_encode($data), 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', ), 'method' => 'POST', 'timeout' => 45, ); $response = wp_remote_post($url, $args); // Handle errors in the API request if (is_wp_error($response)) { //error_log('Error adding email to Loops: ' . $response->get_error_message()); return; } // Check for non-200 HTTP responses $response_code = wp_remote_retrieve_response_code($response); if ($response_code != 200) { $response_body = wp_remote_retrieve_body($response); //error_log('Loops API responded with code ' . $response_code . ': ' . $response_body); } } public function mxchat_handle_pdf_discussion($message, $user_id, $session_id) { // Get the maximum number of pages allowed from admin settings $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69; // Default to 69 // Retrieve options for dynamic texts $trigger_text = $this->options['pdf_intent_trigger_text'] ?? "Please provide the URL to the PDF you'd like to discuss."; $success_text = $this->options['pdf_intent_success_text'] ?? "I've processed the PDF. What questions do you have about it?"; $error_text = $this->options['pdf_intent_error_text'] ?? "Sorry, I couldn't process the PDF. Please ensure it's a valid file."; // Check if we're waiting for a URL $waiting_for_url = get_transient('mxchat_waiting_for_pdf_url_' . $session_id); // Check if we already have a PDF URL stored for this session $pdf_url = get_transient('mxchat_pdf_url_' . $session_id); // Always process a new URL if detected in the current message if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) { $new_pdf_url = $matches[0]; // Validate HTTPS if (!wp_http_validate_url($new_pdf_url) || parse_url($new_pdf_url, PHP_URL_SCHEME) !== 'https') { //error_log("Invalid PDF URL: $new_pdf_url"); $this->fallbackResponse['text'] = $trigger_text; return; } // Reset previous transients if a new URL is provided if ($pdf_url !== $new_pdf_url) { //error_log("New PDF URL detected. Resetting previous transients for session $session_id."); delete_transient('mxchat_pdf_url_' . $session_id); delete_transient('mxchat_pdf_embeddings_' . $session_id); delete_transient('mxchat_include_pdf_in_context_' . $session_id); // Store the new PDF URL set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS); // Fetch and process the new PDF $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages); // Ensure both arguments are passed if ($embeddings === 'too_many_pages') { //error_log("PDF exceeds maximum allowed pages ($max_pages) for URL: $new_pdf_url"); $error_text = sprintf( $this->options['pdf_intent_error_text'] ?? "The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", $max_pages ); $this->fallbackResponse['text'] = $error_text; delete_transient('mxchat_pdf_url_' . $session_id); } elseif ($embeddings) { //error_log("PDF processed successfully for URL: $new_pdf_url"); set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS); set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS); $this->fallbackResponse['text'] = $success_text; } else { //error_log("Failed to process PDF for URL: $new_pdf_url"); $this->fallbackResponse['text'] = $error_text; delete_transient('mxchat_pdf_url_' . $session_id); } return; } } if (!$pdf_url) { //error_log("No PDF URL provided or stored for session $session_id."); set_transient('mxchat_waiting_for_pdf_url_' . $session_id, true, HOUR_IN_SECONDS); $this->fallbackResponse['text'] = $trigger_text; return; } // Retrieve stored embeddings or fetch if missing $embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id); if (!$embeddings) { //error_log("No embeddings found for PDF URL: $pdf_url. Attempting to process again."); $embeddings = $this->fetch_and_split_pdf_pages($pdf_url, $max_pages); // Ensure both arguments are passed if ($embeddings === 'too_many_pages') { //error_log("PDF exceeds maximum allowed pages ($max_pages) for URL: $pdf_url"); $this->fallbackResponse['text'] = "The provided PDF exceeds the maximum allowed limit of {$max_pages} pages. Please provide a smaller document."; delete_transient('mxchat_pdf_url_' . $session_id); } elseif ($embeddings) { //error_log("PDF processed successfully for URL: $pdf_url"); set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS); set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS); $this->fallbackResponse['text'] = $success_text; } else { //error_log("Failed to process PDF for URL: $pdf_url"); $this->fallbackResponse['text'] = $error_text; delete_transient('mxchat_pdf_url_' . $session_id); } } else { //error_log("Using stored embeddings for session $session_id."); set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS); $this->fallbackResponse['text'] = ''; // Proceed without additional message } } private function fetch_and_split_pdf_pages($pdf_url, $max_pages) { $upload_dir = wp_upload_dir(); $local_pdf_path = $upload_dir['path'] . '/' . basename(parse_url($pdf_url, PHP_URL_PATH)); $response = wp_remote_get($pdf_url, ['timeout' => 60]); if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { //error_log("Failed to download PDF. Error: " . print_r($response, true)); return false; } file_put_contents($local_pdf_path, wp_remote_retrieve_body($response)); try { $parser = new \Smalot\PdfParser\Parser(); $pdf = $parser->parseFile($local_pdf_path); $pages = $pdf->getPages(); if (count($pages) > $max_pages) { unlink($local_pdf_path); return 'too_many_pages'; } $embeddings = []; foreach ($pages as $page_number => $page) { $text = $page->getText(); $embedding = $this->mxchat_generate_embedding("Page " . ($page_number + 1) . ": " . $text, $this->options['api_key']); if ($embedding) { $embeddings[] = [ 'page_number' => $page_number + 1, 'embedding' => $embedding, 'text' => $text, ]; } else { //error_log("Failed to generate embedding for page " . ($page_number + 1)); } } unlink($local_pdf_path); return $embeddings; } catch (\Exception $e) { unlink($local_pdf_path); //error_log("Error parsing or processing PDF: " . $e->getMessage()); return false; } } private function find_relevant_pdf_pages($query_embedding, $embeddings) { //error_log("find_relevant_pdf_pages called."); $most_relevant = null; $highest_similarity = -INF; foreach ($embeddings as $page_data) { $similarity = $this->mxchat_calculate_cosine_similarity($query_embedding, $page_data['embedding']); if ($similarity > $highest_similarity) { $highest_similarity = $similarity; $most_relevant = $page_data['page_number']; } } if (!is_null($most_relevant)) { $page_numbers = range(max(1, $most_relevant - 1), min(count($embeddings), $most_relevant + 1)); return array_filter($embeddings, function ($page) use ($page_numbers) { return in_array($page['page_number'], $page_numbers); }); } return []; } /** * Calls the AI API with the provided prompt. * * @param string $prompt The prompt to send to the AI. * @return array An array containing the AI's response text. */ private function mxchat_call_ai_api( $prompt ) { //error_log( 'Calling AI API with the provided prompt.' ); $api_key = $this->options['api_key']; if ( empty( $api_key ) ) { //error_log( 'API key is not set.' ); return [ 'text' => 'API key is not set.' ]; } $url = 'https://api.openai.com/v1/chat/completions'; $messages = [ [ 'role' => 'system', '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.', ], [ 'role' => 'user', 'content' => $prompt, ], ]; $args = [ 'headers' => [ 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', ], 'body' => wp_json_encode( [ 'model' => 'gpt-4o', 'messages' => $messages, 'temperature' => 0.7, ] ), 'timeout' => 10, 'method' => 'POST', ]; $response = wp_remote_post( $url, $args ); if ( is_wp_error( $response ) ) { //error_log( 'Error communicating with AI API: ' . $response->get_error_message() ); return [ 'text' => 'Error communicating with AI API.' ]; } $body = wp_remote_retrieve_body( $response ); //error_log( 'API response body: ' . $body ); $decoded_body = json_decode( $body, true ); if ( isset( $decoded_body['choices'][0]['message']['content'] ) ) { return [ 'text' => $decoded_body['choices'][0]['message']['content'] ]; } else { //error_log( 'Unexpected API response format: ' . wp_json_encode( $decoded_body ) ); return [ 'text' => 'No response received from AI.' ]; } } /** * Fetches the AI response for the given prompt. * * @param string $prompt The prompt to send to the AI. * @return string|null The AI's response text or null if not available. */ private function mxchat_fetch_ai_response( $prompt ) { $response = $this->mxchat_call_ai_api( $prompt ); return isset( $response['text'] ) ? $response['text'] : null; } /** * Generates the AI prompt for product recommendations. * * @param array $recommendations An array of product recommendations. * @return string The generated AI prompt. */ private function mxchat_generate_ai_recommendation_prompt( $recommendations ) { //error_log( 'Generating AI recommendation prompt.' ); $recommendation_list = ''; foreach ( $recommendations as $index => $rec ) { $number = $index + 1; $name = $rec['name']; $price = $rec['price']; $url = $rec['url']; $image = $rec['image']; $recommendation_list .= "{$number}. Product: {$name} (Price: \${$price})\n"; $recommendation_list .= " [Link]({$url})\n"; $recommendation_list .= " ![Image]({$image})\n\n"; } $prompt = "Based on the following list of products, generate a unique, friendly, and personalized response to a user who asked for a recommendation "; $prompt .= "Then, for each product, provide a brief justification of why it's relevant to the user. "; $prompt .= "Please number your responses to match the product numbers.\n\n"; $prompt .= "Products:\n\n{$recommendation_list}"; $prompt .= "Please ensure that the number of each product matches the order in which the products are listed."; //error_log( 'Generated AI prompt: ' . $prompt ); return $prompt; } /** * Handles product recommendations by generating and formatting the AI response. * * @param string $message The user's message. * @param int $user_id The user's ID. * @param int $session_id The session ID. */ public function mxchat_handle_product_recommendations( $message, $user_id, $session_id ) { try { //error_log( "Starting product recommendations for user: $user_id, session: $session_id." ); $recommendation_data = $this->mxchat_generate_recommendations( $user_id ); //error_log( 'Generated recommendation data: ' . wp_json_encode( $recommendation_data ) ); if ( empty( $recommendation_data['recommendations'] ) ) { //error_log( "No recommendations found for user: $user_id." ); $this->fallbackResponse = [ 'text' => __( "I couldn't find any product recommendations for you right now. Please try again later!", 'mxchat' ), ]; return; } // Remove duplicates and limit to top 4 recommendations $unique_recommendations = []; foreach ( $recommendation_data['recommendations'] as $rec ) { $unique_recommendations[ $rec['url'] ] = $rec; } //error_log( 'Unique recommendations: ' . wp_json_encode( $unique_recommendations ) ); $unique_recommendations = array_slice( $unique_recommendations, 0, 4 ); //error_log( 'Top 4 recommendations: ' . wp_json_encode( $unique_recommendations ) ); $recommendations_summary = []; foreach ( $unique_recommendations as $rec ) { $recommendations_summary[] = [ 'name' => $rec['name'], 'price' => strip_tags( $rec['price'] ), 'url' => $rec['url'], 'image' => $rec['image'], ]; } // Generate AI prompt and fetch response $ai_prompt = $this->mxchat_generate_ai_recommendation_prompt( $recommendations_summary ); $ai_response = $this->mxchat_fetch_ai_response( $ai_prompt ); //error_log( 'AI response received: ' . wp_json_encode( $ai_response ) ); if ( empty( $ai_response ) ) { $this->fallbackResponse = [ 'text' => __( 'An unexpected error occurred while generating recommendations. Please try again later.', 'mxchat' ), ]; return; } // Split the AI's response into lines $ai_lines = preg_split( '/\r\n|\r|\n/', $ai_response ); // Initialize variables $formatted_response = ''; $justifications = []; $current_number = 0; $in_introduction = true; $introduction = ''; // Parse the AI response to separate the introduction and the justifications foreach ( $ai_lines as $line ) { if ( preg_match( '/^\s*(\d+)\.\s*(.*)$/', $line, $matches ) ) { // This line is a numbered justification $current_number = intval( $matches[1] ) - 1; $justifications[ $current_number ] = $matches[2]; $in_introduction = false; } elseif ( $in_introduction ) { // This line is part of the introduction $introduction .= $line . ' '; } else { // This line is a continuation of the current justification if ( isset( $justifications[ $current_number ] ) ) { $justifications[ $current_number ] .= ' ' . $line; } } } // Build the formatted response if ( ! empty( $introduction ) ) { $formatted_response .= esc_html( trim( $introduction ) ) . "

"; } foreach ( $recommendations_summary as $index => $rec ) { $name = esc_html( $rec['name'] ); $price = esc_html( $rec['price'] ); $url = esc_url( $rec['url'] ); $image = esc_url( $rec['image'] ); $formatted_response .= ( $index + 1 ) . ". {$name} - \${$price}
"; $formatted_response .= "Check it out here!
"; $formatted_response .= "\"{$name}\"
"; if ( isset( $justifications[ $index ] ) ) { $formatted_response .= "" . esc_html( trim( $justifications[ $index ] ) ) . "

"; } } $this->fallbackResponse = [ 'text' => $formatted_response, ]; //error_log( 'Final formatted response set.' ); } catch ( Exception $e ) { //error_log( 'Error in mxchat_handle_product_recommendations: ' . $e->getMessage() ); $this->fallbackResponse = [ 'text' => __( 'An unexpected error occurred while generating recommendations. Please try again later.', 'mxchat' ), ]; } } // Inside MxChat_Integrator class private function mxchat_generate_recommendations($user_id) { $recommendations = []; $recommendation_sources = []; $added_product_ids = []; // To track unique products // 1. Recommendations based on order history if (is_user_logged_in() && $user_id) { $order_recommendations = $this->mxchat_get_recommendations_from_order_history($user_id); foreach ($order_recommendations as $product) { if (!in_array($product->get_id(), $added_product_ids)) { $recommendations[] = $product; $added_product_ids[] = $product->get_id(); $recommendation_sources[] = "Order history"; } } } // 2. Recommendations based on cart contents if (WC()->cart && WC()->cart->get_cart_contents_count() > 0) { $cart_recommendations = $this->mxchat_get_recommendations_from_cart(); foreach ($cart_recommendations as $product) { if (!in_array($product->get_id(), $added_product_ids)) { $recommendations[] = $product; $added_product_ids[] = $product->get_id(); $recommendation_sources[] = "Cart contents"; } } } // 3. General recommendations (bestsellers or sale items) $general_recommendations = $this->mxchat_get_general_recommendations(); foreach ($general_recommendations as $product) { if (!in_array($product->get_id(), $added_product_ids)) { $recommendations[] = $product; $added_product_ids[] = $product->get_id(); $recommendation_sources[] = "General recommendations"; } } // Format for output $formatted_recommendations = []; foreach ($recommendations as $product) { $formatted_recommendations[] = [ 'name' => $product->get_name(), 'price' => $product->get_price_html(), 'url' => get_permalink($product->get_id()), 'image' => wp_get_attachment_url($product->get_image_id()), ]; } return [ 'recommendations' => $formatted_recommendations, 'sources' => array_unique($recommendation_sources), ]; } private function mxchat_get_recommendations_from_order_history($user_id) { $args = [ 'customer_id' => $user_id, 'limit' => -1, ]; $orders = wc_get_orders($args); $purchased_products = []; foreach ($orders as $order) { foreach ($order->get_items() as $item) { $purchased_products[] = $item->get_product_id(); } } $related_product_ids = wc_get_related_products($purchased_products, 5); // Get up to 5 related products return wc_get_products(['include' => $related_product_ids]); } private function mxchat_get_recommendations_from_cart() { $cart = WC()->cart->get_cart(); $cart_product_ids = array_map(function ($cart_item) { return $cart_item['product_id']; }, $cart); $related_product_ids = wc_get_related_products($cart_product_ids, 5); // Get up to 5 related products return wc_get_products(['include' => $related_product_ids]); } private function mxchat_get_general_recommendations() { $args = [ 'status' => 'publish', 'limit' => 5, 'orderby' => 'popularity', 'meta_query' => [ 'relation' => 'OR', [ 'key' => '_sale_price', 'compare' => '>', 'value' => 0, ] ], ]; return wc_get_products($args); } private function mxchat_get_user_identifier() { return MxChat_User::mxchat_get_user_identifier(); } private function mxchat_generate_embedding($text, $api_key) { $endpoint = 'https://api.openai.com/v1/embeddings'; $body = wp_json_encode([ 'input' => $text, 'model' => 'text-embedding-ada-002' ]); $args = [ 'body' => $body, 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $api_key, ], 'timeout' => 60, 'redirection' => 5, 'blocking' => true, 'httpversion' => '1.0', 'sslverify' => true, ]; $response = wp_remote_post($endpoint, $args); if (is_wp_error($response)) { return null; } $response_body = json_decode(wp_remote_retrieve_body($response), true); if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) { return $response_body['data'][0]['embedding']; } else { return null; } } private function mxchat_find_relevant_content($user_embedding) { global $wpdb; $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content'; // Define a cache key for embeddings $cache_key = 'mxchat_system_prompt_embeddings'; // Attempt to get the embeddings from the cache $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts'); if ($embeddings === false) { // Cache miss, query the database and cache the results $query = "SELECT id, embedding_vector FROM {$system_prompt_table}"; $embeddings = $wpdb->get_results($query); if ($embeddings === null || empty($embeddings)) { return null; // Return null to handle no embeddings gracefully } // Cache the results if successful wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600); // Cache for 1 hour } $most_relevant_id = null; $highest_similarity = -INF; foreach ($embeddings as $embedding) { $database_embedding = $embedding->embedding_vector ? unserialize($embedding->embedding_vector, ['allowed_classes' => false]) : null; if (is_array($database_embedding) && is_array($user_embedding)) { $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding); if ($similarity > $highest_similarity) { $highest_similarity = $similarity; $most_relevant_id = $embedding->id; } } } if ($most_relevant_id !== null) { // Fetch content with product links return $this->fetch_content_with_product_links($most_relevant_id); } return null; // Return null if no relevant content is found } private function fetch_content_with_product_links($most_relevant_id) { global $wpdb; $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content'; // Fetch the article content and associated product URL $query = $wpdb->prepare("SELECT article_content, source_url FROM {$system_prompt_table} WHERE id = %d", $most_relevant_id); $result = $wpdb->get_row($query); if ($result) { // Append the product link to the content if available $content = $result->article_content; if (!empty($result->source_url)) { $content .= "\n\nFor more details, check out this product: " . esc_url($result->source_url); } return $content; } return null; } private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $conversation_history) { if (!$relevant_content) { return "I'm sorry, I couldn't find relevant information on that topic."; } // Check the selected model $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-3.5-turbo'; // Call the appropriate function based on the selected model if (strpos($selected_model, 'claude') !== false) { return $this->mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content); } elseif ($selected_model === 'grok-beta') { return $this->mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content); } else { return $this->mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content); } } private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) { // Get system prompt instructions from options $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : ''; // Add system prompt to relevant content $content_with_instructions = $system_prompt_instructions . " " . $relevant_content; // Prepend system instructions to the conversation history array_unshift($conversation_history, [ 'role' => 'system', 'content' => "Here are your instructions: " . $content_with_instructions ]); // Ensure consistency: Replace 'bot' role with 'assistant' in conversation history foreach ($conversation_history as &$message) { if ($message['role'] === 'bot') { $message['role'] = 'assistant'; } } // Build the request body $body = json_encode([ 'model' => $selected_model, 'messages' => $conversation_history, 'temperature' => 0.8, 'stream' => false ]); // Set up the API request $args = [ 'body' => $body, 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $api_key, ], 'timeout' => 60, 'redirection' => 5, 'blocking' => true, 'httpversion' => '1.0', 'sslverify' => true, ]; // Make the API request $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args); // Process the response if (is_wp_error($response)) { return "Sorry, there was an error processing your request."; } $response_body = json_decode(wp_remote_retrieve_body($response), true); if (isset($response_body['choices'][0]['message']['content'])) { return trim($response_body['choices'][0]['message']['content']); } else { return "Sorry, I couldn't process that request."; } } private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) { // Get system prompt instructions from options $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : ''; // Add system prompt to relevant content $content_with_instructions = $system_prompt_instructions . " " . $relevant_content; // Prepend system instructions to the conversation history array_unshift($conversation_history, [ 'role' => 'system', 'content' => "Here are your instructions: " . $content_with_instructions ]); // Ensure consistency: Replace 'bot' role with 'assistant' in conversation history foreach ($conversation_history as &$message) { if ($message['role'] === 'bot') { $message['role'] = 'assistant'; } } // Build the request body $body = json_encode([ 'model' => $selected_model, 'messages' => $conversation_history, 'temperature' => 0.8, 'stream' => false ]); // Set up the API request $args = [ 'body' => $body, 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $xai_api_key, ], 'timeout' => 60, 'redirection' => 5, 'blocking' => true, 'httpversion' => '1.0', 'sslverify' => true, ]; // Make the API request $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args); // Process the response if (is_wp_error($response)) { return "Sorry, there was an error processing your request."; } $response_body = json_decode(wp_remote_retrieve_body($response), true); if (isset($response_body['choices'][0]['message']['content'])) { return trim($response_body['choices'][0]['message']['content']); } else { return "Sorry, I couldn't process that request."; } } private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) { // Get system prompt instructions from options for Claude's top-level system parameter $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : ''; // Ensure consistency: Replace 'bot' role with 'assistant' in conversation history foreach ($conversation_history as &$message) { if ($message['role'] === 'bot') { $message['role'] = 'assistant'; } } // Add relevant content as the latest user message in conversation history $conversation_history[] = [ 'role' => 'user', 'content' => $relevant_content ]; // Build the request body with Claude's expected structure, using system instructions as a top-level parameter $body = json_encode([ 'model' => $selected_model, 'max_tokens' => 1000, 'temperature' => 0.8, 'system' => $system_prompt_instructions, // Set the system prompt at the top level as required 'messages' => $conversation_history ]); // Set up the API request with the necessary headers $args = [ 'body' => $body, 'headers' => [ 'Content-Type' => 'application/json', 'x-api-key' => $claude_api_key, 'anthropic-version' => '2023-06-01', ], 'timeout' => 60, 'redirection' => 5, 'blocking' => true, 'httpversion' => '1.0', 'sslverify' => true, ]; // Make the API request $response = wp_remote_post('https://api.anthropic.com/v1/messages', $args); /* // Check for errors and log the response for debugging if (is_wp_error($response)) { error_log("Claude API request error: " . print_r($response->get_error_message(), true)); return "Sorry, there was an error processing your request."; } */ // Decode the response and parse according to the expected Claude response structure $response_body = json_decode(wp_remote_retrieve_body($response), true); //error_log("Claude API response: " . print_r($response_body, true)); // Check if the response has the expected 'content' array with 'text' blocks if (isset($response_body['content'][0]['text'])) { return trim($response_body['content'][0]['text']); } else { return "Sorry, I couldn't process that request."; } } public function mxchat_dismiss_pre_chat_message() { // Get and sanitize the user identifier $user_id = $this->mxchat_get_user_identifier(); $user_id = sanitize_key($user_id); // Set a transient to track that the user has dismissed the pre-chat message $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id; set_transient($transient_key, true, DAY_IN_SECONDS); wp_send_json_success(); } public function mxchat_check_pre_chat_message_status() { // Get and sanitize the user identifier $user_id = $this->mxchat_get_user_identifier(); $user_id = sanitize_key($user_id); // Check if the transient exists (i.e., if the message was dismissed) $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id; $dismissed = get_transient($transient_key); // Log the result to see if it's being set correctly //error_log("Check pre-chat message dismissed for $user_id: " . ($dismissed ? 'Yes' : 'No')); if ($dismissed) { wp_send_json_success(['dismissed' => true]); } else { wp_send_json_success(['dismissed' => false]); } wp_die(); } private function mxchat_calculate_cosine_similarity($vectorA, $vectorB) { if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) { return 0; } $dotProduct = array_sum(array_map(function ($a, $b) { return $a * $b; }, $vectorA, $vectorB)); $normA = sqrt(array_sum(array_map(function ($a) { return $a * $a; }, $vectorA))); $normB = sqrt(array_sum(array_map(function ($b) { return $b * $b; }, $vectorB))); if ($normA == 0 || $normB == 0) { return 0; } return $dotProduct / ($normA * $normB); } public function mxchat_enqueue_scripts_styles() { // Define version numbers for the styles and scripts $chat_style_version = '1.4.6'; // Replace with your actual version $chat_script_version = '1.4.6'; // Replace with your actual version // Enqueue the script wp_enqueue_script( 'mxchat-chat-js', plugin_dir_url(__FILE__) . '../js/chat-script.js', array('jquery'), $chat_script_version, true ); // Enqueue the CSS wp_enqueue_style( 'mxchat-chat-css', plugin_dir_url(__FILE__) . '../css/chat-style.css', array(), $chat_style_version ); // Fetch options from the database $this->options = get_option('mxchat_options'); // Prepare settings for JavaScript $style_settings = array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('mxchat_chat_nonce'), 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off', 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.', 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on', 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff', 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121', 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121', 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff', 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121', 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121', 'close_button_color' => $this->options['close_button_color'] ?? '#fff', 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121', 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff', 'icon_color' => $this->options['icon_color'] ?? '#fff', 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121', 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off', 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off', // Example for consistency ); // Pass the settings to the script wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings); } public function mxchat_reset_rate_limits() { global $wpdb; // Define a cache key pattern for rate limits $cache_key_pattern = 'mxchat_chat_limit_%'; // Retrieve all option names matching the pattern // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery $option_names = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'"); // db call ok; no-cache ok // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- db call ok $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'"); // Clear the relevant cache entries foreach ($option_names as $option_name) { wp_cache_delete($option_name, 'options'); } // Optionally, clear a general cache if you have one wp_cache_delete('mxchat_all_chat_limits', 'options'); } private function mxchat_fetch_woocommerce_products() { // Ensure WooCommerce is active if (!class_exists('WooCommerce')) { return []; } $args = array( 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => -1, ); $products = get_posts($args); $product_data = []; foreach ($products as $product) { $product_id = $product->ID; $product_obj = wc_get_product($product_id); $product_data[] = array( 'id' => $product_id, 'name' => $product_obj->get_name(), 'description' => $product_obj->get_description(), 'short_description' => $product_obj->get_short_description(), 'url' => get_permalink($product_id), 'price' => $product_obj->get_regular_price(), 'sale_price' => $product_obj->get_sale_price(), 'stock_status' => $product_obj->get_stock_status(), 'sku' => $product_obj->get_sku(), 'in_stock' => $product_obj->is_in_stock(), 'total_sales' => $product_obj->get_total_sales(), ); } return $product_data; } } ?>