Spaces:
Running
Running
| <html lang="vi"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>V.AISTUDIO - Load All Products</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
| background: #0f172a; | |
| color: #f8fafc; | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| header { | |
| background: #003f62; | |
| padding: 16px 20px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| flex-shrink: 0; | |
| } | |
| header h1 { font-size: 1.2rem; font-weight: 800; } | |
| header h1 span { color: #db9815; } | |
| .status { display: flex; gap: 12px; align-items: center; } | |
| .badge { | |
| padding: 4px 12px; | |
| border-radius: 20px; | |
| font-size: 0.75rem; | |
| font-weight: 700; | |
| background: #334155; | |
| border: 1px solid #475569; | |
| } | |
| .badge.ok { background: #166534; border-color: #22c55e; } | |
| .badge.err { background: #7f1d1d; border-color: #ef4444; } | |
| .content { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 16px; | |
| } | |
| .info-bar { | |
| background: #1e293b; | |
| border-radius: 12px; | |
| padding: 16px; | |
| margin-bottom: 16px; | |
| display: flex; | |
| gap: 12px; | |
| flex-wrap: wrap; | |
| } | |
| .info-item { text-align: center; flex: 1; min-width: 80px; } | |
| .info-item .num { font-size: 1.5rem; font-weight: 900; color: #db9815; } | |
| .info-item .label { font-size: 0.7rem; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px; } | |
| .progress-bar { | |
| width: 100%; | |
| height: 4px; | |
| background: #334155; | |
| border-radius: 2px; | |
| overflow: hidden; | |
| margin: 12px 0; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #003f62, #db9815); | |
| transition: width 0.3s ease; | |
| border-radius: 2px; | |
| } | |
| .product-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | |
| gap: 12px; | |
| } | |
| .product-card { | |
| background: #1e293b; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| border: 1px solid #334155; | |
| transition: all 0.2s ease; | |
| cursor: pointer; | |
| } | |
| .product-card:hover { | |
| border-color: #db9815; | |
| transform: translateY(-2px); | |
| } | |
| .product-card .img-box { | |
| width: 100%; | |
| height: 160px; | |
| background: #0f172a; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| overflow: hidden; | |
| } | |
| .product-card .img-box img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: contain; | |
| padding: 8px; | |
| } | |
| .product-card .img-box .placeholder { | |
| font-size: 2rem; | |
| opacity: 0.3; | |
| } | |
| .product-card .info { | |
| padding: 12px; | |
| } | |
| .product-card .name { | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| line-height: 1.3; | |
| margin-bottom: 6px; | |
| display: -webkit-box; | |
| -webkit-line-clamp: 2; | |
| -webkit-box-orient: vertical; | |
| overflow: hidden; | |
| } | |
| .product-card .brand { | |
| font-size: 0.7rem; | |
| color: #db9815; | |
| margin-bottom: 4px; | |
| } | |
| .product-card .price { | |
| font-size: 0.9rem; | |
| font-weight: 800; | |
| color: #f8fafc; | |
| } | |
| .product-card .sku { | |
| font-size: 0.65rem; | |
| color: #64748b; | |
| margin-top: 4px; | |
| } | |
| .load-more { | |
| display: block; | |
| margin: 16px auto; | |
| padding: 12px 40px; | |
| background: #003f62; | |
| color: #fff; | |
| border: none; | |
| border-radius: 10px; | |
| font-size: 0.85rem; | |
| font-weight: 700; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .load-more:hover { background: #005a8c; } | |
| .load-more:disabled { opacity: 0.4; cursor: default; } | |
| .loading-msg { | |
| text-align: center; | |
| padding: 40px; | |
| color: #94a3b8; | |
| font-size: 0.9rem; | |
| } | |
| .error-msg { | |
| background: #7f1d1d; | |
| color: #fca5a5; | |
| padding: 16px; | |
| border-radius: 12px; | |
| margin-bottom: 16px; | |
| font-size: 0.85rem; | |
| display: none; | |
| } | |
| .error-msg.show { display: block; } | |
| .source-btn { | |
| padding: 8px 16px; | |
| background: #db9815; | |
| color: #0f172a; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 0.75rem; | |
| font-weight: 700; | |
| cursor: pointer; | |
| text-decoration: none; | |
| display: inline-block; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>V.AI <span>STUDIO</span> — Product Loader</h1> | |
| <div class="status"> | |
| <span id="statusBadge" class="badge">⏳ Initializing</span> | |
| <span id="countBadge" class="badge">0 products</span> | |
| </div> | |
| </header> | |
| <div class="content"> | |
| <div id="errorMsg" class="error-msg"></div> | |
| <div class="info-bar"> | |
| <div class="info-item"><div class="num" id="totalNum">0</div><div class="label">Total</div></div> | |
| <div class="info-item"><div class="num" id="loadedNum">0</div><div class="label">Loaded</div></div> | |
| <div class="info-item"><div class="num" id="showingNum">0</div><div class="label">Showing</div></div> | |
| <div class="info-item"><div class="num" id="pageNum">0</div><div class="label">Page</div></div> | |
| </div> | |
| <div class="progress-bar"><div id="progressFill" class="progress-fill" style="width:0%"></div></div> | |
| <div class="product-grid" id="productGrid"></div> | |
| <div class="loading-msg" id="loadingMsg" style="display:none"> | |
| Loading products from backend... | |
| </div> | |
| <button id="loadMoreBtn" class="load-more" style="display:none"> | |
| Load More Products | |
| </button> | |
| <div style="text-align:center; margin-top: 20px; display: none;" id="sourceSection"> | |
| <a href="https://bep40-gemma-avatar.hf.space/" class="source-btn" target="_blank"> | |
| ← Open in Gemma Avatar (full catalog) | |
| </a> | |
| </div> | |
| </div> | |
| <script> | |
| (function() { | |
| 'use strict'; | |
| const API_BASE = 'https://bep40-gemma-avatar.hf.space'; | |
| const ITEMS_PER_LOAD = 100; | |
| let allProducts = []; | |
| let loaded = 0; | |
| let currentPage = 0; | |
| const PAGE_SIZE = 48; | |
| // Check if localStorage has cached data | |
| const STORAGE_KEY = 'vaistudio_all_products'; | |
| let cachedProducts = null; | |
| try { cachedProducts = JSON.parse(localStorage.getItem(STORAGE_KEY)); } catch(e) {} | |
| if (cachedProducts && cachedProducts.length > 0) { | |
| console.log('[Loader] Using cached ' + cachedProducts.length + ' products'); | |
| allProducts = cachedProducts; | |
| loaded = cachedProducts.length; | |
| setStatus('Cache loaded'); | |
| updateInfoBar(); | |
| renderPage(); | |
| return; | |
| } | |
| // Load from VAIX API | |
| loadProducts(); | |
| function loadProducts() { | |
| setStatus('Loading from backend...'); | |
| document.getElementById('loadingMsg').style.display = 'block'; | |
| const url = API_BASE + '/api/vaix/products'; | |
| fetch(url, { signal: AbortSignal.timeout(60_000) }) | |
| .then(async function(resp) { | |
| if (!resp.ok) throw new Error('HTTP ' + resp.status); | |
| const data = await resp.json(); | |
| if (data.products && Array.isArray(data.products)) { | |
| allProducts = data.products; | |
| loaded = allProducts.length; | |
| // Cache in localStorage | |
| try { localStorage.setItem(STORAGE_KEY, JSON.stringify(allProducts)); } catch(e) {} | |
| setStatus('✅ Loaded all ' + allProducts.length.toLocaleString() + ' products from backend'); | |
| updateInfoBar(); | |
| renderPage(); | |
| document.getElementById('sourceSection').style.display = 'block'; | |
| document.getElementById('loadingMsg').style.display = 'none'; | |
| console.log('[Loader] All products loaded:', allProducts.length); | |
| } else { | |
| throw new Error('No products in response'); | |
| } | |
| }) | |
| .catch(function(err) { | |
| console.error('[Loader] Error:', err); | |
| showError('Failed to load products: ' + err.message + '\n\nCheck if the Gemma Avatar space is running, or try refreshing.'); | |
| }); | |
| } | |
| function renderPage() { | |
| const grid = document.getElementById('productGrid'); | |
| const end = Math.min((currentPage + 1) * PAGE_SIZE, allProducts.length); | |
| const products = allProducts.slice(currentPage * PAGE_SIZE, end); | |
| grid.innerHTML = ''; | |
| products.forEach(function(p) { | |
| const card = document.createElement('div'); | |
| card.className = 'product-card'; | |
| const img = p.image || ''; | |
| const name = p.title_clean || p.name || 'Unknown'; | |
| const brand = p.brand || ''; | |
| const price = p.priceNum ? p.priceNum.toLocaleString('vi-VN') + 'đ' : (p.price || (p.priceNum ? p.priceNum.toLocaleString('vi-VN') + 'đ' : 'Liên hệ')); | |
| const sku = p.sku || p.model || ''; | |
| card.innerHTML = | |
| '<div class="img-box">' + | |
| (img ? '<img src="' + img + '" alt="" onerror="this.parentElement.innerHTML=\'<div class=\\'placeholder\\'>📦</div>\'">' : '<div class="placeholder">📦</div>') + | |
| '</div>' + | |
| '<div class="info">' + | |
| '<div class="name">' + escapeHtml(name) + '</div>' + | |
| (brand ? '<div class="brand">' + escapeHtml(brand) + '</div>' : '') + | |
| '<div class="price">' + escapeHtml(price) + '</div>' + | |
| (sku ? '<div class="sku">' + escapeHtml(sku) + '</div>' : '') + | |
| '</div>'; | |
| card.addEventListener('click', function() { | |
| window.open('https://bep40-gemma-avatar.hf.space/', '_blank'); | |
| }); | |
| grid.appendChild(card); | |
| }); | |
| currentPage++; | |
| document.getElementById('showingNum').textContent = (currentPage * PAGE_SIZE).toLocaleString(); | |
| document.getElementById('pageNum').textContent = Math.ceil(allProducts.length / PAGE_SIZE); | |
| // Show/hide "Load More" button | |
| const btn = document.getElementById('loadMoreBtn'); | |
| if (end < allProducts.length) { | |
| btn.style.display = 'block'; | |
| btn.textContent = 'Load More (' + (allProducts.length - end).toLocaleString() + ' remaining)'; | |
| } else { | |
| btn.style.display = 'none'; | |
| } | |
| updateInfoBar(); | |
| } | |
| document.getElementById('loadMoreBtn').addEventListener('click', renderPage); | |
| function setStatus(text) { | |
| document.getElementById('statusBadge').textContent = text; | |
| } | |
| function updateInfoBar() { | |
| document.getElementById('totalNum').textContent = allProducts.length.toLocaleString(); | |
| document.getElementById('loadedNum').textContent = loaded.toLocaleString(); | |
| document.getElementById('countBadge').textContent = loaded.toLocaleString() + ' products'; | |
| document.getElementById('progressFill').style.width = allProducts.length > 0 ? '100%' : '0%'; | |
| } | |
| function showError(text) { | |
| const el = document.getElementById('errorMsg'); | |
| el.textContent = text; | |
| el.classList.add('show'); | |
| setStatus('❌ Error'); | |
| document.getElementById('loadingMsg').style.display = 'none'; | |
| } | |
| function escapeHtml(s) { | |
| return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); | |
| } | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |