// V.AISTUDIO RAG Module — in-memory product search + UI panel integration // Products loaded ONCE from dataset JSON, cached in memory. // Supports ?product=xxx URL param to auto-open product detail // Full-screen image viewer on image click (function() { 'use strict'; const JSON_URL = "https://huggingface.co/datasets/bep40/grob-products-updated/resolve/main/products_with_slugs.json"; let allProducts = []; let loaded = false; let searchDebounceTimer = null; let lastSearchResults = []; let lastShownProduct = null; function sd(s) { return (s||"").normalize("NFD").replace(/[\u0300-\u036f]/g,"").toLowerCase(); } function fmt(pn) { return pn>0?pn.toLocaleString("vi-VN")+"₫":"Liên hệ"; } function extractTerms(query) { const q = sd(query); return q.split(/\s+/).filter(t => t.length > 1); } function scoreProduct(p, queryStr, terms) { let sc = 0; const nm = sd(p.title_clean); const sk = sd(p.sku || p.model); const br = sd(p.brand); const cat = sd(p.category); const all = [nm, br, cat, sd(p.description||""), sd((p.features||[]).join(" ")), sd(p.summary||"")].join(" "); if (nm === queryStr) sc += 200; else if (sk === queryStr) sc += 180; else if (nm.includes(queryStr)) sc += 100; else if (sk.includes(queryStr)) sc += 80; else if (all.includes(queryStr)) sc += 30; for (const t of terms) { if (nm.includes(t)) sc += 12; else if (sk.includes(t)) sc += 10; else if (br.includes(t)) sc += 8; else if (cat.includes(t)) sc += 6; else if (all.includes(t)) sc += 2; } const brandNames = ["malloca","eurogold","grob","canzy","demax","sunhouse","kangaroo","sharp","toshiba","panasonic","samsung","lg","electrolux","bosch","fischer"]; for (const b of brandNames) { if (queryStr.includes(b) && br.includes(b)) sc += 50; } return sc; } async function load() { if (loaded) return; try { const resp = await fetch(JSON_URL); if (!resp.ok) throw new Error("HTTP "+resp.status); const raw = await resp.json(); allProducts = (Array.isArray(raw) ? raw : []).map((p,i)=>({ name: p.n||p.name||"", title_clean: p.n||p.name||"", brand: p.brand||"", price: p.p||p.price||"", priceNum: Number(p.pn??0), category: p.c||p.cat||"", category_slug: p.cs||"", category_icon: p.ci||"fa-box", sku: p.sku||"", model: p.mod||p.model||"", slug: p.slug||"", description: p.desc||"", summary: p.sum||p.summary||"", features: Array.isArray(p.feats)?p.feats:[], specs: (typeof p.specs==="object"&&p.specs!==null)?p.specs:{}, video: p.vid||"", image: p.i||(Array.isArray(p.imgs)?p.imgs[0]:"")||"", images: p.imgs||p.images||[], link: p.l||"", _idx: i })); loaded = true; console.log("[VAIX] Loaded "+allProducts.length+" products"); // After loading, check URL param for ?product=xxx handleProductUrlParam(); } catch(e) { console.error("[VAIX] Load error:", e); const errEl = document.getElementById("vaistudio-error"); const errText = document.getElementById("vaistudio-error-text"); if (errEl) errEl.style.display = "flex"; if (errText) errText.textContent = "Không thể tải sản phẩm: "+e.message; } } // ── Handle ?product=xxx URL param ── function handleProductUrlParam() { try { const params = new URLSearchParams(window.location.search); const productSlug = params.get("product"); if (!productSlug || !allProducts.length) return; const decoded = decodeURIComponent(productSlug.trim()); if (!decoded) return; console.log("[VAIX] URL param product:", decoded); const qs = sd(decoded); let found = null; for (const p of allProducts) { if (p.slug && sd(p.slug) === qs) { found = p; break; } } if (!found) { for (const p of allProducts) { if (p.sku && sd(p.sku) === qs) { found = p; break; } } } if (!found) { for (const p of allProducts) { if (sd(p.title_clean) === qs) { found = p; break; } } } if (!found) { for (const p of allProducts) { if (sd(p.title_clean).includes(qs) || qs.includes(sd(p.title_clean))) { found = p; break; } } } if (found) { setTimeout(function() { renderDetail(found); renderSimilarInPanel(found); renderPanelResults([found]); lastShownProduct = found; document.body.style.overflow = "hidden"; }, 300); } } catch(e) { console.error("[VAIX] URL param error:", e); } } function findProduct(query) { if (!allProducts.length) return null; const qs = sd(query), terms = extractTerms(query); let best = null, bestScore = 0; for (const p of allProducts) { const sc = scoreProduct(p, qs, terms); if (sc > bestScore) { bestScore = sc; best = p; } } return bestScore > 0 ? best : null; } function searchSuggestions(query) { if (!allProducts.length || !query.trim()) return []; const q = query.trim(); const qs = sd(q); const terms = extractTerms(q); const scored = []; for (const p of allProducts) { const sc = scoreProduct(p, qs, terms); if (sc > 2) scored.push({ p, sc }); } scored.sort((a,b) => b.sc - a.sc); return scored.slice(0, 8).map(r => ({ name: r.p.title_clean, brand: r.p.brand, price: r.p.priceNum, priceStr: fmt(r.p.priceNum), category: r.p.category, image: r.p.image, score: r.sc })); } function getCategories() { if (!allProducts.length) return []; const catMap = {}; for (const p of allProducts) { const c = p.category; if (c) catMap[c] = (catMap[c] || 0) + 1; } return Object.entries(catMap).map(([name, count]) => ({ name, count })).sort((a,b) => b.count - a.count); } function getProductsByCategory(categoryName) { if (!allProducts.length) return []; const qs = sd(categoryName); return allProducts.filter(p => sd(p.category).includes(qs)).slice(0, 20); } function getSimilarProducts(product, limit) { if (!allProducts.length || !product) return []; limit = limit || 6; const p = product; const cat = sd(p.category), brand = sd(p.brand); const nameWords = sd(p.title_clean).split(/\s+/).filter(w => w.length > 2); const scored = []; for (const other of allProducts) { if (other._idx === p._idx) continue; let sc = 0; const otherCat = sd(other.category), otherBrand = sd(other.brand); if (otherCat && cat && (otherCat === cat || otherCat.includes(cat) || cat.includes(otherCat))) sc += 40; if (otherBrand && brand && (otherBrand === brand || otherBrand.includes(brand) || brand.includes(otherBrand))) sc += 20; if (p.priceNum > 0 && other.priceNum > 0) { const ratio = Math.max(p.priceNum, other.priceNum) / Math.min(p.priceNum, other.priceNum); if (ratio <= 1.5) sc += 10; else if (ratio <= 2.5) sc += 5; } const otherName = sd(other.title_clean); for (const w of nameWords) { if (otherName.includes(w)) sc += 3; } if (sc > 0) scored.push({ p: other, sc }); } scored.sort((a,b) => b.sc - a.sc); return scored.slice(0, limit).map(r => r.p); } function getLastSearchResults() { return lastSearchResults; } function getLastShownProduct() { return lastShownProduct; } // ── Generate shareable product link ── function getShareLink(product) { if (!product) return ""; const slug = product.slug || product.sku || encodeURIComponent(product.title_clean); const base = window.location.origin + window.location.pathname; return base + "?product=" + slug; } function shareProduct(name) { const p = findProduct(name); if (!p) return 'Không tìm thấy sản phẩm: "'+name+'"'; const url = getShareLink(p); let shareText = "🛒 " + p.title_clean + "\n"; shareText += "💰 " + fmt(p.priceNum) + "\n"; if (p.brand) shareText += "🏷️ " + p.brand + "\n"; if (p.model) shareText += "📋 Model: " + p.model + "\n"; if (p.summary) shareText += "📝 " + p.summary.slice(0, 200) + "\n"; shareText += "\n" + url; if (navigator.share) { navigator.share({ title: p.title_clean, text: shareText, url: url }).catch(() => {}); } else { navigator.clipboard.writeText(shareText).then(function() { showShareToast("✅ Đã sao chép link chia sẻ: " + p.title_clean); }).catch(function() { prompt("Sao chép link chia sẻ:", url); }); } return "Đã chia sẻ: " + p.title_clean; } function showShareToast(msg) { let toast = document.getElementById("vaix-toast"); if (!toast) { toast = document.createElement("div"); toast.id = "vaix-toast"; toast.style.cssText = "position:fixed;bottom:100px;left:50%;transform:translateX(-50%);background:#1e293b;color:#fff;padding:10px 20px;border-radius:10px;font-size:0.82rem;z-index:9999;box-shadow:0 4px 16px rgba(0,0,0,0.25);opacity:0;transition:opacity 0.3s ease;pointer-events:none;max-width:90vw;text-align:center"; document.body.appendChild(toast); } toast.textContent = msg; toast.style.opacity = "1"; clearTimeout(toast._timer); toast._timer = setTimeout(function(){ toast.style.opacity = "0"; }, 3000); } // ── Full-screen image viewer ── function openImageViewer(imageUrl, productName) { if (!imageUrl) return; const existing = document.getElementById("vaix-image-viewer"); if (existing) existing.remove(); const viewer = document.createElement("div"); viewer.id = "vaix-image-viewer"; viewer.style.cssText = "position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.92);display:flex;flex-direction:column;align-items:center;justify-content:center;padding:20px;cursor:zoom-out;animation:modalIn 0.2s ease"; // Close button const closeBtn = document.createElement("button"); closeBtn.innerHTML = "✕"; closeBtn.style.cssText = "position:absolute;top:16px;right:16px;width:40px;height:40px;border-radius:50%;border:none;background:rgba(255,255,255,0.15);color:#fff;font-size:1.3rem;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background 0.2s;z-index:2"; closeBtn.onmouseover = function(){ this.style.background = "rgba(255,255,255,0.3)"; }; closeBtn.onmouseout = function(){ this.style.background = "rgba(255,255,255,0.15)"; }; closeBtn.onclick = function(e){ e.stopPropagation(); viewer.remove(); document.body.style.overflow = ""; }; viewer.appendChild(closeBtn); // Product name caption if (productName) { const caption = document.createElement("div"); caption.textContent = productName; caption.style.cssText = "position:absolute;bottom:24px;left:50%;transform:translateX(-50%);color:rgba(255,255,255,0.7);font-size:0.85rem;text-align:center;max-width:80%;padding:8px 16px;background:rgba(0,0,0,0.5);border-radius:8px;z-index:2"; viewer.appendChild(caption); } const img = document.createElement("img"); img.src = imageUrl; img.alt = productName || ""; img.style.cssText = "max-width:100%;max-height:90vh;object-fit:contain;border-radius:8px;box-shadow:0 8px 40px rgba(0,0,0,0.5);user-select:none;-webkit-user-drag:none"; img.onerror = function(){ this.alt = "Không thể tải ảnh"; this.style.maxWidth = "300px"; }; const hint = document.createElement("div"); hint.textContent = "Nhấn ESC hoặc click bên ngoài để đóng"; hint.style.cssText = "position:absolute;top:16px;left:50%;transform:translateX(-50%);color:rgba(255,255,255,0.4);font-size:0.7rem;z-index:2;white-space:nowrap"; viewer.appendChild(hint); viewer.appendChild(img); viewer.addEventListener("click", function(e) { if (e.target === viewer) { viewer.remove(); document.body.style.overflow = ""; } }); viewer.setAttribute("tabindex", "0"); viewer.focus(); function keyHandler(e) { if (e.key === "Escape") { viewer.remove(); document.body.style.overflow = ""; document.removeEventListener("keydown", keyHandler); } } document.addEventListener("keydown", keyHandler); document.body.appendChild(viewer); document.body.style.overflow = "hidden"; } function createChatProductCard(product) { if (!product) return ""; const img = product.image ? product.image : ""; const name = product.title_clean || product.name || ""; const brand = product.brand || ""; const price = fmt(product.priceNum); const cat = product.category || ""; const rating = product.rating || (Math.random() > 0.3 ? (4 + Math.random()).toFixed(1) : false); const shortName = name.length > 50 ? name.slice(0, 47) + "..." : name; let stars = ""; if (rating) { const full = Math.floor(Number(rating)); for (let i = 0; i < 5; i++) stars += i < full ? "★" : "☆"; } return '
' + '
' + (img ? '
' : '
📦
') + '
' + '
' + shortName + '
' + '
' + (brand ? '' + brand + '' : '') + (stars ? '' + stars + '' : '') + '
' + '
' + price + '
' + (cat ? '
' + cat + '
' : '') + '
' + '
' + '' + '' + '
'; } function attachChatCardHandlers(container) { if (!container) return; container.querySelectorAll(".detail-btn").forEach(function(btn) { btn.addEventListener("click", function(e) { e.stopPropagation(); const name = this.getAttribute("data-product"); const p = findProduct(name); if (p) { renderDetail(p); renderSimilarInPanel(p); renderPanelResults([p]); const panel = document.getElementById("vaistudio-panel"); const toggle = document.getElementById("vaistudio-toggle"); if (panel) { panel.classList.add("open"); panel.style.display = "flex"; } if (toggle) toggle.classList.add("active"); } }); }); container.querySelectorAll(".share-btn").forEach(function(btn) { btn.addEventListener("click", function(e) { e.stopPropagation(); shareProduct(this.getAttribute("data-product")); }); }); container.querySelectorAll(".chat-product-card-inner").forEach(function(inner) { inner.addEventListener("click", function(e) { const card = this.closest(".chat-product-card"); if (!card) return; const name = card.getAttribute("data-product"); const p = findProduct(name); if (p) { renderDetail(p); renderSimilarInPanel(p); renderPanelResults([p]); const panel = document.getElementById("vaistudio-panel"); const toggle = document.getElementById("vaistudio-toggle"); if (panel) { panel.classList.add("open"); panel.style.display = "flex"; } if (toggle) toggle.classList.add("active"); } }); }); } function queryCatalog(query) { if (!allProducts.length) return "Catalog still loading, please wait."; const q = query.trim(); if (!q) return "What product are you looking for?"; const qs = sd(q), terms = extractTerms(q); const scored = []; for (let i = 0; i < allProducts.length; i++) { const p = allProducts[i]; const sc = scoreProduct(p, qs, terms); if (sc > 0) scored.push({ p, sc }); } scored.sort((a,b) => b.sc - a.sc); const top = scored.slice(0, 5); lastSearchResults = top.map(r => r.p); renderPanelResults(top.map(r => r.p)); if (!top.length) { lastSearchResults = []; return 'Không tìm thấy sản phẩm cho "'+q+'". Bạn có thể thử từ khóa khác hoặc xem danh mục sản phẩm.'; } let result = "Tìm thấy " + top.length + ' sản phẩm cho "' + q + '":\n\n'; for (let i = 0; i < top.length; i++) { const p = top[i].p; result += '【' + (i+1) + '】' + p.title_clean + '\n'; result += ' Thương hiệu: ' + (p.brand || "N/A") + ' | Giá: ' + fmt(p.priceNum); if (p.priceNum >= 1000000) result += ' (' + (p.priceNum/1000000).toFixed(1) + ' triệu)'; result += '\n'; if (p.model || p.sku) result += ' Model: ' + (p.model || p.sku) + '\n'; if (p.summary) result += ' ' + p.summary.slice(0, 200) + '\n'; if (p.features && p.features.length) { result += ' Tính năng: ' + p.features.slice(0, 4).join(", ") + (p.features.length > 4 ? "..." : "") + '\n'; } result += '\n'; } const best = top[0].p; lastShownProduct = best; result += 'Sản phẩm phù hợp nhất: ' + best.title_clean + ' (' + fmt(best.priceNum) + '). '; const similar = getSimilarProducts(best, 3); if (similar.length > 0) { result += '\n\n👉 Sản phẩm tương tự bạn có thể quan tâm:\n'; for (let i = 0; i < similar.length; i++) { result += ' • ' + similar[i].title_clean + ' - ' + fmt(similar[i].priceNum); if (similar[i].brand && similar[i].brand !== best.brand) result += ' (' + similar[i].brand + ')'; result += '\n'; } result += '\nGợi ý: Bạn muốn xem chi tiết sản phẩm nào? Em sẽ mở thông tin cho bạn.'; } else { result += 'Bạn muốn xem chi tiết sản phẩm nào?'; } return result; } function showProduct(name) { const p = findProduct(name); if (!p) return 'Không tìm thấy sản phẩm: "'+name+'". Hãy thử tìm kiếm với từ khóa khác.'; lastShownProduct = p; renderDetail(p); return 'Đã mở thông tin: '+p.title_clean+'\nThương hiệu: '+(p.brand||"N/A")+'\nGiá: '+fmt(p.priceNum)+'\nModel: '+(p.model||p.sku||"N/A")+'\n'+(p.summary?p.summary.slice(0,300):""); } // ───────────────────────────────────────────── // UI RENDERING // ───────────────────────────────────────────── function renderPanelResults(products) { const panel = document.getElementById("vaistudio-panel"); const toggle = document.getElementById("vaistudio-toggle"); const loadingEl = document.getElementById("vaistudio-loading"); const productsEl = document.getElementById("vaistudio-products"); const countEl = document.getElementById("vaistudio-count"); if (!productsEl) return; if (panel && !panel.classList.contains("open")) { panel.classList.add("open"); panel.style.display = "flex"; } if (toggle) toggle.classList.add("active"); if (loadingEl) loadingEl.hidden = true; const suggestionsEl = document.getElementById("vaix-suggestions"); if (suggestionsEl) suggestionsEl.style.display = "none"; productsEl.innerHTML = ""; productsEl.style.display = "block"; for (let i = 0; i < products.length; i++) { const p = products[i]; const card = document.createElement("div"); card.className = "product-card"; if (p.image) { const ie = document.createElement("img"); ie.className = "product-card-img"; ie.src = p.image; ie.alt = p.title_clean || ""; ie.loading = "lazy"; ie.onerror = function(){ this.style.display = "none"; }; card.appendChild(ie); } else { const pl = document.createElement("div"); pl.className = "product-card-img"; pl.style.cssText = "flex-shrink:0;background:#e2e8f0;display:flex;align-items:center;justify-content:center;font-size:1.5rem"; pl.textContent = "📦"; card.appendChild(pl); } const info = document.createElement("div"); info.className = "product-card-info"; const t = document.createElement("p"); t.className = "product-card-title"; t.textContent = p.title_clean || p.name; const b = document.createElement("p"); b.className = "product-card-brand"; b.textContent = p.brand || ""; const pr = document.createElement("p"); pr.className = "product-card-price"; pr.textContent = fmt(p.priceNum); info.append(t, b, pr); if (p.category) { const ct = document.createElement("span"); ct.className = "product-card-cat"; ct.textContent = p.category; info.appendChild(ct); } card.appendChild(info); card.addEventListener("click", function(e){ e.stopPropagation(); renderDetail(p); renderSimilarInPanel(p); }); productsEl.appendChild(card); } if (countEl) countEl.textContent = products.length + " kết quả"; } function renderSimilarInPanel(mainProduct) { const productsEl = document.getElementById("vaistudio-products"); if (!productsEl) return; const existing = document.getElementById("vaix-similar-section"); if (existing) existing.remove(); const similar = getSimilarProducts(mainProduct, 4); if (!similar.length) return; const section = document.createElement("div"); section.id = "vaix-similar-section"; section.style.cssText = "margin-top:12px;padding-top:12px;border-top:2px solid #e2e8f0"; const title = document.createElement("div"); title.style.cssText = "font-size:0.8rem;font-weight:700;color:#003f62;margin-bottom:10px;display:flex;align-items:center;gap:6px"; title.innerHTML = ' Sản phẩm tương tự'; section.appendChild(title); const grid = document.createElement("div"); grid.style.cssText = "display:grid;grid-template-columns:1fr 1fr;gap:6px"; for (const sp of similar) { const mini = document.createElement("div"); mini.style.cssText = "display:flex;gap:8px;padding:8px;background:#fff;border:1px solid #e2e8f0;border-radius:8px;cursor:pointer;transition:all 0.2s"; mini.onmouseover = function(){ this.style.borderColor="#0077b6"; this.style.boxShadow="0 1px 4px rgba(0,63,98,0.1)"; }; mini.onmouseout = function(){ this.style.borderColor="#e2e8f0"; this.style.boxShadow="none"; }; const imgDiv = document.createElement("div"); if (sp.image) { imgDiv.innerHTML=''; } else { imgDiv.textContent="📦"; imgDiv.style.cssText="width:40px;height:40px;border-radius:6px;background:#f1f5f9;display:flex;align-items:center;justify-content:center;font-size:1rem;flex-shrink:0"; } const infoDiv = document.createElement("div"); infoDiv.style.cssText = "min-width:0;flex:1"; infoDiv.innerHTML = '
'+(sp.title_clean||"")+'
'+fmt(sp.priceNum)+'
'; mini.append(imgDiv, infoDiv); mini.addEventListener("click", function(e){ e.stopPropagation(); renderDetail(sp); renderSimilarInPanel(sp); }); grid.appendChild(mini); } section.appendChild(grid); productsEl.appendChild(section); } function renderDetail(p) { const overlay = document.getElementById("vaistudio-detail-overlay"); if (!overlay || !p) return; lastShownProduct = p; document.getElementById("detail-title").textContent = p.title_clean || p.name || ""; document.getElementById("detail-brand").textContent = p.brand || ""; document.getElementById("detail-model").textContent = p.model ? "Model: "+p.model : ""; document.getElementById("detail-price").textContent = p.priceNum > 0 ? p.priceNum.toLocaleString("vi-VN")+"₫" : "Liên hệ"; let trieuEl = document.getElementById("detail-price-trieu"); if (!trieuEl) { trieuEl = document.createElement("span"); trieuEl.id = "detail-price-trieu"; trieuEl.style.cssText = "font-size:0.75rem;color:#94a3b8;margin-left:6px"; document.getElementById("detail-price").appendChild(trieuEl); } if (p.priceNum >= 1000000) { trieuEl.textContent = "(" + (p.priceNum/1000000).toFixed(1) + " triệu)"; trieuEl.style.display="inline"; } else trieuEl.style.display = "none"; // Images — with click-to-fullscreen handler const ic = document.getElementById("detail-images"); ic.innerHTML = ""; const imgs = []; if (p.image) imgs.push(p.image); if (p.images && p.images.length) p.images.forEach(function(i){ if(i && !imgs.includes(i)) imgs.push(i); }); if (!imgs.length) { const d = document.createElement("div"); d.style.cssText = "width:120px;height:120px;background:#f1f5f9;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:2rem;flex-shrink:0"; d.textContent = "📦"; ic.appendChild(d); } else { imgs.forEach(function(url, idx){ const wrapper = document.createElement("div"); wrapper.style.cssText = "position:relative;flex-shrink:0;scroll-snap-align:start;cursor:zoom-in"; wrapper.title = "Nhấn để xem ảnh toàn màn hình"; const img = document.createElement("img"); img.src = url; img.style.cssText = "width:120px;height:120px;border-radius:12px;object-fit:cover;border:1px solid #e2e8f0;transition:transform 0.2s, box-shadow 0.2s"; img.onerror = function(){ this.style.display = "none"; }; img.onmouseover = function(){ this.style.transform = "scale(1.05)"; this.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)"; }; img.onmouseout = function(){ this.style.transform = ""; this.style.boxShadow = ""; }; // Click image → open fullscreen viewer img.addEventListener("click", function(e) { e.stopPropagation(); openImageViewer(url, p.title_clean); }); wrapper.appendChild(img); // Small zoom icon overlay const zoomIcon = document.createElement("div"); zoomIcon.textContent = "🔍"; zoomIcon.style.cssText = "position:absolute;bottom:4px;right:4px;font-size:0.7rem;background:rgba(0,0,0,0.5);border-radius:50%;width:22px;height:22px;display:flex;align-items:center;justify-content:center;pointer-events:none;opacity:0.7"; wrapper.appendChild(zoomIcon); ic.appendChild(wrapper); }); } const se = document.getElementById("detail-summary"); if (p.summary) { se.textContent=p.summary; se.style.display="block"; } else if (p.description) { se.textContent=p.description.slice(0,300)+(p.description.length>300?"…":""); se.style.display="block"; } else se.style.display="none"; const fc = document.getElementById("detail-features"), fl = document.getElementById("detail-features-list"); fl.innerHTML=""; if (p.features&&p.features.length) { p.features.forEach(function(f){const li=document.createElement("li"); li.style.cssText="padding:6px 0;border-bottom:1px solid #f1f5f9;font-size:0.82rem;color:#475569"; li.textContent="✦ "+f; fl.appendChild(li)}); fc.style.display="block"; } else fc.style.display="none"; const sc = document.getElementById("detail-specs"), sb = document.querySelector("#detail-specs-table tbody"); sb.innerHTML=""; if (p.specs&&typeof p.specs==="object"&&Object.keys(p.specs).length) { Object.entries(p.specs).forEach(function([k,v]){if(!v||v==="None")return;const tr=document.createElement("tr"),td1=document.createElement("td"),td2=document.createElement("td");td1.style.cssText="padding:6px 8px;color:#64748b;font-weight:600;width:40%;vertical-align:top;border-bottom:1px solid #f1f5f9";td1.textContent=k;td2.style.cssText="padding:6px 8px;color:#334155;border-bottom:1px solid #f1f5f9;vertical-align:top";td2.textContent=String(v).replace(/^\[|\]$/g,"");tr.append(td1,td2);sb.appendChild(tr)}); sc.style.display = sb.children.length?"block":"none"; } else sc.style.display="none"; // Share button section let shareSection = document.getElementById("detail-share-section"); if (!shareSection) { shareSection = document.createElement("div"); shareSection.id = "detail-share-section"; shareSection.style.cssText = "padding:10px 16px 4px;display:block"; shareSection.innerHTML = ''; const specsEl = document.getElementById("detail-specs"); if (specsEl && specsEl.nextSibling) specsEl.parentNode.insertBefore(shareSection, specsEl.nextSibling); else document.getElementById("vaistudio-detail-modal").appendChild(shareSection); } const shareBtn = document.getElementById("detail-share-btn"); if (shareBtn) { shareBtn.onclick = function(e) { e.stopPropagation(); shareProduct(p.title_clean); }; } // Similar products in modal let similarContainer = document.getElementById("detail-similar"); if (!similarContainer) { const modal = document.getElementById("vaistudio-detail-modal"); const container = document.createElement("div"); container.id = "detail-similar"; container.style.cssText = "padding:12px 16px 20px;display:none"; container.innerHTML = '

