Spaces:
Running
Running
Fix giỏ hàng mờ (raise drawer, not blurred overlay); URL-add lấy đúng danh mục từ scraper; thêm dán văn bản (AI bóc tách) khi thêm SP
Browse files- cart-quote.js: openCart() raise #cartDrawer (không raise #cartOverlay) — overlay có backdrop-filter blur + rgba đen, nếu nằm trên drawer thì giỏ hàng nhìn mờ/tối
- vaix-add-product.js:
* scrapeViaApi giữ category từ scraper (Chậu rửa bát...) → autoFillCategory dùng category thật của trang thay vì chỉ suy đoán từ tên
* parseProduct + parseCategory: lấy danh mục từ JSON-LD/breadcrumb/og
* runTextExtractAddProduct: tính năng mới — dán văn bản sản phẩm → gọi /api/text_extract → tự điền tên/mã/giá/thương hiệu/danh mục/mô tả/tính năng/thông số
- index.html: thêm ô dán văn bản + nút bóc tách; renumber bước 2→3→4; cache-bust gc=35, v=17
- version.js: v29
- index.html +9 -4
- src/cart-quote.js +7 -1
- src/vaix-add-product.js +83 -7
- src/version.js +1 -1
index.html
CHANGED
|
@@ -356,7 +356,12 @@
|
|
| 356 |
<label>URL sản phẩm</label>
|
| 357 |
<input class="add-product-input" id="addProductUrl" type="url" placeholder="https://.../san-pham/...">
|
| 358 |
|
| 359 |
-
<div class="add-product-sep"><span>2 · Hoặc dán
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
<div class="add-paste-box" id="addPasteBox">
|
| 361 |
<textarea class="add-paste-input" id="addPasteInput" rows="2" placeholder="Dán ảnh vào đây (bấm giữ → Dán), hoặc gõ ghi chú…"></textarea>
|
| 362 |
<div class="add-paste-bar">
|
|
@@ -412,7 +417,7 @@
|
|
| 412 |
</div>
|
| 413 |
|
| 414 |
<div id="addPreviewWrap" style="display:none;margin-top:16px">
|
| 415 |
-
<div class="add-product-sep"><span>
|
| 416 |
<div style="text-align:right;margin:8px 0 4px">
|
| 417 |
<button class="add-product-btn secondary" style="padding:7px 12px;font-size:.75rem" onclick="goBackFromPreview()"><i class="fas fa-arrow-left"></i> Quay lại bước dán URL / OCR</button>
|
| 418 |
</div>
|
|
@@ -831,7 +836,7 @@
|
|
| 831 |
<script defer src="./src/modal-top.js?gc=2"></script>
|
| 832 |
<!-- App modules: defer keeps execution order (parse order) but doesn't block
|
| 833 |
first paint — the welcome/avatar-picker modal renders immediately. -->
|
| 834 |
-
<script defer src="./src/cart-quote.js?gc=
|
| 835 |
<script defer src="./src/customers.js?gc=18"></script> <!-- Customer list (Danh sách khách hàng) -->
|
| 836 |
<script defer src="./src/vaix-rag.js?gc=66"></script> <!-- V.AI STUDIO RAG Module - loads FASTER via light index; lazy galleries; spec-aware search -->
|
| 837 |
<script defer src="./src/greeting-news.js?gc=41"></script> <!-- Greeting HOT news cards + source links -->
|
|
@@ -865,7 +870,7 @@
|
|
| 865 |
|
| 866 |
<!-- ── Thêm SP (Add Product) module: 100% port from V.AISTUDIO, gated by the
|
| 867 |
homepage V.AISTUDIO access code (window.isVaAdmin()). ── -->
|
| 868 |
-
<script defer src="./src/vaix-add-product.js?v=
|
| 869 |
<script defer src="./src/vaix-bulk-import.js?v=10"></script>
|
| 870 |
</body>
|
| 871 |
</html>
|
|
|
|
| 356 |
<label>URL sản phẩm</label>
|
| 357 |
<input class="add-product-input" id="addProductUrl" type="url" placeholder="https://.../san-pham/...">
|
| 358 |
|
| 359 |
+
<div class="add-product-sep" style="margin-top:12px"><span>2 · Hoặc dán văn bản sản phẩm (AI bóc tách)</span></div>
|
| 360 |
+
<label>Dán nội dung sản phẩm (tên, mã, giá, mô tả, thông số…) — AI tự điền vào các trường bên dưới</label>
|
| 361 |
+
<textarea class="add-product-input" id="addTextInput" rows="4" placeholder="Dán văn bản sản phẩm vào đây. VD: Chậu rửa 1 hố mạ nano inox 304 Mã: BN7546 Giá: 5.803.500đ Chất liệu: Inox 304..."></textarea>
|
| 362 |
+
<button class="add-img-scan" id="addTextScan" onclick="runTextExtractAddProduct()"><i class="fas fa-wand-magic-sparkles"></i> Bóc tách văn bản → điền trường</button>
|
| 363 |
+
|
| 364 |
+
<div class="add-product-sep" style="margin-top:14px"><span>3 · Hoặc dán / tải ảnh sản phẩm (OCR)</span></div>
|
| 365 |
<div class="add-paste-box" id="addPasteBox">
|
| 366 |
<textarea class="add-paste-input" id="addPasteInput" rows="2" placeholder="Dán ảnh vào đây (bấm giữ → Dán), hoặc gõ ghi chú…"></textarea>
|
| 367 |
<div class="add-paste-bar">
|
|
|
|
| 417 |
</div>
|
| 418 |
|
| 419 |
<div id="addPreviewWrap" style="display:none;margin-top:16px">
|
| 420 |
+
<div class="add-product-sep"><span>4 · Xem trước sản phẩm</span></div>
|
| 421 |
<div style="text-align:right;margin:8px 0 4px">
|
| 422 |
<button class="add-product-btn secondary" style="padding:7px 12px;font-size:.75rem" onclick="goBackFromPreview()"><i class="fas fa-arrow-left"></i> Quay lại bước dán URL / OCR</button>
|
| 423 |
</div>
|
|
|
|
| 836 |
<script defer src="./src/modal-top.js?gc=2"></script>
|
| 837 |
<!-- App modules: defer keeps execution order (parse order) but doesn't block
|
| 838 |
first paint — the welcome/avatar-picker modal renders immediately. -->
|
| 839 |
+
<script defer src="./src/cart-quote.js?gc=35"></script> <!-- Cart + Quote module (Giỏ hàng / Báo giá) -->
|
| 840 |
<script defer src="./src/customers.js?gc=18"></script> <!-- Customer list (Danh sách khách hàng) -->
|
| 841 |
<script defer src="./src/vaix-rag.js?gc=66"></script> <!-- V.AI STUDIO RAG Module - loads FASTER via light index; lazy galleries; spec-aware search -->
|
| 842 |
<script defer src="./src/greeting-news.js?gc=41"></script> <!-- Greeting HOT news cards + source links -->
|
|
|
|
| 870 |
|
| 871 |
<!-- ── Thêm SP (Add Product) module: 100% port from V.AISTUDIO, gated by the
|
| 872 |
homepage V.AISTUDIO access code (window.isVaAdmin()). ── -->
|
| 873 |
+
<script defer src="./src/vaix-add-product.js?v=17"></script>
|
| 874 |
<script defer src="./src/vaix-bulk-import.js?v=10"></script>
|
| 875 |
</body>
|
| 876 |
</html>
|
src/cart-quote.js
CHANGED
|
@@ -222,7 +222,13 @@ window.openCart = function() {
|
|
| 222 |
drawer.style.transform = 'translateX(0)';
|
| 223 |
document.body.style.overflow = 'hidden';
|
| 224 |
renderCart();
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
};
|
| 227 |
|
| 228 |
window.closeCart = function() {
|
|
|
|
| 222 |
drawer.style.transform = 'translateX(0)';
|
| 223 |
document.body.style.overflow = 'hidden';
|
| 224 |
renderCart();
|
| 225 |
+
// IMPORTANT: raise the DRAWER (not the overlay) to the top of the modal
|
| 226 |
+
// stack. The overlay is a semi-transparent blurred backdrop — if it were
|
| 227 |
+
// raised above the drawer (via VAI_modalTop(overlay)), the dark blurred
|
| 228 |
+
// backdrop would render ON TOP of the cart contents, making the cart look
|
| 229 |
+
// dark and blurry ("giỏ hàng mờ"). Keeping the drawer on top leaves the
|
| 230 |
+
// backdrop dimming the page behind the cart, exactly as intended.
|
| 231 |
+
if (window.VAI_modalTop) window.VAI_modalTop(drawer);
|
| 232 |
};
|
| 233 |
|
| 234 |
window.closeCart = function() {
|
src/vaix-add-product.js
CHANGED
|
@@ -53,6 +53,7 @@
|
|
| 53 |
// Same shared scraper backend the V.AISTUDIO Space uses (may be paused -> degrades).
|
| 54 |
var SCRAPER = 'https://bep40-vai-product-scraper.hf.space/api/scrape';
|
| 55 |
var OCR_API = 'https://bep40-vai-product-scraper.hf.space/api/ocr';
|
|
|
|
| 56 |
var SCRAPER_TOKEN = 'VAISTUDIO2024';
|
| 57 |
var PERSIST_KEY = 'vai_padded_products'; // localStorage durable list of added products
|
| 58 |
|
|
@@ -149,12 +150,14 @@
|
|
| 149 |
return '';
|
| 150 |
}
|
| 151 |
// Auto-fill the "Danh mục" field from an inferred category, but only when the
|
| 152 |
-
// admin hasn't typed their own category yet.
|
| 153 |
-
|
|
|
|
|
|
|
| 154 |
var catEl = $('addOcrCat');
|
| 155 |
if (!catEl) return;
|
| 156 |
if (catEl.value && catEl.value.trim()) return; // admin override wins
|
| 157 |
-
var cat = inferCategory(name, brand);
|
| 158 |
if (cat) catEl.value = cat;
|
| 159 |
}
|
| 160 |
|
|
@@ -582,7 +585,7 @@
|
|
| 582 |
setF('addOcrDesc', d.desc || '');
|
| 583 |
var ft = (Array.isArray(d.feats) ? d.feats : []).join('\n');
|
| 584 |
if (ft) setF('addOcrFeats', ft);
|
| 585 |
-
autoFillCategory(d.name, d.brand); // ✨ auto-categorize OCR product
|
| 586 |
var fields = $('addOcrFields'); if (fields) fields.style.display = 'block';
|
| 587 |
var found = (d.name ? 1 : 0) + (d.sku ? 1 : 0) + (d.pn ? 1 : 0) + (d.brand ? 1 : 0);
|
| 588 |
var slug2 = slugify((val('addOcrSku') ? val('addOcrSku') + ' ' : '') + (d.name || ''));
|
|
@@ -606,6 +609,55 @@
|
|
| 606 |
};
|
| 607 |
function setF(id, v) { var e = $(id); if (e) e.value = v == null ? '' : v; }
|
| 608 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
/* ── PREVIEW ── */
|
| 610 |
function currentAddPreviewData() {
|
| 611 |
var specs = {}; val('addOcrSpecs').split(/\n+/).forEach(function (l) { var m = l.match(/^([^::]{1,60})\s*[::]\s*(.+)$/); if (m) { var k = m[1].trim(), v = m[2].trim(); if (k && v) specs[k] = v; } });
|
|
@@ -692,6 +744,7 @@
|
|
| 692 |
IMG = [];
|
| 693 |
var prv = $('addImgPreview'); if (prv) prv.innerHTML = '';
|
| 694 |
var ta = $('addPasteInput'); if (ta) ta.value = '';
|
|
|
|
| 695 |
var fields = $('addOcrFields'); if (fields) fields.style.display = 'none';
|
| 696 |
['addOcrName', 'addOcrSku', 'addOcrPrice', 'addOcrListPrice', 'addOcrBrand', 'addOcrCat', 'addOcrDesc', 'addOcrFeats', 'addOcrSpecs', 'addOcrNote'].forEach(function (id) { var e = $(id); if (e) e.value = ''; });
|
| 697 |
var subBtn = $('addProductSubmit');
|
|
@@ -1045,10 +1098,32 @@
|
|
| 1045 |
price: priceInfo.salePrice || priceInfo.price, pn: priceInfo.salePn || priceInfo.pn,
|
| 1046 |
listPrice: priceInfo.listPrice, listPn: priceInfo.listPn,
|
| 1047 |
salePrice: priceInfo.salePrice, salePn: priceInfo.salePn,
|
| 1048 |
-
specs: specs, feats: feats, brand: brand, ld: ld
|
| 1049 |
};
|
| 1050 |
}
|
| 1051 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1052 |
/* ── SCRAPE (server API → proxies → parser) ── */
|
| 1053 |
async function scrapeViaApi(url) {
|
| 1054 |
try {
|
|
@@ -1058,7 +1133,7 @@
|
|
| 1058 |
if (data && (data.name || data.sku || data.gallery || data.price)) {
|
| 1059 |
var sp = Number(data.salePn != null ? data.salePn : (data.pn || 0));
|
| 1060 |
var lp = Number(data.listPn != null ? data.listPn : 0);
|
| 1061 |
-
var out = { name: data.name || '', sku: data.sku || '', img: data.img || '', gallery: Array.isArray(data.gallery) ? data.gallery.filter(Boolean) : [], desc: data.desc || data.summary || '', price: data.salePrice || data.price || (sp ? money(sp) : ''), pn: sp || Number(data.pn) || 0, listPrice: data.listPrice || (lp ? money(lp) : ''), listPn: lp, salePrice: data.salePrice || (sp ? money(sp) : ''), salePn: sp, specs: data.specs || {}, feats: Array.isArray(data.feats) ? data.feats.slice() : (Array.isArray(data.features) ? data.features.slice() : []), brand: data.brand || '' };
|
| 1062 |
// If the scraper returned sparse data (missing features/specs or two prices),
|
| 1063 |
// enrich it with a local parse of the HTML (defensive fallback).
|
| 1064 |
var sparse = (!out.feats.length || !Object.keys(out.specs).length || !out.listPn || !out.brand || /^(https?:|www\.)/.test(out.brand));
|
|
@@ -1075,6 +1150,7 @@
|
|
| 1075 |
// Prefer the longer/richer description.
|
| 1076 |
if ((parsed.desc && !out.desc) || (parsed.desc && out.desc && parsed.desc.length > out.desc.length + 40)) out.desc = parsed.desc;
|
| 1077 |
if (!out.sku && parsed.sku) out.sku = parsed.sku;
|
|
|
|
| 1078 |
// If the scraper brand is a URL (og:site_name mis-parsed), replace with real brand.
|
| 1079 |
if (/^(https?:|www\.)/.test(out.brand) && parsed.brand) out.brand = parsed.brand;
|
| 1080 |
}
|
|
@@ -1148,7 +1224,7 @@
|
|
| 1148 |
setF('addOcrName', r.name || ''); setF('addOcrSku', r.sku || '');
|
| 1149 |
setF('addOcrPrice', r.pn || (r.salePn || '')); setF('addOcrListPrice', r.listPn || '');
|
| 1150 |
setF('addOcrBrand', r.brand || '');
|
| 1151 |
-
autoFillCategory(r.name, r.brand); // ✨ auto-categorize URL-scraped product
|
| 1152 |
if (r.specs && typeof r.specs === 'object') setF('addOcrSpecs', Object.keys(r.specs).map(function (k) { return k + ': ' + r.specs[k]; }).join('\n'));
|
| 1153 |
if (Array.isArray(r.feats) && r.feats.length) setF('addOcrFeats', r.feats.join('\n'));
|
| 1154 |
if (r.desc) setF('addOcrDesc', r.desc);
|
|
|
|
| 53 |
// Same shared scraper backend the V.AISTUDIO Space uses (may be paused -> degrades).
|
| 54 |
var SCRAPER = 'https://bep40-vai-product-scraper.hf.space/api/scrape';
|
| 55 |
var OCR_API = 'https://bep40-vai-product-scraper.hf.space/api/ocr';
|
| 56 |
+
var TEXT_EXTRACT_API = 'https://bep40-vai-product-scraper.hf.space/api/text_extract';
|
| 57 |
var SCRAPER_TOKEN = 'VAISTUDIO2024';
|
| 58 |
var PERSIST_KEY = 'vai_padded_products'; // localStorage durable list of added products
|
| 59 |
|
|
|
|
| 150 |
return '';
|
| 151 |
}
|
| 152 |
// Auto-fill the "Danh mục" field from an inferred category, but only when the
|
| 153 |
+
// admin hasn't typed their own category yet. `hint` (optional) is an explicit
|
| 154 |
+
// category already parsed from the page (e.g. scraper JSON-LD breadcrumb
|
| 155 |
+
// "Chậu rửa bát") — prefer it, and fall back to name/brand inference.
|
| 156 |
+
function autoFillCategory(name, brand, hint) {
|
| 157 |
var catEl = $('addOcrCat');
|
| 158 |
if (!catEl) return;
|
| 159 |
if (catEl.value && catEl.value.trim()) return; // admin override wins
|
| 160 |
+
var cat = (hint && String(hint).trim()) ? String(hint).trim() : inferCategory(name, brand);
|
| 161 |
if (cat) catEl.value = cat;
|
| 162 |
}
|
| 163 |
|
|
|
|
| 585 |
setF('addOcrDesc', d.desc || '');
|
| 586 |
var ft = (Array.isArray(d.feats) ? d.feats : []).join('\n');
|
| 587 |
if (ft) setF('addOcrFeats', ft);
|
| 588 |
+
autoFillCategory(d.name, d.brand, d.category); // ✨ auto-categorize OCR product (prefer API category)
|
| 589 |
var fields = $('addOcrFields'); if (fields) fields.style.display = 'block';
|
| 590 |
var found = (d.name ? 1 : 0) + (d.sku ? 1 : 0) + (d.pn ? 1 : 0) + (d.brand ? 1 : 0);
|
| 591 |
var slug2 = slugify((val('addOcrSku') ? val('addOcrSku') + ' ' : '') + (d.name || ''));
|
|
|
|
| 609 |
};
|
| 610 |
function setF(id, v) { var e = $(id); if (e) e.value = v == null ? '' : v; }
|
| 611 |
|
| 612 |
+
/* ── PASTE-TEXT extraction (AI bóc tách văn bản) ── */
|
| 613 |
+
window.runTextExtractAddProduct = async function () {
|
| 614 |
+
if (!isVaAdmin()) { alert('🔒 Cần nhập mã V.AISTUDIO trên trang chủ để dùng tính năng này.'); return; }
|
| 615 |
+
var st = $('addProductStatus');
|
| 616 |
+
function status(msg, cls) { if (st) { st.textContent = msg; st.className = 'add-product-status ' + (cls || ''); } }
|
| 617 |
+
var ta = $('addTextInput');
|
| 618 |
+
var raw = ta ? ta.value.trim() : '';
|
| 619 |
+
if (!raw) { status('❌ Chưa có văn bản. Hãy dán nội dung sản phẩm vào ô phía trên.', 'err'); return; }
|
| 620 |
+
var btn = $('addTextScan');
|
| 621 |
+
if (btn) { btn.disabled = true; btn.textContent = '⏳ Đang bóc tách…'; }
|
| 622 |
+
status('⏳ Đang phân tích văn bản & điền vào các trường…');
|
| 623 |
+
try {
|
| 624 |
+
var note = val('addOcrNote');
|
| 625 |
+
var r = await fetch(TEXT_EXTRACT_API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: SCRAPER_TOKEN, text: raw, note: note }) });
|
| 626 |
+
if (!r.ok) throw new Error((await r.text().catch(function(){return 'HTTP ' + r.status;})) || ('HTTP ' + r.status));
|
| 627 |
+
var d = await r.json();
|
| 628 |
+
setF('addOcrName', d.name || '');
|
| 629 |
+
setF('addOcrSku', d.sku || '');
|
| 630 |
+
setF('addOcrPrice', d.pn || '');
|
| 631 |
+
setF('addOcrListPrice', d.listPn || '');
|
| 632 |
+
setF('addOcrBrand', d.brand || '');
|
| 633 |
+
autoFillCategory(d.name, d.brand, d.category);
|
| 634 |
+
if (d.specs && typeof d.specs === 'object') setF('addOcrSpecs', Object.keys(d.specs).map(function (k) { return k + ': ' + d.specs[k]; }).join('\n'));
|
| 635 |
+
var ft = (Array.isArray(d.feats) ? d.feats : []).join('\n');
|
| 636 |
+
if (ft) setF('addOcrFeats', ft);
|
| 637 |
+
if (d.desc) setF('addOcrDesc', d.desc);
|
| 638 |
+
var fields = $('addOcrFields'); if (fields) fields.style.display = 'block';
|
| 639 |
+
var found = (d.name ? 1 : 0) + (d.sku ? 1 : 0) + (d.pn ? 1 : 0) + (d.brand ? 1 : 0) + (Object.keys(d.specs || {}).length ? 1 : 0) + ((d.feats || []).length ? 1 : 0);
|
| 640 |
+
if (!d.name && !found) { status('❌ Không bóc tách được thông tin từ văn bản này. Kiểm tra lại nội dung dán.', 'err'); return; }
|
| 641 |
+
var slug2 = slugify((val('addOcrSku') ? val('addOcrSku') + ' ' : '') + (d.name || ''));
|
| 642 |
+
var pyrDesc = d.desc || '';
|
| 643 |
+
if (!pyrDesc && d.specs && typeof d.specs === 'object') {
|
| 644 |
+
var leftover = [];
|
| 645 |
+
Object.keys(d.specs).forEach(function (k) {
|
| 646 |
+
if (['Điện áp', 'Công suất', 'Kích thước', 'Chất liệu', 'Xuất xứ', 'Bảo hành', 'Trọng lượng', 'Màu sắc', 'Model', 'Loại'].indexOf(k) < 0) leftover.push(k + ': ' + d.specs[k]);
|
| 647 |
+
});
|
| 648 |
+
pyrDesc = leftover.join('; ');
|
| 649 |
+
}
|
| 650 |
+
_pendingAdd = { r: { name: d.name || '', sku: d.sku || '', price: d.pn ? money(d.pn) : '', pn: d.pn || 0, img: IMG[0], gallery: IMG.slice(), desc: pyrDesc, brand: d.brand || '', specs: d.specs || {}, feats: Array.isArray(d.feats) ? d.feats.slice() : [] }, slug: slug2, pageUrl: '', usingOCR: IMG.length > 0 };
|
| 651 |
+
if (IMG.length) _pendingAdd.r.imgs_data = IMG.slice();
|
| 652 |
+
enterPreviewMode();
|
| 653 |
+
status('✅ Bóc tách xong: nhận được ' + found + ' trường thông tin. Preview đã hiển thị — bạn có thể chỉnh sửa, rồi bấm "Xác nhận thêm SP".', 'ok');
|
| 654 |
+
} catch (e) {
|
| 655 |
+
status('❌ Lỗi bóc tách văn bản: ' + (e && e.message || e), 'err');
|
| 656 |
+
} finally {
|
| 657 |
+
if (btn) { btn.disabled = false; btn.textContent = '✨ Bóc tách văn bản → điền trường'; }
|
| 658 |
+
}
|
| 659 |
+
};
|
| 660 |
+
|
| 661 |
/* ── PREVIEW ── */
|
| 662 |
function currentAddPreviewData() {
|
| 663 |
var specs = {}; val('addOcrSpecs').split(/\n+/).forEach(function (l) { var m = l.match(/^([^::]{1,60})\s*[::]\s*(.+)$/); if (m) { var k = m[1].trim(), v = m[2].trim(); if (k && v) specs[k] = v; } });
|
|
|
|
| 744 |
IMG = [];
|
| 745 |
var prv = $('addImgPreview'); if (prv) prv.innerHTML = '';
|
| 746 |
var ta = $('addPasteInput'); if (ta) ta.value = '';
|
| 747 |
+
var txtIn = $('addTextInput'); if (txtIn) txtIn.value = '';
|
| 748 |
var fields = $('addOcrFields'); if (fields) fields.style.display = 'none';
|
| 749 |
['addOcrName', 'addOcrSku', 'addOcrPrice', 'addOcrListPrice', 'addOcrBrand', 'addOcrCat', 'addOcrDesc', 'addOcrFeats', 'addOcrSpecs', 'addOcrNote'].forEach(function (id) { var e = $(id); if (e) e.value = ''; });
|
| 750 |
var subBtn = $('addProductSubmit');
|
|
|
|
| 1098 |
price: priceInfo.salePrice || priceInfo.price, pn: priceInfo.salePn || priceInfo.pn,
|
| 1099 |
listPrice: priceInfo.listPrice, listPn: priceInfo.listPn,
|
| 1100 |
salePrice: priceInfo.salePrice, salePn: priceInfo.salePn,
|
| 1101 |
+
specs: specs, feats: feats, brand: brand, ld: ld, category: parseCategory(html, ld)
|
| 1102 |
};
|
| 1103 |
}
|
| 1104 |
|
| 1105 |
+
// Best-effort category pulled from the page itself (breadcrumb / JSON-LD /
|
| 1106 |
+
// og:category), used to auto-fill the "Danh mục" field. Returns '' when no
|
| 1107 |
+
// reliable value is found.
|
| 1108 |
+
function parseCategory(html, ld) {
|
| 1109 |
+
if (ld && ld.category && String(ld.category).trim()) return String(ld.category).trim().slice(0, 80);
|
| 1110 |
+
try {
|
| 1111 |
+
var doc = new DOMParser().parseFromString(html, 'text/html');
|
| 1112 |
+
var bc = doc.querySelectorAll('nav.woocommerce-breadcrumb a, .woocommerce-breadcrumb a, .breadcrumb a, .breadcrumbs a, nav[aria-label="breadcrumb"] a');
|
| 1113 |
+
if (bc && bc.length) {
|
| 1114 |
+
// Take the deepest NON-product breadcrumb (skip the page itself / product name).
|
| 1115 |
+
var last = bc[bc.length - 1];
|
| 1116 |
+
if (last) {
|
| 1117 |
+
var t = (last.textContent || '').replace(/\s+/g, ' ').trim();
|
| 1118 |
+
if (t && t.length <= 60) return t;
|
| 1119 |
+
}
|
| 1120 |
+
}
|
| 1121 |
+
var cat = doc.querySelector('[property="product:category"], meta[itemprop="category"]');
|
| 1122 |
+
if (cat && cat.getAttribute('content')) return String(cat.getAttribute('content')).trim().slice(0, 80);
|
| 1123 |
+
} catch (e) {}
|
| 1124 |
+
return '';
|
| 1125 |
+
}
|
| 1126 |
+
|
| 1127 |
/* ── SCRAPE (server API → proxies → parser) ── */
|
| 1128 |
async function scrapeViaApi(url) {
|
| 1129 |
try {
|
|
|
|
| 1133 |
if (data && (data.name || data.sku || data.gallery || data.price)) {
|
| 1134 |
var sp = Number(data.salePn != null ? data.salePn : (data.pn || 0));
|
| 1135 |
var lp = Number(data.listPn != null ? data.listPn : 0);
|
| 1136 |
+
var out = { name: data.name || '', sku: data.sku || '', img: data.img || '', gallery: Array.isArray(data.gallery) ? data.gallery.filter(Boolean) : [], desc: data.desc || data.summary || '', price: data.salePrice || data.price || (sp ? money(sp) : ''), pn: sp || Number(data.pn) || 0, listPrice: data.listPrice || (lp ? money(lp) : ''), listPn: lp, salePrice: data.salePrice || (sp ? money(sp) : ''), salePn: sp, specs: data.specs || {}, feats: Array.isArray(data.feats) ? data.feats.slice() : (Array.isArray(data.features) ? data.features.slice() : []), brand: data.brand || '', category: data.category || '' };
|
| 1137 |
// If the scraper returned sparse data (missing features/specs or two prices),
|
| 1138 |
// enrich it with a local parse of the HTML (defensive fallback).
|
| 1139 |
var sparse = (!out.feats.length || !Object.keys(out.specs).length || !out.listPn || !out.brand || /^(https?:|www\.)/.test(out.brand));
|
|
|
|
| 1150 |
// Prefer the longer/richer description.
|
| 1151 |
if ((parsed.desc && !out.desc) || (parsed.desc && out.desc && parsed.desc.length > out.desc.length + 40)) out.desc = parsed.desc;
|
| 1152 |
if (!out.sku && parsed.sku) out.sku = parsed.sku;
|
| 1153 |
+
if (!out.category && parsed.category) out.category = parsed.category;
|
| 1154 |
// If the scraper brand is a URL (og:site_name mis-parsed), replace with real brand.
|
| 1155 |
if (/^(https?:|www\.)/.test(out.brand) && parsed.brand) out.brand = parsed.brand;
|
| 1156 |
}
|
|
|
|
| 1224 |
setF('addOcrName', r.name || ''); setF('addOcrSku', r.sku || '');
|
| 1225 |
setF('addOcrPrice', r.pn || (r.salePn || '')); setF('addOcrListPrice', r.listPn || '');
|
| 1226 |
setF('addOcrBrand', r.brand || '');
|
| 1227 |
+
autoFillCategory(r.name, r.brand, r.category); // ✨ auto-categorize URL-scraped product (prefer page category)
|
| 1228 |
if (r.specs && typeof r.specs === 'object') setF('addOcrSpecs', Object.keys(r.specs).map(function (k) { return k + ': ' + r.specs[k]; }).join('\n'));
|
| 1229 |
if (Array.isArray(r.feats) && r.feats.length) setF('addOcrFeats', r.feats.join('\n'));
|
| 1230 |
if (r.desc) setF('addOcrDesc', r.desc);
|
src/version.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
// V.AI AVATAR — version banner (used for cache-busting / diagnostics)
|
| 2 |
-
window.VAI_AVATAR_VERSION = "
|
| 3 |
window.VAI_AVATAR_CACHE = "v26-gc-" + Date.now();
|
|
|
|
| 1 |
// V.AI AVATAR — version banner (used for cache-busting / diagnostics)
|
| 2 |
+
window.VAI_AVATAR_VERSION = "v29-cart-fix-text-extract";
|
| 3 |
window.VAI_AVATAR_CACHE = "v26-gc-" + Date.now();
|