Commit 2ce88d1a authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

feat: optimize stylist retrieval with StarRocks SQL classification and UI grouping

parent c1f361ca
This diff is collapsed.
......@@ -124,7 +124,8 @@ async function loadProduct(code) {
const desc = metaJson.description || {};
const dd = desc.description_data || desc.description || {};
aiMatches = fmJson.ok ? (fmJson.ai_matches || {}) : {};
window.aiMatches = fmJson.ok ? (fmJson.ai_matches || {}) : {};
window.aiClassifications = fmJson.ok ? (fmJson.classifications || {}) : {};
// Populate header
document.getElementById('prodImage').src = desc.product_image_url || '';
......@@ -154,40 +155,104 @@ function switchDetailTab(tab, btn) {
document.getElementById('tabTest').style.display = tab === 'test' ? 'flex' : 'none';
}
// ══ MATCHES TAB ═══════════════════════════════
// ══ MATCHES TAB & SQL GROUPING ═══════════════════════════════
let currentGroupBy = 'occ';
let activeGroupTab = null;
const COLOR_TABS = {
neutral: "<i data-lucide='circle' class='icon-sm'></i> Màu Neutral",
light: "<i data-lucide='sun-dim' class='icon-sm'></i> Màu Light",
dark: "<i data-lucide='moon' class='icon-sm'></i> Màu Dark"
};
const MATERIAL_TABS = {
summer: "<i data-lucide='sun' class='icon-sm'></i> Mùa hè",
winter: "<i data-lucide='snowflake' class='icon-sm'></i> Mùa đông"
};
function getVirtualMatches() {
if (currentGroupBy === 'occ') {
return window.aiMatches || {};
} else if (currentGroupBy === 'color' || currentGroupBy === 'material') {
return window.aiClassifications?.[currentGroupBy] || {};
}
return {};
}
function setGroupBy(type) {
currentGroupBy = type;
document.querySelectorAll('#groupTabs .btn').forEach(btn => {
btn.classList.remove('active');
btn.style.color = "var(--muted-fg)";
});
const activeBtn = document.getElementById(
type === 'occ' ? 'btnGroupOcc' : (type === 'color' ? 'btnGroupColor' : 'btnGroupMaterial')
);
if(activeBtn) {
activeBtn.classList.add('active');
activeBtn.style.color = "var(--primary)";
}
if (type === 'occ') activeGroupTab = Object.keys(OCC_LABELS)[0];
else if (type === 'color') activeGroupTab = 'neutral';
else if (type === 'material') activeGroupTab = 'summer';
renderMatchesTab();
}
function renderMatchesTab() {
renderOccTabs();
if (!activeOcc || !aiMatches[activeOcc]) {
activeOcc = Object.keys(aiMatches)[0] || Object.keys(OCC_LABELS)[0];
renderGroupTabs();
const virtual = getVirtualMatches();
if (!activeGroupTab || !virtual[activeGroupTab]) {
const keys = Object.keys(virtual);
activeGroupTab = keys.length ? keys[0] : activeGroupTab;
}
renderMatchContent();
}
function renderOccTabs() {
function renderGroupTabs() {
const container = document.getElementById('occTabs');
container.innerHTML = Object.entries(OCC_LABELS).map(([occ, label]) => {
const count = Object.values(aiMatches[occ] || {}).reduce((s, a) => s + a.length, 0);
const active = occ === activeOcc ? 'active' : '';
return `<button class="occ-tab ${active}" onclick="switchOcc('${occ}')">
let LABELS = OCC_LABELS;
if (currentGroupBy === 'color') LABELS = COLOR_TABS;
else if (currentGroupBy === 'material') LABELS = MATERIAL_TABS;
const virtual = getVirtualMatches();
container.innerHTML = Object.entries(LABELS).map(([key, label]) => {
let count = 0;
const grpF = virtual[key];
if (Array.isArray(grpF)) {
count = grpF.length;
} else if (grpF && typeof grpF === 'object') {
count = Object.values(grpF).reduce((s, a) => s + (Array.isArray(a) ? a.length : 0), 0);
}
const active = key === activeGroupTab ? 'active' : '';
return `<button class="occ-tab ${active}" onclick="switchGroupTab('${key}')">
${label} <span class="occ-count">${count}</span>
</button>`;
}).join('');
if (window.lucide) lucide.createIcons();
}
function switchOcc(occ) {
activeOcc = occ;
renderOccTabs();
renderMatchContent();
function switchGroupTab(key) {
activeGroupTab = key;
renderMatchesTab();
}
function renderMatchContent() {
const container = document.getElementById('matchContent');
const empty = document.getElementById('emptyState');
const occData = aiMatches[activeOcc] || {};
const hasAny = Object.values(occData).some(arr => arr.length > 0);
const virtual = getVirtualMatches();
const occData = virtual[activeGroupTab] || {};
let hasAny = false;
if (Array.isArray(occData)) {
hasAny = occData.length > 0;
} else {
hasAny = Object.values(occData).some(arr => (Array.isArray(arr) && arr.length > 0));
}
if (!hasAny && !Object.keys(aiMatches).length) {
if (!hasAny && !Object.keys(virtual).length) {
container.style.display = 'none';
empty.style.display = 'flex';
return;
......@@ -195,23 +260,37 @@ function renderMatchContent() {
container.style.display = 'flex';
empty.style.display = 'none';
const roles = ['bottom', 'outerwear', 'accessory'];
container.innerHTML = roles.map(role => {
const items = occData[role] || [];
const roleInfo = ROLE_LABELS[role] || { label: role, emoji: '📦' };
const cards = items.map((item, idx) => renderMatchCard(item, activeOcc, role, idx)).join('');
const addBtn = `<div class="add-card" onclick="openAddModal('${activeOcc}','${role}')">
<div class="add-card-icon">➕</div>
<div class="add-card-label">Thêm SP</div>
</div>`;
return `<div class="role-section">
<div class="role-header">
<div class="role-title">${roleInfo.emoji} ${roleInfo.label} <span class="badge badge-info">${items.length}</span></div>
<button class="btn btn-ghost btn-sm" onclick="openAddModal('${activeOcc}','${role}')">+ Thêm</button>
</div>
<div class="role-body">${cards}${addBtn}</div>
</div>`;
}).join('');
if (Array.isArray(occData)) {
// Flat Array Render for SQL Super Classifications
const items = occData;
const cards = items.map((item, idx) => renderMatchCard(item, activeGroupTab, 'bottom', idx)).join('');
container.innerHTML = `<div class="role-section">
<div class="role-header">
<div class="role-title"><i data-lucide="database" class="icon-sm"></i> Raw SQL SQL Match Results <span class="badge badge-info">${items.length}</span></div>
</div>
<div class="role-body">${cards}</div>
</div>`;
} else {
// Nested Roles Render
const roles = ['bottom', 'outerwear', 'accessory'];
container.innerHTML = roles.map(role => {
const items = occData[role] || [];
const roleInfo = ROLE_LABELS[role] || { label: role, emoji: '📦' };
const cards = items.map((item, idx) => renderMatchCard(item, activeGroupTab, role, idx)).join('');
const addBtn = `<div class="add-card" onclick="openAddModal('${activeGroupTab}','${role}')">
<div class="add-card-icon">➕</div>
<div class="add-card-label">Thêm SP</div>
</div>`;
return `<div class="role-section">
<div class="role-header">
<div class="role-title">${roleInfo.emoji} ${roleInfo.label} <span class="badge badge-info">${items.length}</span></div>
<button class="btn btn-ghost btn-sm" onclick="openAddModal('${activeGroupTab}','${role}')">+ Thêm</button>
</div>
<div class="role-body">${cards}${addBtn}</div>
</div>`;
}).join('');
}
if (window.lucide) lucide.createIcons();
}
function renderMatchCard(item, occ, role, idx) {
......@@ -235,10 +314,11 @@ function renderMatchCard(item, occ, role, idx) {
}
function removeItem(occ, role, idx) {
if (!aiMatches[occ]?.[role]) return;
aiMatches[occ][role].splice(idx, 1);
if (!aiMatches[occ][role].length) delete aiMatches[occ][role];
if (!Object.keys(aiMatches[occ] || {}).length) delete aiMatches[occ];
const virtual = getVirtualMatches();
if (!virtual[occ]?.[role]) return;
virtual[occ][role].splice(idx, 1);
if (!virtual[occ][role].length) delete virtual[occ][role];
if (!Object.keys(virtual[occ] || {}).length) delete virtual[occ];
renderMatchesTab();
}
......@@ -430,7 +510,7 @@ function addItemFromModal(code, name, image) {
}
aiMatches[occ][role].push({ code, name, image, score: 70, reason: 'Thêm thủ công' });
document.getElementById('addModal').style.display = 'none';
addingCtx = null; activeOcc = occ;
addingCtx = null; activeGroupTab = occ;
renderMatchesTab();
showToast(`✅ Đã thêm ${name}`, 'success');
}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment