bep40 commited on
Commit
6cd80e4
·
1 Parent(s): b084ead

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

Files changed (4) hide show
  1. index.html +9 -4
  2. src/cart-quote.js +7 -1
  3. src/vaix-add-product.js +83 -7
  4. 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 / tải ảnh sản phẩm (OCR)</span></div>
 
 
 
 
 
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>3 · Xem trước sản phẩm</span></div>
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=34"></script> <!-- Cart + Quote module (Giỏ hàng / Báo giá) -->
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=16"></script>
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:&#10;Chậu rửa 1 hố mạ nano inox 304&#10;Mã: BN7546&#10;Giá: 5.803.500đ&#10;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
- if (window.VAI_modalTop) window.VAI_modalTop(overlay);
 
 
 
 
 
 
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
- function autoFillCategory(name, brand) {
 
 
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 = "v28-bulk-row-imgbox";
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();