|
@@ -81,7 +81,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-var FAKE_API_RESPONSE = {
|
|
|
|
|
|
|
+var API_RESPONSE_AI = {
|
|
|
// results: [
|
|
// results: [
|
|
|
// { product_id: 'SKU001', mandatory: { 'Clothing Neck Style': 'V-Neck', 'Clothing Top Style': 'Pullover', 'Condition': 'New', 'T-Shirt Type': 'Classic T-Shirt' }, additional: { 'Material': 'Turkish Pima Cotton', 'Size': 'Large', 'Color': 'Blue', 'Brand': 'Sierra', 'Fabric Type': 'Soft & Breathable', 'Fabric Composition': '95% Turkish Pima cotton', 'Care Instructions': 'Machine Washable', 'Sizes Available': 'S-XL' } },
|
|
// { product_id: 'SKU001', mandatory: { 'Clothing Neck Style': 'V-Neck', 'Clothing Top Style': 'Pullover', 'Condition': 'New', 'T-Shirt Type': 'Classic T-Shirt' }, additional: { 'Material': 'Turkish Pima Cotton', 'Size': 'Large', 'Color': 'Blue', 'Brand': 'Sierra', 'Fabric Type': 'Soft & Breathable', 'Fabric Composition': '95% Turkish Pima cotton', 'Care Instructions': 'Machine Washable', 'Sizes Available': 'S-XL' } },
|
|
|
// { product_id: 'SKU002', mandatory: { 'Shoe Type': 'Running', 'Closure': 'Lace-Up', 'Condition': 'New', 'Gender': 'Men' }, additional: { 'Upper Material': 'Engineered Mesh', 'Midsole': 'EVA Foam', 'Outsole': 'Rubber', 'Color': 'Black/White', 'Brand': 'Adidas', 'Size': 'UK 9', 'Care Instructions': 'Surface Clean' } },
|
|
// { product_id: 'SKU002', mandatory: { 'Shoe Type': 'Running', 'Closure': 'Lace-Up', 'Condition': 'New', 'Gender': 'Men' }, additional: { 'Upper Material': 'Engineered Mesh', 'Midsole': 'EVA Foam', 'Outsole': 'Rubber', 'Color': 'Black/White', 'Brand': 'Adidas', 'Size': 'UK 9', 'Care Instructions': 'Surface Clean' } },
|
|
@@ -678,51 +678,194 @@ function renderProductsTable(items = getCurrentSlice()) {
|
|
|
wrap.appendChild(table);
|
|
wrap.appendChild(table);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// function renderInlineForCards() {
|
|
|
|
|
+// const api = API_RESPONSE_AI;
|
|
|
|
|
+// // Clear all inline sections first
|
|
|
|
|
+// document.querySelectorAll('.attr-inline').forEach(div => div.innerHTML = '');
|
|
|
|
|
+
|
|
|
|
|
+// PRODUCT_BASE.forEach((p, idx) => {
|
|
|
|
|
+// const inline = document.querySelector(`.attr-inline[data-pid="${p.item_id}"]`);
|
|
|
|
|
+// if (!inline) return;
|
|
|
|
|
+
|
|
|
|
|
+// // --- CHANGE HERE: Use the new helper function ---
|
|
|
|
|
+// if (!isProductSelected(p.item_id)) return; // only show for selected
|
|
|
|
|
+
|
|
|
|
|
+// const res = findApiResultForProduct(p, idx, api);
|
|
|
|
|
+// const pid = p.item_id;
|
|
|
|
|
+// if (!lastSeen.has(pid)) lastSeen.set(pid, { mandatory: new Map(), additional: new Map(), ocr_results: new Map(), visual_results: new Map() });
|
|
|
|
|
+// const mem = lastSeen.get(pid);
|
|
|
|
|
+
|
|
|
|
|
+// // Build sections
|
|
|
|
|
+// const manTitle = el('div', 'section-title'); manTitle.innerHTML = '<strong>Mandatory</strong>';
|
|
|
|
|
+// const manChips = el('div', 'chips');
|
|
|
|
|
+// const addTitle = el('div', 'section-title'); addTitle.innerHTML = '<strong>Additional</strong>';
|
|
|
|
|
+// const addChips = el('div', 'chips');
|
|
|
|
|
+
|
|
|
|
|
+// const addOcr = el('div', 'section-title'); addOcr.innerHTML = '<strong>Ocr</strong>';
|
|
|
|
|
+// const ocrChips = el('div', 'chips');
|
|
|
|
|
+// const addVisual = el('div', 'section-title'); addVisual.innerHTML = '<strong>Visual</strong>';
|
|
|
|
|
+// const visualChips = el('div', 'chips');
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// const mandCount = renderChips(manChips, res?.mandatory || {}, mem.mandatory);
|
|
|
|
|
+// const addCount = renderChips(addChips, res?.additional || {}, mem.additional);
|
|
|
|
|
+// const ocrCount = renderChips(ocrChips, res?.ocr_results?.extracted_attributes || {}, mem?.ocr_results);
|
|
|
|
|
+// const visualCount = renderChips(visualChips, res?.visual_results?.visual_attributes || {}, mem?.visual_results);
|
|
|
|
|
+
|
|
|
|
|
+// const counts = el('div'); counts.style.display = 'flex'; counts.style.gap = '8px'; counts.style.margin = '8px 0 0';
|
|
|
|
|
+// const c1 = el('span', 'pill'); c1.textContent = `Mandatory: ${mandCount}`;
|
|
|
|
|
+// const c2 = el('span', 'pill'); c2.textContent = `Additional: ${addCount}`;
|
|
|
|
|
+// const c3 = el('span', 'pill'); c3.textContent = `OCR: ${ocrCount}`;
|
|
|
|
|
+// const c4 = el('span', 'pill'); c4.textContent = `Visuals: ${visualCount}`;
|
|
|
|
|
+// counts.appendChild(c1); counts.appendChild(c2); counts.appendChild(c3); counts.appendChild(c4);
|
|
|
|
|
+
|
|
|
|
|
+// inline.appendChild(manTitle); inline.appendChild(manChips);
|
|
|
|
|
+// inline.appendChild(addTitle); inline.appendChild(addChips);
|
|
|
|
|
+// inline.appendChild(addOcr); inline.appendChild(ocrChips);
|
|
|
|
|
+// inline.appendChild(addVisual); inline.appendChild(visualChips);
|
|
|
|
|
+// inline.appendChild(counts);
|
|
|
|
|
+// });
|
|
|
|
|
+
|
|
|
|
|
+// // Update summary
|
|
|
|
|
+// $('#statTotal').textContent = api.total_products ?? 0;
|
|
|
|
|
+// $('#statOk').textContent = api.successful ?? 0;
|
|
|
|
|
+// $('#statKo').textContent = api.failed ?? 0;
|
|
|
|
|
+// $('#api-summary').style.display = 'block';
|
|
|
|
|
+// }
|
|
|
|
|
+
|
|
|
|
|
+// -----------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+// function renderInlineForTable() {
|
|
|
|
|
+// const api = API_RESPONSE_AI;
|
|
|
|
|
+// const table = $('#tableContainer');
|
|
|
|
|
+// if (!table) return;
|
|
|
|
|
+// // Remove existing detail rows
|
|
|
|
|
+// table.querySelectorAll('tr.detail-row').forEach(r => r.remove());
|
|
|
|
|
+
|
|
|
|
|
+// PRODUCT_BASE.forEach((p, idx) => {
|
|
|
|
|
+// // --- CHANGE HERE: Use the new helper function ---
|
|
|
|
|
+// if (!isProductSelected(p.item_id)) return;
|
|
|
|
|
+
|
|
|
|
|
+// const res = findApiResultForProduct(p, idx, api);
|
|
|
|
|
+// const pid = p.item_id;
|
|
|
|
|
+// if (!lastSeen.has(pid)) lastSeen.set(pid, { mandatory: new Map(), additional: new Map(), ocr_results: new Map(), visual_results: new Map() });
|
|
|
|
|
+// const mem = lastSeen.get(pid);
|
|
|
|
|
+
|
|
|
|
|
+// const tbody = table.querySelector('tbody');
|
|
|
|
|
+// // NOTE: The table rendering uses p.id for the row ID: `row-${p.id}`.
|
|
|
|
|
+// // Assuming p.id is still valid for finding the base row, as your original code used it.
|
|
|
|
|
+// const baseRow = tbody.querySelector(`#row-${p.id}`);
|
|
|
|
|
+// if (!baseRow) return;
|
|
|
|
|
+
|
|
|
|
|
+// const detail = el('tr', 'detail-row');
|
|
|
|
|
+// const td = el('td'); td.colSpan = 6; // number of columns
|
|
|
|
|
+// const content = el('div', 'detail-content');
|
|
|
|
|
+
|
|
|
|
|
+// const manTitle = el('div', 'section-title'); manTitle.innerHTML = '<strong>Mandatory</strong>';
|
|
|
|
|
+// const manChips = el('div', 'chips');
|
|
|
|
|
+// const addTitle = el('div', 'section-title'); addTitle.innerHTML = '<strong>Additional</strong>';
|
|
|
|
|
+// const addChips = el('div', 'chips');
|
|
|
|
|
+// const addOcr = el('div', 'section-title'); addOcr.innerHTML = '<strong>Ocr</strong>';
|
|
|
|
|
+// const ocrChips = el('div', 'chips');
|
|
|
|
|
+// const addVisuals = el('div', 'section-title'); addVisuals.innerHTML = '<strong>Visuals</strong>';
|
|
|
|
|
+// const visualsChips = el('div', 'chips');
|
|
|
|
|
+
|
|
|
|
|
+// const mandCount = renderChips(manChips, res?.mandatory || {}, mem.mandatory);
|
|
|
|
|
+// const addCount = renderChips(addChips, res?.additional || {}, mem.additional);
|
|
|
|
|
+// const ocrCount = renderChips(ocrChips, res?.ocr_results?.extracted_attributes || {}, mem.ocr_results);
|
|
|
|
|
+// const visualCount = renderChips(visualsChips, res?.visual_results?.visual_attributes || {}, mem.visual_results);
|
|
|
|
|
+
|
|
|
|
|
+// const counts = el('div'); counts.style.display = 'flex'; counts.style.gap = '8px'; counts.style.margin = '8px 0 0';
|
|
|
|
|
+// const c1 = el('span', 'pill'); c1.textContent = `Mandatory: ${mandCount}`;
|
|
|
|
|
+// const c2 = el('span', 'pill'); c2.textContent = `Additional: ${addCount}`;
|
|
|
|
|
+// const c3 = el('span', 'pill'); c3.textContent = `Ocr: ${ocrCount}`;
|
|
|
|
|
+// const c4 = el('span', 'pill'); c4.textContent = `Visuals: ${visualCount}`;
|
|
|
|
|
+// counts.appendChild(c1); counts.appendChild(c2); counts.appendChild(c3); counts.appendChild(c4);
|
|
|
|
|
+
|
|
|
|
|
+// content.appendChild(manTitle); content.appendChild(manChips);
|
|
|
|
|
+// content.appendChild(addTitle); content.appendChild(addChips);
|
|
|
|
|
+// content.appendChild(addOcr); content.appendChild(ocrChips);
|
|
|
|
|
+// content.appendChild(addVisuals); content.appendChild(visualsChips);
|
|
|
|
|
+// content.appendChild(counts);
|
|
|
|
|
+// td.appendChild(content); detail.appendChild(td);
|
|
|
|
|
+
|
|
|
|
|
+// // insert after base row
|
|
|
|
|
+// baseRow.insertAdjacentElement('afterend', detail);
|
|
|
|
|
+// });
|
|
|
|
|
+
|
|
|
|
|
+// // Update summary
|
|
|
|
|
+// $('#statTotal').textContent = api.total_products ?? 0;
|
|
|
|
|
+// $('#statOk').textContent = api.successful ?? 0;
|
|
|
|
|
+// $('#statKo').textContent = api.failed ?? 0;
|
|
|
|
|
+// $('#api-summary').style.display = 'block';
|
|
|
|
|
+// }
|
|
|
|
|
+
|
|
|
function renderInlineForCards() {
|
|
function renderInlineForCards() {
|
|
|
- const api = FAKE_API_RESPONSE;
|
|
|
|
|
- // Clear all inline sections first
|
|
|
|
|
|
|
+ const api = API_RESPONSE_AI;
|
|
|
document.querySelectorAll('.attr-inline').forEach(div => div.innerHTML = '');
|
|
document.querySelectorAll('.attr-inline').forEach(div => div.innerHTML = '');
|
|
|
|
|
|
|
|
PRODUCT_BASE.forEach((p, idx) => {
|
|
PRODUCT_BASE.forEach((p, idx) => {
|
|
|
const inline = document.querySelector(`.attr-inline[data-pid="${p.item_id}"]`);
|
|
const inline = document.querySelector(`.attr-inline[data-pid="${p.item_id}"]`);
|
|
|
if (!inline) return;
|
|
if (!inline) return;
|
|
|
|
|
|
|
|
- // --- CHANGE HERE: Use the new helper function ---
|
|
|
|
|
- if (!isProductSelected(p.item_id)) return; // only show for selected
|
|
|
|
|
|
|
+ if (!isProductSelected(p.item_id)) return;
|
|
|
|
|
|
|
|
const res = findApiResultForProduct(p, idx, api);
|
|
const res = findApiResultForProduct(p, idx, api);
|
|
|
- const pid = p.item_id;
|
|
|
|
|
- if (!lastSeen.has(pid)) lastSeen.set(pid, { mandatory: new Map(), additional: new Map(), ocr_results: new Map(), visual_results: new Map() });
|
|
|
|
|
- const mem = lastSeen.get(pid);
|
|
|
|
|
-
|
|
|
|
|
- // Build sections
|
|
|
|
|
- const manTitle = el('div', 'section-title'); manTitle.innerHTML = '<strong>Mandatory</strong>';
|
|
|
|
|
- const manChips = el('div', 'chips');
|
|
|
|
|
- const addTitle = el('div', 'section-title'); addTitle.innerHTML = '<strong>Additional</strong>';
|
|
|
|
|
- const addChips = el('div', 'chips');
|
|
|
|
|
-
|
|
|
|
|
- const addOcr = el('div', 'section-title'); addOcr.innerHTML = '<strong>Ocr</strong>';
|
|
|
|
|
- const ocrChips = el('div', 'chips');
|
|
|
|
|
- const addVisual = el('div', 'section-title'); addVisual.innerHTML = '<strong>Visual</strong>';
|
|
|
|
|
- const visualChips = el('div', 'chips');
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Clear existing content
|
|
|
|
|
+ inline.innerHTML = '';
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ // 1. MANDATORY SECTION: RENDER AS COMPARISON CARDS
|
|
|
|
|
+ const mandatorySection = renderMandatoryComparisonCards(
|
|
|
|
|
+ res?.mandatory || {},
|
|
|
|
|
+ 'Mandatory Attributes Comparison'
|
|
|
|
|
+ );
|
|
|
|
|
+ inline.appendChild(mandatorySection);
|
|
|
|
|
+ const mandCount = Object.keys(res?.mandatory || {}).length;
|
|
|
|
|
|
|
|
|
|
|
|
|
- const mandCount = renderChips(manChips, res?.mandatory || {}, mem.mandatory);
|
|
|
|
|
- const addCount = renderChips(addChips, res?.additional || {}, mem.additional);
|
|
|
|
|
- const ocrCount = renderChips(ocrChips, res?.ocr_results?.extracted_attributes || {}, mem?.ocr_results);
|
|
|
|
|
- const visualCount = renderChips(visualChips, res?.visual_results?.visual_attributes || {}, mem?.visual_results);
|
|
|
|
|
|
|
+ const combinedTitle = el('div', 'section-title');
|
|
|
|
|
+ combinedTitle.innerHTML = '<h2>Additional & AI-Driven Attributes</h2>';
|
|
|
|
|
+ inline.appendChild(combinedTitle);
|
|
|
|
|
|
|
|
|
|
+ const combinedAttributesContainer = el('div', 'combined-attributes-container');
|
|
|
|
|
+
|
|
|
|
|
+ // 2. ADDITIONAL SECTION: RENDER AS SIMPLE TABLE
|
|
|
|
|
+ const additionalSection = renderAttributesAsTable(
|
|
|
|
|
+ res?.additional || {},
|
|
|
|
|
+ 'Additional Attributes'
|
|
|
|
|
+ );
|
|
|
|
|
+ // inline.appendChild(additionalSection);
|
|
|
|
|
+ const addCount = Object.keys(res?.additional || {}).length;
|
|
|
|
|
+
|
|
|
|
|
+ // 3. OCR SECTION: RENDER AS SIMPLE TABLE
|
|
|
|
|
+ const ocrSection = renderAttributesAsTable(
|
|
|
|
|
+ res?.ocr_results?.extracted_attributes || {},
|
|
|
|
|
+ 'OCR Results'
|
|
|
|
|
+ );
|
|
|
|
|
+ // inline.appendChild(ocrSection);
|
|
|
|
|
+ const ocrCount = Object.keys(res?.ocr_results?.extracted_attributes || {}).length;
|
|
|
|
|
+
|
|
|
|
|
+ // 4. VISUAL SECTION: RENDER AS SIMPLE TABLE
|
|
|
|
|
+ const visualSection = renderAttributesAsTable(
|
|
|
|
|
+ res?.visual_results?.visual_attributes || {},
|
|
|
|
|
+ 'Visual Attributes'
|
|
|
|
|
+ );
|
|
|
|
|
+ // inline.appendChild(visualSection);
|
|
|
|
|
+ const visualCount = Object.keys(res?.visual_results?.visual_attributes || {}).length;
|
|
|
|
|
+ combinedAttributesContainer.appendChild(additionalSection);
|
|
|
|
|
+ combinedAttributesContainer.appendChild(ocrSection);
|
|
|
|
|
+ combinedAttributesContainer.appendChild(visualSection);
|
|
|
|
|
+ inline.appendChild(combinedAttributesContainer);
|
|
|
|
|
+ // --- Summary Counts (Pills) ---
|
|
|
const counts = el('div'); counts.style.display = 'flex'; counts.style.gap = '8px'; counts.style.margin = '8px 0 0';
|
|
const counts = el('div'); counts.style.display = 'flex'; counts.style.gap = '8px'; counts.style.margin = '8px 0 0';
|
|
|
const c1 = el('span', 'pill'); c1.textContent = `Mandatory: ${mandCount}`;
|
|
const c1 = el('span', 'pill'); c1.textContent = `Mandatory: ${mandCount}`;
|
|
|
const c2 = el('span', 'pill'); c2.textContent = `Additional: ${addCount}`;
|
|
const c2 = el('span', 'pill'); c2.textContent = `Additional: ${addCount}`;
|
|
|
const c3 = el('span', 'pill'); c3.textContent = `OCR: ${ocrCount}`;
|
|
const c3 = el('span', 'pill'); c3.textContent = `OCR: ${ocrCount}`;
|
|
|
const c4 = el('span', 'pill'); c4.textContent = `Visuals: ${visualCount}`;
|
|
const c4 = el('span', 'pill'); c4.textContent = `Visuals: ${visualCount}`;
|
|
|
- counts.appendChild(c1); counts.appendChild(c2); counts.appendChild(c3); counts.appendChild(c4);
|
|
|
|
|
|
|
+ counts.appendChild(c1); counts.appendChild(c2); counts.appendChild(c3); counts.appendChild(c4);
|
|
|
|
|
|
|
|
- inline.appendChild(manTitle); inline.appendChild(manChips);
|
|
|
|
|
- inline.appendChild(addTitle); inline.appendChild(addChips);
|
|
|
|
|
- inline.appendChild(addOcr); inline.appendChild(ocrChips);
|
|
|
|
|
- inline.appendChild(addVisual); inline.appendChild(visualChips);
|
|
|
|
|
inline.appendChild(counts);
|
|
inline.appendChild(counts);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -733,76 +876,559 @@ function renderInlineForCards() {
|
|
|
$('#api-summary').style.display = 'block';
|
|
$('#api-summary').style.display = 'block';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// -----------------------------------------------------------
|
|
|
|
|
|
|
+// function renderInlineForCards() {
|
|
|
|
|
+// const api = API_RESPONSE_AI;
|
|
|
|
|
+// // Clear all inline sections first
|
|
|
|
|
+// document.querySelectorAll('.attr-inline').forEach(div => div.innerHTML = '');
|
|
|
|
|
+
|
|
|
|
|
+// PRODUCT_BASE.forEach((p, idx) => {
|
|
|
|
|
+// const inline = document.querySelector(`.attr-inline[data-pid="${p.item_id}"]`);
|
|
|
|
|
+// if (!inline) return;
|
|
|
|
|
+
|
|
|
|
|
+// if (!isProductSelected(p.item_id)) return;
|
|
|
|
|
+
|
|
|
|
|
+// const res = findApiResultForProduct(p, idx, api);
|
|
|
|
|
+// const pid = p.item_id;
|
|
|
|
|
+// // Memory map (mem) is no longer needed since we removed chip rendering
|
|
|
|
|
+// // I'll keep the variable declarations for count consistency, but remove the memory map usage.
|
|
|
|
|
+
|
|
|
|
|
+// // --- 1. MANDATORY SECTION: RENDER AS COMPARISON CARDS ---
|
|
|
|
|
+// const mandatorySection = renderMandatoryComparisonCards(
|
|
|
|
|
+// res?.mandatory || {},
|
|
|
|
|
+// 'Mandatory Attributes Comparison'
|
|
|
|
|
+// );
|
|
|
|
|
+// inline.appendChild(mandatorySection);
|
|
|
|
|
+
|
|
|
|
|
+// const mandCount = Object.keys(res?.mandatory || {}).length;
|
|
|
|
|
+
|
|
|
|
|
+// // --- 2. ADDITIONAL SECTION: RENDER AS SIMPLE CARDS ---
|
|
|
|
|
+// const additionalSection = renderAttributesAsTable(
|
|
|
|
|
+// res?.additional || {},
|
|
|
|
|
+// 'Additional Attributes'
|
|
|
|
|
+// );
|
|
|
|
|
+// inline.appendChild(additionalSection);
|
|
|
|
|
+// const addCount = Object.keys(res?.additional || {}).length;
|
|
|
|
|
+
|
|
|
|
|
+// // --- 3. OCR SECTION: RENDER AS SIMPLE CARDS ---
|
|
|
|
|
+// const ocrSection = renderAttributesAsTable(
|
|
|
|
|
+// res?.ocr_results?.extracted_attributes || {},
|
|
|
|
|
+// 'OCR Results'
|
|
|
|
|
+// );
|
|
|
|
|
+// inline.appendChild(ocrSection);
|
|
|
|
|
+// const ocrCount = Object.keys(res?.ocr_results?.extracted_attributes || {}).length;
|
|
|
|
|
+
|
|
|
|
|
+// // --- 4. VISUAL SECTION: RENDER AS SIMPLE CARDS ---
|
|
|
|
|
+// const visualSection = renderAttributesAsTable(
|
|
|
|
|
+// res?.visual_results?.visual_attributes || {},
|
|
|
|
|
+// 'Visual Results'
|
|
|
|
|
+// );
|
|
|
|
|
+// inline.appendChild(visualSection);
|
|
|
|
|
+// const visualCount = Object.keys(res?.visual_results?.visual_attributes || {}).length;
|
|
|
|
|
+
|
|
|
|
|
+// // --- Summary Counts (Pills) ---
|
|
|
|
|
+// const counts = el('div'); counts.style.display = 'flex'; counts.style.gap = '8px'; counts.style.margin = '8px 0 0';
|
|
|
|
|
+// const c1 = el('span', 'pill'); c1.textContent = `Mandatory: ${mandCount}`;
|
|
|
|
|
+// const c2 = el('span', 'pill'); c2.textContent = `Additional: ${addCount}`;
|
|
|
|
|
+// const c3 = el('span', 'pill'); c3.textContent = `OCR: ${ocrCount}`;
|
|
|
|
|
+// const c4 = el('span', 'pill'); c4.textContent = `Visuals: ${visualCount}`;
|
|
|
|
|
+// counts.appendChild(c1); counts.appendChild(c2); counts.appendChild(c3); counts.appendChild(c4);
|
|
|
|
|
+
|
|
|
|
|
+// inline.appendChild(counts);
|
|
|
|
|
+// });
|
|
|
|
|
+
|
|
|
|
|
+// // Update summary
|
|
|
|
|
+// $('#statTotal').textContent = api.total_products ?? 0;
|
|
|
|
|
+// $('#statOk').textContent = api.successful ?? 0;
|
|
|
|
|
+// $('#statKo').textContent = api.failed ?? 0;
|
|
|
|
|
+// $('#api-summary').style.display = 'block';
|
|
|
|
|
+// }
|
|
|
|
|
+
|
|
|
|
|
+// ----------------------------------------------------------------
|
|
|
|
|
+// NOTE: You MUST include renderMandatoryComparisonCards (from previous response)
|
|
|
|
|
+// and the necessary CSS for both card styles!
|
|
|
|
|
+// ----------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+// ------------------------------------------------------------------
|
|
|
|
|
+// --- 1. MANDATORY COMPARISON HELPER (Existing vs. AI, with Highlighting) ---
|
|
|
|
|
+// ------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+// function renderInlineForCards() {
|
|
|
|
|
+// const api = API_RESPONSE_AI;
|
|
|
|
|
+// // Clear all inline sections first
|
|
|
|
|
+// document.querySelectorAll('.attr-inline').forEach(div => div.innerHTML = '');
|
|
|
|
|
+
|
|
|
|
|
+// PRODUCT_BASE.forEach((p, idx) => {
|
|
|
|
|
+// const inline = document.querySelector(`.attr-inline[data-pid="${p.item_id}"]`);
|
|
|
|
|
+// if (!inline) return;
|
|
|
|
|
+
|
|
|
|
|
+// // --- CHANGE HERE: Use the new helper function ---
|
|
|
|
|
+// if (!isProductSelected(p.item_id)) return; // only show for selected
|
|
|
|
|
+
|
|
|
|
|
+// const res = findApiResultForProduct(p, idx, api);
|
|
|
|
|
+// const pid = p.item_id;
|
|
|
|
|
+// // Keep memory logic for old chip renderer, even though card renderer doesn't need it
|
|
|
|
|
+// if (!lastSeen.has(pid)) lastSeen.set(pid, { mandatory: new Map(), additional: new Map(), ocr_results: new Map(), visual_results: new Map() });
|
|
|
|
|
+// const mem = lastSeen.get(pid);
|
|
|
|
|
+
|
|
|
|
|
+// // ------------------------------------------------
|
|
|
|
|
+// // 1. MANDATORY SECTION: RENDER AS COMPARISON CARDS
|
|
|
|
|
+// // ------------------------------------------------
|
|
|
|
|
+// const mandatorySection = renderMandatoryComparisonCards(
|
|
|
|
|
+// res?.mandatory || {},
|
|
|
|
|
+// 'Mandatory Attributes Comparison'
|
|
|
|
|
+// );
|
|
|
|
|
+// inline.appendChild(mandatorySection);
|
|
|
|
|
+
|
|
|
|
|
+// // Count the attributes for the summary pill
|
|
|
|
|
+// const mandCount = Object.keys(res?.mandatory || {}).length;
|
|
|
|
|
+
|
|
|
|
|
+// // ------------------------------------------------
|
|
|
|
|
+// // 2. ADDITIONAL/OCR/VISUALS: RENDER AS CHIPS (Original Logic)
|
|
|
|
|
+// // ------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+// // ADDITIONAL
|
|
|
|
|
+// const addTitle = el('div', 'section-title'); addTitle.innerHTML = '<strong>Additional</strong>';
|
|
|
|
|
+// const addChips = el('div', 'chips');
|
|
|
|
|
+// const addCount = renderChips(addChips, res?.additional || {}, mem.additional);
|
|
|
|
|
+// inline.appendChild(addTitle); inline.appendChild(addChips);
|
|
|
|
|
+
|
|
|
|
|
+// // OCR
|
|
|
|
|
+// const addOcr = el('div', 'section-title'); addOcr.innerHTML = '<strong>Ocr</strong>';
|
|
|
|
|
+// const ocrChips = el('div', 'chips');
|
|
|
|
|
+// const ocrCount = renderChips(ocrChips, res?.ocr_results?.extracted_attributes || {}, mem?.ocr_results);
|
|
|
|
|
+// inline.appendChild(addOcr); inline.appendChild(ocrChips);
|
|
|
|
|
+
|
|
|
|
|
+// // VISUALS
|
|
|
|
|
+// const addVisual = el('div', 'section-title'); addVisual.innerHTML = '<strong>Visual</strong>';
|
|
|
|
|
+// const visualChips = el('div', 'chips');
|
|
|
|
|
+// const visualCount = renderChips(visualChips, res?.visual_results?.visual_attributes || {}, mem?.visual_results);
|
|
|
|
|
+// inline.appendChild(addVisual); inline.appendChild(visualChips);
|
|
|
|
|
+
|
|
|
|
|
+// // --- Summary Counts (Pills) ---
|
|
|
|
|
+// const counts = el('div'); counts.style.display = 'flex'; counts.style.gap = '8px'; counts.style.margin = '8px 0 0';
|
|
|
|
|
+// const c1 = el('span', 'pill'); c1.textContent = `Mandatory: ${mandCount}`;
|
|
|
|
|
+// const c2 = el('span', 'pill'); c2.textContent = `Additional: ${addCount}`;
|
|
|
|
|
+// const c3 = el('span', 'pill'); c3.textContent = `OCR: ${ocrCount}`;
|
|
|
|
|
+// const c4 = el('span', 'pill'); c4.textContent = `Visuals: ${visualCount}`;
|
|
|
|
|
+// counts.appendChild(c1); counts.appendChild(c2); counts.appendChild(c3); counts.appendChild(c4);
|
|
|
|
|
+
|
|
|
|
|
+// inline.appendChild(counts);
|
|
|
|
|
+// });
|
|
|
|
|
+
|
|
|
|
|
+// // Update summary
|
|
|
|
|
+// $('#statTotal').textContent = api.total_products ?? 0;
|
|
|
|
|
+// $('#statOk').textContent = api.successful ?? 0;
|
|
|
|
|
+// $('#statKo').textContent = api.failed ?? 0;
|
|
|
|
|
+// $('#api-summary').style.display = 'block';
|
|
|
|
|
+// }
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Renders a table for Mandatory attributes, comparing AI-extracted value ('value')
|
|
|
|
|
+ * against the existing value ('original_value'). Includes a scroll wrapper.
|
|
|
|
|
+ * @param {Object} attributes - The mandatory attribute data.
|
|
|
|
|
+ * @param {string} title - The title for the table section.
|
|
|
|
|
+ * @returns {HTMLElement} A div containing the comparison table.
|
|
|
|
|
+ */
|
|
|
|
|
+// function renderMandatoryComparisonTable(attributes, title) {
|
|
|
|
|
+// const section = el('div', 'attribute-section');
|
|
|
|
|
+
|
|
|
|
|
+// let attributeEntries = [];
|
|
|
|
|
+
|
|
|
|
|
+// Object.keys(attributes).forEach(key => {
|
|
|
|
|
+// const valuesArray = Array.isArray(attributes[key]) ? attributes[key] : [attributes[key]];
|
|
|
|
|
+// valuesArray.forEach(v => {
|
|
|
|
|
+// const aiValue = v.value || 'N/A';
|
|
|
|
|
+// const originalValue = v.original_value || 'N/A';
|
|
|
|
|
+// const source = v.source || 'N/A';
|
|
|
|
|
+
|
|
|
|
|
+// // Comparison is case-insensitive and ignores leading/trailing whitespace
|
|
|
|
|
+// const isMatch = (String(aiValue).trim().toLowerCase() === String(originalValue).trim().toLowerCase());
|
|
|
|
|
+
|
|
|
|
|
+// attributeEntries.push({
|
|
|
|
|
+// name: key,
|
|
|
|
|
+// aiValue: aiValue,
|
|
|
|
|
+// originalValue: originalValue,
|
|
|
|
|
+// source: source,
|
|
|
|
|
+// isMatch: isMatch
|
|
|
|
|
+// });
|
|
|
|
|
+// });
|
|
|
|
|
+// });
|
|
|
|
|
+
|
|
|
|
|
+// const titleEl = el('div', 'section-title');
|
|
|
|
|
+// titleEl.innerHTML = `<h3>${title} (${attributeEntries.length})</h3>`;
|
|
|
|
|
+// section.appendChild(titleEl);
|
|
|
|
|
+
|
|
|
|
|
+// if (attributeEntries.length === 0) {
|
|
|
|
|
+// const msg = el('p', 'no-attributes-message');
|
|
|
|
|
+// msg.textContent = `No ${title.toLowerCase()} found.`;
|
|
|
|
|
+// section.appendChild(msg);
|
|
|
|
|
+// return section;
|
|
|
|
|
+// }
|
|
|
|
|
+
|
|
|
|
|
+// // --- SCROLL WRAPPER ADDITION ---
|
|
|
|
|
+// const scrollWrapper = el('div', 'attribute-scroll-wrapper');
|
|
|
|
|
+// const table = el('table', 'attribute-detail-table comparison-table');
|
|
|
|
|
+
|
|
|
|
|
+// const thead = el('thead');
|
|
|
|
|
+// const headerRow = el('tr');
|
|
|
|
|
+
|
|
|
|
|
+// ['Attribute Name', 'Source', 'Manually Identified Value', 'AI Generated Value'].forEach(text => {
|
|
|
|
|
+// const th = el('th');
|
|
|
|
|
+// th.textContent = text;
|
|
|
|
|
+// headerRow.appendChild(th);
|
|
|
|
|
+// });
|
|
|
|
|
+// thead.appendChild(headerRow);
|
|
|
|
|
+// table.appendChild(thead);
|
|
|
|
|
+
|
|
|
|
|
+// const tbody = el('tbody');
|
|
|
|
|
+
|
|
|
|
|
+// attributeEntries.forEach(attr => {
|
|
|
|
|
+// // Highlight the entire row in red if the values do not match
|
|
|
|
|
+// const row = el('tr', attr.isMatch ? 'match' : 'mismatch-row');
|
|
|
|
|
+
|
|
|
|
|
+// // 1. Attribute Name
|
|
|
|
|
+// const nameTd = el('td', 'attribute-name');
|
|
|
|
|
+// nameTd.textContent = attr.name.replace(/_/g, ' ');
|
|
|
|
|
+// row.appendChild(nameTd);
|
|
|
|
|
+
|
|
|
|
|
+// // 2. Source
|
|
|
|
|
+// const sourceTd = el('td', 'attribute-source');
|
|
|
|
|
+// sourceTd.textContent = formatString(attr.source);
|
|
|
|
|
+// row.appendChild(sourceTd);
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// // 3. Existing Value
|
|
|
|
|
+// const originalTd = el('td', 'original-value');
|
|
|
|
|
+// originalTd.textContent = formatString(attr.originalValue);
|
|
|
|
|
+// row.appendChild(originalTd);
|
|
|
|
|
+
|
|
|
|
|
+// // 4. AI Extracted Value (Highlight if mismatch)
|
|
|
|
|
+// const aiTd = el('td', `ai-value ${attr.aiValue ? '' : 'mismatch-value'}`);
|
|
|
|
|
+// aiTd.textContent = attr.aiValue;
|
|
|
|
|
+// row.appendChild(aiTd);
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// // 5. Match Status
|
|
|
|
|
+// // const matchTd = el('td', 'match-status');
|
|
|
|
|
+// // const matchPill = el('span', `pill status-pill status-${attr.isMatch ? 'match' : 'mismatch'}`);
|
|
|
|
|
+// // matchPill.textContent = attr.isMatch ? '✅ MATCH' : '❌ MISMATCH';
|
|
|
|
|
+// // matchTd.appendChild(matchPill);
|
|
|
|
|
+// // row.appendChild(matchTd);
|
|
|
|
|
+
|
|
|
|
|
+// tbody.appendChild(row);
|
|
|
|
|
+// });
|
|
|
|
|
+
|
|
|
|
|
+// table.appendChild(tbody);
|
|
|
|
|
+// scrollWrapper.appendChild(table); // Append table to wrapper
|
|
|
|
|
+// section.appendChild(scrollWrapper); // Append wrapper to section
|
|
|
|
|
+// return section;
|
|
|
|
|
+// }
|
|
|
|
|
+
|
|
|
|
|
+// ------------------------------------------------------------------
|
|
|
|
|
+// --- 2. GENERAL ATTRIBUTE HELPER (Name, Value, Source, with Scroll) ---
|
|
|
|
|
+// ------------------------------------------------------------------
|
|
|
|
|
+/**
|
|
|
|
|
+ * Renders a table for Mandatory attributes, comparing AI-extracted value ('value')
|
|
|
|
|
+ * against the existing value ('original_value'). Includes a scroll wrapper and mismatch highlighting.
|
|
|
|
|
+ * @param {Object} attributes - The mandatory attribute data.
|
|
|
|
|
+ * @param {string} title - The title for the table section.
|
|
|
|
|
+ * @returns {HTMLElement} A div containing the comparison table.
|
|
|
|
|
+ */
|
|
|
|
|
+function renderMandatoryComparisonTable(attributes, title) {
|
|
|
|
|
+ const section = el('div', 'attribute-section');
|
|
|
|
|
+
|
|
|
|
|
+ let attributeEntries = [];
|
|
|
|
|
+
|
|
|
|
|
+ Object.keys(attributes).forEach(key => {
|
|
|
|
|
+ const valuesArray = Array.isArray(attributes[key]) ? attributes[key] : [attributes[key]];
|
|
|
|
|
+ valuesArray.forEach(v => {
|
|
|
|
|
+ const aiValue = v.value || 'N/A';
|
|
|
|
|
+ const originalValue = v.original_value || 'N/A';
|
|
|
|
|
+ const source = v.source || 'N/A';
|
|
|
|
|
+
|
|
|
|
|
+ // Comparison is case-insensitive and ignores leading/trailing whitespace
|
|
|
|
|
+ const isMatch = (String(aiValue).trim().toLowerCase() === String(originalValue).trim().toLowerCase());
|
|
|
|
|
+
|
|
|
|
|
+ attributeEntries.push({
|
|
|
|
|
+ name: key,
|
|
|
|
|
+ aiValue: aiValue,
|
|
|
|
|
+ originalValue: originalValue,
|
|
|
|
|
+ source: source,
|
|
|
|
|
+ isMatch: isMatch
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const titleEl = el('div', 'section-title');
|
|
|
|
|
+ titleEl.innerHTML = `<h3>${title} (${attributeEntries.length})</h3>`;
|
|
|
|
|
+ section.appendChild(titleEl);
|
|
|
|
|
+
|
|
|
|
|
+ if (attributeEntries.length === 0) {
|
|
|
|
|
+ const msg = el('p', 'no-attributes-message');
|
|
|
|
|
+ msg.textContent = `No ${title.toLowerCase()} found.`;
|
|
|
|
|
+ section.appendChild(msg);
|
|
|
|
|
+ return section;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // --- SCROLL WRAPPER ADDITION ---
|
|
|
|
|
+ const scrollWrapper = el('div', 'attribute-scroll-wrapper');
|
|
|
|
|
+ const table = el('table', 'attribute-detail-table comparison-table');
|
|
|
|
|
+
|
|
|
|
|
+ const thead = el('thead');
|
|
|
|
|
+ const headerRow = el('tr');
|
|
|
|
|
+
|
|
|
|
|
+ // Updated Headers for Comparison Table
|
|
|
|
|
+ ['Attribute Name', 'Manually Identified Value', 'AI Generated Value', 'Source', 'Match'].forEach(text => {
|
|
|
|
|
+ const th = el('th');
|
|
|
|
|
+ th.textContent = text;
|
|
|
|
|
+ headerRow.appendChild(th);
|
|
|
|
|
+ });
|
|
|
|
|
+ thead.appendChild(headerRow);
|
|
|
|
|
+ table.appendChild(thead);
|
|
|
|
|
+
|
|
|
|
|
+ const tbody = el('tbody');
|
|
|
|
|
+
|
|
|
|
|
+ attributeEntries.forEach(attr => {
|
|
|
|
|
+ // Highlight the entire row in red if the values do not match
|
|
|
|
|
+ const row = el('tr', attr.isMatch ? 'match-row' : 'mismatch-row');
|
|
|
|
|
+
|
|
|
|
|
+ // 1. Attribute Name
|
|
|
|
|
+ const nameTd = el('td', 'attribute-name');
|
|
|
|
|
+ nameTd.textContent = formatString(attr.name).replace(/_/g, ' ');
|
|
|
|
|
+ row.appendChild(nameTd);
|
|
|
|
|
+
|
|
|
|
|
+ // 2. Existing Value (Manually added value)
|
|
|
|
|
+ const originalTd = el('td', 'original-value');
|
|
|
|
|
+ originalTd.textContent = formatString(attr.originalValue);
|
|
|
|
|
+ row.appendChild(originalTd);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. AI Extracted Value
|
|
|
|
|
+ const aiTd = el('td', `ai-value ${attr.isMatch ? '' : 'mismatch-value'}`);
|
|
|
|
|
+ aiTd.textContent = attr.aiValue;
|
|
|
|
|
+ row.appendChild(aiTd);
|
|
|
|
|
+
|
|
|
|
|
+ // 4. Source
|
|
|
|
|
+ const sourceTd = el('td', 'attribute-source');
|
|
|
|
|
+ sourceTd.textContent = formatString(attr.source);
|
|
|
|
|
+ row.appendChild(sourceTd);
|
|
|
|
|
+
|
|
|
|
|
+ // 5. Match Status
|
|
|
|
|
+ const matchTd = el('td', 'match-status');
|
|
|
|
|
+ const matchPill = el('span', `pill status-pill status-${attr.isMatch ? 'match' : 'mismatch'}`);
|
|
|
|
|
+ matchPill.textContent = attr.isMatch ? '✅ MATCH' : '❌ MISMATCH';
|
|
|
|
|
+ matchTd.appendChild(matchPill);
|
|
|
|
|
+ row.appendChild(matchTd);
|
|
|
|
|
+
|
|
|
|
|
+ tbody.appendChild(row);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ table.appendChild(tbody);
|
|
|
|
|
+ scrollWrapper.appendChild(table); // Append table to wrapper
|
|
|
|
|
+ section.appendChild(scrollWrapper); // Append wrapper to section
|
|
|
|
|
+ return section;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Renders a table for Additional, OCR, or Visual attributes (Name, Value, Source).
|
|
|
|
|
+ * @param {Object} attributes - The attribute data.
|
|
|
|
|
+ * @param {string} title - The title for the table section.
|
|
|
|
|
+ * @returns {HTMLElement} A div containing the table.
|
|
|
|
|
+ */
|
|
|
|
|
+function renderAttributesAsTable(attributes, title) {
|
|
|
|
|
+ const section = el('div', 'attribute-section');
|
|
|
|
|
+ let attributeEntries = [];
|
|
|
|
|
+
|
|
|
|
|
+ const processAttribute = (key, values) => {
|
|
|
|
|
+ const valuesArray = Array.isArray(values) ? values : [values];
|
|
|
|
|
+ valuesArray.forEach(v => {
|
|
|
|
|
+ attributeEntries.push({
|
|
|
|
|
+ name: key,
|
|
|
|
|
+ value: v.value,
|
|
|
|
|
+ source: v.source || 'N/A'
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ Object.keys(attributes).forEach(key => {
|
|
|
|
|
+ const attribute = attributes[key];
|
|
|
|
|
+
|
|
|
|
|
+ if (Array.isArray(attribute)) {
|
|
|
|
|
+ processAttribute(key, attribute);
|
|
|
|
|
+ } else if (typeof attribute === 'object' && attribute !== null) {
|
|
|
|
|
+ Object.keys(attribute).forEach(subKey => {
|
|
|
|
|
+ const subAttribute = attribute[subKey];
|
|
|
|
|
+ if (Array.isArray(subAttribute)) {
|
|
|
|
|
+ // Combines parent key (e.g., 'size') and sub-key (e.g., 'waist_size')
|
|
|
|
|
+ processAttribute(`${key} (${subKey.replace(/_/g, ' ')})`, subAttribute);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const titleEl = el('div', 'section-title');
|
|
|
|
|
+ titleEl.innerHTML = `<h3>${title} (${attributeEntries.length})</h3>`;
|
|
|
|
|
+ section.appendChild(titleEl);
|
|
|
|
|
+
|
|
|
|
|
+ if (attributeEntries.length === 0) {
|
|
|
|
|
+ const msg = el('p', 'no-attributes-message');
|
|
|
|
|
+ msg.textContent = `No ${title.toLowerCase()} found.`;
|
|
|
|
|
+ section.appendChild(msg);
|
|
|
|
|
+ return section;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // --- SCROLL WRAPPER ADDITION ---
|
|
|
|
|
+ const scrollWrapper = el('div', 'attribute-scroll-wrapper');
|
|
|
|
|
+ const table = el('table', 'attribute-detail-table');
|
|
|
|
|
+
|
|
|
|
|
+ const thead = el('thead');
|
|
|
|
|
+ const headerRow = el('tr');
|
|
|
|
|
+
|
|
|
|
|
+ ['Attribute Name', 'Value'].forEach(text => {
|
|
|
|
|
+ const th = el('th');
|
|
|
|
|
+ th.textContent = text;
|
|
|
|
|
+ headerRow.appendChild(th);
|
|
|
|
|
+ });
|
|
|
|
|
+ thead.appendChild(headerRow);
|
|
|
|
|
+ table.appendChild(thead);
|
|
|
|
|
+
|
|
|
|
|
+ const tbody = el('tbody');
|
|
|
|
|
+
|
|
|
|
|
+ attributeEntries.forEach(attr => {
|
|
|
|
|
+ const row = el('tr');
|
|
|
|
|
+
|
|
|
|
|
+ const nameTd = el('td', 'attribute-name');
|
|
|
|
|
+ nameTd.textContent = formatString(attr.name).replace(/_/g, ' ');
|
|
|
|
|
+ row.appendChild(nameTd);
|
|
|
|
|
+
|
|
|
|
|
+ const valueTd = el('td', 'attribute-value');
|
|
|
|
|
+ const displayValue = Array.isArray(attr.value)
|
|
|
|
|
+ ? (attr.value.map(v => formatString(v.value))).join(', ') +'('+ formatString(attr.source)+ ')'
|
|
|
|
|
+ : formatString(attr.value) +'('+ formatString(attr.source)+ ')';
|
|
|
|
|
+ valueTd.textContent = displayValue || 'N/A';
|
|
|
|
|
+ row.appendChild(valueTd);
|
|
|
|
|
+
|
|
|
|
|
+ // const sourceTd = el('td', 'attribute-source');
|
|
|
|
|
+ // sourceTd.textContent = attr.source || 'Unknown';
|
|
|
|
|
+ // row.appendChild(sourceTd);
|
|
|
|
|
+
|
|
|
|
|
+ tbody.appendChild(row);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ table.appendChild(tbody);
|
|
|
|
|
+ scrollWrapper.appendChild(table); // Append table to wrapper
|
|
|
|
|
+ section.appendChild(scrollWrapper); // Append wrapper to section
|
|
|
|
|
+ return section;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ------------------------------------------------------------------
|
|
|
|
|
+// --- 3. MAIN RENDER FUNCTION (REPLACEMENT) ---
|
|
|
|
|
+// ------------------------------------------------------------------
|
|
|
|
|
|
|
|
function renderInlineForTable() {
|
|
function renderInlineForTable() {
|
|
|
- const api = FAKE_API_RESPONSE;
|
|
|
|
|
|
|
+ const api = API_RESPONSE_AI;
|
|
|
const table = $('#tableContainer');
|
|
const table = $('#tableContainer');
|
|
|
if (!table) return;
|
|
if (!table) return;
|
|
|
|
|
+
|
|
|
// Remove existing detail rows
|
|
// Remove existing detail rows
|
|
|
table.querySelectorAll('tr.detail-row').forEach(r => r.remove());
|
|
table.querySelectorAll('tr.detail-row').forEach(r => r.remove());
|
|
|
|
|
|
|
|
PRODUCT_BASE.forEach((p, idx) => {
|
|
PRODUCT_BASE.forEach((p, idx) => {
|
|
|
- // --- CHANGE HERE: Use the new helper function ---
|
|
|
|
|
if (!isProductSelected(p.item_id)) return;
|
|
if (!isProductSelected(p.item_id)) return;
|
|
|
|
|
|
|
|
const res = findApiResultForProduct(p, idx, api);
|
|
const res = findApiResultForProduct(p, idx, api);
|
|
|
- const pid = p.item_id;
|
|
|
|
|
- if (!lastSeen.has(pid)) lastSeen.set(pid, { mandatory: new Map(), additional: new Map(), ocr_results: new Map(), visual_results: new Map() });
|
|
|
|
|
- const mem = lastSeen.get(pid);
|
|
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const tbody = table.querySelector('tbody');
|
|
const tbody = table.querySelector('tbody');
|
|
|
- // NOTE: The table rendering uses p.id for the row ID: `row-${p.id}`.
|
|
|
|
|
- // Assuming p.id is still valid for finding the base row, as your original code used it.
|
|
|
|
|
- const baseRow = tbody.querySelector(`#row-${p.id}`);
|
|
|
|
|
|
|
+ const baseRow = tbody ? tbody.querySelector(`#row-${p.id}`) : null;
|
|
|
if (!baseRow) return;
|
|
if (!baseRow) return;
|
|
|
|
|
|
|
|
|
|
+ // --- Detail Row Construction ---
|
|
|
const detail = el('tr', 'detail-row');
|
|
const detail = el('tr', 'detail-row');
|
|
|
- const td = el('td'); td.colSpan = 6; // number of columns
|
|
|
|
|
- const content = el('div', 'detail-content');
|
|
|
|
|
-
|
|
|
|
|
- const manTitle = el('div', 'section-title'); manTitle.innerHTML = '<strong>Mandatory</strong>';
|
|
|
|
|
- const manChips = el('div', 'chips');
|
|
|
|
|
- const addTitle = el('div', 'section-title'); addTitle.innerHTML = '<strong>Additional</strong>';
|
|
|
|
|
- const addChips = el('div', 'chips');
|
|
|
|
|
- const addOcr = el('div', 'section-title'); addOcr.innerHTML = '<strong>Ocr</strong>';
|
|
|
|
|
- const ocrChips = el('div', 'chips');
|
|
|
|
|
- const addVisuals = el('div', 'section-title'); addVisuals.innerHTML = '<strong>Visuals</strong>';
|
|
|
|
|
- const visualsChips = el('div', 'chips');
|
|
|
|
|
-
|
|
|
|
|
- const mandCount = renderChips(manChips, res?.mandatory || {}, mem.mandatory);
|
|
|
|
|
- const addCount = renderChips(addChips, res?.additional || {}, mem.additional);
|
|
|
|
|
- const ocrCount = renderChips(ocrChips, res?.ocr_results?.extracted_attributes || {}, mem.ocr_results);
|
|
|
|
|
- const visualCount = renderChips(visualsChips, res?.visual_results?.visual_attributes || {}, mem.visual_results);
|
|
|
|
|
-
|
|
|
|
|
- const counts = el('div'); counts.style.display = 'flex'; counts.style.gap = '8px'; counts.style.margin = '8px 0 0';
|
|
|
|
|
- const c1 = el('span', 'pill'); c1.textContent = `Mandatory: ${mandCount}`;
|
|
|
|
|
- const c2 = el('span', 'pill'); c2.textContent = `Additional: ${addCount}`;
|
|
|
|
|
- const c3 = el('span', 'pill'); c3.textContent = `Ocr: ${ocrCount}`;
|
|
|
|
|
- const c4 = el('span', 'pill'); c4.textContent = `Visuals: ${visualCount}`;
|
|
|
|
|
|
|
+ // td.colSpan must match the number of columns in your main table
|
|
|
|
|
+ const td = el('td'); td.colSpan = 7;
|
|
|
|
|
+ const content = el('div', 'detail-content-tables');
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ // // 1. MANDATORY Attributes Table (NOW USES CARD COMPARISON)
|
|
|
|
|
+ // const mandatorySection = renderMandatoryComparisonCards( // <-- NEW FUNCTION NAME
|
|
|
|
|
+ // res?.mandatory || {},
|
|
|
|
|
+ // 'Mandatory Attributes Comparison'
|
|
|
|
|
+ // );
|
|
|
|
|
+ // content.appendChild(mandatorySection);
|
|
|
|
|
+
|
|
|
|
|
+ // // 2. COMBINED Attributes Section (Additional, OCR, Visuals) - REMAINS THE SAME
|
|
|
|
|
+ // content.appendChild(el('hr', 'section-separator'));
|
|
|
|
|
+
|
|
|
|
|
+ // 1. MANDATORY Attributes Table (USES COMPARISON FUNCTION)
|
|
|
|
|
+ const mandatoryTable = renderMandatoryComparisonTable(
|
|
|
|
|
+ res?.mandatory || {},
|
|
|
|
|
+ 'Mandatory Attributes Comparison'
|
|
|
|
|
+ );
|
|
|
|
|
+ content.appendChild(mandatoryTable);
|
|
|
|
|
+
|
|
|
|
|
+ // 2. COMBINED Attributes Section (Additional, OCR, Visuals)
|
|
|
|
|
+ content.appendChild(el('hr', 'section-separator'));
|
|
|
|
|
+
|
|
|
|
|
+ const combinedTitle = el('div', 'section-title');
|
|
|
|
|
+ combinedTitle.innerHTML = '<h2>Additional & AI-Driven Attributes</h2>';
|
|
|
|
|
+ content.appendChild(combinedTitle);
|
|
|
|
|
+
|
|
|
|
|
+ const combinedAttributesContainer = el('div', 'combined-attributes-container');
|
|
|
|
|
+
|
|
|
|
|
+ // Use the general renderer for these sections
|
|
|
|
|
+ const additionalTable = renderAttributesAsTable(
|
|
|
|
|
+ res?.additional || {},
|
|
|
|
|
+ 'Additional Attributes'
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ const ocrTable = renderAttributesAsTable(
|
|
|
|
|
+ res?.ocr_results?.extracted_attributes || {},
|
|
|
|
|
+ 'OCR Results'
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ const visualsTable = renderAttributesAsTable(
|
|
|
|
|
+ res?.visual_results?.visual_attributes || {},
|
|
|
|
|
+ 'Visual Results'
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // Append all sections to the combined container
|
|
|
|
|
+ combinedAttributesContainer.appendChild(additionalTable);
|
|
|
|
|
+ combinedAttributesContainer.appendChild(ocrTable);
|
|
|
|
|
+ combinedAttributesContainer.appendChild(visualsTable);
|
|
|
|
|
+ content.appendChild(combinedAttributesContainer);
|
|
|
|
|
+
|
|
|
|
|
+ // --- Summary Counts ---
|
|
|
|
|
+ const mandCount = Object.keys(res?.mandatory || {}).length;
|
|
|
|
|
+ const addCount = Object.keys(res?.additional || {}).length;
|
|
|
|
|
+ const ocrExtractedCount = Object.keys(res?.ocr_results?.extracted_attributes || {}).length;
|
|
|
|
|
+ const visualExtractedCount = Object.keys(res?.visual_results?.visual_attributes || {}).length;
|
|
|
|
|
+
|
|
|
|
|
+ const counts = el('div', 'attribute-summary-pills');
|
|
|
|
|
+ const c1 = el('span', 'pill primary'); c1.textContent = `Mandatory: ${mandCount}`;
|
|
|
|
|
+ const c2 = el('span', 'pill secondary'); c2.textContent = `Additional: ${addCount}`;
|
|
|
|
|
+ const c3 = el('span', 'pill secondary'); c3.textContent = `OCR Keys: ${ocrExtractedCount}`;
|
|
|
|
|
+ const c4 = el('span', 'pill secondary'); c4.textContent = `Visual Keys: ${visualExtractedCount}`;
|
|
|
counts.appendChild(c1); counts.appendChild(c2); counts.appendChild(c3); counts.appendChild(c4);
|
|
counts.appendChild(c1); counts.appendChild(c2); counts.appendChild(c3); counts.appendChild(c4);
|
|
|
-
|
|
|
|
|
- content.appendChild(manTitle); content.appendChild(manChips);
|
|
|
|
|
- content.appendChild(addTitle); content.appendChild(addChips);
|
|
|
|
|
- content.appendChild(addOcr); content.appendChild(ocrChips);
|
|
|
|
|
- content.appendChild(addVisuals); content.appendChild(visualsChips);
|
|
|
|
|
content.appendChild(counts);
|
|
content.appendChild(counts);
|
|
|
- td.appendChild(content); detail.appendChild(td);
|
|
|
|
|
-
|
|
|
|
|
- // insert after base row
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Final assembly and insertion
|
|
|
|
|
+ td.appendChild(content);
|
|
|
|
|
+ detail.appendChild(td);
|
|
|
baseRow.insertAdjacentElement('afterend', detail);
|
|
baseRow.insertAdjacentElement('afterend', detail);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // Update summary
|
|
|
|
|
|
|
+ // Update summary statistics
|
|
|
$('#statTotal').textContent = api.total_products ?? 0;
|
|
$('#statTotal').textContent = api.total_products ?? 0;
|
|
|
$('#statOk').textContent = api.successful ?? 0;
|
|
$('#statOk').textContent = api.successful ?? 0;
|
|
|
$('#statKo').textContent = api.failed ?? 0;
|
|
$('#statKo').textContent = api.failed ?? 0;
|
|
|
- $('#api-summary').style.display = 'block';
|
|
|
|
|
|
|
+ const apiSummary = $('#api-summary');
|
|
|
|
|
+ if (apiSummary) apiSummary.style.display = 'block';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderInlineAttributes() {
|
|
function renderInlineAttributes() {
|
|
|
if (layoutMode === 'cards') renderInlineForCards(); else renderInlineForTable();
|
|
if (layoutMode === 'cards') renderInlineForCards(); else renderInlineForTable();
|
|
|
|
|
+ // renderInlineForCards();
|
|
|
|
|
+ // renderInlineForTable();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// --- Main rendering ---
|
|
// --- Main rendering ---
|
|
@@ -821,7 +1447,7 @@ function renderProducts() {
|
|
|
renderPagination();
|
|
renderPagination();
|
|
|
|
|
|
|
|
// If there is a selection, re-render inline attributes (persist across toggle)
|
|
// If there is a selection, re-render inline attributes (persist across toggle)
|
|
|
- if (selectedIds.size > 0) renderInlineAttributes();
|
|
|
|
|
|
|
+ if (selectedProductsWithAttributes.length > 0) renderInlineAttributes();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// --- Submit & Reset ---
|
|
// --- Submit & Reset ---
|
|
@@ -894,7 +1520,7 @@ function submitAttributes() {
|
|
|
.then(response => response.json())
|
|
.then(response => response.json())
|
|
|
.then(data => {
|
|
.then(data => {
|
|
|
// console.log("response data",data);
|
|
// console.log("response data",data);
|
|
|
- FAKE_API_RESPONSE = data;
|
|
|
|
|
|
|
+ API_RESPONSE_AI = data;
|
|
|
renderInlineAttributes();
|
|
renderInlineAttributes();
|
|
|
jQuery('#full-page-loader').hide();
|
|
jQuery('#full-page-loader').hide();
|
|
|
});
|
|
});
|
|
@@ -1212,3 +1838,296 @@ function getThreshold() {
|
|
|
// console.log("parseFloat(thresholdInput.value)",parseFloat(thresholdInput.value));
|
|
// console.log("parseFloat(thresholdInput.value)",parseFloat(thresholdInput.value));
|
|
|
return parseFloat(thresholdInput.value);
|
|
return parseFloat(thresholdInput.value);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Renders Mandatory attributes using a card-based comparison layout.
|
|
|
|
|
+ * Highlights mismatches prominently.
|
|
|
|
|
+ * @param {Object} attributes - The mandatory attribute data.
|
|
|
|
|
+ * @param {string} title - The title for the section.
|
|
|
|
|
+ * @returns {HTMLElement} A div containing the comparison cards.
|
|
|
|
|
+ */
|
|
|
|
|
+function renderMandatoryComparisonCards(attributes, title) {
|
|
|
|
|
+ const section = el('div', 'attribute-section');
|
|
|
|
|
+
|
|
|
|
|
+ // --- 1. Flatten Mandatory Attributes ---
|
|
|
|
|
+ let attributeEntries = [];
|
|
|
|
|
+
|
|
|
|
|
+ Object.keys(attributes).forEach(key => {
|
|
|
|
|
+ const valuesArray = Array.isArray(attributes[key]) ? attributes[key] : [attributes[key]];
|
|
|
|
|
+ valuesArray.forEach(v => {
|
|
|
|
|
+ const aiValue = v.value || 'N/A';
|
|
|
|
|
+ const originalValue = v.original_value || 'N/A';
|
|
|
|
|
+
|
|
|
|
|
+ // Comparison is case-insensitive and ignores leading/trailing whitespace
|
|
|
|
|
+ const isMatch = (String(aiValue).trim().toLowerCase() === String(originalValue).trim().toLowerCase());
|
|
|
|
|
+
|
|
|
|
|
+ attributeEntries.push({
|
|
|
|
|
+ name: key,
|
|
|
|
|
+ aiValue: aiValue,
|
|
|
|
|
+ originalValue: originalValue,
|
|
|
|
|
+ isMatch: isMatch,
|
|
|
|
|
+ source: v.source || 'N/A'
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // --- 2. Section Header ---
|
|
|
|
|
+ const titleEl = el('div', 'section-title');
|
|
|
|
|
+ titleEl.innerHTML = `<h3>${title} (${attributeEntries.length})</h3>`;
|
|
|
|
|
+ section.appendChild(titleEl);
|
|
|
|
|
+
|
|
|
|
|
+ if (attributeEntries.length === 0) {
|
|
|
|
|
+ const msg = el('p', 'no-attributes-message');
|
|
|
|
|
+ msg.textContent = `No ${title.toLowerCase()} found.`;
|
|
|
|
|
+ section.appendChild(msg);
|
|
|
|
|
+ return section;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // --- 3. Card Container ---
|
|
|
|
|
+ const cardsContainer = el('div', 'comparison-cards-container');
|
|
|
|
|
+
|
|
|
|
|
+ attributeEntries.forEach(attr => {
|
|
|
|
|
+ // Main Card Element
|
|
|
|
|
+ const card = el('div', `comparison-card ${attr.isMatch ? 'match' : 'mismatch-card'}`);
|
|
|
|
|
+
|
|
|
|
|
+ // Card Header (Attribute Name)
|
|
|
|
|
+ const header = el('div', 'card-header');
|
|
|
|
|
+ header.textContent = attr.name.replace(/_/g, ' ');
|
|
|
|
|
+ card.appendChild(header);
|
|
|
|
|
+
|
|
|
|
|
+ // Content Wrapper
|
|
|
|
|
+ const content = el('div', 'card-content');
|
|
|
|
|
+
|
|
|
|
|
+ // Existing Value Box
|
|
|
|
|
+ const originalBox = el('div', 'value-box original-box');
|
|
|
|
|
+ originalBox.innerHTML = `
|
|
|
|
|
+ <div class="value-label">Manually Identified Value</div>
|
|
|
|
|
+ <div class="value-text">${attr.originalValue}</div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ content.appendChild(originalBox);
|
|
|
|
|
+
|
|
|
|
|
+ // AI Value Box
|
|
|
|
|
+ const aiBox = el('div', `value-box ai-box ${attr.isMatch ? '' : 'mismatch-value'}`);
|
|
|
|
|
+ aiBox.innerHTML = `
|
|
|
|
|
+ <div class="value-label">AI Generated Value <span class="value-source">(${attr.source})</span></div>
|
|
|
|
|
+ <div class="value-text">${attr.aiValue}</div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ content.appendChild(aiBox);
|
|
|
|
|
+
|
|
|
|
|
+ card.appendChild(content);
|
|
|
|
|
+
|
|
|
|
|
+ // Mismatch Indicator (only visible on mismatch-card via CSS)
|
|
|
|
|
+ if (!attr.isMatch) {
|
|
|
|
|
+ const indicator = el('div', 'mismatch-indicator');
|
|
|
|
|
+ // indicator.innerHTML = '❌ MISMATCH';
|
|
|
|
|
+ indicator.innerHTML = attr.isMatch ? '✅ MATCH' : '❌ MISMATCH';
|
|
|
|
|
+ card.appendChild(indicator);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ cardsContainer.appendChild(card);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ section.appendChild(cardsContainer);
|
|
|
|
|
+ return section;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Example JavaScript (Assuming you have access to API_RESPONSE_AI)
|
|
|
|
|
+// document.getElementById('downloadResultBtn').addEventListener('click', () => {
|
|
|
|
|
+// // 1. Convert the data to a JSON string
|
|
|
|
|
+// const jsonString = JSON.stringify(API_RESPONSE_AI, null, 2);
|
|
|
|
|
+
|
|
|
|
|
+// // 2. Create a Blob from the JSON string
|
|
|
|
|
+// const blob = new Blob([jsonString], { type: 'application/json' });
|
|
|
|
|
+
|
|
|
|
|
+// // 3. Create a temporary URL and link element
|
|
|
|
|
+// const url = URL.createObjectURL(blob);
|
|
|
|
|
+// const a = document.createElement('a');
|
|
|
|
|
+
|
|
|
|
|
+// // 4. Set download attributes
|
|
|
|
|
+// a.href = url;
|
|
|
|
|
+// a.download = 'api_generated_results.json';
|
|
|
|
|
+
|
|
|
|
|
+// // 5. Simulate a click to trigger download
|
|
|
|
|
+// document.body.appendChild(a);
|
|
|
|
|
+// a.click();
|
|
|
|
|
+
|
|
|
|
|
+// // 6. Clean up
|
|
|
|
|
+// document.body.removeChild(a);
|
|
|
|
|
+// URL.revokeObjectURL(url);
|
|
|
|
|
+// });
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Renders Mandatory attributes using a card-based comparison layout.
|
|
|
|
|
+ * Highlights mismatches prominently.
|
|
|
|
|
+ * @param {Object} attributes - The mandatory attribute data.
|
|
|
|
|
+ * @param {string} title - The title for the section (used for the header).
|
|
|
|
|
+ * @returns {HTMLElement} A div containing the comparison cards.
|
|
|
|
|
+ */
|
|
|
|
|
+function renderMandatoryComparisonCards(attributes, title) {
|
|
|
|
|
+ const section = el('div', 'attribute-section mandatory-comparison-section');
|
|
|
|
|
+
|
|
|
|
|
+ // --- 1. Flatten Mandatory Attributes ---
|
|
|
|
|
+ let attributeEntries = [];
|
|
|
|
|
+
|
|
|
|
|
+ Object.keys(attributes).forEach(key => {
|
|
|
|
|
+ const valuesArray = Array.isArray(attributes[key]) ? attributes[key] : [attributes[key]];
|
|
|
|
|
+ valuesArray.forEach(v => {
|
|
|
|
|
+ const aiValue = v.value || 'N/A';
|
|
|
|
|
+ const originalValue = v.original_value || 'N/A';
|
|
|
|
|
+
|
|
|
|
|
+ const isMatch = (String(aiValue).trim().toLowerCase() === String(originalValue).trim().toLowerCase());
|
|
|
|
|
+
|
|
|
|
|
+ attributeEntries.push({
|
|
|
|
|
+ name: key,
|
|
|
|
|
+ aiValue: aiValue,
|
|
|
|
|
+ originalValue: originalValue,
|
|
|
|
|
+ isMatch: isMatch,
|
|
|
|
|
+ source: v.source || 'N/A'
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // --- 2. Section Header ---
|
|
|
|
|
+ const titleEl = el('div', 'section-title');
|
|
|
|
|
+ titleEl.innerHTML = `<h3>${title} (${attributeEntries.length})</h3>`;
|
|
|
|
|
+ section.appendChild(titleEl);
|
|
|
|
|
+
|
|
|
|
|
+ if (attributeEntries.length === 0) {
|
|
|
|
|
+ const msg = el('p', 'no-attributes-message');
|
|
|
|
|
+ msg.textContent = `No ${title.toLowerCase()} found.`;
|
|
|
|
|
+ section.appendChild(msg);
|
|
|
|
|
+ return section;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // --- 3. Card Container ---
|
|
|
|
|
+ const cardsContainer = el('div', 'comparison-cards-container');
|
|
|
|
|
+
|
|
|
|
|
+ attributeEntries.forEach(attr => {
|
|
|
|
|
+ const card = el('div', `comparison-card ${attr.isMatch ? 'match' : 'mismatch-card'}`);
|
|
|
|
|
+
|
|
|
|
|
+ const header = el('div', 'card-header');
|
|
|
|
|
+ header.textContent = attr.name.replace(/_/g, ' ');
|
|
|
|
|
+ card.appendChild(header);
|
|
|
|
|
+
|
|
|
|
|
+ const content = el('div', 'card-content');
|
|
|
|
|
+
|
|
|
|
|
+ // Existing Value Box
|
|
|
|
|
+ const originalBox = el('div', 'value-box original-box');
|
|
|
|
|
+ originalBox.innerHTML = `
|
|
|
|
|
+ <div class="value-label">Manually Identified Value</div>
|
|
|
|
|
+ <div class="value-text">${attr.originalValue}</div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ content.appendChild(originalBox);
|
|
|
|
|
+
|
|
|
|
|
+ // AI Value Box
|
|
|
|
|
+ const aiBox = el('div', `value-box ai-box ${attr.isMatch ? 'match' : 'mismatch-value'}`);
|
|
|
|
|
+ aiBox.innerHTML = `
|
|
|
|
|
+ <div class="value-label">AI Generated Value <span class="value-source">(${attr.source})</span></div>
|
|
|
|
|
+ <div class="value-text">${attr.aiValue}</div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ content.appendChild(aiBox);
|
|
|
|
|
+
|
|
|
|
|
+ card.appendChild(content);
|
|
|
|
|
+
|
|
|
|
|
+ // Mismatch Indicator
|
|
|
|
|
+ if (!attr.isMatch) {
|
|
|
|
|
+ const indicator = el('div', 'mismatch-indicator');
|
|
|
|
|
+ // indicator.innerHTML = '❌ MISMATCH';
|
|
|
|
|
+ indicator.innerHTML = attr.isMatch ? '✅ MATCH' : '❌ MISMATCH';
|
|
|
|
|
+ card.appendChild(indicator);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ cardsContainer.appendChild(card);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ section.appendChild(cardsContainer);
|
|
|
|
|
+ return section;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Renders Additional, OCR, or Visual attributes in a simple card layout.
|
|
|
|
|
+ * @param {Object} attributes - The attribute data (Additional, OCR, or Visual).
|
|
|
|
|
+ * @param {string} title - The title for the section.
|
|
|
|
|
+ * @returns {HTMLElement} A div containing the attribute cards.
|
|
|
|
|
+ */
|
|
|
|
|
+function renderSimpleAttributeCards(attributes, title) {
|
|
|
|
|
+ const section = el('div', 'attribute-section simple-attribute-section');
|
|
|
|
|
+
|
|
|
|
|
+ // --- 1. Flatten Attributes ---
|
|
|
|
|
+ let attributeEntries = [];
|
|
|
|
|
+
|
|
|
|
|
+ const processAttribute = (key, values) => {
|
|
|
|
|
+ const valuesArray = Array.isArray(values) ? values : [values];
|
|
|
|
|
+ valuesArray.forEach(v => {
|
|
|
|
|
+ attributeEntries.push({
|
|
|
|
|
+ name: key,
|
|
|
|
|
+ value: v.value,
|
|
|
|
|
+ source: v.source || 'N/A'
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ Object.keys(attributes).forEach(key => {
|
|
|
|
|
+ const attribute = attributes[key];
|
|
|
|
|
+
|
|
|
|
|
+ if (Array.isArray(attribute)) {
|
|
|
|
|
+ processAttribute(key, attribute);
|
|
|
|
|
+ } else if (typeof attribute === 'object' && attribute !== null) {
|
|
|
|
|
+ Object.keys(attribute).forEach(subKey => {
|
|
|
|
|
+ const subAttribute = attribute[subKey];
|
|
|
|
|
+ if (Array.isArray(subAttribute)) {
|
|
|
|
|
+ processAttribute(`${key} (${subKey.replace(/_/g, ' ')})`, subAttribute);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // --- 2. Section Header ---
|
|
|
|
|
+ const titleEl = el('div', 'section-title');
|
|
|
|
|
+ titleEl.innerHTML = `<h3>${title} (${attributeEntries.length})</h3>`;
|
|
|
|
|
+ section.appendChild(titleEl);
|
|
|
|
|
+
|
|
|
|
|
+ if (attributeEntries.length === 0) {
|
|
|
|
|
+ const msg = el('p', 'no-attributes-message');
|
|
|
|
|
+ msg.textContent = `No ${title.toLowerCase()} found.`;
|
|
|
|
|
+ section.appendChild(msg);
|
|
|
|
|
+ return section;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // --- 3. Card Container ---
|
|
|
|
|
+ const cardsContainer = el('div', 'comparison-cards-container simple-cards-container');
|
|
|
|
|
+
|
|
|
|
|
+ attributeEntries.forEach(attr => {
|
|
|
|
|
+ // Simple Card Element
|
|
|
|
|
+ const card = el('div', 'simple-card');
|
|
|
|
|
+
|
|
|
|
|
+ // Card Header (Attribute Name)
|
|
|
|
|
+ const header = el('div', 'card-header');
|
|
|
|
|
+ header.textContent = formatString(attr.name).replace(/_/g, ' ');
|
|
|
|
|
+ card.appendChild(header);
|
|
|
|
|
+
|
|
|
|
|
+ // Content Wrapper
|
|
|
|
|
+ const content = el('div', 'card-content');
|
|
|
|
|
+
|
|
|
|
|
+ // Value Box
|
|
|
|
|
+ const valueBox = el('div', 'value-box single-value-box');
|
|
|
|
|
+ const displayValue = Array.isArray(attr.value)
|
|
|
|
|
+ ? (attr.value.map(v => formatString(v.value))).join(', ') +'('+ formatString(attr.source)+ ')'
|
|
|
|
|
+ : formatString(attr.value) +'('+ formatString(attr.source)+ ')';
|
|
|
|
|
+ // : attr.value;
|
|
|
|
|
+
|
|
|
|
|
+ valueBox.innerHTML = `
|
|
|
|
|
+ <div class="value-label">Extracted Value <span class="value-source">(${formatString(attr.source)})</span></div>
|
|
|
|
|
+ <div class="value-text">${displayValue || 'N/A'}</div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ content.appendChild(valueBox);
|
|
|
|
|
+
|
|
|
|
|
+ card.appendChild(content);
|
|
|
|
|
+ cardsContainer.appendChild(card);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ section.appendChild(cardsContainer);
|
|
|
|
|
+ return section;
|
|
|
|
|
+}
|