Sản phẩm tương tự

'; modal.appendChild(container); similarContainer = container; } const similarGrid = document.getElementById("detail-similar-grid"); if (similarGrid) { const similar = getSimilarProducts(p, 4); similarGrid.innerHTML = ""; if (similar.length) { for (const sp of similar) { const item = document.createElement("div"); item.style.cssText = "display:flex;gap:8px;padding:8px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;cursor:pointer;transition:all 0.2s"; item.onmouseover = function(){ this.style.borderColor="#0077b6"; this.style.boxShadow="0 1px 4px rgba(0,63,98,0.1)"; }; item.onmouseout = function(){ this.style.borderColor="#e2e8f0"; this.style.boxShadow="none"; }; const imgDiv = document.createElement("div"); if (sp.image) { imgDiv.innerHTML=''; } else { imgDiv.textContent="📦"; imgDiv.style.cssText="width:44px;height:44px;border-radius:6px;background:#e2e8f0;display:flex;align-items:center;justify-content:center;font-size:1.1rem;flex-shrink:0"; } const infoDiv = document.createElement("div"); infoDiv.style.cssText = "min-width:0;flex:1"; infoDiv.innerHTML = '
'+(sp.title_clean||"")+'
'+ '
'+fmt(sp.priceNum)+'
'+ (sp.brand&&sp.brand!==p.brand?'
'+sp.brand+'
':''); item.append(imgDiv, infoDiv); item.addEventListener("click", function(e){ e.stopPropagation(); renderDetail(sp); }); similarGrid.appendChild(item); } similarContainer.style.display = "block"; } else similarContainer.style.display = "none"; } overlay.style.display = "flex"; } function showSuggestions(suggestions) { const dropdown = document.getElementById("vaix-suggestions"); if (!dropdown) return; if (!suggestions || !suggestions.length) { dropdown.style.display="none"; return; } dropdown.innerHTML = ""; dropdown.style.display = "block"; for (const sug of suggestions) { const item = document.createElement("div"); item.className = "vaix-suggestion-item"; const imgDiv = document.createElement("div"); if (sug.image) { imgDiv.innerHTML=''; } else { imgDiv.textContent="📦"; imgDiv.style.cssText="width:32px;height:32px;border-radius:6px;background:#f1f5f9;display:flex;align-items:center;justify-content:center;font-size:0.9rem;flex-shrink:0"; } const textDiv = document.createElement("div"); textDiv.style.cssText = "flex:1;min-width:0"; textDiv.innerHTML = '
'+(sug.name||"")+'
'+ '
'+ (sug.brand?''+sug.brand+'':'')+ ''+sug.priceStr+''+ (sug.category?''+sug.category+'':'')+'
'; item.append(imgDiv, textDiv); item.addEventListener("click", function(){ const p = findProduct(sug.name); if (p) { renderDetail(p); renderSimilarInPanel(p); renderPanelResults([p]); } dropdown.style.display = "none"; document.getElementById("vaix-search").value = sug.name; }); dropdown.appendChild(item); } } function setupSearchInput() { const input = document.getElementById("vaix-search"); if (!input) return; input.addEventListener("input", function() { clearTimeout(searchDebounceTimer); const val = this.value.trim(); if (!val) { const d=document.getElementById("vaix-suggestions"); if(d)d.style.display="none"; return; } searchDebounceTimer = setTimeout(function() { showSuggestions(searchSuggestions(val)); }, 200); }); input.addEventListener("focus", function() { if (this.value.trim()) showSuggestions(searchSuggestions(this.value.trim())); }); input.addEventListener("keydown", function(e) { if (e.key === "Escape") { const d=document.getElementById("vaix-suggestions"); if(d)d.style.display="none"; } if (e.key === "Enter") { const val=this.value.trim(); if(val){const d=document.getElementById("vaix-suggestions"); if(d)d.style.display="none"; queryCatalog(val);} } }); document.addEventListener("click", function(e) { const d=document.getElementById("vaix-suggestions"); if (d && !e.target.closest("#vaix-suggestions") && !e.target.closest("#vaix-search")) d.style.display="none"; }); } function setupCategoryChips() { const container = document.getElementById("vaix-category-chips"); if (!container) return; const cats = getCategories().slice(0, 12); if (!cats.length) return; container.innerHTML = ""; container.style.display = "flex"; for (const cat of cats) { const chip = document.createElement("button"); chip.className = "vaix-category-chip"; chip.textContent = cat.name + " (" + cat.count + ")"; chip.addEventListener("click", function() { const results = getProductsByCategory(cat.name); if (results.length) { renderPanelResults(results); document.getElementById("vaix-search").value=""; const d=document.getElementById("vaix-suggestions"); if(d)d.style.display="none"; } }); container.appendChild(chip); } } function renderSearchResultsInChat(container) { if (!container) return; const results = lastSearchResults; if (!results || !results.length) return; let html = '
'; for (const p of results) html += createChatProductCard(p); html += '
'; container.innerHTML = html; attachChatCardHandlers(container); } function renderProductInChat(container, product) { if (!container || !product) return; container.innerHTML = '
' + createChatProductCard(product) + '
'; attachChatCardHandlers(container); } // Export to window window.vaix = { load: load, queryCatalog: queryCatalog, showProduct: showProduct, findProduct: findProduct, searchSuggestions: searchSuggestions, getSimilarProducts: getSimilarProducts, getCategories: getCategories, getProductsByCategory: getProductsByCategory, getShareLink: getShareLink, shareProduct: shareProduct, createChatProductCard: createChatProductCard, getLastSearchResults: getLastSearchResults, getLastShownProduct: getLastShownProduct, renderSearchResultsInChat: renderSearchResultsInChat, renderProductInChat: renderProductInChat, attachChatCardHandlers: attachChatCardHandlers, openImageViewer: openImageViewer, allProducts: function(){ return allProducts; }, isLoaded: function(){ return loaded; }, renderPanelResults: renderPanelResults }; // Auto-load load().catch(() => {}); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", function() { setupSearchInput(); setupCategoryChips(); }); } else { setupSearchInput(); setupCategoryChips(); } })();