true, "count" => $count, "type" => "kb"]));\n }\n }\n\n public static function rebuild(): int {\n $llmTxt = get_option("woo_ai_llm_txt", "");\n $feedPath = WOO_AI_FEED_PATH;\n $products = file_exists($feedPath) ? file($feedPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) : [];\n\n $chunks = [];\n \n if ($llmTxt) {\n $sections = preg_split("/\n#+\s*/", $llmTxt, -1, PREG_SPLIT_NO_EMPTY);\n foreach ($sections as $section) {\n $text = trim($section);\n if (!$text) continue;\n $words = explode(" ", $text);\n if (count($words) <= self::CHUNK_SIZE) {\n $chunks[] = ["text" => $text, "source" => "llm"];\n } else {\n for ($i = 0; $i < count($words); $i += self::CHUNK_SIZE - self::CHUNK_OVERLAP) {\n $chunkWords = array_slice($words, $i, self::CHUNK_SIZE);\n $chunks[] = ["text" => implode(" ", $chunkWords), "source" => "llm"];\n }\n }\n }\n }\n\n foreach ($products as $line) {\n $item = json_decode($line, true);\n if (!$item) continue;\n $text = sprintf(\n "%s | %s | Price: %s | %s | %s",\n $item["t"] ?? "",\n $item["c"] ?? "",\n $item["p"] ?? "",\n $item["d"] ?? "",\n home_url("/" . ltrim($item["u"] ?? "", "/"))\n );\n $chunks[] = ["text" => $text, "source" => "product", "id" => $item["id"] ?? 0];\n }\n\n $embeddings = [];\n $batchSize = 20;\n for ($i = 0; $i < count($chunks); $i += $batchSize) {\n $batch = array_slice($chunks, $i, $batchSize);\n $texts = array_column($batch, "text");\n $vectors = self::getEmbeddings($texts);\n if ($vectors) {\n foreach ($vectors as $j => $vec) {\n $embeddings[] = [\n "text" => $batch[$j]["text"],\n "source" => $batch[$j]["source"] ?? "llm",\n "id" => $batch[$j]["id"] ?? 0,\n "vector" => $vec\n ];\n }\n }\n if ($i + $batchSize < count($chunks)) {\n usleep(50000);\n }\n }\n\n update_option(self::CACHE_KEY, $embeddings, false);\n update_option("woo_ai_kb_updated", time(), false);\n\n return count($embeddings);\n }\n\n private static function getEmbeddings(array $texts): ?array {\n $key = get_option("woo_ai_gemini_key", "");\n if (!$key || empty($texts)) return null;\n\n $ch = curl_init("https://generativelanguage.googleapis.com/v1beta/models/" . self::EMBEDDING_MODEL . ":batchEmbedContents?key=" . $key);\n \n $requests = [];\n foreach ($texts as $text) {\n $requests[] = ["model" => "models/" . self::EMBEDDING_MODEL, "content" => ["parts" => [["text" => mb_substr($text, 0, 2000)]]]];\n }\n\n curl_setopt_array($ch, [\n CURLOPT_RETURNTRANSFER => true,\n CURLOPT_POST => true,\n CURLOPT_HTTPHEADER => ["Content-Type: application/json"],\n CURLOPT_POSTFIELDS => json_encode(["requests" => $requests]),\n CURLOPT_TIMEOUT => 30\n ]);\n\n $raw = curl_exec($ch);\n $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);\n curl_close($ch);\n\n if ($code !== 200 || !$raw) return null;\n $data = json_decode($raw, true);\n \n $vectors = [];\n foreach ($data["embeddings"] ?? [] as $emb) {\n $vectors[] = $emb["values"] ?? null;\n }\n return !empty($vectors) ? $vectors : null;\n }\n\n public static function search(string $query, int $topK = 5): array {\n $cache = get_option(self::CACHE_KEY, []);\n if (empty($cache)) return [];\n\n $queryVec = self::getQueryEmbedding($query);\n if (!$queryVec) return [];\n\n $scored = [];\n foreach ($cache as $chunk) {\n if (!isset($chunk["vector"])) continue;\n $score = self::cosineSimilarity($queryVec, $chunk["vector"]);\n $scored[] = ["score" => $score, "chunk" => $chunk];\n }\n\n usort($scored, fn($a, $b) => $b["score"] <=> $a["score"]);\n return array_slice($scored, 0, $topK);\n }\n\n private static function getQueryEmbedding(string $query): ?array {\n $key = get_option("woo_ai_gemini_key", "");\n if (!$key) return null;\n\n $ch = curl_init("https://generativelanguage.googleapis.com/v1beta/models/" . self::EMBEDDING_MODEL . ":embedContent?key=" . $key);\n curl_setopt_array($ch, [\n CURLOPT_RETURNTRANSFER => true,\n CURLOPT_POST => true,\n CURLOPT_HTTPHEADER => ["Content-Type: application/json"],\n CURLOPT_POSTFIELDS => json_encode(["content" => ["parts" => [["text" => $query]]]]),\n CURLOPT_TIMEOUT => 10\n ]);\n\n $raw = curl_exec($ch);\n $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);\n curl_close($ch);\n\n if ($code !== 200 || !$raw) return null;\n $data = json_decode($raw, true);\n return $data["embedding"]["values"] ?? null;\n }\n\n private static function cosineSimilarity(array $a, array $b): float {\n $dot = 0.0; $normA = 0.0; $normB = 0.0;\n $len = min(count($a), count($b));\n for ($i = 0; $i < $len; $i++) {\n $dot += $a[$i] * $b[$i];\n $normA += $a[$i] * $a[$i];\n $normB += $b[$i] * $b[$i];\n }\n $denom = sqrt($normA) * sqrt($normB);\n return $denom > 0 ? $dot / $denom : 0.0;\n }\n}\n\n// ======================= INDEXER =======================\nclass WooAI_JSONL_Indexer {\n private static function getFilePath(): string {\n return WOO_AI_FEED_PATH;\n }\n\n public static function init(): void {\n add_action("woocommerce_update_product", [self::class, "scheduleRebuild"], 20);\n add_action("woocommerce_new_product", [self::class, "scheduleRebuild"], 20);\n add_action("woocommerce_product_set_stock_status", [self::class, "scheduleRebuild"], 20);\n add_action("before_delete_post", [self::class, "scheduleRebuild"], 20);\n add_action("woo_ai_rebuild_jsonl_cron", [self::class, "buildIndex"]);\n add_action("admin_init", [self::class, "handleManualTrigger"]);\n }\n\n public static function scheduleRebuild(): void {\n if (!wp_next_scheduled("woo_ai_rebuild_jsonl_cron")) {\n wp_schedule_single_event(time() + 30, "woo_ai_rebuild_jsonl_cron");\n }\n }\n\n public static function handleManualTrigger(): void {\n if (isset($_GET["rebuild_ai_feed"]) && current_user_can("manage_options")) {\n $count = self::buildIndex();\n wp_die(json_encode(["success" => true, "count" => $count, "path" => self::getFilePath()]));\n }\n }\n\n public static function buildIndex(): int {\n $products = get_posts([\n "post_type" => "product",\n "post_status" => "publish",\n "numberposts" => -1,\n "fields" => "ids",\n ]);\n\n if (empty($products)) return 0;\n\n $currency = get_woocommerce_currency();\n $lines = [];\n $lang = get_locale();\n $isAr = str_starts_with($lang, "ar");\n\n foreach ($products as $id) {\n $product = wc_get_product($id);\n if (!$product) continue;\n\n $title = self::cleanText($product->get_name());\n\n $cats = wp_strip_all_tags(wc_get_product_category_list($id, ", "));\n $tags = wp_strip_all_tags(wc_get_product_tag_list($id, " "));\n\n if ($product->is_type("variable")) {\n $min = $product->get_variation_price("min", true);\n $max = $product->get_variation_price("max", true);\n $priceStr = ($min === $max) ? "{$min} {$currency}" : "{$min}-{$max} {$currency}";\n } else {\n $price = $product->get_price();\n $reg = $product->get_regular_price();\n $priceStr = ($product->is_on_sale() && $reg)\n ? "{$price} {$currency} (was {$reg})"\n : "{$price} {$currency}";\n }\n\n $attrSummary = [];\n foreach ($product->get_attributes() as $attr) {\n $name = wc_attribute_label($attr->get_name());\n $vals = $product->get_attribute($attr->get_name());\n if ($vals) {\n $attrSummary[] = "{$name}: {$vals}";\n }\n }\n\n $shortDesc = self::cleanText($product->get_short_description() ?: $product->get_description());\n $shortDesc = mb_substr($shortDesc, 0, 140);\n $specLine = trim(implode(" | ", $attrSummary) . ($shortDesc ? " - {$shortDesc}" : ""), " -|");\n\n $relUrl = wp_make_link_relative($product->get_permalink());\n\n $entry = [\n "id" => $id,\n "t" => $title,\n "c" => $cats ?: "General",\n "p" => $priceStr,\n "s" => $product->is_in_stock() ? "in" : "out",\n "k" => mb_strtolower(trim("{$product->get_sku()} {$tags}")),\n "u" => $relUrl,\n "d" => $specLine,\n "ar" => $isAr ? 1 : 0,\n ];\n\n $lines[] = json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);\n }\n\n $finalPath = self::getFilePath();\n $tmpPath = $finalPath . ".tmp";\n\n file_put_contents($tmpPath, implode("\n", $lines));\n rename($tmpPath, $finalPath);\n\n return count($lines);\n }\n\n private static function cleanText(string $text): string {\n $text = strip_shortcodes($text);\n $text = wp_strip_all_tags($text);\n $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, "UTF-8");\n return trim(preg_replace("/\s+/u", " ", $text));\n }\n}\n\n// ======================= SESSIONS =======================\nclass WooAI_Session_Store {\n private const LIMIT = 4; // keep last 4 turns\n\n public static function get(string $sessionId): array {\n $key = "woo_ai_session_" . md5($sessionId);\n $raw = get_option($key, []);\n return is_array($raw) ? $raw : [];\n }\n\n public static function set(string $sessionId, array $history): void {\n $key = "woo_ai_session_" . md5($sessionId);\n $trimmed = array_slice($history, -self::LIMIT);\n update_option($key, $trimmed, false);\n }\n\n public static function add(string $sessionId, array $turn): void {\n $history = self::get($sessionId);\n $history[] = $turn;\n self::set($sessionId, $history);\n }\n\n public static function clear(string $sessionId): void {\n $key = "woo_ai_session_" . md5($sessionId);\n delete_option($key);\n }\n\n public static function cleanupOld(int $maxAgeSeconds = 86400): int {\n global $wpdb;\n $like = $wpdb->esc_like("woo_ai_session_") . "%";\n $rows = $wpdb->get_results($wpdb->prepare("SELECT option_id, option_name, UNIX_TIMESTAMP(option_time) AS ts FROM {$wpdb->options} WHERE option_name LIKE %s", $like));\n $cutoff = time() - $maxAgeSeconds;\n $count = 0;\n foreach ($rows as $row) {\n if ((int) $row->ts < $cutoff) {\n $wpdb->delete($wpdb->options, ["option_id" => $row->option_id]);\n $count++;\n }\n }\n return $count;\n }\n\n public static function listAll(): array {\n global $wpdb;\n $like = $wpdb->esc_like("woo_ai_session_") . "%";\n $rows = $wpdb->get_results($wpdb->prepare("SELECT option_name, option_length FROM {$wpdb->options} WHERE option_name LIKE %s ORDER BY option_id DESC", $like));\n $out = [];\n foreach ($rows as $row) {\n $id = str_replace("woo_ai_session_", "", $row->option_name);\n $history = get_option($row->option_name, []);\n $last = 0;\n if (is_array($history)) {\n foreach (array_reverse($history) as $turn) {\n if (isset($turn["ts"])) { $last = (int) $turn["ts"]; break; }\n }\n }\n $out[] = [\n "id" => $id,\n "turns" => is_array($history) ? count($history) : 0,\n "last_active" => $last ? date("Y-m-d H:i", $last) : "-",\n "size" => (int) $row->option_length,\n "history" => is_array($history) ? $history : [],\n ];\n }\n return $out;\n }\n}\n\n// ======================= BRAIN =======================\nclass EnterpriseWooAgent {\n private array $models;\n private string $feedPath;\n private string $llmTxt;\n\n public function __construct() {\n $this->feedPath = WOO_AI_FEED_PATH;\n $this->llmTxt = get_option(WOO_AI_LLM_TXT_OPTION, "");\n\n $this->models = [];\n $providers = get_option("woo_ai_providers", []);\n if (!is_array($providers)) $providers = [];\n\n // Default free models if none configured\n if (empty($providers)) {\n $providers = [\n ["name" => "Groq", "url" => "https://api.groq.com/openai/v1/chat/completions", "key" => "", "model" => "llama3-8b-8192"],\n ["name" => "Gemini", "url" => "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", "key" => "", "model" => "gemini-3.8-flash"],\n ];\n }\n foreach ($providers as $p) {\n if (!empty($p["key"]) && !empty($p["url"]) && !empty($p["model"])) {\n $this->models[] = [\n "name" => $p["name"],\n "url" => $p["url"],\n "key" => $p["key"],\n "model" => $p["model"],\n ];\n }\n }\n }\n\n private function searchProducts(string $query, int $limit = 6): string {\n if (!file_exists($this->feedPath)) return "No catalog available.";\n\n $cleanQuery = mb_strtolower(trim(preg_replace("/[^\p{L}\p{N}\s]+/u", " ", $query)));\n $words = array_filter(explode(" ", $cleanQuery), fn($w) => mb_strlen($w) >= 2);\n\n // Arabic-to-English mapping for common cosmetics/hosting terms\n $arToEn = [\n "شعر" => "hair", "بشرة" => "skin", "جسم" => "body", "وجه" => "face",\n "عين" => "eye", "شفت" => "lip", "اسنان" => "teeth", "جفون" => "eyelids",\n "ميو" => "meso", "ميسو" => "meso", "ناد" => "nad", "كيو" => "q10",\n "مساج" => "massage", "سكري" => "sugar", "دايت" => "diet", "رجيم" => "diet",\n "تنحيف" => "slim", "تسمين" => "gain", "نحت" => "sculpting",\n "مبيض" => "whitening", "تفتيح" => "lightening", "ترطيب" => "hydration",\n "تجميل" => "beauty", "عنايه" => "care", "عناية" => "care",\n "ميه" => "water", "ماء" => "water", " Collier" => "collagen",\n "كولاجين" => "collagen", "فيتامين" => "vitamin", "هيالورونيك" => "hyaluronic",\n "رتينول" => "retinol", "بوتكس" => "botox", "بوتوكس" => "botox",\n "فلور" => "fluor", "فلورا" => "flora", "بكتيريا" => "bacteria",\n "منظف" => "cleanser", "تونر" => "toner", "سيروم" => "serum",\n "كريم" => "cream", "جلك" => "gel", "ماسك" => "mask",\n "صبغة" => "color", "شامبو" => "shampoo", "بلسم" => "conditioner",\n "زيت" => "oil", "كريم" => "cream", "لوشن" => "lotion",\n ];\n\n $enWords = $words;\n foreach ($words as $word) {\n if (preg_match("/\p{Arabic}/u", $word)) {\n foreach ($arToEn as $ar => $en) {\n if (mb_strpos($word, $ar) !== false) {\n $enWords[] = mb_strtolower($en);\n }\n }\n }\n }\n\n $lines = file($this->feedPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);\n $scored = [];\n\n foreach ($lines as $line) {\n $item = json_decode($line, true);\n if (!$item) continue;\n\n $haystack = mb_strtolower($item["t"] . " " . $item["c"] . " " . ($item["k"] ?? "") . " " . ($item["d"] ?? ""));\n $score = 0;\n\n foreach ($enWords as $word) {\n if (mb_strpos($haystack, $word) !== false) {\n $score += (mb_strpos(mb_strtolower($item["t"]), $word) !== false) ? 3 : 1;\n }\n }\n\n if ($score > 0) {\n $scored[] = ["score" => $score, "item" => $item];\n }\n }\n\n usort($scored, fn($a, $b) => $b["score"] <=> $a["score"]);\n\n $selected = array_slice($scored, 0, $limit);\n if (empty($selected)) {\n foreach (array_slice($lines, 0, 5) as $line) {\n $selected[] = ["item" => json_decode($line, true)];\n }\n }\n\n $out = "ID|Title|Category|Price|Stock|Spec|URL\n";\n foreach ($selected as $row) {\n $p = $row["item"];\n $url = home_url("/" . ltrim($p["u"], "/"));\n $out .= "{$p["id"]}|{$p["t"]}|{$p["c"]}|{$p["p"]}|{$p["s"]}|{$p["d"]}|{$url}\n";\n }\n return $out;\n }\n\n private function detectLang(string $text): string {\n $ar = preg_match_all("/\p{Arabic}/u", $text, $m);\n return ($ar > 0) ? "Arabic" : "English";\n }\n\n public function ask(string $userMessage, array $chatHistory = [], string $sessionId = "", string $lang = "", string $userName = ""): string {\n $llmTxt = $this->llmTxt ?: "";\n $matchedProducts = $this->searchProducts($userMessage, 6);\n $agentName = get_option("woo_ai_agent_name", "Assistant");\n $effectiveLang = $lang ?: $this->detectLang($userMessage);\n $askNameEnabled = get_option("woo_ai_ask_name", "1") === "1";\n\n if ($askNameEnabled && empty($userName)) {\n $askNameRule = "If you do not know the user's name yet, ask for it naturally in your first reply and remember it for the rest of the conversation.";\n } else if ($askNameEnabled && !empty($userName)) {\n $askNameRule = "The user's name is " . $userName . ". Use their name naturally in your replies.";\n } else {\n $askNameRule = "";\n }\n\n if (!empty($userName)) {\n $nameRule = "Personalization: address the user as " . $userName . " when appropriate.";\n } else {\n $nameRule = "";\n }\n\n $productCount = count(file(WOO_AI_FEED_PATH, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES));\n\n $orderData = null;\n $orderKeywords = ["order status", "track order", "where is my order", "my order", "order id", "order #", "tracking", " shipment"];\n foreach ($orderKeywords as $kw) {\n if (stripos($userMessage, $kw) !== false) {\n preg_match("/#?\d+/", $userMessage, $m);\n $oid = $m[0] ?? "";\n preg_match("/[\w.-]+@[\w.-]+\.\w+/", $userMessage, $em);\n $email = $em[0] ?? "";\n if ($oid || $email) {\n $orderData = WooAI_Tools::getOrder(ltrim($oid, "#"), $email ?: null);\n }\n break;\n }\n }\n\n $cartAction = null;\n if (preg_match("/(add|put|move|buy)\s+(?:this\s+)?(?:product\s+)?(?:to\s+)?(?:my\s+)?(?:cart|bag)/i", $userMessage, $m)) {\n $productTitle = trim(preg_replace("/" . preg_quote($m[0], "/") . "/i", "", $userMessage));\n $products = WooAI_Tools::searchProducts($productTitle ?: $userMessage, 1);\n if (!empty($products)) {\n if (null === WC()->cart) {\n wc_load_cart();\n }\n $cartAction = WooAI_Tools::addToCart($products[0]["id"], 1);\n }\n }\n\n $contextBlock = "";\n if ($orderData) {\n $contextBlock .= "=== ORDER STATUS ===\n";\n $contextBlock .= "Order #" . $orderData["id"] . " | Status: " . $orderData["status"] . " | Total: " . $orderData["total"] . " | Date: " . $orderData["date"] . "\n";\n $contextBlock .= "Items: " . implode(", ", $orderData["items"]) . "\n\n";\n }\n if ($cartAction) {\n $contextBlock .= "=== CART ACTION ===\n";\n $contextBlock .= "The item has been added to the user's cart. Confirm this clearly and naturally. Cart count: " . $cartAction["cart_count"] . "\n\n";\n }\n\n $systemStatic = << "system", "content" => $systemStatic]];\n\n foreach (array_slice($chatHistory, -4) as $turn) {\n $messages[] = $turn;\n }\n $messages[] = ["role" => "user", "content" => $userWithContext];\n\n // Try each configured provider in order\n foreach ($this->models as $model) {\n $res = $this->callOpenAICompatible(\n $model["url"],\n $model["key"],\n $model["model"],\n $messages\n );\n if ($res !== false) {\n // Save session\n if ($sessionId) {\n WooAI_Session_Store::add($sessionId, ["role" => "user", "content" => $userMessage]);\n WooAI_Session_Store::add($sessionId, ["role" => "assistant", "content" => $res]);\n }\n return $res;\n }\n }\n\n return "Sorry, our assistant is momentarily unavailable. Please try again.";\n }\n\n private function callOpenAICompatible(string $url, string $key, string $model, array $messages): string|false {\n if (empty($key)) return false;\n\n $ch = curl_init($url);\n curl_setopt_array($ch, [\n CURLOPT_RETURNTRANSFER => true,\n CURLOPT_POST => true,\n CURLOPT_HTTPHEADER => [\n "Content-Type: application/json",\n "Authorization: Bearer " . $key,\n ],\n CURLOPT_POSTFIELDS => json_encode([\n "model" => $model,\n "messages" => $messages,\n "temperature" => 0.1,\n "max_tokens" => 800,\n ]),\n CURLOPT_TIMEOUT => 8,\n ]);\n\n $raw = curl_exec($ch);\n $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);\n curl_close($ch);\n\n if ($code !== 200 || !$raw) {\n error_log("WooAI API error HTTP=" . $code . " response=" . substr($raw, 0, 200));\n return false;\n }\n $data = json_decode($raw, true);\n if (isset($data["error"])) {\n error_log("WooAI API error msg=" . json_encode($data["error"]));\n return false;\n }\n return $data["choices"][0]["message"]["content"] ?? false;\n }\n}\n\n// ======================= WOOCOMMERCE TOOLS =======================\nclass WooAI_Tools {\n public static function searchProducts(string $query, int $limit = 5): array {\n $results = [];\n if (!class_exists("WooCommerce")) return $results;\n $args = [\n "post_type" => "product",\n "post_status" => "publish",\n "posts_per_page" => $limit,\n "s" => $query,\n "fields" => "ids",\n ];\n $ids = get_posts($args);\n foreach ($ids as $pid) {\n $p = wc_get_product($pid);\n if (!$p) continue;\n $results[] = [\n "id" => $p->get_id(),\n "title" => $p->get_name(),\n "price" => $p->get_price() ? wc_price($p->get_price()) : "",\n "url" => get_permalink($p->get_id()),\n "stock" => $p->is_in_stock() ? "In stock" : "Out of stock",\n "add_to_cart_url" => $p->add_to_cart_url(),\n ];\n }\n return $results;\n }\n\n public static function getOrder(string $orderId, ?string $email = null): ?array {\n if (!class_exists("WooCommerce")) return null;\n $order = null;\n if ($orderId) {\n $order = wc_get_order($orderId);\n }\n if (!$order && $email) {\n $orders = wc_get_orders(["billing_email" => $email, "limit" => 1]);\n $order = $orders[0] ?? null;\n }\n if (!$order) return null;\n return [\n "id" => $order->get_id(),\n "status" => wc_get_order_status_name($order->get_status()),\n "total" => $order->get_total(),\n "date" => $order->get_date_created() ? $order->get_date_created()->date("Y-m-d H:i") : "",\n "items" => array_map(function($item) {\n return $item->get_name() . " x" . $item->get_quantity();\n }, $order->get_items()),\n ];\n }\n\n public static function addToCart(int $productId, int $quantity = 1, int $variationId = 0): array {\n if (!class_exists("WooCommerce")) return ["ok" => false, "msg" => "WooCommerce missing"];\n $added = WC()->cart->add_to_cart($productId, $quantity, $variationId);\n if (!$added) return ["ok" => false, "msg" => "Failed to add to cart"];\n return ["ok" => true, "msg" => "Added to cart", "cart_count" => WC()->cart->get_cart_contents_count()];\n }\n\n public static function getCartCount(): int {\n if (!class_exists("WooCommerce") || !WC()->cart) return 0;\n return (int) WC()->cart->get_cart_contents_count();\n }\n}\n\n// ======================= WOOCOMMERCE REST TOOLS =======================\nadd_action("rest_api_init", function () {\n register_rest_route("woo-ai/v1/wc", "/order-status", [\n "methods" => "POST",\n "callback" => "woo_ai_wc_order_status",\n "permission_callback" => "__return_true",\n ]);\n register_rest_route("woo-ai/v1/wc", "/add-to-cart", [\n "methods" => "POST",\n "callback" => "woo_ai_wc_add_to_cart",\n "permission_callback" => "__return_true",\n ]);\n});\n\nfunction woo_ai_wc_order_status($request) {\n $params = $request->get_json_params();\n $orderId = sanitize_text_field($params["order_id"] ?? "");\n $email = sanitize_email($params["email"] ?? "");\n if (empty($orderId) && empty($email)) {\n return new WP_Error("missing", "Provide order_id or email", ["status" => 400]);\n }\n $data = WooAI_Tools::getOrder($orderId, $email ?: null);\n if (!$data) return new WP_Error("not_found", "Order not found", ["status" => 404]);\n return rest_ensure_response($data);\n}\n\nfunction woo_ai_wc_add_to_cart($request) {\n $params = $request->get_json_params();\n $productId = (int) ($params["product_id"] ?? 0);\n $quantity = (int) ($params["quantity"] ?? 1);\n $variationId = (int) ($params["variation_id"] ?? 0);\n if (!$productId) return new WP_Error("missing", "product_id required", ["status" => 400]);\n\n // Ensure cart is initialized in REST context\n if (null === WC()->cart) {\n wc_load_cart();\n }\n\n // Set cart cookies so frontend sees updated cart count\n if (WC()->cart && WC()->cart->get_cart()) {\n wc_setcookie("woocommerce_items_in_cart", 1);\n wc_setcookie("woocommerce_cart_hash", md5(json_encode(WC()->cart->get_cart())));\n }\n\n $res = WooAI_Tools::addToCart($productId, $quantity, $variationId);\n return rest_ensure_response($res);\n}\n\n// ======================= REST API =======================\nadd_action("rest_api_init", function () {\n register_rest_route("woo-ai/v1", "/chat", [\n "methods" => "POST",\n "callback" => "woo_ai_chat_handler",\n "permission_callback" => "__return_true",\n ]);\n});\n\nfunction woo_ai_chat_handler($request) {\n $params = $request->get_json_params();\n $message = sanitize_text_field($params["message"] ?? "");\n $session = sanitize_text_field($params["session"] ?? "");\n $lang = sanitize_text_field($params["lang"] ?? "");\n $name = sanitize_text_field($params["name"] ?? "");\n\n if (empty($message)) {\n return new WP_Error("empty_message", "Message is required", ["status" => 400]);\n }\n\n $history = [];\n if ($session) {\n $history = WooAI_Session_Store::get($session);\n }\n\n $agent = new EnterpriseWooAgent();\n $reply = $agent->ask($message, $history, $session ?: md5(($_SERVER["REMOTE_ADDR"] ?? "0") . time()), $lang, $name);\n\n return rest_ensure_response([\n "reply" => $reply,\n "session" => $session ?: "",\n ]);\n}\n\nadd_action("rest_api_init", function () {\n register_rest_route("woo-ai/v1", "/session/clear", [\n "methods" => "POST",\n "callback" => "woo_ai_session_clear_handler",\n "permission_callback" => "__return_true",\n ]);\n});\n\nfunction woo_ai_session_clear_handler($request) {\n $session = sanitize_text_field($request->get_json_params()["session"] ?? "");\n if ($session) {\n WooAI_Session_Store::clear($session);\n }\n return rest_ensure_response(["ok" => true]);\n}\n\n// ======================= WIDGET (auto-inject or shortcode) =======================\nadd_action("wp_footer", "woo_ai_auto_inject_widget");\n\nfunction woo_ai_auto_inject_widget() {\n $auto = get_option("woo_ai_widget_auto_inject", "1");\n if ($auto !== "1") return;\n\n // Avoid double injection if shortcode is used on the same page\n if (did_action("woo_ai_chat_shortcode_rendered")) return;\n\n echo do_shortcode("[woo_ai_chat]");\n}\n\nadd_action("woocommerce_before_shop_loop_item_title", function () {\n // Mark that product archive is rendering, we can still inject widget\n}, 1);\n\nadd_shortcode("woo_ai_chat", function () {\n // Prevent auto-inject on same page when shortcode is explicitly used\n add_filter("woo_ai_auto_inject_widget_disable", "__return_true");\n do_action("woo_ai_chat_shortcode_rendered");\n\n wp_enqueue_script("woo-ai-chat", plugin_dir_url(__FILE__) . "chat-widget.js", [], "2.0", true);\n wp_localize_script("woo-ai-chat", "wooAi", [\n "endpoint" => rest_url("woo-ai/v1/chat"),\n "nonce" => wp_create_nonce("wp_rest"),\n "title" => $widgetTitle,\n "placeholder" => get_option("woo_ai_widget_placeholder", "Ask anything..."),\n "agentName" => get_option("woo_ai_agent_name", "Assistant"),\n "welcomeMessage" => get_option("woo_ai_welcome_message", "Hi there 👋 How can I help?"),\n "chips" => array_map(function($line){\n $parts = explode("|", $line, 2);\n return ["label"=>$parts[0]?? "", "msg"=>$parts[1]?? ""];\n }, preg_split("/\r?\n|\\n/", $chips)),\n "langSelector" => $langSelector === "1",\n "askName" => $askName === "1",\n "productCount" => 11,\n "userLoggedIn" => is_user_logged_in(),\n "userName" => is_user_logged_in() ? wp_get_current_user()->display_name : "",\n ]);\n return '
';\n});\n\n// ======================= ADMIN MENU =======================\nadd_action("admin_menu", function () {\n add_submenu_page(\n "woocommerce",\n "AI Agent",\n "AI Agent",\n "manage_options",\n "woo-ai-agent",\n "woo_ai_admin_page"\n );\n});\n\nfunction woo_ai_admin_page() {\n if (isset($_POST["woo_ai_save_keys"]) && check_admin_referer("woo_ai_save")) {\n update_option("woo_ai_groq_key", sanitize_text_field($_POST["groq_key"]));\n update_option("woo_ai_gemini_key", sanitize_text_field($_POST["gemini_key"]));\n update_option("woo_ai_openrouter_key", sanitize_text_field($_POST["openrouter_key"]));\n update_option("woo_ai_openai_key", sanitize_text_field($_POST["openai_key"]));\n update_option("woo_ai_openai_url", esc_url_raw($_POST["openai_url"]));\n update_option("woo_ai_openai_model", sanitize_text_field($_POST["openai_model"]));\n\n $providers = [];\n if (!empty($_POST["groq_key"])) {\n $providers[] = ["name" => "Groq", "url" => "https://api.groq.com/openai/v1/chat/completions", "key" => sanitize_text_field($_POST["groq_key"]), "model" => "llama3-8b-8192"];\n }\n if (!empty($_POST["gemini_key"])) {\n $providers[] = ["name" => "Gemini", "url" => "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", "key" => sanitize_text_field($_POST["gemini_key"]), "model" => "gemini-3.8-flash"];\n }\n if (!empty($_POST["openrouter_key"])) {\n $providers[] = ["name" => "OpenRouter", "url" => "https://openrouter.ai/api/v1/chat/completions", "key" => sanitize_text_field($_POST["openrouter_key"]), "model" => "openrouter/free"];\n }\n if (!empty($_POST["openai_key"]) && !empty($_POST["openai_url"])) {\n $providers[] = ["name" => "Custom OpenAI", "url" => esc_url_raw($_POST["openai_url"]), "key" => sanitize_text_field($_POST["openai_key"]), "model" => sanitize_text_field($_POST["openai_model"])];\n }\n update_option("woo_ai_providers", $providers);\n\n update_option("woo_ai_llm_txt", wp_kses_post($_POST["llm_txt"] ?? ""));\n update_option("woo_ai_widget_title", sanitize_text_field($_POST["widget_title"]));\n update_option("woo_ai_widget_placeholder", sanitize_text_field($_POST["widget_placeholder"]));\n update_option("woo_ai_widget_auto_inject", isset($_POST["widget_auto_inject"]) ? "1" : "0");\n update_option("woo_ai_agent_name", sanitize_text_field($_POST["agent_name"]));\n update_option("woo_ai_agent_tone", sanitize_text_field($_POST["agent_tone"]));\n update_option("woo_ai_welcome_message", sanitize_textarea_field($_POST["welcome_message"]));\n update_option("woo_ai_chips", sanitize_textarea_field($_POST["chips"] ?? ""));\n update_option("woo_ai_lang_selector", isset($_POST["lang_selector"]) ? "1" : "0");\n update_option("woo_ai_ask_name", isset($_POST["ask_name"]) ? "1" : "0");\n\n echo '

