id, $allowed, true) ) return; try { $key = base64_encode( random_bytes(32) ); } catch ( Exception $e ) { $key = ''; } echo '

'; echo 'AI Supabase Search: No master key defined for secret encryption.
'; echo 'Add the following line to your wp-config.php above /* That\'s all, stop editing! */:'; echo '

define(\'aivesese_MASTER_KEY_B64\', \'' . esc_html( $key ) . '\');
'; echo '

'; } } // Add the notice only once if ( ! has_action('admin_notices', 'aivesese_master_key_notice') ) { add_action('admin_notices', 'aivesese_master_key_notice'); } if (!function_exists('aivesese_encrypt')) { function aivesese_encrypt(string $plaintext): array { $key = aivesese_get_master_key(); if (function_exists('sodium_crypto_secretbox')) { $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $cipher = sodium_crypto_secretbox($plaintext, $nonce, $key); return [ 'v' => 1, 'alg' => 'secretbox', 'nonce' => base64_encode($nonce), 'cipher' => base64_encode($cipher), ]; } $iv = random_bytes(12); $tag = ''; $cipher = openssl_encrypt($plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag); if ($cipher === false) { return ['v'=>1, 'alg'=>'plain', 'cipher'=>base64_encode($plaintext)]; } return [ 'v' => 1, 'alg' => 'aes-256-gcm', 'iv' => base64_encode($iv), 'tag' => base64_encode($tag), 'cipher' => base64_encode($cipher), ]; } } if (!function_exists('aivesese_decrypt')) { function aivesese_decrypt($stored): ?string { if (!is_array($stored) || empty($stored['alg'])) { return is_string($stored) ? $stored : null; } $key = aivesese_get_master_key(); if ($stored['alg'] === 'secretbox' && function_exists('sodium_crypto_secretbox_open')) { $nonce = base64_decode($stored['nonce']); $cipher = base64_decode($stored['cipher']); $plain = sodium_crypto_secretbox_open($cipher, $nonce, $key); return $plain === false ? null : $plain; } if ($stored['alg'] === 'aes-256-gcm') { $iv = base64_decode($stored['iv']); $tag = base64_decode($stored['tag']); $cipher = base64_decode($stored['cipher']); $plain = openssl_decrypt($cipher, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag); return $plain === false ? null : $plain; } if ($stored['alg'] === 'plain') { return base64_decode($stored['cipher']); } return null; } } // Transparent encryption on update add_filter('pre_update_option_aivesese_key', function($value, $old_value, $option) { if (is_string($value) && $value !== '') return aivesese_encrypt($value); return $value; }, 10, 3); add_filter('pre_update_option_aivesese_openai', function($value, $old_value, $option) { if (is_string($value) && $value !== '') return aivesese_encrypt($value); return $value; }, 10, 3); // Transparent decryption on read add_filter('option_aivesese_key', function($value, $option = null) { return aivesese_decrypt($value); }, 10, 2); add_filter('option_aivesese_openai', function($value, $option = null) { return aivesese_decrypt($value); }, 10, 2); // One-time migration: if legacy plaintext existed, re-save to trigger encryption add_action('admin_init', function () { if (!current_user_can('manage_options')) return; $targets = ['aivesese_key', 'aivesese_openai']; foreach ($targets as $opt) { $val = get_option($opt, null); if (is_string($val) && $val !== '') { update_option($opt, $val, false); // pre_update filter will encrypt } } }); /* ------------------------------------------------------------------------- * AUTO SYNC ON PRODUCT CHANGES * ------------------------------------------------------------------------- */ // Sync single product when it's updated add_action('woocommerce_update_product', 'aivesese_auto_sync_product', 10, 1); add_action('woocommerce_new_product', 'aivesese_auto_sync_product', 10, 1); function aivesese_auto_sync_product($product_id) { if (get_option('aivesese_auto_sync') !== '1') { return; } $product = wc_get_product($product_id); if (!$product) { return; } $with_embeddings = (get_option('aivesese_semantic_toggle') === '1' && get_option('aivesese_openai')); aivesese_sync_products([$product], $with_embeddings); } /* PRODUCT SYNC FUNCTIONS * ------------------------------------------------------------------------- */ function aivesese_get_synced_count() { $store_id = get_option('aivesese_store'); if (!$store_id) return 0; $result = aivesese_request('GET', '/rest/v1/products', null, [ 'select' => 'id', 'store_id' => 'eq.' . $store_id, ]); return is_array($result) ? count($result) : 0; } function aivesese_transform_product($product) { $store_id = get_option('aivesese_store'); // Get categories $categories = []; $terms = wp_get_post_terms($product->get_id(), 'product_cat'); foreach ($terms as $term) { $categories[] = $term->name; } // Get tags $tags = []; $tag_terms = wp_get_post_terms($product->get_id(), 'product_tag'); foreach ($tag_terms as $term) { $tags[] = $term->name; } // Get brand (if using a brand plugin/taxonomy) $brand = ''; $brand_terms = wp_get_post_terms($product->get_id(), 'product_brand'); if (!empty($brand_terms) && !is_wp_error($brand_terms)) { $brand = $brand_terms[0]->name; } // Get attributes $attributes = []; $product_attributes = $product->get_attributes(); foreach ($product_attributes as $attribute) { if ($attribute->is_taxonomy()) { $terms = wp_get_post_terms($product->get_id(), $attribute->get_name()); if (!is_wp_error($terms) && !empty($terms)) { $values = array_map(function($term) { return $term->name; }, $terms); $attributes[$attribute->get_name()] = implode(', ', $values); } } else { $attributes[$attribute->get_name()] = $attribute->get_options()[0] ?? ''; } } // Get image URL $image_url = ''; $image_id = $product->get_image_id(); if ($image_id) { $image_url = wp_get_attachment_image_url($image_id, 'full'); } return [ 'id' => wp_generate_uuid4(), 'store_id' => $store_id, 'woocommerce_id' => $product->get_id(), 'sku' => $product->get_sku(), 'gtin' => get_post_meta($product->get_id(), '_gtin', true) ?: get_post_meta($product->get_id(), '_ean', true), 'name' => $product->get_name(), 'description' => wp_strip_all_tags($product->get_description()), 'image_url' => $image_url, 'brand' => $brand, 'categories' => $categories, 'tags' => $tags, 'regular_price' => $product->get_regular_price() ? floatval($product->get_regular_price()) : null, 'sale_price' => $product->get_sale_price() ? floatval($product->get_sale_price()) : null, 'cost_price' => get_post_meta($product->get_id(), '_cost_price', true) ? floatval(get_post_meta($product->get_id(), '_cost_price', true)) : null, 'stock_quantity' => $product->get_stock_quantity(), 'stock_status' => $product->get_stock_status() === 'instock' ? 'in' : 'out', 'attributes' => $attributes, 'status' => $product->get_status(), 'average_rating' => $product->get_average_rating() ? floatval($product->get_average_rating()) : null, 'review_count' => $product->get_review_count() ? intval($product->get_review_count()) : 0, ]; } function aivesese_batch_embed($texts) { $key = trim(get_option('aivesese_openai')); if (!$key || empty($texts)) { return []; } $embeddings = []; $chunks = array_chunk($texts, 50); foreach ($chunks as $chunk) { $args = [ 'method' => 'POST', 'timeout' => 30, 'headers' => [ 'Authorization' => 'Bearer ' . $key, 'Content-Type' => 'application/json', ], 'body' => wp_json_encode([ 'model' => 'text-embedding-3-small', 'input' => $chunk, ]), ]; $response = wp_remote_post('https://api.openai.com/v1/embeddings', $args); if (is_wp_error($response)) { continue; } $data = json_decode(wp_remote_retrieve_body($response), true); if (isset($data['data'])) { foreach ($data['data'] as $embedding_data) { $embeddings[] = $embedding_data['embedding']; } } // Small delay to respect rate limits usleep(100000); // 0.1 second } return $embeddings; } function aivesese_sync_products($products, $with_embeddings = false) { if (empty($products)) { return ['success' => false, 'message' => 'No products to sync']; } $transformed_products = []; $texts_for_embedding = []; foreach ($products as $product) { $transformed = aivesese_transform_product($product); $transformed_products[] = $transformed; if ($with_embeddings) { $text = $transformed['name'] . "\n" . $transformed['description'] . "\n" . implode(' ', $transformed['categories']) . ' ' . implode(' ', $transformed['tags']) . ' ' . $transformed['brand']; $texts_for_embedding[] = $text; } } // Generate embeddings if requested if ($with_embeddings && !empty($texts_for_embedding)) { $embeddings = aivesese_batch_embed($texts_for_embedding); if (!empty($embeddings)) { foreach ($transformed_products as $i => $product) { if (isset($embeddings[$i])) { $transformed_products[$i]['embedding'] = $embeddings[$i]; } } } } // Sync to Supabase in batches $success_count = 0; $batches = array_chunk($transformed_products, 100); // Supabase batch limit foreach ($batches as $batch) { $result = aivesese_request('POST', '/rest/v1/products', $batch, [ 'on_conflict' => 'store_id,woocommerce_id' ]); if (!empty($result) || !is_wp_error($result)) { $success_count += count($batch); } } return [ 'success' => true, 'synced' => $success_count, 'total' => count($products) ]; } function aivesese_handle_sync_all() { $args = [ 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => -1, 'meta_query' => [ [ 'key' => '_visibility', 'value' => ['exclude-from-search', 'exclude-from-catalog'], 'compare' => 'NOT IN' ] ] ]; $product_posts = get_posts($args); $products = []; foreach ($product_posts as $post) { $product = wc_get_product($post->ID); if ($product) { $products[] = $product; } } $with_embeddings = (get_option('aivesese_semantic_toggle') === '1' && get_option('aivesese_openai')); $result = aivesese_sync_products($products, $with_embeddings); if ($result['success']) { echo '

Successfully synced ' . esc_attr($result['synced']) . '/' . esc_attr($result['total']) . ' products to Supabase!

'; } else { echo '

Sync failed: ' . esc_html($result['message']) . '

'; } } function aivesese_handle_sync_batch($batch_size, $offset) { $args = [ 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => $batch_size, 'offset' => $offset, 'meta_query' => [ [ 'key' => '_visibility', 'value' => ['exclude-from-search', 'exclude-from-catalog'], 'compare' => 'NOT IN' ] ] ]; $product_posts = get_posts($args); $products = []; foreach ($product_posts as $post) { $product = wc_get_product($post->ID); if ($product) { $products[] = $product; } } if (empty($products)) { echo '

' . sprintf( /* translators: %d: numeric offset */ esc_html__( 'No products found at offset %d', 'ai-vector-search-semantic' ), absint( $offset ) ) . '

'; return; } $with_embeddings = (get_option('aivesese_semantic_toggle') === '1' && get_option('aivesese_openai')); $result = aivesese_sync_products($products, $with_embeddings); if ($result['success']) { echo '

Successfully synced batch: ' . esc_attr($result['synced']) . '/' . esc_attr($result['total']) . ' products (offset: ' . esc_attr($offset) . ')

'; // Suggest next batch $next_offset = $offset + $batch_size; echo '
'; echo '

Continue with next batch:

'; echo '
'; wp_nonce_field('aivesese_sync'); echo ''; echo ''; echo ''; echo ''; echo '
'; echo '
'; } else { echo '

Batch sync failed: ' . esc_html($result['message']) . '

'; } } function aivesese_handle_generate_embeddings() { // Get products without embeddings $store_id = get_option('aivesese_store'); $products_without_embeddings = aivesese_request('GET', '/rest/v1/products', null, [ 'select' => 'woocommerce_id,name,description,categories,tags,brand', 'store_id' => 'eq.' . $store_id, 'embedding' => 'is.null', 'limit' => 50 ]); if (empty($products_without_embeddings)) { echo '

All products already have embeddings!

'; return; } $texts = []; foreach ($products_without_embeddings as $product) { $text = $product['name'] . "\n" . ($product['description'] ?: '') . "\n" . implode(' ', $product['categories'] ?: []) . ' ' . implode(' ', $product['tags'] ?: []) . ' ' . ($product['brand'] ?: ''); $texts[] = $text; } $embeddings = aivesese_batch_embed($texts); if (empty($embeddings)) { echo '

Failed to generate embeddings. Check OpenAI API key.

'; return; } // Update products with embeddings $updates = []; foreach ($products_without_embeddings as $i => $product) { if (isset($embeddings[$i])) { $updates[] = [ 'woocommerce_id' => $product['woocommerce_id'], 'store_id' => $store_id, 'embedding' => $embeddings[$i] ]; } } if (!empty($updates)) { $result = aivesese_request('POST', '/rest/v1/products', $updates, [ 'on_conflict' => 'store_id,woocommerce_id' ]); echo '

Generated embeddings for ' . count($updates) . ' products!

'; } } define( 'AIVESESE_PLUGIN_VERSION', '0.12.2' ); /* ------------------------------------------------------------------------- * 0. Validation * ------------------------------------------------------------------------- */ function aivesese_validate_url( $url ) { return esc_url_raw( $url ); } function aivesese_validate_key( $key ) { return sanitize_text_field( $key ); } /* ------------------------------------------------------------------------- * 1. Settings — Settings ▸ AI Supabase * ------------------------------------------------------------------------- */ add_action( 'admin_init', function () { foreach ( [ 'url' => 'Supabase URL (https://xyz.supabase.co)', 'key' => 'Supabase service / anon key', 'store' => 'Store ID (UUID)', 'openai' => 'OpenAI API key (only if semantic search is enabled)', 'semantic_toggle' => 'Enable semantic (vector) search', 'auto_sync' => 'Auto-sync products on save', ] as $id => $label ) { // ── Sanitise each option ────────────────────────────────────────────────────── $sanitizers = [ 'url' => 'esc_url_raw', 'key' => 'sanitize_text_field', 'store' => 'sanitize_text_field', 'openai'=> 'sanitize_text_field', ]; register_setting( 'aivesese_settings', "aivesese_$id", [ 'type' => 'string', 'sanitize_callback' => $sanitizers[ $id ] ?? 'sanitize_text_field', 'default' => '', ] ); if (isset($sanitizers[$id]) && function_exists("aivesese_validate_$id")) { add_filter("option_update_aivesese_$id", "aivesese_validate_$id"); } } add_settings_section( 'aivesese_section', 'Supabase connection', '__return_false', 'aivesese' ); foreach ( [ 'url', 'key', 'store', 'openai' ] as $id ) { add_settings_field( "aivesese_$id", $label = ucfirst( str_replace( '_', ' ', $id ) ), function () use ( $id ) { printf( '', esc_attr( $id ), esc_attr( get_option( "aivesese_$id" ) ) ); }, 'aivesese', 'aivesese_section' ); } // Checkbox for semantic toggle add_settings_field( 'aivesese_semantic_toggle', 'Enable semantic (vector) search', function () { $val = get_option( 'aivesese_semantic_toggle' ); echo ''; }, 'aivesese', 'aivesese_section' ); // Checkbox for auto-sync add_settings_field( 'aivesese_auto_sync', 'Auto-sync products', function () { $val = get_option( 'aivesese_auto_sync' ); echo ''; }, 'aivesese', 'aivesese_section' ); // Register options (simple sanitize to keep values '1' or '0') register_setting('aivesese', 'aivesese_enable_pdp_similar', [ 'type' => 'string', 'sanitize_callback' => function($v){ return $v === '1' ? '1' : '0'; }, 'default' => '1', ]); register_setting('aivesese', 'aivesese_enable_cart_below', [ 'type' => 'string', 'sanitize_callback' => function($v){ return $v === '1' ? '1' : '0'; }, 'default' => '1', ]); // Checkbox: PDP "Similar products" add_settings_field('aivesese_enable_pdp_similar', 'PDP "Similar products"', function () { $val = get_option('aivesese_enable_pdp_similar', '1'); echo ''; }, 'aivesese', 'aivesese_section'); // Checkbox: Below-cart recommendations add_settings_field('aivesese_enable_cart_below', 'Below-cart recommendations', function () { $val = get_option('aivesese_enable_cart_below', '1'); echo ''; }, 'aivesese', 'aivesese_section'); } ); add_action( 'admin_menu', function () { // Main settings page add_options_page( 'AI Supabase', 'AI Supabase', 'manage_options', 'aivesese', function () { echo '
'; echo '

' . esc_html__( 'AI Supabase Settings', 'ai-vector-search-semantic' ) . '

'; echo '

' . esc_html__( 'Configure the connection to your Supabase project and optionally enable semantic search using OpenAI.', 'ai-vector-search-semantic' ) . '

'; $user_id = get_current_user_id(); $is_open = get_user_meta($user_id, '_aivesese_help_open', true); $is_open = ($is_open === '' ? '1' : $is_open); $open_attr = ($is_open === '1') ? ' open' : ''; echo '
'; echo '
'; echo 'Setup Guide click to expand/collapse'; echo '

' . esc_html__( 'How to find your Supabase credentials:', 'ai-vector-search-semantic' ) . '

'; echo '
    '; /* translators: %s: Supabase dashboard URL (e.g., https://app.supabase.com). */ echo '
  1. ' . sprintf( esc_html__( 'Go to your Supabase project dashboard at %s.', 'ai-vector-search-semantic' ), 'https://app.supabase.io/' ) . '
  2. '; echo '
  3. ' . esc_html__( 'Navigate to "Project Settings" > "API".', 'ai-vector-search-semantic' ) . '
  4. '; echo '
  5. ' . esc_html__( 'Your Supabase URL is the "URL" value (e.g., https://xyz.supabase.co).', 'ai-vector-search-semantic' ) . '
  6. '; echo '
  7. ' . esc_html__( 'Your Supabase service role key or anon key can be found under "Project API keys". Use the anon key for client-side requests and the service role key for server-side operations (like data syncing, if you implement it).', 'ai-vector-search-semantic' ) . '
  8. '; echo '
'; echo '

' . esc_html__( 'How to find your Store ID:', 'ai-vector-search-semantic' ) . '

'; echo '

' . esc_html__( 'The Store ID is a UUID you define to identify your specific store within Supabase. If you are using a multi-store setup in Supabase, ensure this matches the ID associated with your WooCommerce store data.', 'ai-vector-search-semantic' ) . '

'; echo '

' . esc_html__( 'How to find your OpenAI API key:', 'ai-vector-search-semantic' ) . '

'; /* translators: %s: OpenAI website URL. */ echo '

' . sprintf( esc_html__( 'If you enable semantic search, you will need an OpenAI API key. You can obtain one from the %s.', 'ai-vector-search-semantic' ), 'OpenAI website' ) . '

'; echo '
'; $__base = plugin_dir_path(__FILE__); $__candidates = [ $__base . 'assets/sql/supabase.sql', $__base . 'admin/sql/supabase.sql', $__base . 'supabase.sql', ]; $__sql = ''; foreach ($__candidates as $__p) { if (file_exists($__p)) { $__sql = file_get_contents($__p); break; } } echo '

' . esc_html__( 'Install the SQL in Supabase', 'ai-vector-search-semantic' ) . '

'; echo '
    '; echo '
  1. ' . esc_html__( 'Open your Supabase project → SQL Editor → New query.', 'ai-vector-search-semantic' ) . '
  2. '; echo '
  3. ' . esc_html__( 'Click "Copy SQL" below and paste it into the editor.', 'ai-vector-search-semantic' ) . '
  4. '; echo '
  5. ' . esc_html__( 'Press RUN and wait for success.', 'ai-vector-search-semantic' ) . '
  6. '; echo '
  7. ' . esc_html__( 'Verify tables/views, RPCs and extensions were created.', 'ai-vector-search-semantic' ) . '
  8. '; echo '
'; if (! $__sql) { echo '

' . esc_html__( 'Could not find supabase.sql. Place it at assets/sql/supabase.sql (recommended).', 'ai-vector-search-semantic' ) . '

'; } else { echo '

' . '' . esc_html__( 'Paste into Supabase → SQL Editor.', 'ai-vector-search-semantic' ) . '

'; echo ''; echo ''; } echo '
'; echo '
'; settings_fields( 'aivesese' ); do_settings_sections( 'aivesese' ); submit_button(); echo '
'; }); add_submenu_page( 'options-general.php', 'AI Supabase Status', 'Supabase Status', 'manage_options', 'aivesese-status', 'aivesese_status_page' ); add_submenu_page( 'options-general.php', 'Sync Products to Supabase', 'Sync Products', 'manage_options', 'aivesese-sync', 'aivesese_sync_page' ); } ); // STATUS PAGE FUNCTION function aivesese_status_page() { echo '
'; echo '

' . esc_html__( 'AI Supabase Status', 'ai-vector-search-semantic' ) . '

'; $store_id = get_option('aivesese_store'); $url = get_option('aivesese_url'); $key = get_option('aivesese_key'); // Check if configuration is complete if (!$store_id || !$url || !$key) { echo '

'; echo esc_html__('Configuration incomplete! Please configure your Supabase settings first.', 'ai-vector-search-semantic'); echo ' ' . esc_html__( 'Go to Settings', 'ai-vector-search-semantic' ) . ''; echo '

'; echo '
'; return; } // Test connection and get health data $health = aivesese_request('POST', '/rest/v1/rpc/store_health_check', [ 'check_store_id' => $store_id ]); if (empty($health)) { echo '

'; echo esc_html__('Unable to connect to Supabase or no data found. Check your configuration and ensure the SQL has been installed.', 'ai-vector-search-semantic'); echo '

'; echo ''; return; } // Display health information $data = $health[0]; echo '

' . esc_html__('✅ Successfully connected to Supabase!', 'ai-vector-search-semantic') . '

'; echo '

' . esc_html__('Store Health Overview', 'ai-vector-search-semantic') . '

'; echo ''; echo ''; echo ''; $total = intval($data['total_products']); $published = intval($data['published_products']); $in_stock = intval($data['in_stock_products']); $with_embeddings = intval($data['with_embeddings']); echo ''; echo ''; echo ''; echo ''; echo '
MetricCountStatus
Total Products' . number_format($total) . '' . ($total > 0 ? '✅' : '⚠️') . '
Published Products' . number_format($published) . '' . ($published > 0 ? '✅' : '⚠️') . '
In Stock Products' . number_format($in_stock) . '' . ($in_stock > 0 ? '✅' : '⚠️') . '
With Embeddings' . number_format($with_embeddings) . ''; if ($with_embeddings == 0) { echo '❌ No embeddings found'; } elseif ($with_embeddings == $published) { echo '✅ All products have embeddings'; } else { $percent = round( ( $with_embeddings / $published ) * 100, 1 ); /* translators: %s: percentage (0–100) */ printf(esc_html__( '⚠️ %s%% coverage', 'ai-vector-search-semantic' ), esc_html( $percent )); } echo '
'; // Configuration summary echo '

' . esc_html__('Configuration Summary', 'ai-vector-search-semantic') . '

'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo '
Store ID' . esc_html($store_id) . '
Supabase URL' . esc_html($url) . '
Semantic Search' . (get_option('aivesese_semantic_toggle') === '1' ? '✅ Enabled' : '❌ Disabled') . '
OpenAI Key' . (get_option('aivesese_openai') ? '✅ Configured' : '❌ Not set') . '
'; // Quick actions echo '

' . esc_html__('Quick Actions', 'ai-vector-search-semantic') . '

'; echo '

'; echo '' . esc_html__( 'Configure Settings', 'ai-vector-search-semantic' ) . ' '; echo '' . esc_html__( 'Refresh Status', 'ai-vector-search-semantic' ) . ''; echo '

'; echo ''; } // PRODUCT SYNC PAGE FUNCTION function aivesese_sync_page() { echo '
'; echo '

' . esc_html__('Sync Products to Supabase', 'ai-vector-search-semantic') . '

'; // Check configuration $store_id = get_option('aivesese_store'); $url = get_option('aivesese_url'); $key = get_option('aivesese_key'); if (!$store_id || !$url || !$key) { echo '

'; echo esc_html__('Configuration incomplete! Please configure your Supabase settings first.', 'ai-vector-search-semantic'); echo ' Go to Settings'; echo '

'; echo '
'; return; } // Handle form submissions if (isset($_POST['action']) && check_admin_referer('aivesese_sync')) { $action_raw = wp_unslash( $_POST['action'] ); $action = sanitize_key( $action_raw ); switch ($action) { case 'sync_all': aivesese_handle_sync_all(); break; case 'sync_batch': $batch_size = isset( $_POST['batch_size'] ) ? absint( wp_unslash( $_POST['batch_size'] ) ) : 50; $offset = isset( $_POST['offset'] ) ? absint( wp_unslash( $_POST['offset'] ) ) : 0; aivesese_handle_sync_batch($batch_size, $offset); break; case 'generate_embeddings': aivesese_handle_generate_embeddings(); break; } } // Get product counts $total_products = wp_count_posts('product')->publish; $synced_count = aivesese_get_synced_count(); echo '

' . esc_html__('Sync Overview', 'ai-vector-search-semantic') . '

'; echo ''; echo ''; echo ''; echo ''; echo ''; echo '
WooCommerce Products' . number_format($total_products) . '
Synced to Supabase' . number_format($synced_count) . '
Sync Status'; if ($synced_count == 0) { echo '❌ No products synced'; } elseif ($synced_count >= $total_products) { echo '✅ All products synced'; } else { $percent = round( ( $synced_count / $total_products ) * 100, 1 ); /* translators: 1: percentage, 2: synced, 3: total */ printf( esc_html__( '⚠️ %1$s%% synced (%2$d/%3$d)', 'ai-vector-search-semantic' ), esc_html( $percent ), absint( $synced_count ), absint( $total_products ) ); } echo '
'; // Sync options echo '

' . esc_html__('Sync Actions', 'ai-vector-search-semantic') . '

'; echo '
'; echo '

🔄 Full Sync

'; echo '

Sync all WooCommerce products to Supabase. This may take a while for large catalogs.

'; echo '
'; wp_nonce_field('aivesese_sync'); echo ''; echo ''; echo '
'; echo '
'; echo '
'; echo '

⚡ Batch Sync

'; echo '

Sync products in smaller batches to avoid timeouts.

'; echo '
'; wp_nonce_field('aivesese_sync'); echo ''; echo ' '; echo ' '; echo ''; echo '
'; echo '
'; if (get_option('aivesese_semantic_toggle') === '1' && get_option('aivesese_openai')) { echo '
'; echo '

🧠 Generate Embeddings

'; echo '

Generate or update OpenAI embeddings for products that don\'t have them.

'; echo '
'; wp_nonce_field('aivesese_sync'); echo ''; echo ''; echo '
'; echo '
'; } // Progress/Status area echo '
'; echo ''; } /* ------------------------------------------------------------------------- * 2. Utilities * ------------------------------------------------------------------------- */ function aivesese_request( string $method, string $path, $body = null, array $query = [], ?string $cache_key = null, int $cache_ttl = 30 ) { if ( $cache_key ) { $hit = get_transient( $cache_key ); if ( false !== $hit ) { return $hit; } } $base = rtrim( get_option( 'aivesese_url' ), '/' ) . '/'; $url = $base . ltrim( $path, '/' ); if ( $query ) { $url = add_query_arg( $query, $url ); } $args = [ 'method' => $method, 'headers' => [ 'apikey' => get_option( 'aivesese_key' ), 'Authorization' => 'Bearer ' . get_option( 'aivesese_key' ), 'Content-Type' => 'application/json', ], 'timeout' => 12, ]; if ( $body ) { $args['body'] = wp_json_encode( $body ); } $res = wp_remote_request( $url, $args ); if ( is_wp_error( $res ) ) { return []; } // Check for HTTP errors $response_code = wp_remote_retrieve_response_code( $res ); if ( $response_code >= 400 ) { if (defined('WP_DEBUG') && WP_DEBUG) { error_log( 'AI Supabase API error (' . $response_code . '): ' . wp_remote_retrieve_body( $res ) ); } return []; } $out = json_decode( wp_remote_retrieve_body( $res ), true ) ?: []; if ( $cache_key ) { set_transient( $cache_key, $out, $cache_ttl ); } return $out; } function ai_openai_embed( string $text ): ?array { $key = trim( get_option( 'aivesese_openai' ) ); if ( ! $key ) { return null; } $args = [ 'method' => 'POST', 'timeout' => 12, 'headers' => [ 'Authorization' => 'Bearer ' . $key, 'Content-Type' => 'application/json', ], 'body' => wp_json_encode( [ 'model' => 'text-embedding-3-small', 'input' => $text, ] ), ]; $r = wp_remote_post( 'https://api.openai.com/v1/embeddings', $args ); if ( is_wp_error( $r ) ) { return null; } $j = json_decode( wp_remote_retrieve_body( $r ), true ); return $j['data'][0]['embedding'] ?? null; } /* ------------------------------------------------------------------------- * 3. Search helpers * ------------------------------------------------------------------------- */ function aivesese_fts_ids(string $term, int $limit = 20): array { $term = trim($term); if ($term === '' || mb_strlen($term) < 3) { return []; } // keep it reasonable $limit = max(1, min($limit, 50)); $store = get_option('aivesese_store'); if (!$store) { return []; } $params = [ 'select' => 'woocommerce_id', 'store_id' => 'eq.' . $store, // EITHER keep strict tsquery: // 'index_data' => 'fts.' . $term, // OR nicer, web-style search in 'simple' config: 'index_data' => 'wfts.simple.' . $term, 'order' => 'fts.rank().desc', 'limit' => $limit, ]; $cache_key = 'fts_' . $store . '_' . $limit . '_' . md5($term); $rows = aivesese_request('GET', '/rest/v1/search_indexes', null, $params, $cache_key, 20); return wp_list_pluck((array) $rows, 'woocommerce_id'); } function aivesese_semantic_ids( string $term, int $limit = 20 ): array { $store = get_option( 'aivesese_store' ); $embedding = ai_openai_embed( $term ); if ( ! $embedding ) { return []; } $rows = aivesese_request( 'POST', '/rest/v1/rpc/semantic_search', [ 'store_id' => $store, 'query_embedding' => $embedding, 'match_threshold' => 0.35, 'p_k' => $limit, ], [], 'sem_' . md5( $term ), 20 ); return wp_list_pluck( $rows, 'woocommerce_id' ); } function aivesese_search_ids( string $term, int $limit = 20 ): array { $use_sem = get_option( 'aivesese_semantic_toggle' ) === '1' && strlen( $term ) >= 3; if ( $use_sem ) { $ids = aivesese_semantic_ids( $term, $limit ); if ( $ids ) { return $ids; } } return aivesese_fts_ids( $term, $limit ); } /* ------------------------------------------------------------------------- * 4. Intercept product search (catalog + Woodmart live‑search) * ------------------------------------------------------------------------- */ add_action( 'pre_get_posts', function ( WP_Query $q ) { if ( is_admin() || 'product' !== $q->get( 'post_type' ) ) { return; } $s = $q->get( 's' ); if ( ! $s || strlen( $s ) < 3 ) { return; } $ids = aivesese_search_ids( $s ); if ( ! $ids ) { return; } $q->set( 'post__in', $ids ); $q->set( 'orderby', 'post__in' ); $q->set( 'posts_per_page', 20 ); }, 20 ); /* ------------------------------------------------------------------------- * 5. Mini‑cart recommendations * ------------------------------------------------------------------------- */ add_filter('woocommerce_cart_item_name', function ($name, $cart_item) { if (get_option('aivesese_enable_cart_below', '1') !== '1') { return $name; } if (! did_action('aivesese_recs_once')) { add_action('woocommerce_after_cart', 'aivesese_render_recs'); do_action('aivesese_recs_once'); } return $name; }, 10, 2); function aivesese_render_recs() { if (get_option('aivesese_enable_cart_below', '1') !== '1') { return; } $store = get_option('aivesese_store'); if (!$store || !function_exists('WC')) { return; } $cart_items = WC()->cart->get_cart_contents(); $cart_ids = array_map(function ($ci) { return $ci['product_id']; }, $cart_items); $rows = aivesese_request('POST', '/rest/v1/rpc/get_recommendations', [ 'store_id' => $store, 'cart' => $cart_ids, 'p_k' => 4, ], [], 'recs_' . md5(wp_json_encode($cart_ids)), 60); if (empty($rows)) { return; } echo '

You might also like

'; } /* ------------------------------------------------------------------------- * 6. PDP "Similar products" * ------------------------------------------------------------------------- */ if (get_option('aivesese_enable_pdp_similar', '1') === '1') { /* Preserve the order of our Supabase-ranked IDs */ add_filter('woocommerce_related_products_args', function ($args) { $args['orderby'] = 'post__in'; $args['posts_per_page'] = isset($args['posts_per_page']) ? (int) $args['posts_per_page'] : 8; return $args; }, 20); /** * Replace Woo's related-products list with Supabase "similar_products". */ add_filter('woocommerce_related_products', function ($related, $product_id, $args) { if (get_option('aivesese_enable_pdp_similar', '1') !== '1') { return $related; } $limit = isset($args['posts_per_page']) ? (int) $args['posts_per_page'] : 4; $rows = aivesese_request('POST', '/rest/v1/rpc/similar_products', [ 'prod_wc_id' => $product_id, 'k' => $limit, ], [], 'aivesese_sim_' . $product_id, 300); $ids = wp_list_pluck((array) $rows, 'woocommerce_id'); return !empty($ids) ? $ids : $related; }, 10, 3); } // Save open/closed state per user (1=open, 0=closed) add_action('wp_ajax_aivesese_toggle_help', function () { check_ajax_referer('aivesese_help_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'forbidden'), 403); } $open = (isset($_POST['open']) && wp_unslash($_POST['open']) === '1') ? '1' : '0'; update_user_meta(get_current_user_id(), '_aivesese_help_open', $open); wp_send_json_success(array('open' => $open)); }); add_action('admin_enqueue_scripts', function($hook){ $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; if ( ! in_array( $page, array('aivesese', 'aivesese-status', 'aivesese-sync'), true ) ) { return; } // Keep the small "help toggle" bootstrap in its own handle wp_register_script('aivesese-help', false, array(), defined('AIVESESE_PLUGIN_VERSION') ? AIVESESE_PLUGIN_VERSION : false, true); wp_enqueue_script('aivesese-help'); $data = array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aivesese_help_nonce'), ); wp_add_inline_script('aivesese-help', 'window.AISupabaseHelp=' . wp_json_encode($data) . ';', 'before'); wp_add_inline_script('aivesese-help', "(function(){ var el = document.getElementById('ai-supabase-help-details'); if (!el) return; el.addEventListener('toggle', function(){ var body = new FormData(); body.append('action', 'aivesese_toggle_help'); body.append('open', el.open ? '1' : '0'); body.append('nonce', window.AISupabaseHelp.nonce); fetch(window.AISupabaseHelp.ajax_url, { method:'POST', credentials:'same-origin', body: body }); }, { passive: true }); })();"); // --- Admin CSS (migrated from inline