// 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();
}
})();