Saved successfully.

';\n }\n\n $groq = get_option("woo_ai_groq_key", "");\n $gemini = get_option("woo_ai_gemini_key", "");\n $openrouter = get_option("woo_ai_openrouter_key", "");\n $openai_key = get_option("woo_ai_openai_key", "");\n $openai_url = get_option("woo_ai_openai_url", "https://api.openai.com/v1/chat/completions");\n $openai_model = get_option("woo_ai_openai_model", "gpt-4o-mini");\n\n $llmTxt = get_option("woo_ai_llm_txt", "");\n $widgetTitle = get_option("woo_ai_widget_title", ""); if ($widgetTitle === "") $widgetTitle = get_option("woo_ai_agent_name", "Assistant");\n $widgetPlaceholder = get_option("woo_ai_widget_placeholder", "Ask anything...");\n $widgetAutoInject = get_option("woo_ai_widget_auto_inject", "1");\n $agentName = get_option("woo_ai_agent_name", "Assistant");\n $agentTone = get_option("woo_ai_agent_tone", "professional");\n $welcomeMessage = get_option("woo_ai_welcome_message", "Hi there 👋 How can I help?");\n $chips = get_option("woo_ai_chips", "Products|What do you sell?\nPricing|What are your prices?\nSupport|Support & delivery");\n $langSelector = get_option("woo_ai_lang_selector", "1");\n $askName = get_option("woo_ai_ask_name", "1");\n\n $rebuildUrl = add_query_arg(["rebuild_ai_feed" => "1"], admin_url("admin.php?page=woo-ai-agent"));\n\n ?>\n
\n

WooCommerce AI Agent

\n

\n \n \n

\n\n \n \n \n\n

Providers & Models (Pro Plan)

\n

Add multiple providers. The agent will try them in order until one succeeds.

\n
\n