'POST', 'callback' => [$this, 'handle_chat'], 'permission_callback' => [$this, 'verify_rest_nonce'] ]); } public function verify_rest_nonce(\WP_REST_Request $request) { $nonce = $request->get_header('X-WP-Nonce'); if ($nonce && wp_verify_nonce($nonce, 'wp_rest')) { return true; } return new \WP_Error('rest_forbidden', __('Access Denied: Invalid security token.', 'ai-conversational-commerce-assistant-for-woocommerce'), ['status' => 403]); } public function get_product_card() { check_ajax_referer('shoponix_chat_nonce', 'nonce'); $product_id = isset($_POST['product_id']) ? intval(wp_unslash($_POST['product_id'])) : 0; if (!$product_id) wp_send_json_error(); $product = wc_get_product($product_id); if (!$product) wp_send_json_error(); $type = $product->get_type(); $name = $product->get_name(); ob_start(); ?>
get_image([60, 60], ['style' => 'border-radius: 8px; object-fit: cover; width: 60px; height: 60px; flex-shrink: 0;'])); ?>
get_price_html()); ?>
get_average_rating() > 0): $rating = $product->get_average_rating(); $review_count = $product->get_review_count(); $stars = str_repeat('⭐', round($rating)); ?>
()
View get_wishlist_url(); echo '❤️'; } ?>
$html]); } public function handle_chat(\WP_REST_Request $request) { // Manually authenticate user from logged-in cookie in stateless REST API context if (function_exists('wp_validate_auth_cookie') && !is_user_logged_in()) { $user_id = wp_validate_auth_cookie('', 'logged_in'); if ($user_id) { wp_set_current_user($user_id); } } // Force WooCommerce to load session and cart in stateless REST API context if (function_exists('WC') && WC()) { if (method_exists(WC(), 'frontend_includes')) { WC()->frontend_includes(); } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound $session_class = apply_filters('woocommerce_session_handler', 'WC_Session_Handler'); if (class_exists($session_class)) { WC()->session = new $session_class(); WC()->session->init(); } if (class_exists('WC_Customer')) { WC()->customer = new \WC_Customer(get_current_user_id(), true); } if (class_exists('WC_Cart')) { WC()->cart = new \WC_Cart(); } } // Rate limiting $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '127.0.0.1'; $rate_key = 'shoponix_rate_' . md5($ip); $requests_count = (int) get_transient($rate_key); if ($requests_count > 20) { return new \WP_Error('rate_limit', 'Too many requests. Please slow down.', ['status' => 429]); } set_transient($rate_key, $requests_count + 1, MINUTE_IN_SECONDS); $messages = $request->get_param('messages'); $language = sanitize_text_field($request->get_param('language')); $viewed_products = $request->get_param('viewed_products'); if (empty($language)) { $language = 'en'; } if (!is_array($messages) || empty($messages)) { return new \WP_Error('invalid_request', 'Missing messages array', ['status' => 400]); } // Security: Hard cap the history array $messages = array_slice($messages, -10); $api_key_encrypted = get_option('shoponix_mistral_api_key'); if (!$api_key_encrypted) { return new \WP_Error('config_error', 'Mistral API key not configured', ['status' => 500]); } $api_key = \Shoponix\Security\Encryption::decrypt($api_key_encrypted); $last_user_message = ''; foreach (array_reverse($messages) as $msg) { if ($msg['role'] === 'user') { $last_user_message = sanitize_text_field($msg['content']); break; } } if (empty($last_user_message)) { return new \WP_Error('invalid_request', 'No user message found', ['status' => 400]); } // 1. Embed the query $provider = get_option('shoponix_ai_provider', 'mistral'); $embedding = new \Shoponix\AI\UniversalEmbedder($provider, $api_key, get_option('shoponix_embedding_model')); $query_vec = $embedding->embed($last_user_message); if (!$query_vec) { return new \WP_Error('api_error', 'Failed to embed query. Please try again.', ['status' => 500]); } // 2. Find similar products via hybrid search (Semantic Vector + MySQL Keyword Search) $vector_store = new \Shoponix\VectorDB\Store(); $similar = $vector_store->find_similar($query_vec, get_option('shoponix_top_k', 5)); $keyword_results = []; $args = array( 'post_type' => 'product', 'post_status' => 'publish', 's' => $last_user_message, 'posts_per_page' => 3, 'fields' => 'ids', ); $kw_query = new \WP_Query($args); if (!empty($kw_query->posts)) { foreach ($kw_query->posts as $kw_id) { $content = $vector_store->get_product_content($kw_id); if (empty($content)) { $product = wc_get_product($kw_id); if ($product) { $content = "Name: " . $product->get_name() . " | Price: " . $product->get_price() . " | Description: " . wp_strip_all_tags($product->get_short_description()); } } if (!empty($content)) { $keyword_results[] = [ 'product_id' => $kw_id, 'content' => $content ]; } } } // Merge & deduplicate results by product ID $merged = []; $seen = []; // Semantic results first foreach ($similar as $item) { $pid = $item['product_id']; if (!in_array($pid, $seen)) { $seen[] = $pid; $merged[] = [ 'product_id' => $pid, 'content' => $item['content'] ]; } } // Keyword results next foreach ($keyword_results as $item) { $pid = $item['product_id']; if (!in_array($pid, $seen)) { $seen[] = $pid; $merged[] = [ 'product_id' => $pid, 'content' => $item['content'] ]; } } // Slice to the top-K retrieved limit $merged = array_slice($merged, 0, get_option('shoponix_top_k', 5)); // Fallback: If no semantic or keyword matches are found, inject top selling products if (empty($merged)) { $fallback_args = array( 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => get_option('shoponix_top_k', 5), 'meta_key' => 'total_sales', 'orderby' => 'meta_value_num', 'order' => 'DESC', 'fields' => 'ids', ); $top_query = new \WP_Query($fallback_args); if (!empty($top_query->posts)) { foreach ($top_query->posts as $fallback_id) { $content = $vector_store->get_product_content($fallback_id); if (empty($content)) { $product = wc_get_product($fallback_id); if ($product) { $content = "Name: " . $product->get_name() . " | Price: " . $product->get_price() . " | Description: " . wp_strip_all_tags($product->get_short_description()); } } if (!empty($content)) { $merged[] = [ 'product_id' => $fallback_id, 'content' => $content ]; } } } } $context = "Here is relevant product information from the store:\n\n"; if (empty($merged)) { $context .= "No specific product matches found.\n"; } else { foreach ($merged as $item) { $context .= "Product ID: " . $item['product_id'] . " | Details: " . $item['content'] . "\n\n"; } } if (!empty($viewed_products) && is_array($viewed_products)) { $context .= "Products recently viewed by the user in this session:\n"; foreach ($viewed_products as $vp) { if (isset($vp['id']) && isset($vp['name'])) { $context .= "- " . sanitize_text_field($vp['name']) . " (ID: " . intval($vp['id']) . ")\n"; } } $context .= "\n"; } $is_cart = rest_sanitize_boolean($request->get_param('is_cart')); $is_checkout = rest_sanitize_boolean($request->get_param('is_checkout')); if ($is_checkout) { $context .= "PAGE CONTEXT: The user is currently on the CHECKOUT page. Your goal is to reassure them, answer last minute questions, and close the sale.\n\n"; } elseif ($is_cart) { $context .= "PAGE CONTEXT: The user is currently on the CART page. Review their cart items, suggest complimentary products (upsell), and encourage them to proceed to checkout.\n\n"; } // 3. Check for logged in user orders $order_context = ""; $user_id = get_current_user_id(); if ($user_id) { $orders = wc_get_orders([ 'customer' => $user_id, 'limit' => 5, 'orderby' => 'date', 'order' => 'DESC', ]); if (!empty($orders)) { $order_context = "\n\nUser's Recent Orders:\n"; foreach ($orders as $order) { $items = []; foreach ($order->get_items() as $item) { $items[] = $item->get_name() . " (x" . $item->get_quantity() . ")"; } $order_context .= "Order ID: " . $order->get_id() . " | Status: " . wc_get_order_status_name($order->get_status()) . " | Date: " . $order->get_date_created()->date('Y-m-d') . " | Total: " . $order->get_formatted_order_total() . " | Items: " . implode(', ', $items) . "\n"; } } } // 3.5 Check current cart context $cart_context = "\n\nCurrent Shopping Cart Content:\n"; if (function_exists('wc_load_cart') && function_exists('WC') && WC() && is_null(WC()->cart)) { wc_load_cart(); } if (function_exists('WC') && WC() && WC()->cart) { if (WC()->cart->is_empty()) { $cart_context .= "The user's shopping cart is currently empty.\n"; } else { $cart_context .= "The user has the following items in their cart:\n"; foreach (WC()->cart->get_cart() as $cart_item_key => $values) { $product = $values['data']; $cart_context .= "- " . $product->get_name() . " (Qty: " . $values['quantity'] . ", Subtotal: " . wc_price($values['line_total']) . ")\n"; } $cart_context .= "Cart Subtotal: " . wp_strip_all_tags(WC()->cart->get_cart_subtotal()) . "\n"; $cart_context .= "Cart Total: " . wp_strip_all_tags(WC()->cart->get_total()) . "\n"; $applied_coupons = WC()->cart->get_applied_coupons(); if (!empty($applied_coupons)) { $cart_context .= "Applied Coupons: " . implode(', ', $applied_coupons) . "\n"; } else { $cart_context .= "No coupons have been applied to this cart yet. You can politely ask: 'Do you have a coupon or promo code to apply? You can enter it in the cart summary card below or tell me to apply it! Or click Proceed to Checkout below to finalize your order!'\n"; } } } else { $cart_context .= "The shopping cart is currently unavailable.\n"; } // 3.5. Expose active promotional coupons $coupon_context = ""; $coupon_args = array( 'posts_per_page' => 5, 'orderby' => 'date', 'order' => 'DESC', 'post_type' => 'shop_coupon', 'post_status' => 'publish', ); $coupons = get_posts($coupon_args); if (!empty($coupons)) { $coupon_context .= "\nAvailable Store Coupons:\n"; foreach ($coupons as $c) { $coupon_obj = new \WC_Coupon($c->ID); if ($coupon_obj->is_valid()) { $desc = $coupon_obj->get_description() ?: 'Special discount for our customers!'; $amount = $coupon_obj->get_amount(); $type = $coupon_obj->get_discount_type() === 'percent' ? '%' : ' off'; $coupon_context .= "- Code: " . strtoupper($coupon_obj->get_code()) . " (Saves " . $amount . $type . ". " . $desc . ")\n"; } } $coupon_context .= "Proactively suggest these active coupons to the user when relevant, like 'Use code SAVE10 for 10% off!'.\n"; } else { $coupon_context = "\nAvailable Store Coupons:\n- There are currently no active promotional coupons or discount codes available in the store. Do not mention, suggest, or hallucinate any coupon codes (such as SAVE10 or FREESHIPPING) under any circumstances. If the user asks, politely explain that no discount campaigns are running right now.\n"; } // 4. Build prompt $ab_variation = sanitize_text_field($request->get_param('ab_variation')); $base_system_prompt = get_option('shoponix_system_prompt', 'You are a warm, friendly, and highly conversational retail assistant.'); if ($ab_variation === 'B') { $prompt_b = get_option('shoponix_system_prompt_b'); if (!empty($prompt_b)) { $base_system_prompt = $prompt_b; } } // AI Personality Tone Presets $personality = get_option('shoponix_ai_personality', 'default'); $personality_instructions = ""; switch ($personality) { case 'luxury': $personality_instructions = "TONE & STYLE: You are an extremely elegant, polite, and premium luxury boutique concierge. Speak with high sophistication, polish, and impeccable manners. Avoid slang, exclamation marks, or overly casual remarks. Treat the customer as an esteemed VIP.\n\n"; break; case 'energetic': $personality_instructions = "TONE & STYLE: You are an incredibly friendly, highly energetic, and enthusiastic shopping assistant! Use a welcoming, cheerful tone, positive vocabulary, and expressive formatting (with friendly shopping emojis like 🛍️, ✨, 🚀 where appropriate) to create an exciting, fun shopping experience.\n\n"; break; case 'direct': $personality_instructions = "TONE & STYLE: You are a direct, concise, and business-focused assistant. Give extremely brief, straight-to-the-point, and precise answers without any small talk or fluff. Present specifications, sizing, and pricing in clear bullet points.\n\n"; break; case 'default': default: $personality_instructions = "TONE & STYLE: You are a helpful, expert retail assistant. Be highly concise, natural, and direct. Provide accurate product expertise without unnecessary fluff or excessive enthusiasm. Use emojis very sparingly (max 1 per message). Do not use repetitive boilerplate greetings.\n\n"; break; } $system_prompt = $personality_instructions . $base_system_prompt . "\n\n" . "CRITICAL INSTRUCTION 1: When recommending products or suggesting alternatives, you MUST include its ID in this exact format: [product_id: ID] (e.g. [product_id: 123]). If recommending multiple items, group them consecutively (one after another) and label them with a warm, creative header like '🔥 Best Sellers', '⭐ Trending Products', '🛍️ Related Products', or '✨ Frequently Bought Together'. Introduce these recommendations in a highly natural, conversational manner rather than using sterile lists. Never list recommended product alternatives in a plain-text list without their tags.\n" . "CRITICAL INSTRUCTION 2: If the user asks you to add a product to their cart, find the exact product ID (or Variation ID if they requested a specific size/color) from the context and output EXACTLY: [add_to_cart: ID: QUANTITY]. For example: [add_to_cart: 123: 2]. Do not output anything else in that line. Do not say you cannot add items to the cart.\n" . "CRITICAL INSTRUCTION 3: If the user asks about their orders, use the 'User's Recent Orders' context below to answer them. Only discuss orders present in the context.\n" . "CRITICAL INSTRUCTION 4: If a user asks for a product with a specific color, size, or variation that does not exist in the context, politely inform them it's not available, but immediately recommend the closest available matching options (e.g. different colors of the same style, or similar style products) and ALWAYS include their [product_id: ID] tags so the customer sees gorgeous visual cards.\n" . "CRITICAL INSTRUCTION 5: If there are no products matching the user's query, or no products currently on sale, or if you cannot answer their request based on the context, politely explain the limitation and do not add any special tags. Simply provide a helpful apology or suggestion.\n" . "CRITICAL INSTRUCTION 6: When the customer shows interest in checking out, views their cart, or requests checkout: if their cart is empty, warmly suggest adding items first and you MUST output the [show_home_button] tag on a new line. If their cart has items, you MUST output BOTH [show_checkout_button] and [show_home_button] tags on a new line so they have visual options to continue or browse. Never say 'Here is your option' or suggest options without outputting these exact bracketed tags.\n" . "CRITICAL INSTRUCTION 7: Detect the language used by the customer in their message. You MUST translate all product details, options, recommendations, and respond naturally in that exact same language (e.g., if they ask in Spanish, respond entirely in Spanish).\n" . "CRITICAL INSTRUCTION 8: If the customer requests to compare two or more products, identify their IDs from the context and output exactly: [compare_products: ID1, ID2] (e.g., [compare_products: 12, 15]). This will automatically render a premium comparison grid.\n" . "CRITICAL INSTRUCTION 9: If the customer is highly frustrated, angry, complaining, or explicitly requests to speak to a human or file a complaint, output exactly: [escalate_support] on a new line, along with a polite and helpful support apology.\n\n" . "CRITICAL INSTRUCTION 10: The customer has selected their preferred language as: " . esc_html($language) . ". You MUST completely and naturally formulate all responses, recommendations, dynamic titles, and options in " . esc_html($language) . ". Do not reply in any other language.\n\n" . "CRITICAL INSTRUCTION 11: If the customer asks for available discounts, coupons, or campaign offers, you MUST look at the 'Available Store Coupons' list. If there is an active coupon code listed, politely present it and output EXACTLY: [apply_coupon: CODE] on a new line so the customer can apply it. If the list states there are no active coupons, politely inform the customer that there are no active promotions running right now, and NEVER suggest, invent, or hallucinate any coupon codes (such as SAVE10 or FREESHIPPING) under any circumstances.\n\n"; $custom_faq = get_option('shoponix_custom_faq', ''); $faq_context = !empty($custom_faq) ? "\nStore Policies & Knowledge Base:\n" . $custom_faq . "\n" : ""; $system_prompt .= "Context:\n$context" . $order_context . $cart_context . $coupon_context . $faq_context; $formatted_messages = [['role' => 'system', 'content' => $system_prompt]]; $recent_messages = array_slice($messages, -6); foreach ($recent_messages as $msg) { if (in_array($msg['role'], ['user', 'assistant'])) { $formatted_messages[] = [ 'role' => $msg['role'] === 'assistant' ? 'assistant' : 'user', 'content' => sanitize_text_field($msg['content']) ]; } } // 4. Stream response $this->stream_llm_response($provider, $api_key, $formatted_messages); exit; } private function stream_llm_response($provider, $api_key, $messages) { $endpoint = 'https://api.mistral.ai/v1/chat/completions'; $chat_model = get_option('shoponix_chat_model'); if ($provider === 'gemini') { $endpoint = 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions'; if (empty($chat_model)) { $chat_model = 'gemini-1.5-flash'; } } elseif ($provider === 'openai') { $endpoint = 'https://api.openai.com/v1/chat/completions'; if (empty($chat_model)) { $chat_model = 'gpt-4o-mini'; } } elseif ($provider === 'ollama') { $ollama_url = get_option('shoponix_ollama_url', 'http://localhost:11434'); $endpoint = rtrim($ollama_url, '/') . '/v1/chat/completions'; if (empty($chat_model)) { $chat_model = 'llama3'; } } else { if (empty($chat_model)) { $chat_model = 'mistral-tiny'; } } // Set up temporary filter to hook into Requests curl options for streaming $stream_callback = function (&$handle, $r) { if (isset($r['is_shoponix_stream']) && $r['is_shoponix_stream']) { // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt curl_setopt($handle, CURLOPT_WRITEFUNCTION, function ($ch, $data) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $data; if (ob_get_level() > 0) { ob_flush(); } flush(); return strlen($data); }); } }; add_action('http_api_curl', $stream_callback, 10, 2); $args = [ 'headers' => [ 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', 'Accept' => 'text/event-stream', ], 'body' => json_encode([ 'model' => $chat_model, 'messages' => $messages, 'temperature' => 0.3, 'stream' => true, ]), 'timeout' => 120, 'blocking' => true, 'is_shoponix_stream' => true, ]; while (ob_get_level()) { ob_end_clean(); } header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); header('Connection: keep-alive'); header('X-Accel-Buffering: no'); $response = wp_remote_post($endpoint, $args); remove_action('http_api_curl', $stream_callback, 10); if (is_wp_error($response)) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo "data: " . json_encode(['error' => $response->get_error_message()]) . "\n\n"; } } public function get_product_specs() { check_ajax_referer('shoponix_chat_nonce', 'nonce'); $product_id = isset($_POST['product_id']) ? intval(wp_unslash($_POST['product_id'])) : 0; if (!$product_id) wp_send_json_error(['message' => 'Invalid Product ID']); $product = wc_get_product($product_id); if (!$product) wp_send_json_error(['message' => 'Product not found']); // Fetch standard details $price = $product->get_price_html(); $stock = $product->is_in_stock() ? 'In Stock' : 'Out of Stock'; $type = $product->get_type(); $short_desc = $product->get_short_description(); // Fetch variations details if variable $sizes = []; $colors = []; $attributes_list = []; foreach ($product->get_attributes() as $attr_name => $attr) { $name = wc_attribute_label($attr_name); $values = $attr->is_taxonomy() ? implode(', ', wc_get_product_terms($product_id, $attr_name, ['fields' => 'names'])) : implode(', ', $attr->get_options()); if (strtolower($name) === 'size') { $sizes = $attr->is_taxonomy() ? wc_get_product_terms($product_id, $attr_name, ['fields' => 'names']) : $attr->get_options(); } elseif (strtolower($name) === 'color') { $colors = $attr->is_taxonomy() ? wc_get_product_terms($product_id, $attr_name, ['fields' => 'names']) : $attr->get_options(); } else { $attributes_list[] = ['name' => $name, 'value' => $values]; } } ob_start(); ?>
📋 Product Specifications
Price
Stock
Sizes Available
Colors Available
$html, 'type' => $type, 'has_variants' => ($type === 'variable' && (!empty($sizes) || !empty($colors))) ]); } public function get_variation_options() { check_ajax_referer('shoponix_chat_nonce', 'nonce'); $product_id = isset($_POST['product_id']) ? intval(wp_unslash($_POST['product_id'])) : 0; if (!$product_id) wp_send_json_error(['message' => 'Invalid Product ID']); $product = wc_get_product($product_id); if (!$product || !$product->is_type('variable')) { wp_send_json_error(['message' => 'Variable product not found']); } // Get the variation attributes that are actually used for variations $variation_attributes = $product->get_variation_attributes(); $attributes_data = []; foreach ($variation_attributes as $attr_name => $options) { $attributes_data[] = [ 'name' => $attr_name, 'label' => wc_attribute_label($attr_name, $product), 'options' => $options ]; } $variations = $product->get_available_variations(); $variation_tree = []; foreach ($variations as $var) { $variation_tree[] = [ 'variation_id' => $var['variation_id'], 'attributes' => $var['attributes'], // e.g. ["attribute_pa_color" => "red", "attribute_pa_size" => "s"] 'price_html' => $var['price_html'], 'price' => $var['display_price'], 'is_in_stock' => $var['is_in_stock'] && $var['is_purchasable'] ]; } wp_send_json_success([ 'attributes' => $attributes_data, 'variations' => $variation_tree ]); } public function log_chat_transcript() { check_ajax_referer('shoponix_chat_nonce', 'nonce'); $session_id = isset($_POST['session_id']) ? sanitize_text_field(wp_unslash($_POST['session_id'])) : ''; $messages_raw = isset($_POST['messages']) ? sanitize_textarea_field(wp_unslash($_POST['messages'])) : ''; if (empty($session_id) || empty($messages_raw)) { wp_send_json_error('Invalid parameters'); } global $wpdb; $table_name = esc_sql($wpdb->prefix . 'shoponix_chat_transcripts'); // Check if there is an existing session // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $existing = $wpdb->get_row($wpdb->prepare("SELECT id, status FROM {$table_name} WHERE session_id = %s", $session_id)); // Deduce dynamic status $status = 'active'; if (strpos(strtolower($messages_raw), 'placed successfully') !== false || strpos(strtolower($messages_raw), 'order placed successfully') !== false) { $status = 'checkout'; } elseif (strpos(strtolower($messages_raw), 'live support') !== false || strpos(strtolower($messages_raw), 'shoponix-support-btn') !== false) { $status = 'escalated'; } elseif (strpos(strtolower($messages_raw), 'network error') !== false || strpos(strtolower($messages_raw), 'sorry, i encountered an error') !== false) { $status = 'failed'; } $user_id = get_current_user_id(); $user_ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '127.0.0.1'; $ab_variation = isset($_POST['ab_variation']) ? sanitize_text_field(wp_unslash($_POST['ab_variation'])) : ''; $last_user_message = isset($_POST['last_user_message']) ? sanitize_text_field(wp_unslash($_POST['last_user_message'])) : ''; if ($existing) { // Update // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->update( $table_name, [ 'user_id' => $user_id, 'user_ip' => $user_ip, 'messages' => $messages_raw, 'status' => $status, 'ab_variation' => $ab_variation, 'last_user_message' => $last_user_message, 'updated_at' => current_time('mysql') ], ['id' => $existing->id] ); } else { // Insert // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->insert( $table_name, [ 'session_id' => $session_id, 'user_id' => $user_id, 'user_ip' => $user_ip, 'messages' => $messages_raw, 'status' => $status, 'ab_variation' => $ab_variation, 'last_user_message' => $last_user_message, 'created_at' => current_time('mysql'), 'updated_at' => current_time('mysql') ] ); } wp_send_json_success(); } public function compare_products() { check_ajax_referer('shoponix_chat_nonce', 'nonce'); $raw_ids = isset($_POST['product_ids']) ? sanitize_text_field(wp_unslash($_POST['product_ids'])) : ''; if (empty($raw_ids)) { wp_send_json_error(['message' => 'No product IDs provided']); } $ids = array_map('intval', explode(',', $raw_ids)); $products = []; foreach ($ids as $id) { $p = wc_get_product($id); if ($p && $p->is_visible()) { $products[] = $p; } } if (count($products) < 2) { wp_send_json_error(['message' => 'Please provide at least 2 valid products for comparison']); } ob_start(); ?>
⚖️ Product Comparison
Feature get_image([40, 40], ['style' => 'border-radius: 6px; object-fit: cover; width: 40px; height: 40px; display: block; margin: 0 auto 6px;'])); ?>
get_name()); ?>
Price get_price_html()); ?>
Rating get_average_rating(); echo esc_html($rating > 0 ? str_repeat('⭐', round($rating)) . ' (' . $p->get_review_count() . ')' : 'No reviews'); ?>
Stock is_in_stock() ? 'In Stock' : 'Out of Stock'; ?>
Short Info get_short_description()), 8, '...')); ?>
get_type() === 'variable'): ?>
$html]); } public function submit_escalation() { check_ajax_referer('shoponix_chat_nonce', 'nonce'); $name = isset($_POST['name']) ? sanitize_text_field(wp_unslash($_POST['name'])) : ''; $email = isset($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : ''; $phone = isset($_POST['phone']) ? sanitize_text_field(wp_unslash($_POST['phone'])) : ''; $message = isset($_POST['message']) ? sanitize_textarea_field(wp_unslash($_POST['message'])) : ''; if (empty($name) || empty($email) || empty($message)) { wp_send_json_error(['message' => 'Please fill in all required fields.']); } // Email to Administrator $admin_email = get_option('admin_email'); $subject = 'Priority AI Support Escalation from ' . esc_html($name); $body = "A customer has requested priority human support assistance via the AI Chatbot.\n\n" . "Customer Details:\n" . "Name: $name\n" . "Email: $email\n" . "Phone: $phone\n\n" . "Message:\n$message\n\n" . "Please respond to this request as soon as possible."; $headers = ['Content-Type: text/plain; charset=UTF-8', 'Reply-To: ' . $name . ' <' . $email . '>']; wp_mail($admin_email, $subject, $body, $headers); wp_send_json_success(['message' => 'Your priority support ticket has been submitted successfully. A human agent will contact you shortly.']); } public function ai_search() { check_ajax_referer('shoponix_chat_nonce', 'nonce'); $query = isset($_POST['query']) ? sanitize_text_field(wp_unslash($_POST['query'])) : ''; if (empty($query)) { wp_send_json_error(['message' => 'Please enter a search term.']); } $api_key_encrypted = get_option('shoponix_mistral_api_key'); if (!$api_key_encrypted) { wp_send_json_error(['message' => 'API Key is not configured.']); } $api_key = \Shoponix\Security\Encryption::decrypt($api_key_encrypted); $provider = get_option('shoponix_ai_provider', 'mistral'); $embedding = new \Shoponix\AI\UniversalEmbedder($provider, $api_key, get_option('shoponix_embedding_model')); $query_vec = $embedding->embed($query); if (!$query_vec) { wp_send_json_error(['message' => 'Failed to process AI embedding.']); } $vector_store = new \Shoponix\VectorDB\Store(); $similar = $vector_store->find_similar($query_vec, 6); // Fetch top 6 similar products // Keyword query $args = array( 'post_type' => 'product', 'post_status' => 'publish', 's' => $query, 'posts_per_page' => 3, 'fields' => 'ids', ); $kw_query = new \WP_Query($args); $matched_ids = []; if ($similar) { foreach ($similar as $s) { $matched_ids[] = intval($s['product_id']); } } if (!empty($kw_query->posts)) { foreach ($kw_query->posts as $kw_id) { $matched_ids[] = intval($kw_id); } } $matched_ids = array_unique($matched_ids); $is_fallback = false; if (empty($matched_ids)) { $is_fallback = true; $args = array( 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => 6, 'meta_key' => 'total_sales', 'orderby' => 'meta_value_num', 'order' => 'DESC', 'fields' => 'ids', ); $top_query = new \WP_Query($args); if (!empty($top_query->posts)) { $matched_ids = array_map('intval', $top_query->posts); } } ob_start(); if (empty($matched_ids)) { echo '
No products available.
'; $html = ob_get_clean(); wp_send_json_success(['html' => $html]); } if ($is_fallback) { echo '
No exact matches found. Here are our top products:
'; } ?>
= 6) break; $product = wc_get_product($id); if (!$product || !$product->is_visible()) continue; $count++; $type = $product->get_type(); ?>
get_image([80, 80], ['style' => 'border-radius: 8px; object-fit: cover; width: 100%; height: 90px; margin-bottom: 4px; display: block;'])); ?>
get_name()); ?>
get_price_html()); ?>
get_wishlist_url(); echo '❤️'; } ?>
$html]); } public function get_product_fit_details() { check_ajax_referer('shoponix_chat_nonce', 'nonce'); $product_id = isset($_POST['product_id']) ? intval(wp_unslash($_POST['product_id'])) : 0; $product = wc_get_product($product_id); if (!$product) { wp_send_json_error(['message' => 'Product not found']); } wp_send_json_success([ 'id' => $product_id, 'name' => $product->get_name(), 'image' => wp_get_attachment_url($product->get_image_id()) ?: wc_placeholder_img_src(), 'has_sizes' => true, 'sizes' => ['S', 'M', 'L', 'XL'], 'fit_preferences' => [ ['id' => 'slim', 'label' => 'Slim Fit', 'desc' => 'Tailored close to body'], ['id' => 'regular', 'label' => 'Standard Fit', 'desc' => 'Normal room for comfort'], ['id' => 'athletic', 'label' => 'Athletic Fit', 'desc' => 'Extra space, relaxed profile'] ] ]); } public function get_product_cross_sells() { check_ajax_referer('shoponix_chat_nonce', 'nonce'); $product_id = isset($_POST['product_id']) ? intval(wp_unslash($_POST['product_id'])) : 0; $product = wc_get_product($product_id); if (!$product) { wp_send_json_error(['message' => 'Product not found']); } $cross_sells = $product->get_cross_sell_ids(); if (empty($cross_sells)) { $cross_sells = wc_get_related_products($product_id, 3); } if (empty($cross_sells)) { $cross_sells = wc_get_products(['limit' => 3, 'return' => 'ids', 'status' => 'publish']); } $data = []; foreach (array_slice($cross_sells, 0, 3) as $id) { $item = wc_get_product($id); if ($item && $item->is_visible()) { $data[] = [ 'id' => $id, 'name' => $item->get_name(), 'price_html' => $item->get_price_html(), 'image' => wp_get_attachment_url($item->get_image_id()) ?: wc_placeholder_img_src(), 'permalink' => $item->get_permalink(), 'type' => $item->get_type() ]; } } wp_send_json_success($data); } public function visual_image_search() { check_ajax_referer('shoponix_chat_nonce', 'nonce'); if (empty($_FILES['search_image']) || empty($_FILES['search_image']['tmp_name'])) { wp_send_json_error(['message' => 'No image uploaded']); } $tmp_file = isset($_FILES['search_image']['tmp_name']) ? sanitize_text_field(wp_unslash($_FILES['search_image']['tmp_name'])) : ''; $filename = isset($_FILES['search_image']['name']) ? sanitize_text_field(wp_unslash($_FILES['search_image']['name'])) : 'unknown.jpg'; // Step 1: Read and base64-encode the image for AI vision // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $image_data = file_get_contents($tmp_file); if (!$image_data) { wp_send_json_error(['message' => 'Failed to read uploaded image']); } $base64_image = base64_encode($image_data); $mime_type = mime_content_type($tmp_file) ?: 'image/jpeg'; // Step 2: Send to AI for image analysis $ai_analysis = $this->analyze_image_with_ai($base64_image, $mime_type); $search_keywords = []; $ai_category = ''; $ai_product_name = ''; $search_source = 'filename'; if ($ai_analysis && !empty($ai_analysis['keywords'])) { $search_keywords = $ai_analysis['keywords']; $ai_category = $ai_analysis['category'] ?? ''; $ai_product_name = $ai_analysis['product_name'] ?? ''; $search_source = 'ai_vision'; } else { // Fallback: extract keywords from filename $keywords = preg_split('/[\s_\-\.]+/', strtolower($filename)); $stop_words = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'image', 'photo', 'search', 'upload', 'reference', 'screenshot', 'img', 'pic']; $search_keywords = array_values(array_filter($keywords, function ($word) use ($stop_words) { return strlen($word) > 2 && !in_array($word, $stop_words, true); })); } // Step 3: Search WooCommerce products by AI-identified keywords $products = []; if (!empty($search_keywords)) { $search_query = implode(' ', array_slice($search_keywords, 0, 5)); $products = wc_get_products([ 'status' => 'publish', 'limit' => 6, 'visibility' => 'visible', 's' => $search_query ]); } // Step 4: If no results, try individual keywords if (empty($products) && count($search_keywords) > 1) { foreach ($search_keywords as $kw) { $products = wc_get_products([ 'status' => 'publish', 'limit' => 6, 'visibility' => 'visible', 's' => $kw ]); if (!empty($products)) break; } } // Step 5: If still no results, search by AI-identified category $category_fallback = false; if (empty($products) && !empty($ai_category)) { $cat_term = get_term_by('name', $ai_category, 'product_cat'); if (!$cat_term) { // Try partial match $all_cats = get_terms(['taxonomy' => 'product_cat', 'hide_empty' => true]); if (!is_wp_error($all_cats)) { foreach ($all_cats as $cat) { if (stripos($cat->name, $ai_category) !== false || stripos($ai_category, $cat->name) !== false) { $cat_term = $cat; break; } } } } if ($cat_term) { $products = wc_get_products([ 'status' => 'publish', 'limit' => 6, 'visibility' => 'visible', 'category' => [$cat_term->slug] ]); if (!empty($products)) { $category_fallback = true; } } } // Step 6: Last resort — show best-selling/recent products if (empty($products)) { $products = wc_get_products([ 'status' => 'publish', 'limit' => 6, 'visibility' => 'visible', 'orderby' => 'popularity' ]); } // Step 7: Format response $data = []; foreach ($products as $prod) { $data[] = [ 'id' => $prod->get_id(), 'name' => $prod->get_name(), 'price_html' => $prod->get_price_html(), 'image' => wp_get_attachment_url($prod->get_image_id()) ?: wc_placeholder_img_src(), 'permalink' => $prod->get_permalink(), 'type' => $prod->get_type() ]; } wp_send_json_success([ 'filename' => $filename, 'ai_product_name' => $ai_product_name, 'ai_category' => $ai_category, 'tags_matched' => array_values($search_keywords), 'search_source' => $search_source, 'category_fallback' => $category_fallback, 'products' => $data ]); } /** * Analyze an uploaded image using the configured AI vision model. * Returns an array with 'product_name', 'category', and 'keywords'. */ private function analyze_image_with_ai($base64_image, $mime_type) { $api_key_encrypted = get_option('shoponix_mistral_api_key'); if (!$api_key_encrypted) { return null; } $api_key = \Shoponix\Security\Encryption::decrypt($api_key_encrypted); if (!$api_key) { return null; } $provider = get_option('shoponix_ai_provider', 'mistral'); $chat_model = get_option('shoponix_chat_model'); // Determine endpoint and model if ($provider === 'gemini') { $endpoint = 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions'; if (empty($chat_model)) { $chat_model = 'gemini-2.0-flash'; } } elseif ($provider === 'openai') { $endpoint = 'https://api.openai.com/v1/chat/completions'; if (empty($chat_model)) { $chat_model = 'gpt-4o-mini'; } } else { // Mistral — use pixtral for vision $endpoint = 'https://api.mistral.ai/v1/chat/completions'; $chat_model = 'pixtral-12b-2409'; } $vision_prompt = 'Analyze this product image. Respond ONLY in this exact JSON format with no other text: {"product_name": "specific product name", "category": "product category", "keywords": ["keyword1", "keyword2", "keyword3", "keyword4", "keyword5"]} Rules: - product_name: What specific product is this (e.g. "Samsung Galaxy S24", "Nike Air Max 90", "Bosch Washing Machine") - category: The general product category (e.g. "Electronics", "Shoes", "Clothing", "Home Appliances", "Furniture") - keywords: 5 search keywords that would help find this or similar products in an online store. Include brand, type, color, material, use-case.'; $messages = [ [ 'role' => 'user', 'content' => [ [ 'type' => 'text', 'text' => $vision_prompt ], [ 'type' => 'image_url', 'image_url' => [ 'url' => "data:{$mime_type};base64,{$base64_image}" ] ] ] ] ]; $response = wp_remote_post($endpoint, [ 'timeout' => 30, 'headers' => [ 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json' ], 'body' => wp_json_encode([ 'model' => $chat_model, 'messages' => $messages, 'temperature' => 0.1, 'max_tokens' => 300 ]) ]); if (is_wp_error($response)) { return null; } $body = wp_remote_retrieve_body($response); $decoded = json_decode($body, true); if (!$decoded || empty($decoded['choices'][0]['message']['content'])) { return null; } $ai_text = trim($decoded['choices'][0]['message']['content']); // Extract JSON from the response (handle markdown code blocks) if (preg_match('/\{[\s\S]*\}/', $ai_text, $json_match)) { $parsed = json_decode($json_match[0], true); if ($parsed && !empty($parsed['keywords'])) { return [ 'product_name' => $parsed['product_name'] ?? '', 'category' => $parsed['category'] ?? '', 'keywords' => array_map('sanitize_text_field', $parsed['keywords']) ]; } } return null; } public function cancel_order() { check_ajax_referer('shoponix_chat_nonce', 'nonce'); if (!is_user_logged_in()) { wp_send_json_error(['message' => 'Please log in to cancel an order.']); } $order_id = isset($_POST['order_id']) ? intval($_POST['order_id']) : 0; if (!$order_id) { wp_send_json_error(['message' => 'Invalid order ID provided.']); } $order = wc_get_order($order_id); if (!$order || $order->get_customer_id() !== get_current_user_id()) { wp_send_json_error(['message' => 'Order not found or access denied.']); } if ($order->has_status(['pending', 'processing', 'on-hold'])) { $order->update_status('cancelled', 'Cancelled via AI Chatbot by customer.'); wp_send_json_success(['message' => 'Order #' . $order_id . ' has been successfully cancelled.']); } else { wp_send_json_error(['message' => 'This order cannot be cancelled as it is currently marked as: ' . wc_get_order_status_name($order->get_status())]); } } }