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 '
';
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 ' Better relevance (needs OpenAI key) ';
}, 'aivesese', 'aivesese_section' );
// Checkbox for auto-sync
add_settings_field( 'aivesese_auto_sync', 'Auto-sync products', function () {
$val = get_option( 'aivesese_auto_sync' );
echo ' Automatically sync products when saved/updated ';
}, '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 ' Show similar products on product pages ';
}, '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 ' Show recommendations under cart ';
}, '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 '' . sprintf( esc_html__( 'Go to your Supabase project dashboard at %s.', 'ai-vector-search-semantic' ), 'https://app.supabase.io/ ' ) . ' ';
echo '' . esc_html__( 'Navigate to "Project Settings" > "API".', 'ai-vector-search-semantic' ) . ' ';
echo '' . esc_html__( 'Your Supabase URL is the "URL" value (e.g., https://xyz.supabase.co).', 'ai-vector-search-semantic' ) . ' ';
echo '' . 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' ) . ' ';
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 '' . esc_html__( 'Open your Supabase project → SQL Editor → New query.', 'ai-vector-search-semantic' ) . ' ';
echo '' . esc_html__( 'Click "Copy SQL" below and paste it into the editor.', 'ai-vector-search-semantic' ) . ' ';
echo '' . esc_html__( 'Press RUN and wait for success.', 'ai-vector-search-semantic' ) . ' ';
echo '' . esc_html__( 'Verify tables/views, RPCs and extensions were created.', 'ai-vector-search-semantic' ) . ' ';
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 'Copy SQL ' .
'' .
esc_html__( 'Paste into Supabase → SQL Editor.', 'ai-vector-search-semantic' ) .
'
';
echo '';
echo '
';
}
echo ' ';
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 '
';
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 'Metric Count Status ';
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 'Total Products ' . number_format($total) . ' ' . ($total > 0 ? '✅' : '⚠️') . ' ';
echo 'Published Products ' . number_format($published) . ' ' . ($published > 0 ? '✅' : '⚠️') . ' ';
echo 'In Stock Products ' . number_format($in_stock) . ' ' . ($in_stock > 0 ? '✅' : '⚠️') . ' ';
echo '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 ' ';
echo '
';
// Configuration summary
echo '' . esc_html__('Configuration Summary', 'ai-vector-search-semantic') . ' ';
echo '';
echo '';
echo 'Store ID ' . esc_html($store_id) . ' ';
echo 'Supabase URL ' . esc_html($url) . ' ';
echo 'Semantic Search ' . (get_option('aivesese_semantic_toggle') === '1' ? '✅ Enabled' : '❌ Disabled') . ' ';
echo 'OpenAI Key ' . (get_option('aivesese_openai') ? '✅ Configured' : '❌ Not set') . ' ';
echo '
';
// 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 'WooCommerce Products ' . number_format($total_products) . ' ';
echo 'Synced to Supabase ' . number_format($synced_count) . ' ';
echo '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 ' ';
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 '
';
echo '
';
echo '';
echo '
⚡ Batch Sync ';
echo '
Sync products in smaller batches to avoid timeouts.
';
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 '
';
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