Spaces:
Paused
Paused
Revert to 0f8aedf — restore src/vaix-rag.js
Browse files- src/vaix-rag.js +44 -65
src/vaix-rag.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
| 2 |
// Products loaded ONCE from dataset JSON, cached in memory.
|
| 3 |
// Supports ?product=xxx URL param to auto-open product detail
|
| 4 |
// Full-screen image viewer on image click
|
| 5 |
-
// SSR: window.__VAIX_PRODUCT_DATA__ from server renders instantly before full JSON load
|
| 6 |
|
| 7 |
(function() {
|
| 8 |
'use strict';
|
|
@@ -10,7 +9,6 @@
|
|
| 10 |
const JSON_URL = "https://huggingface.co/datasets/bep40/grob-products-updated/resolve/main/products_with_slugs.json";
|
| 11 |
let allProducts = [];
|
| 12 |
let loaded = false;
|
| 13 |
-
let fullCatalogLoading = false;
|
| 14 |
let searchDebounceTimer = null;
|
| 15 |
let lastSearchResults = [];
|
| 16 |
let lastShownProduct = null;
|
|
@@ -54,52 +52,6 @@ function scoreProduct(p, queryStr, terms) {
|
|
| 54 |
|
| 55 |
async function load() {
|
| 56 |
if (loaded) return;
|
| 57 |
-
|
| 58 |
-
// ── Check for server-injected product data (?product=xxx) ──
|
| 59 |
-
const injectedData = window.__VAIX_PRODUCT_DATA__;
|
| 60 |
-
if (injectedData && injectedData.title_clean) {
|
| 61 |
-
console.log("[VAIX] Using injected product data:", injectedData.title_clean);
|
| 62 |
-
// Quick-set as 1-item catalog, will be replaced by full JSON in background
|
| 63 |
-
allProducts = [injectedData];
|
| 64 |
-
loaded = true;
|
| 65 |
-
fullCatalogLoading = true;
|
| 66 |
-
|
| 67 |
-
// Immediately render from injected data — no waiting
|
| 68 |
-
setTimeout(function() {
|
| 69 |
-
// Open panel and show the product right away
|
| 70 |
-
handleProductUrlParam();
|
| 71 |
-
}, 10);
|
| 72 |
-
|
| 73 |
-
// Also trigger full JSON fetch in background for search/related
|
| 74 |
-
fullCatalogLoading = true;
|
| 75 |
-
fetch(JSON_URL).then(r => {
|
| 76 |
-
if (!r.ok) throw new Error("HTTP "+r.status);
|
| 77 |
-
return r.json();
|
| 78 |
-
}).then(raw => {
|
| 79 |
-
// Only replace if we don't already have the full catalog
|
| 80 |
-
if (allProducts.length <= 1 || !fullCatalogLoading) return;
|
| 81 |
-
allProducts = (Array.isArray(raw) ? raw : []).map((p,i)=>({
|
| 82 |
-
name: p.n||p.name||"", title_clean: p.n||p.name||"",
|
| 83 |
-
brand: p.brand||"", price: p.p||p.price||"", priceNum: Number(p.pn??0),
|
| 84 |
-
category: p.c||p.cat||"", category_slug: p.cs||"", category_icon: p.ci||"fa-box",
|
| 85 |
-
sku: p.sku||"", model: p.mod||p.model||"", slug: p.slug||"",
|
| 86 |
-
description: p.desc||"", summary: p.sum||p.summary||"",
|
| 87 |
-
features: Array.isArray(p.feats)?p.feats:[],
|
| 88 |
-
specs: (typeof p.specs==="object"&&p.specs!==null)?p.specs:{},
|
| 89 |
-
video: p.vid||"", image: p.i||(Array.isArray(p.imgs)?p.imgs[0]:"")||"",
|
| 90 |
-
images: p.imgs||p.images||[], link: p.l||"", _idx: i
|
| 91 |
-
}));
|
| 92 |
-
loaded = true;
|
| 93 |
-
fullCatalogLoading = false;
|
| 94 |
-
console.log("[VAIX] Full catalog loaded in background:", allProducts.length);
|
| 95 |
-
}).catch(e => {
|
| 96 |
-
console.error("[VAIX] Background catalog load failed:", e);
|
| 97 |
-
fullCatalogLoading = false;
|
| 98 |
-
});
|
| 99 |
-
return;
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
// ── Normal: full JSON load ──
|
| 103 |
try {
|
| 104 |
const resp = await fetch(JSON_URL);
|
| 105 |
if (!resp.ok) throw new Error("HTTP "+resp.status);
|
|
@@ -118,10 +70,8 @@ async function load() {
|
|
| 118 |
loaded = true;
|
| 119 |
console.log("[VAIX] Loaded "+allProducts.length+" products");
|
| 120 |
|
| 121 |
-
//
|
| 122 |
-
|
| 123 |
-
handleProductUrlParam();
|
| 124 |
-
}
|
| 125 |
} catch(e) {
|
| 126 |
console.error("[VAIX] Load error:", e);
|
| 127 |
const errEl = document.getElementById("vaistudio-error");
|
|
@@ -166,11 +116,13 @@ function handleProductUrlParam() {
|
|
| 166 |
}
|
| 167 |
|
| 168 |
if (found) {
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
| 174 |
}
|
| 175 |
} catch(e) {
|
| 176 |
console.error("[VAIX] URL param error:", e);
|
|
@@ -229,7 +181,7 @@ function getSimilarProducts(product, limit) {
|
|
| 229 |
const nameWords = sd(p.title_clean).split(/\s+/).filter(w => w.length > 2);
|
| 230 |
const scored = [];
|
| 231 |
for (const other of allProducts) {
|
| 232 |
-
if (other._idx === p._idx
|
| 233 |
let sc = 0;
|
| 234 |
const otherCat = sd(other.category), otherBrand = sd(other.brand);
|
| 235 |
if (otherCat && cat && (otherCat === cat || otherCat.includes(cat) || cat.includes(otherCat))) sc += 40;
|
|
@@ -306,6 +258,7 @@ function openImageViewer(imageUrl, productName) {
|
|
| 306 |
viewer.id = "vaix-image-viewer";
|
| 307 |
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";
|
| 308 |
|
|
|
|
| 309 |
const closeBtn = document.createElement("button");
|
| 310 |
closeBtn.innerHTML = "✕";
|
| 311 |
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";
|
|
@@ -314,6 +267,7 @@ function openImageViewer(imageUrl, productName) {
|
|
| 314 |
closeBtn.onclick = function(e){ e.stopPropagation(); viewer.remove(); document.body.style.overflow = ""; };
|
| 315 |
viewer.appendChild(closeBtn);
|
| 316 |
|
|
|
|
| 317 |
if (productName) {
|
| 318 |
const caption = document.createElement("div");
|
| 319 |
caption.textContent = productName;
|
|
@@ -335,13 +289,20 @@ function openImageViewer(imageUrl, productName) {
|
|
| 335 |
viewer.appendChild(img);
|
| 336 |
|
| 337 |
viewer.addEventListener("click", function(e) {
|
| 338 |
-
if (e.target === viewer) {
|
|
|
|
|
|
|
|
|
|
| 339 |
});
|
| 340 |
|
| 341 |
viewer.setAttribute("tabindex", "0");
|
| 342 |
viewer.focus();
|
| 343 |
function keyHandler(e) {
|
| 344 |
-
if (e.key === "Escape") {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
}
|
| 346 |
document.addEventListener("keydown", keyHandler);
|
| 347 |
|
|
@@ -400,7 +361,10 @@ function attachChatCardHandlers(container) {
|
|
| 400 |
});
|
| 401 |
});
|
| 402 |
container.querySelectorAll(".share-btn").forEach(function(btn) {
|
| 403 |
-
btn.addEventListener("click", function(e) {
|
|
|
|
|
|
|
|
|
|
| 404 |
});
|
| 405 |
container.querySelectorAll(".chat-product-card-inner").forEach(function(inner) {
|
| 406 |
inner.addEventListener("click", function(e) {
|
|
@@ -541,7 +505,11 @@ function renderPanelResults(products) {
|
|
| 541 |
info.appendChild(ct);
|
| 542 |
}
|
| 543 |
card.appendChild(info);
|
| 544 |
-
card.addEventListener("click", function(e){
|
|
|
|
|
|
|
|
|
|
|
|
|
| 545 |
productsEl.appendChild(card);
|
| 546 |
}
|
| 547 |
if (countEl) countEl.textContent = products.length + " kết quả";
|
|
@@ -609,6 +577,7 @@ function renderDetail(p) {
|
|
| 609 |
if (p.priceNum >= 1000000) { trieuEl.textContent = "(" + (p.priceNum/1000000).toFixed(1) + " triệu)"; trieuEl.style.display="inline"; }
|
| 610 |
else trieuEl.style.display = "none";
|
| 611 |
|
|
|
|
| 612 |
const ic = document.getElementById("detail-images");
|
| 613 |
ic.innerHTML = "";
|
| 614 |
const imgs = [];
|
|
@@ -620,7 +589,7 @@ function renderDetail(p) {
|
|
| 620 |
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";
|
| 621 |
d.textContent = "📦"; ic.appendChild(d);
|
| 622 |
} else {
|
| 623 |
-
imgs.forEach(function(url){
|
| 624 |
const wrapper = document.createElement("div");
|
| 625 |
wrapper.style.cssText = "position:relative;flex-shrink:0;scroll-snap-align:start;cursor:zoom-in";
|
| 626 |
wrapper.title = "Nhấn để xem ảnh toàn màn hình";
|
|
@@ -632,13 +601,19 @@ function renderDetail(p) {
|
|
| 632 |
img.onmouseover = function(){ this.style.transform = "scale(1.05)"; this.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)"; };
|
| 633 |
img.onmouseout = function(){ this.style.transform = ""; this.style.boxShadow = ""; };
|
| 634 |
|
| 635 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 636 |
|
| 637 |
wrapper.appendChild(img);
|
| 638 |
|
|
|
|
| 639 |
const zoomIcon = document.createElement("div");
|
| 640 |
zoomIcon.textContent = "🔍";
|
| 641 |
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";
|
|
|
|
| 642 |
wrapper.appendChild(zoomIcon);
|
| 643 |
ic.appendChild(wrapper);
|
| 644 |
});
|
|
@@ -663,6 +638,7 @@ function renderDetail(p) {
|
|
| 663 |
sc.style.display = sb.children.length?"block":"none";
|
| 664 |
} else sc.style.display="none";
|
| 665 |
|
|
|
|
| 666 |
let shareSection = document.getElementById("detail-share-section");
|
| 667 |
if (!shareSection) {
|
| 668 |
shareSection = document.createElement("div");
|
|
@@ -674,8 +650,11 @@ function renderDetail(p) {
|
|
| 674 |
else document.getElementById("vaistudio-detail-modal").appendChild(shareSection);
|
| 675 |
}
|
| 676 |
const shareBtn = document.getElementById("detail-share-btn");
|
| 677 |
-
if (shareBtn)
|
|
|
|
|
|
|
| 678 |
|
|
|
|
| 679 |
let similarContainer = document.getElementById("detail-similar");
|
| 680 |
if (!similarContainer) {
|
| 681 |
const modal = document.getElementById("vaistudio-detail-modal");
|
|
|
|
| 2 |
// Products loaded ONCE from dataset JSON, cached in memory.
|
| 3 |
// Supports ?product=xxx URL param to auto-open product detail
|
| 4 |
// Full-screen image viewer on image click
|
|
|
|
| 5 |
|
| 6 |
(function() {
|
| 7 |
'use strict';
|
|
|
|
| 9 |
const JSON_URL = "https://huggingface.co/datasets/bep40/grob-products-updated/resolve/main/products_with_slugs.json";
|
| 10 |
let allProducts = [];
|
| 11 |
let loaded = false;
|
|
|
|
| 12 |
let searchDebounceTimer = null;
|
| 13 |
let lastSearchResults = [];
|
| 14 |
let lastShownProduct = null;
|
|
|
|
| 52 |
|
| 53 |
async function load() {
|
| 54 |
if (loaded) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
try {
|
| 56 |
const resp = await fetch(JSON_URL);
|
| 57 |
if (!resp.ok) throw new Error("HTTP "+resp.status);
|
|
|
|
| 70 |
loaded = true;
|
| 71 |
console.log("[VAIX] Loaded "+allProducts.length+" products");
|
| 72 |
|
| 73 |
+
// After loading, check URL param for ?product=xxx
|
| 74 |
+
handleProductUrlParam();
|
|
|
|
|
|
|
| 75 |
} catch(e) {
|
| 76 |
console.error("[VAIX] Load error:", e);
|
| 77 |
const errEl = document.getElementById("vaistudio-error");
|
|
|
|
| 116 |
}
|
| 117 |
|
| 118 |
if (found) {
|
| 119 |
+
setTimeout(function() {
|
| 120 |
+
renderDetail(found);
|
| 121 |
+
renderSimilarInPanel(found);
|
| 122 |
+
renderPanelResults([found]);
|
| 123 |
+
lastShownProduct = found;
|
| 124 |
+
document.body.style.overflow = "hidden";
|
| 125 |
+
}, 300);
|
| 126 |
}
|
| 127 |
} catch(e) {
|
| 128 |
console.error("[VAIX] URL param error:", e);
|
|
|
|
| 181 |
const nameWords = sd(p.title_clean).split(/\s+/).filter(w => w.length > 2);
|
| 182 |
const scored = [];
|
| 183 |
for (const other of allProducts) {
|
| 184 |
+
if (other._idx === p._idx) continue;
|
| 185 |
let sc = 0;
|
| 186 |
const otherCat = sd(other.category), otherBrand = sd(other.brand);
|
| 187 |
if (otherCat && cat && (otherCat === cat || otherCat.includes(cat) || cat.includes(otherCat))) sc += 40;
|
|
|
|
| 258 |
viewer.id = "vaix-image-viewer";
|
| 259 |
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";
|
| 260 |
|
| 261 |
+
// Close button
|
| 262 |
const closeBtn = document.createElement("button");
|
| 263 |
closeBtn.innerHTML = "✕";
|
| 264 |
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";
|
|
|
|
| 267 |
closeBtn.onclick = function(e){ e.stopPropagation(); viewer.remove(); document.body.style.overflow = ""; };
|
| 268 |
viewer.appendChild(closeBtn);
|
| 269 |
|
| 270 |
+
// Product name caption
|
| 271 |
if (productName) {
|
| 272 |
const caption = document.createElement("div");
|
| 273 |
caption.textContent = productName;
|
|
|
|
| 289 |
viewer.appendChild(img);
|
| 290 |
|
| 291 |
viewer.addEventListener("click", function(e) {
|
| 292 |
+
if (e.target === viewer) {
|
| 293 |
+
viewer.remove();
|
| 294 |
+
document.body.style.overflow = "";
|
| 295 |
+
}
|
| 296 |
});
|
| 297 |
|
| 298 |
viewer.setAttribute("tabindex", "0");
|
| 299 |
viewer.focus();
|
| 300 |
function keyHandler(e) {
|
| 301 |
+
if (e.key === "Escape") {
|
| 302 |
+
viewer.remove();
|
| 303 |
+
document.body.style.overflow = "";
|
| 304 |
+
document.removeEventListener("keydown", keyHandler);
|
| 305 |
+
}
|
| 306 |
}
|
| 307 |
document.addEventListener("keydown", keyHandler);
|
| 308 |
|
|
|
|
| 361 |
});
|
| 362 |
});
|
| 363 |
container.querySelectorAll(".share-btn").forEach(function(btn) {
|
| 364 |
+
btn.addEventListener("click", function(e) {
|
| 365 |
+
e.stopPropagation();
|
| 366 |
+
shareProduct(this.getAttribute("data-product"));
|
| 367 |
+
});
|
| 368 |
});
|
| 369 |
container.querySelectorAll(".chat-product-card-inner").forEach(function(inner) {
|
| 370 |
inner.addEventListener("click", function(e) {
|
|
|
|
| 505 |
info.appendChild(ct);
|
| 506 |
}
|
| 507 |
card.appendChild(info);
|
| 508 |
+
card.addEventListener("click", function(e){
|
| 509 |
+
e.stopPropagation();
|
| 510 |
+
renderDetail(p);
|
| 511 |
+
renderSimilarInPanel(p);
|
| 512 |
+
});
|
| 513 |
productsEl.appendChild(card);
|
| 514 |
}
|
| 515 |
if (countEl) countEl.textContent = products.length + " kết quả";
|
|
|
|
| 577 |
if (p.priceNum >= 1000000) { trieuEl.textContent = "(" + (p.priceNum/1000000).toFixed(1) + " triệu)"; trieuEl.style.display="inline"; }
|
| 578 |
else trieuEl.style.display = "none";
|
| 579 |
|
| 580 |
+
// Images — with click-to-fullscreen handler
|
| 581 |
const ic = document.getElementById("detail-images");
|
| 582 |
ic.innerHTML = "";
|
| 583 |
const imgs = [];
|
|
|
|
| 589 |
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";
|
| 590 |
d.textContent = "📦"; ic.appendChild(d);
|
| 591 |
} else {
|
| 592 |
+
imgs.forEach(function(url, idx){
|
| 593 |
const wrapper = document.createElement("div");
|
| 594 |
wrapper.style.cssText = "position:relative;flex-shrink:0;scroll-snap-align:start;cursor:zoom-in";
|
| 595 |
wrapper.title = "Nhấn để xem ảnh toàn màn hình";
|
|
|
|
| 601 |
img.onmouseover = function(){ this.style.transform = "scale(1.05)"; this.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)"; };
|
| 602 |
img.onmouseout = function(){ this.style.transform = ""; this.style.boxShadow = ""; };
|
| 603 |
|
| 604 |
+
// Click image → open fullscreen viewer
|
| 605 |
+
img.addEventListener("click", function(e) {
|
| 606 |
+
e.stopPropagation();
|
| 607 |
+
openImageViewer(url, p.title_clean);
|
| 608 |
+
});
|
| 609 |
|
| 610 |
wrapper.appendChild(img);
|
| 611 |
|
| 612 |
+
// Small zoom icon overlay
|
| 613 |
const zoomIcon = document.createElement("div");
|
| 614 |
zoomIcon.textContent = "🔍";
|
| 615 |
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";
|
| 616 |
+
|
| 617 |
wrapper.appendChild(zoomIcon);
|
| 618 |
ic.appendChild(wrapper);
|
| 619 |
});
|
|
|
|
| 638 |
sc.style.display = sb.children.length?"block":"none";
|
| 639 |
} else sc.style.display="none";
|
| 640 |
|
| 641 |
+
// Share button section
|
| 642 |
let shareSection = document.getElementById("detail-share-section");
|
| 643 |
if (!shareSection) {
|
| 644 |
shareSection = document.createElement("div");
|
|
|
|
| 650 |
else document.getElementById("vaistudio-detail-modal").appendChild(shareSection);
|
| 651 |
}
|
| 652 |
const shareBtn = document.getElementById("detail-share-btn");
|
| 653 |
+
if (shareBtn) {
|
| 654 |
+
shareBtn.onclick = function(e) { e.stopPropagation(); shareProduct(p.title_clean); };
|
| 655 |
+
}
|
| 656 |
|
| 657 |
+
// Similar products in modal
|
| 658 |
let similarContainer = document.getElementById("detail-similar");
|
| 659 |
if (!similarContainer) {
|
| 660 |
const modal = document.getElementById("vaistudio-detail-modal");
|