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 REST API routes registration
add_action('rest_api_init', array($this, 'register_routes'));
add_action('wp_ajax_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
add_action('wp_ajax_nopriv_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
add_action('wp_ajax_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
add_action('wp_ajax_nopriv_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
add_action('wp_ajax_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
add_action('wp_ajax_nopriv_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
}
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
$chat_mode = get_option("mxchat_mode_{$session_id}", 'ai'); // Get current chat mode
if (empty($history)) {
// Even if history is empty, return the chat mode
wp_send_json_success([
'conversation' => [],
'chat_mode' => $chat_mode
]);
wp_die();
}
wp_send_json_success([
'conversation' => $history,
'chat_mode' => $chat_mode
]);
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;
}
public function register_routes() {
//error_log('Registering MxChat REST routes');
register_rest_route('mxchat/v1', '/stream', [
'methods' => 'GET',
'callback' => [$this, 'mxchat_stream_events'],
'permission_callback' => [$this, 'verify_chat_session'],
]);
register_rest_route('mxchat/v1', '/agent-response', [
'methods' => 'POST',
'callback' => [$this, 'mxchat_handle_agent_response'],
'permission_callback' => [$this, 'verify_slack_request'],
]);
register_rest_route('mxchat/v1', '/slack-interaction', [
'methods' => 'POST',
'callback' => [$this, 'handle_slack_interaction'],
'permission_callback' => [$this, 'verify_slack_request'],
]);
//error_log('MxChat REST routes registered');
}
/**
* Verify valid chat session
*/
public function verify_chat_session($request) {
$session_id = $request->get_param('session_id');
if (empty($session_id)) {
//error_log('Empty session ID in chat request');
return false;
}
$chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
return $chat_mode === 'agent';
}
/**
* Verify request is coming from Slack.
*
* @param WP_REST_Request $request
* @return bool True if valid, false otherwise.
*/
public function verify_slack_request($request) {
// Get the Slack signing secret from your plugin options
$valid_key = $this->options['live_agent_secret_key'] ?? '';
if (empty($valid_key)) {
//error_log('Slack signing secret not configured');
return false;
}
$timestamp = $request->get_header('X-Slack-Request-Timestamp');
$slack_signature = $request->get_header('X-Slack-Signature');
// Verify timestamp to prevent replay attacks
if (abs(time() - intval($timestamp)) > 300) {
//error_log('Slack request timestamp too old');
return false;
}
// Get raw request body
$request_body = file_get_contents('php://input');
// Create the signature base string
$sig_basestring = "v0:{$timestamp}:{$request_body}";
// Calculate expected signature
$my_signature = 'v0=' . hash_hmac('sha256', $sig_basestring, $valid_key);
// Compare signatures
return hash_equals($my_signature, $slack_signature);
}
public function mxchat_stream_events(WP_REST_Request $request) {
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
$session_id = sanitize_text_field($request->get_param('session_id'));
$last_seen_id = sanitize_text_field($request->get_param('last_seen_id')) ?: '';
if (empty($session_id)) {
echo "event: error\ndata: Missing session_id\n\n";
flush();
exit;
}
$history = get_option("mxchat_history_{$session_id}", []);
// Filter only new messages
$new_messages = array_filter($history, function ($message) use ($last_seen_id) {
return !empty($message['id']) && $message['id'] > $last_seen_id;
});
// Send new messages if available
if (!empty($new_messages)) {
echo "event: newMessages\ndata: " . json_encode(array_values($new_messages)) . "\n\n";
} else {
// Keep the connection alive
echo "event: keepAlive\ndata: {}\n\n";
}
flush();
exit;
}
private function mxchat_save_chat_message($session_id, $role, $message) {
global $wpdb;
// Extract agent name if present in the message
$agent_name = '';
if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
$agent_name = $matches[1];
$message = str_replace("Agent: $agent_name - ", '', $message);
// Store the agent name in session metadata if it's not already set
$session_meta_key = "mxchat_agent_name_{$session_id}";
if (empty(get_option($session_meta_key))) {
update_option($session_meta_key, $agent_name);
}
}
// Fetch the agent name from session metadata if available
$agent_name = $agent_name ?: get_option("mxchat_agent_name_{$session_id}", 'Unknown Agent');
// Define the table name for chat transcripts
$table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
// Generate a unique message ID
$message_id = uniqid();
// Use agent name as the user identifier if set
$user_id = 0;
$user_identifier = $agent_name ?: MxChat_User::mxchat_get_user_identifier();
$user_email = MxChat_User::mxchat_get_user_email();
// Save the message to the session history
$history = get_option("mxchat_history_{$session_id}", []);
$history[] = [
'id' => $message_id,
'role' => $role,
'content' => $message,
'timestamp' => round(microtime(true) * 1000),
'agent_name' => $agent_name, // Add agent name to the message history
];
update_option("mxchat_history_{$session_id}", $history);
// Save the message to the database
$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),
]);
return $message_id;
}
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);
//error_log("User ID: $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']) : '';
//error_log("Session ID: $session_id");
if (empty($session_id)) {
//error_log("Error: Session ID is missing.");
wp_send_json_error('Session ID is missing.');
wp_die();
}
// Validate and sanitize the incoming message
if (empty($_POST['message'])) {
//error_log("Error: No message received.");
wp_send_json_error('No message received.');
wp_die();
}
$message = wp_strip_all_tags($_POST['message'], false);
$message = trim($message);
// Save the user's message
$this->mxchat_save_chat_message($session_id, 'user', $message);
// Check if the message is an email address
if (is_email($message)) {
// Add the email to Loops
$this->add_email_to_loops($message);
// Send success response
$response_message = $this->options['email_capture_response'] ??
'Thank you! Your coupon is on the way!';
wp_send_json([
'success' => true,
'status' => 'email_captured',
'message' => $response_message
]);
wp_die();
}
// Initialize response variables
$this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
$this->productCardHtml = '';
$intent_info = '';
// Check chat mode
$chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
//error_log("Chat Mode: $chat_mode");
// Handle agent mode
if ($chat_mode === 'agent') {
// First, check for switch intent before doing anything else
$intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
// If we matched an intent and it's the switch intent, handle it
if ($intent_matched && !empty($this->fallbackResponse['text'])) {
//error_log("Switch to chatbot intent detected");
// Update chat mode first
update_option("mxchat_mode_{$session_id}", 'ai');
// Clear any existing PDF context to start fresh
$this->clear_pdf_transients($session_id);
// Prepare clean switch response
$response_data = [
'text' => $this->fallbackResponse['text'],
'html' => '',
'session_id' => $session_id,
'chat_mode' => 'ai'
];
// Save the mode switch message
$this->mxchat_save_chat_message($session_id, 'system', 'Switched to AI chat mode');
$this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
// Send response and exit
wp_send_json($response_data);
wp_die();
} elseif (!$intent_matched) {
// No intent matched, handle live agent message
try {
$this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
//error_log("Message sent to agent.");
wp_send_json_success([
'status' => 'waiting_for_agent',
'message' => 'Message sent to live agent.'
]);
} catch (\Exception $e) {
//error_log("Error sending message to agent: " . $e->getMessage());
wp_send_json_error('Failed to send message to agent');
}
wp_die();
}
}
// 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 ($existing_pdf_url !== $new_pdf_url) {
// Clear all PDF-related transients
$this->clear_pdf_transients($session_id);
// Process new PDF
$max_pages = $this->options['pdf_max_pages'] ?? 69;
$embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
if ($embeddings === 'too_many_pages') {
$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;
} elseif ($embeddings) {
// Store new PDF information
set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
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 {
$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;
}
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);
//error_log("Intent Matched: " . ($intent_matched ? "Yes" : "No"));
// Step 3: If intent is matched and handled, respond immediately
if ($intent_matched && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
//error_log("Intent response triggered.");
$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();
}
// If no intent matched or product not found, proceed with AI response
//error_log("No matching intent or fallback. Generating AI response.");
// Step 4: Generate AI response
$conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
$this->mxchat_increment_chat_count();
// 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)) {
//error_log("Failed to generate message embedding for session $session_id");
wp_send_json_error('Error processing your message.');
wp_die();
}
// Build context with both knowledge base and PDF content if available
$context_content = "User asked: '{$message}'\n\n";
// Get relevant content from knowledge base
$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";
}
// Check for and include PDF content if available
$pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
$pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
$relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
if (!empty($relevant_pdf_pages)) {
$context_content .= "Relevant content from PDF:\n";
foreach ($relevant_pdf_pages as $page_data) {
$context_content .= "Page {$page_data['page_number']}: {$page_data['text']}\n";
}
$context_content .= "\n";
}
}
// Generate the response using the full context
$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 5: Save additional content if available
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 6: 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();
}
// Helper function to clear PDF-related transients
private function clear_pdf_transients($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);
delete_transient('mxchat_waiting_for_pdf_url_' . $session_id);
}
// 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;
// Check chat mode
$chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
//error_log("Checking intents for mode: " . $chat_mode);
// Generate the user embedding
$user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
if (!is_array($user_embedding)) {
//error_log("Failed to generate user embedding");
return false;
}
// Fetch intents from the database
$table_name = $wpdb->prefix . 'mxchat_intents';
if ($chat_mode === 'agent') {
// Only fetch the switch intent when in agent mode
$query = $wpdb->prepare(
"SELECT * FROM $table_name WHERE callback_function = %s",
'mxchat_handle_switch_to_chatbot_intent'
);
//error_log("Searching for switch intent with query: " . $query);
$intents = $wpdb->get_results($query);
//error_log("Found " . count($intents) . " switch intents");
} else {
$intents = $wpdb->get_results("SELECT * FROM $table_name");
}
if (empty($intents)) {
//error_log("No intents found in database");
return false;
}
$highest_similarity = -INF;
$matched_intent = null;
foreach ($intents as $intent) {
//error_log("Checking intent: " . $intent->intent_label);
$intent_embedding_serialized = $intent->embedding_vector;
$intent_embedding = $intent_embedding_serialized
? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
: null;
if (!is_array($intent_embedding)) {
//error_log("Invalid embedding for intent: " . $intent->intent_label);
continue;
}
$similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
$intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
//error_log("Similarity for " . $intent->intent_label . ": " . $similarity . " (threshold: " . $intent_threshold . ")");
if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
$highest_similarity = $similarity;
$matched_intent = $intent;
//error_log("New best match: " . $intent->intent_label . " with similarity " . $similarity);
}
}
if ($matched_intent && method_exists($this, $matched_intent->callback_function)) {
//error_log("Calling callback function: " . $matched_intent->callback_function);
call_user_func([$this, $matched_intent->callback_function], $message, $user_id, $session_id);
return true;
}
//error_log("No matching intent found");
return false;
}
//verified good
public function mxchat_handle_order_history($message, $user_id, $session_id) {
if (!class_exists('WooCommerce')) {
$this->fallbackResponse['text'] = "I can't access order information right now. The order system seems to be unavailable.";
return true;
}
$orderDetails = MxChat_WooCommerce::mxchat_fetch_user_orders_details('all');
if (empty($orderDetails)) {
$this->fallbackResponse['text'] = "I don't see any orders associated with your account. Are you logged in?";
return true;
}
// Generate AI prompt with context
$prompt = "User asked about their orders: '{$message}'\n\n";
$prompt .= "Order information:\n";
foreach ($orderDetails as $order) {
$items_list = array_map(function($item) {
return "{$item['name']} ({$item['quantity']})";
}, $order['items']);
$prompt .= "Order #{$order['order_id']}: {$order['formatted_total']} on {$order['date']}\n";
$prompt .= "Items: " . implode(', ', $items_list) . "\n";
}
$prompt .= "\nProvide a natural, conversational response focusing on the specific information the user asked about. ";
$prompt .= "If they ask about a specific order or detail, provide just that information. ";
$prompt .= "If they ask about license keys or sensitive information, inform them to check their email or contact support.";
// Get AI response
$ai_response = $this->mxchat_call_ai_api($prompt);
$this->fallbackResponse['text'] = $ai_response['text'];
return true;
}
//verified good
public function mxchat_handle_product_inquiry($message, $user_id, $session_id) {
// Attempt to extract product ID from the message
$product_id = MxChat_WooCommerce::mxchat_extract_product_id_from_message($message);
// Use last discussed product if no product ID is found
if (!$product_id) {
$product_id = get_transient('mxchat_last_discussed_product_' . $user_id);
}
// Handle specific product inquiries
if ($product_id && class_exists('WooCommerce')) {
$product = wc_get_product($product_id);
if ($product) {
// Prepare product details
$product_name = esc_html($product->get_name());
$product_price = $product->get_price_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);
// Get product description
$product_description = $product->get_description() ?: $product->get_short_description();
// Get relevant content based on user query
$user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
$relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
// Build AI prompt with context and product details
$ai_prompt = "You are a knowledgeable product assistant. ";
$ai_prompt .= "Respond to this user query: '{$message}'\n\n";
// Add relevant content if available
if (!empty($relevant_content)) {
$ai_prompt .= "Relevant information from our knowledge base:\n{$relevant_content}\n\n";
}
// Add product details
$ai_prompt .= "Product details:\n";
$ai_prompt .= "Name: {$product_name}\n";
$ai_prompt .= "Price: " . strip_tags($product_price) . "\n";
if ($product_description) {
$ai_prompt .= "Description: {$product_description}\n";
}
// Add instructions for response format
$ai_prompt .= "\nInstructions:\n";
$ai_prompt .= "1. Address the user's specific question or concern about the product\n";
$ai_prompt .= "2. Incorporate relevant information from our knowledge base if provided\n";
$ai_prompt .= "3. Highlight key product features that relate to their query\n";
$ai_prompt .= "4. Include a natural suggestion to check out the product\n";
$ai_prompt .= "5. Keep the response conversational and helpful\n";
// Get AI response
$ai_response = $this->mxchat_call_ai_api($ai_prompt);
// Generate product card HTML
$product_card_html = <<
{$product_name}
{$product_price}
Add to Cart
HTML;
// Save the response and product card
$this->productCardHtml = $product_card_html;
$this->fallbackResponse = [
'text' => $ai_response['text'],
'html' => $this->productCardHtml,
];
// Save the last discussed product
set_transient('mxchat_last_discussed_product_' . $user_id, $product_id, HOUR_IN_SECONDS);
return true;
}
}
// If no product found, skip intent handling
return false;
}
//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 = ' ';
$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: \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: \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 = '';
foreach ($body['results'] as $image) {
$image_url = isset($image['url']) ? esc_url($image['url']) : '';
$thumbnail_url = isset($image['thumbnail']['src']) ? esc_url($image['thumbnail']['src']) : '';
$title = isset($image['title']) ? esc_html($image['title']) : __('Image', 'mxchat');
if ($image_url && $thumbnail_url) {
$html_output .= '
';
}
}
$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);
}
}
public function mxchat_handle_add_to_cart_intent($message, $user_id, $session_id) {
if (!class_exists('WooCommerce')) {
$this->fallbackResponse['text'] = "I apologize, but the shopping cart feature isn't available at the moment.";
return true;
}
$sanitized_user_id = sanitize_key($user_id);
$last_product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
if (!$last_product_id) {
$this->fallbackResponse['text'] = "I couldn't find which product you'd like to add. Could you mention the product name again?";
return true;
}
$product = wc_get_product($last_product_id);
if (!$product) {
$this->fallbackResponse['text'] = "I'm sorry, but I couldn't find that product. Could you try again?";
return true;
}
$added = WC()->cart->add_to_cart($last_product_id);
if ($added) {
$product_name = esc_html($product->get_name());
$cart_url = wc_get_cart_url();
$this->fallbackResponse['text'] = "Great! I've added '{$product_name}' to your cart. You can view your cart anytime by asking me to show it, or proceed to checkout when you're ready.";
} else {
$this->fallbackResponse['text'] = "I couldn't add the product to your cart. Please try again or let me know if you need help.";
}
return true;
}
public function mxchat_handle_checkout_intent($message, $user_id, $session_id) {
if (!class_exists('WooCommerce')) {
$this->fallbackResponse['text'] = "I apologize, but the checkout feature isn't available at the moment.";
return true;
}
// Check if cart has items
if (WC()->cart->is_empty()) {
$this->fallbackResponse['text'] = "Your cart is empty at the moment. Would you like to see our products?";
return true;
}
// Get cart summary
$cart_count = WC()->cart->get_cart_contents_count();
$cart_total = WC()->cart->get_total();
// Get and validate checkout URL
$checkout_url = wc_get_checkout_url();
if (!$checkout_url) {
$this->fallbackResponse['text'] = "I'm having trouble accessing the checkout page. Please try again in a moment.";
return true;
}
wp_send_json([
'text' => sprintf(
"You have %d item%s in your cart totaling %s. I'll redirect you to checkout now.",
$cart_count,
$cart_count > 1 ? 's' : '',
strip_tags($cart_total)
),
'redirect_url' => esc_url_raw($checkout_url)
]);
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_source, $max_pages) {
$upload_dir = wp_upload_dir();
$temp_file = null;
try {
// Handle URL vs local file
if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
// Validate and download the file from URL
$temp_file = wp_tempnam($pdf_source); // Safe temporary file name
$response = wp_remote_get($pdf_source, ['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($temp_file, wp_remote_retrieve_body($response));
// Validate that the downloaded file is a PDF
$mime_type = mime_content_type($temp_file);
if ($mime_type !== 'application/pdf') {
//error_log("Invalid MIME type detected for PDF: $mime_type");
unlink($temp_file);
return false;
}
} else {
// For local files, use the provided path directly
$temp_file = $pdf_source;
}
// Parse and process the PDF
$parser = new \Smalot\PdfParser\Parser();
$pdf = $parser->parseFile($temp_file);
$pages = $pdf->getPages();
if (count($pages) > $max_pages) {
//error_log("PDF exceeds the maximum allowed pages: " . count($pages));
if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
unlink($temp_file);
}
return 'too_many_pages';
}
$embeddings = [];
foreach ($pages as $page_number => $page) {
$text = $page->getText();
// Ensure text is non-empty before generating embeddings
if (empty(trim($text))) {
//error_log("Skipping empty page: " . ($page_number + 1));
continue;
}
$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));
}
}
// Clean up downloaded file if it was from URL
if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
unlink($temp_file);
}
return $embeddings;
} catch (\Exception $e) {
// error_log("Error parsing or processing PDF: " . $e->getMessage());
// Cleanup in case of exception
if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
unlink($temp_file);
}
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 [];
}
// Add this to your class
public function handle_pdf_upload() {
check_ajax_referer('mxchat_chat_nonce', 'nonce');
if (!isset($_FILES['pdf_file']) || !isset($_POST['session_id'])) {
wp_send_json_error('Missing required parameters.');
return;
}
$file = $_FILES['pdf_file'];
$session_id = sanitize_text_field($_POST['session_id']);
$original_filename = sanitize_text_field($file['name']);
$file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
if ($file_type['type'] !== 'application/pdf') {
wp_send_json_error('Invalid file type. Only PDF files are allowed.');
return;
}
$upload_dir = wp_upload_dir();
$pdf_filename = 'mxchat_' . $session_id . '_' . time() . '.pdf';
$pdf_path = $upload_dir['path'] . '/' . $pdf_filename;
if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
wp_send_json_error('Failed to upload file.');
return;
}
$this->clear_pdf_transients($session_id);
$max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
$embeddings = $this->fetch_and_split_pdf_pages($pdf_path, $max_pages);
if ($embeddings === 'too_many_pages') {
unlink($pdf_path);
$error_message = 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
);
wp_send_json_error($error_message);
return;
}
if ($embeddings === false || empty($embeddings)) {
unlink($pdf_path);
$error_message = $this->options['pdf_intent_error_text'] ??
'The uploaded PDF appears to be empty or contains unsupported content.';
wp_send_json_error($error_message);
return;
}
if (!empty($embeddings)) {
set_transient('mxchat_pdf_url_' . $session_id, $pdf_path, HOUR_IN_SECONDS);
set_transient('mxchat_pdf_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
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_message = $this->options['pdf_intent_success_text'] ??
"I've processed the PDF. What questions do you have about it?";
wp_send_json_success([
'message' => $success_message,
'filename' => $original_filename
]);
return;
}
unlink($pdf_path);
$error_message = $this->options['pdf_intent_error_text'] ??
'Sorry, I couldn\'t process the PDF. Please ensure it\'s a valid file.';
wp_send_json_error($error_message);
return;
}
public function handle_pdf_remove() {
check_ajax_referer('mxchat_chat_nonce', 'nonce');
if (empty($_POST['session_id'])) {
wp_send_json_error('Session ID missing.');
wp_die();
}
$session_id = sanitize_text_field($_POST['session_id']);
$pdf_path = get_transient('mxchat_pdf_url_' . $session_id);
if ($pdf_path && file_exists($pdf_path)) {
unlink($pdf_path);
}
$this->clear_pdf_transients($session_id);
wp_send_json_success([
'message' => 'PDF removed successfully.'
]);
wp_die();
}
/**
* 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 .= " \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 .= " ";
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);
}
function mxchat_fetch_new_messages() {
$session_id = sanitize_text_field($_POST['session_id']);
$last_seen_id = sanitize_text_field($_POST['last_seen_id']);
$persistence_enabled = $_POST['persistence_enabled'] === 'true';
$initial_timestamp = isset($_POST['initial_timestamp']) ? intval($_POST['initial_timestamp']) : 0;
if (empty($session_id)) {
//error_log('Fetch new messages error: Session ID missing.');
wp_send_json_error(['message' => 'Session ID missing.']);
wp_die();
}
$history = get_option("mxchat_history_{$session_id}", []);
$new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
// If persistence is enabled, show all new messages
if ($persistence_enabled) {
return !empty($message['id']) &&
strcmp($message['id'], $last_seen_id) > 0 &&
$message['role'] === 'agent';
}
// If persistence is disabled, only show messages after initial timestamp
return !empty($message['id']) &&
$message['role'] === 'agent' &&
$message['timestamp'] > $initial_timestamp;
});
//error_log("New agent messages fetched for session $session_id. Last seen ID: $last_seen_id");
wp_send_json_success([
'new_messages' => array_values($new_messages)
]);
wp_die();
}
public function mxchat_live_agent_handover($message, $user_id, $session_id) {
// First check if live agents are available
$live_agent_available = $this->options['live_agent_status'] ?? 'offline';
if ($live_agent_available !== 'online') {
$away_message = $this->options['live_agent_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
$this->fallbackResponse = [
'text' => $away_message,
'html' => '',
'images' => [],
'chat_mode' => 'ai'
];
//error_log('Live agent handover attempted but agents are offline');
wp_send_json([
'text' => $away_message,
'html' => '',
'chat_mode' => 'ai',
'session_id' => $session_id
]);
wp_die();
}
$slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
//error_log('Slack Webhook URL retrieved: ' . $slack_webhook_url);
if (empty($slack_webhook_url)) {
//error_log('Slack Webhook URL is not configured.');
return false;
}
update_option("mxchat_mode_{$session_id}", 'agent');
$webhook_data = [
'blocks' => [
[
'type' => 'header',
'text' => [
'type' => 'plain_text',
'text' => '🔔 New Live Agent Request',
'emoji' => true
]
],
[
'type' => 'section',
'fields' => [
[
'type' => 'mrkdwn',
'text' => "*User ID:*\n`$user_id`"
],
[
'type' => 'mrkdwn',
'text' => "*Session ID:*\n`$session_id`"
]
]
],
[
'type' => 'section',
'text' => [
'type' => 'mrkdwn',
'text' => "*Initial Message:*\n$message"
]
],
[
'type' => 'actions',
'elements' => [
[
'type' => 'button',
'text' => [
'type' => 'plain_text',
'text' => '✍️ Reply',
'emoji' => true
],
'value' => $session_id,
'action_id' => 'reply_to_user',
'style' => 'primary'
]
]
]
]
];
$response = wp_remote_post($slack_webhook_url, [
'body' => json_encode($webhook_data),
'headers' => [
'Content-Type' => 'application/json',
],
]);
if (is_wp_error($response)) {
//error_log('Error sending live agent handover: ' . $response->get_error_message());
return false;
}
//error_log('Live agent handover triggered successfully.');
$success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
$this->mxchat_save_chat_message($session_id, 'bot', $success_message);
$this->fallbackResponse = [
'text' => $success_message,
'html' => '',
'images' => [],
'chat_mode' => 'agent'
];
wp_send_json([
'success' => true,
'text' => $success_message,
'html' => '',
'chat_mode' => 'agent',
'session_id' => $session_id,
'fallbackResponse' => $this->fallbackResponse
]);
wp_die();
}
public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
$slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
if (empty($slack_webhook_url)) {
//error_log('Slack Webhook URL is not configured.');
return false;
}
$webhook_data = [
'blocks' => [
[
'type' => 'header',
'text' => [
'type' => 'plain_text',
'text' => '📩 New Chat Message',
'emoji' => true
]
],
[
'type' => 'section',
'fields' => [
[
'type' => 'mrkdwn',
'text' => "*User ID:*\n`$user_id`"
],
[
'type' => 'mrkdwn',
'text' => "*Session ID:*\n`$session_id`"
]
]
],
[
'type' => 'section',
'text' => [
'type' => 'mrkdwn',
'text' => "*Message:*\n$message"
]
],
[
'type' => 'actions',
'elements' => [
[
'type' => 'button',
'text' => [
'type' => 'plain_text',
'text' => '✍️ Reply',
'emoji' => true
],
'value' => $session_id,
'action_id' => 'reply_to_user',
'style' => 'primary'
]
]
]
]
];
$response = wp_remote_post($slack_webhook_url, [
'body' => json_encode($webhook_data),
'headers' => [
'Content-Type' => 'application/json',
],
]);
if (is_wp_error($response)) {
//error_log('Error sending message to Slack: ' . $response->get_error_message());
return false;
}
//error_log('Message sent to Slack successfully.');
return true;
}
public function handle_slack_interaction(WP_REST_Request $request) {
//error_log('Received Slack interaction');
$payload = json_decode($request->get_param('payload'), true);
//error_log('Payload: ' . print_r($payload, true));
// Handle button click
if ($payload['type'] === 'block_actions' && $payload['actions'][0]['action_id'] === 'reply_to_user') {
$session_id = $payload['actions'][0]['value'];
$trigger_id = $payload['trigger_id'];
// Get Bot Token from settings
$slack_token = $this->options['live_agent_bot_token'] ?? '';
if (empty($slack_token)) {
//error_log('Slack Bot Token not configured');
return new WP_REST_Response(['error' => 'Bot token not configured'], 400);
}
$response = wp_remote_post('https://slack.com/api/views.open', [
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $slack_token
],
'body' => json_encode([
'trigger_id' => $trigger_id,
'view' => [
'type' => 'modal',
'callback_id' => 'reply_modal',
'title' => [
'type' => 'plain_text',
'text' => 'Reply to User'
],
'submit' => [
'type' => 'plain_text',
'text' => 'Send'
],
'close' => [
'type' => 'plain_text',
'text' => 'Cancel'
],
'blocks' => [
[
'type' => 'input',
'block_id' => 'reply_block',
'label' => [
'type' => 'plain_text',
'text' => "Reply to session: $session_id"
],
'element' => [
'type' => 'plain_text_input',
'action_id' => 'message',
'multiline' => true,
'placeholder' => [
'type' => 'plain_text',
'text' => 'Type your message here...'
]
]
]
],
'private_metadata' => $session_id
]
])
]);
//error_log('Views.open response: ' . print_r($response, true));
// Return immediate acknowledgment
return new WP_REST_Response(['ok' => true]);
}
// Handle modal submission
if ($payload['type'] === 'view_submission') {
$session_id = $payload['view']['private_metadata'];
$message = $payload['view']['state']['values']['reply_block']['message']['value'];
// Save the message
$this->mxchat_save_chat_message($session_id, 'agent', $message);
return new WP_REST_Response([
'response_action' => 'clear'
]);
}
// Default acknowledgment
return new WP_REST_Response(['ok' => true]);
}
public function mxchat_handle_agent_response(WP_REST_Request $request) {
//error_log('Received agent response request');
//error_log('Request data: ' . print_r($request->get_params(), true));
// error_log('Raw body: ' . file_get_contents('php://input'));
// Get the data from Slack's slash command format
$command_text = $request->get_param('text');
// error_log('Command text: ' . $command_text);
if (empty($command_text)) {
error_log('Agent response error: No command text received');
return new WP_REST_Response([
'error' => 'Command text is required. Format: /reply session_id message'
], 400);
}
// Split the command text into session_id and message
$parts = explode(' ', $command_text, 2);
if (count($parts) !== 2) {
//error_log('Agent response error: Invalid command format');
return new WP_REST_Response([
'error' => 'Invalid format. Use: /reply session_id message'
], 400);
}
$session_id = sanitize_text_field($parts[0]);
$message = sanitize_text_field($parts[1]);
//error_log("Processing agent response - Session ID: $session_id, Message: $message");
// Save the message
$message_id = $this->mxchat_save_chat_message($session_id, 'agent', $message);
if (!$message_id) {
// error_log('Failed to save agent message');
return new WP_REST_Response([
'error' => 'Failed to save message'
], 500);
}
// Return success response in Slack's expected format
return new WP_REST_Response([
'response_type' => 'in_channel',
'text' => "Message sent successfully to session $session_id"
], 200);
}
public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
//error_log("Switching back to chatbot mode via intent.");
// Just update mode to AI
update_option("mxchat_mode_{$session_id}", 'ai');
// Initialize states
$this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
$this->productCardHtml = '';
// Set the response message
$this->fallbackResponse['text'] = 'You are now chatting with the AI chatbot.';
return true; // Intent was handled
}
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';
$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 ''; // Return an empty string for compatibility
}
wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
}
// Initialize variables to track the two most relevant results
$most_relevant_id = null;
$second_most_relevant_id = null;
$highest_similarity = -INF;
$second_highest_similarity = -INF;
// Iterate through all embeddings
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) {
// Shift the current highest to second highest
$second_highest_similarity = $highest_similarity;
$second_most_relevant_id = $most_relevant_id;
// Update the new highest
$highest_similarity = $similarity;
$most_relevant_id = $embedding->id;
} elseif ($similarity > $second_highest_similarity) {
// Update the second highest if applicable
$second_highest_similarity = $similarity;
$second_most_relevant_id = $embedding->id;
}
}
}
// Retrieve the similarity threshold and convert it to decimal
$similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100; // Convert to decimal
if ($highest_similarity >= $similarity_threshold) {
// Fetch content for the most relevant match
$content = $this->fetch_content_with_product_links($most_relevant_id);
// Fetch content for the second most relevant match (if applicable)
$second_content = '';
if ($second_highest_similarity >= $similarity_threshold && $second_most_relevant_id !== null) {
$second_content = $this->fetch_content_with_product_links($second_most_relevant_id);
}
// If the most relevant content is PDF-related, handle surrounding content
if (strpos($content, '{"document_type":"pdf"') !== false) {
$surrounding_content = $wpdb->get_results($wpdb->prepare(
"SELECT article_content FROM {$system_prompt_table}
WHERE id IN (
(SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
(SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
)",
$most_relevant_id,
$most_relevant_id
));
$combined_content = '';
// Add previous content if exists
if (!empty($surrounding_content[0])) {
$combined_content .= $surrounding_content[0]->article_content . "\n\n";
}
// Add main content
$combined_content .= $content;
// Add next content if exists
if (!empty($surrounding_content[1])) {
$combined_content .= "\n\n" . $surrounding_content[1]->article_content;
}
return $combined_content;
}
// Combine most relevant and second most relevant content if available
if (!empty($second_content)) {
return $content . "\n\n---\n\n" . $second_content; // Separate the two contents with a delimiter
}
return $content; // Return only the most relevant content if no second content is found
}
return ''; // Return an empty string for compatibility
}
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' and 'agent' roles with supported values
foreach ($conversation_history as &$message) {
if ($message['role'] === 'bot') {
$message['role'] = 'assistant';
} elseif ($message['role'] === 'agent') {
// Tag the message as coming from a live agent
$message['role'] = 'assistant';
if (!isset($message['metadata'])) {
$message['metadata'] = ['source' => 'live_agent'];
}
}
// Ensure all roles are valid
if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
$message['role'] = 'user'; // Default to 'user'
}
}
// Build the request body
$body = json_encode([
'model' => $selected_model,
'messages' => $conversation_history,
'temperature' => 0.8,
'stream' => false
]);
//error_log("OpenAI API Request Body: " . $body);
// 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);
// Log the response or error
if (is_wp_error($response)) {
//error_log("OpenAI API Error: " . $response->get_error_message());
return "Sorry, there was an error processing your request.";
}
// Log raw response for debugging
//error_log("OpenAI API Raw Response: " . print_r($response, true));
$response_body = wp_remote_retrieve_body($response);
$decoded_response = json_decode($response_body, true);
// Log the decoded response body
//error_log("OpenAI API Decoded Response: " . print_r($decoded_response, true));
if (isset($decoded_response['choices'][0]['message']['content'])) {
return trim($decoded_response['choices'][0]['message']['content']);
} else {
// Log an error if the expected response format is missing
//error_log("OpenAI API Response Format Error: Expected 'choices[0][message][content]' not found.");
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' and 'agent' roles with supported values
foreach ($conversation_history as &$message) {
if ($message['role'] === 'bot') {
$message['role'] = 'assistant';
} elseif ($message['role'] === 'agent') {
// Tag the message as coming from a live agent
$message['role'] = 'assistant';
if (!isset($message['metadata'])) {
$message['metadata'] = ['source' => 'live_agent'];
}
}
// Ensure all roles are valid
if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
$message['role'] = 'user'; // Default to 'user'
}
}
// 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' and 'agent' roles with supported values
foreach ($conversation_history as &$message) {
if ($message['role'] === 'bot') {
$message['role'] = 'assistant';
} elseif ($message['role'] === 'agent') {
// Tag the message as coming from a live agent
$message['role'] = 'assistant';
if (!isset($message['metadata'])) {
$message['metadata'] = ['source' => 'live_agent'];
}
}
// Ensure all roles are valid
if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
$message['role'] = 'user'; // Default to 'user'
}
}
// 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.5.4'; // Replace with your actual version
$chat_script_version = '1.5.4'; // 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
'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
);
// 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;
}
}
?>