V.AISTUDIO3 / vai_loader.html
bep40's picture
Upload vai_loader.html
b36a4aa verified
Raw
History Blame Contribute Delete
10.8 kB
<!DOCTYPE html>
<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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
})();
</script>
</body>
</html>