jQuery.noConflict(); // Release $ to other libraries // console.log(typeof jQuery); // $ = jQuery; // --- Config --- const UPLOAD_API_URL = '/attr/products/upload-excel/'; // TODO: set to your upload endpoint const ACCEPT_TYPES = '*'; // e.g., 'image/*,.csv,.xlsx' const thresholdInput = document.getElementById('thresholdRange'); const thresholdValueDisplay = document.getElementById('thresholdValue'); var attributesFullData = []; var PRODUCT_BASE = [ // { id: 1, item_id: 'SKU001', product_name: "Levi's Jeans", product_long_description: 'Classic blue denim jeans with straight fit.', product_short_description: 'Blue denim jeans.', product_type: 'Clothing', image_path: 'media/products/jeans.jpg', image: 'http://127.0.0.1:8000/media/products/jeans.png' }, // { id: 2, item_id: 'SKU002', product_name: 'Adidas Running Shoes', product_long_description: 'Lightweight running shoes with breathable mesh and cushioned sole.', product_short_description: "Men's running shoes.", product_type: 'Footwear', image_path: 'media/products/shoes.png', image: 'http://127.0.0.1:8000/media/products/shoes.png' }, // { id: 3, item_id: 'SKU003', product_name: 'Nike Sports T-Shirt', product_long_description: 'Moisture-wicking sports tee ideal for training and outdoor activities.', product_short_description: 'Performance t-shirt.', product_type: 'Clothing', image_path: 'media/products/tshirt.png', image: 'http://127.0.0.1:8000/media/products/tshirt.png' }, // { id: 4, item_id: 'SKU004', product_name: 'Puma Hoodie', product_long_description: 'Soft fleece hoodie with kangaroo pocket and adjustable drawstring.', product_short_description: 'Casual hoodie.', product_type: 'Clothing', image_path: 'media/products/hoodie.png', image: 'http://127.0.0.1:8000/media/products/hoodie.png' }, // { id: 5, item_id: 'SKU005', product_name: 'Ray-Ban Sunglasses', product_long_description: 'Classic aviator sunglasses with UV protection lenses.', product_short_description: 'Aviator sunglasses.', product_type: 'Accessories', image_path: 'media/products/sunglasses.png', image: 'http://127.0.0.1:8000/media/products/sunglasses.png' } ]; // --- Data --- const mediaUrl = "./../"; document.addEventListener('DOMContentLoaded', () => { jQuery('#full-page-loader').show(); fetch('/attr/products', { method: 'GET', // or 'POST' if your API expects POST headers: { 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || '' } }) .then(response => response.json()) .then(data => { // console.log("data",data); // --- Wire up --- PRODUCT_BASE = data; PRODUCT_BASE = PRODUCT_BASE.map((d)=>{return {...d,mandatoryAttributes:["color","size"]}}); // console.log("PRODUCT_BASE",PRODUCT_BASE); if(PRODUCT_BASE.length > 0){ $('#paginationBar').style.display = 'block'; } renderProducts(); getAtributeList(); document.getElementById('btnSubmit').addEventListener('click', submitAttributes); // document.getElementById('btnReset').addEventListener('click', resetAll); // document.getElementById('btnSelectAll').addEventListener('click', () => { // if (selectedIds.size === PRODUCT_BASE.length) { selectedIds.clear(); } else { selectedIds = new Set(PRODUCT_BASE.map(p => p.id)); } // // renderProducts(); // }); // Replace your existing Select All listener with this: document.getElementById('btnSelectAll').addEventListener('click', () => { // Use the container for the active layout const container = (layoutMode === 'cards') ? document.getElementById('cardsContainer') : document.getElementById('tableContainer'); // Collect all visible checkboxes const boxes = Array.from(container.querySelectorAll('input[type="checkbox"]')); // If every visible checkbox is already checked, we'll deselect; otherwise select all const allChecked = boxes.length > 0 && boxes.every(cb => cb.checked); boxes.forEach(cb => { const target = !allChecked; // true to select, false to deselect if (cb.checked !== target) { cb.checked = target; // Trigger your existing "change" handler so selectedIds & row .selected class update cb.dispatchEvent(new Event('change', { bubbles: true })); } }); // Update the selection pill text (doesn't re-render the list) updateSelectionInfo(); }); document.getElementById('btnCards').addEventListener('click', () => setLayout('cards')); document.getElementById('btnTable').addEventListener('click', () => setLayout('table')); jQuery('#full-page-loader').hide(); // if (data.success) { // } }); }); var API_RESPONSE_AI = { // 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: '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: 'SKU003', mandatory: { 'Clothing Neck Style': 'Crew Neck', 'Sleeve Length': 'Short Sleeve', 'Condition': 'New', 'T-Shirt Type': 'Performance' }, additional: { 'Material': 'Polyester Blend', 'Color': 'Red', 'Brand': 'Nike', 'Size': 'Medium', 'Fabric Technology': 'Dri-FIT', 'Care Instructions': 'Machine Wash Cold' } }, // { product_id: 'SKU004', mandatory: { 'Clothing Top Style': 'Hoodie', 'Closure': 'Pullover', 'Condition': 'New', 'Fit': 'Relaxed' }, additional: { 'Material': 'Cotton Fleece', 'Color': 'Charcoal', 'Brand': 'Puma', 'Size': 'Large', 'Care Instructions': 'Machine Wash Warm' } }, // { product_id: 'SKU005', mandatory: { 'Accessory Type': 'Sunglasses', 'Frame Style': 'Aviator', 'Condition': 'New', 'Lens Protection': 'UV 400' }, additional: { 'Frame Material': 'Metal', 'Lens Color': 'Green', 'Brand': 'Ray-Ban', 'Size': 'Standard', 'Case Included': 'Yes', 'Care Instructions': 'Clean with microfiber' } } // ], // total_products: 5, // successful: 5, // failed: 0 }; // --- State --- let selectedIds = new Set(); // NEW: Array of objects { item_id: string, mandatory_attrs: { [attribute_name]: string[] } } let selectedProductsWithAttributes = []; let selectedAttributes = new Array(); const lastSeen = new Map(); // per-product memory for NEW highlighting (product_id -> maps) let layoutMode = 'table'; // 'cards' | 'table' // --- Helpers --- const $ = (sel) => document.querySelector(sel); const el = (tag, cls) => { const e = document.createElement(tag); if (cls) e.className = cls; return e; } function updateSelectionInfo() { const pill = $('#selectionInfo'); const total = PRODUCT_BASE.length; // const count = selectedIds.size; const count = selectedProductsWithAttributes.length; pill.textContent = count === 0 ? 'No products selected' : `${count} of ${total} selected`; } function setChecked(id, checked) { if (checked) selectedIds.add(id); else selectedIds.delete(id); updateSelectionInfo(); } // function setCheckedAttributes(id,attribute, checked) { if (checked) selectedAttributes.add({id: [attribute]}); else selectedIds.delete({id:[attribute]}); updateSelectionInfo(); } function formatString(str) { return str // Replace underscores with spaces .replace(/_/g, ' ') // Insert a space before any uppercase letter (except at the start) .replace(/([a-z])([A-Z])/g, '$1 $2') // Capitalize the first letter .replace(/^./, char => char.toUpperCase()); } // --- Chips rendering --- function renderChips(container, obj, memoryMap) { container.innerHTML = ''; let count = 0; Object.entries(obj || {}).forEach(([k, v]) => { const chip = el('span', 'chip'); const kEl = el('span', 'k'); kEl.textContent = formatString(k) + ':'; // console.log("v",v); let resVal = ""; let sourceVal = ""; if(v instanceof Array){ resVal = String(v.map(v => formatString(v.value)).join(", ")); sourceVal = String(formatString(v[0]?.source)); const vEl = el('span', 'v'); vEl.textContent = ' ' + resVal +' (' + sourceVal + ')'; chip.appendChild(kEl); chip.appendChild(vEl); } // console.log("k",k); if(v instanceof Array){ const was = memoryMap.get(k); if (was === undefined || was !== v) chip.classList.add('new'); container.appendChild(chip); memoryMap.set(k, v); count++; } }); return count; } function findApiResultForProduct(p, index, api) { return api.results?.find(r => r.product_id === p.item_id) || api.results?.[index] || null; } // --- Cards layout --- function createProductCard(p) { const row = el('div', 'product'); // Check selection using the new helper if (isProductSelected(p.item_id)) row.classList.add('selected'); // if (selectedIds.has(p.item_id)) row.classList.add('selected'); // const left = el('div', 'thumb'); // const img = new Image(); img.src = p.image_path || p.image || ''; // // console.log("image path",p.image_path); // img.alt = `${p.product_name} image`; // // console.log("img",img); // // img.onerror = () => { img.remove(); const fb = el('div', 'fallback'); fb.textContent = (p.product_name || 'Product').split(' ').map(w => w[0]).slice(0,2).join('').toUpperCase(); left.appendChild(fb); }; // img.onerror = () => { img.src = mediaUrl+"media/images/no-product.png" }; // left.appendChild(img); // Assume 'el' is a function that creates an element (e.g., document.createElement) // Assume 'p' (product data) and 'mediaUrl' are available in scope. // 1. Create the main container for the hover effect const container = el('div', 'mini-thumb-container'); // 2. Create the visible thumbnail wrapper (your original 'left' element) // This will serve as the base for the small image const left = el('div', 'thumb'); // Get the image source (same as before) const imageSrc = p.image_path || p.image || ''; // 3. Create the thumbnail image element (same as before) const thumbImg = new Image(); thumbImg.src = imageSrc; thumbImg.alt = `${p.product_name} image`; thumbImg.onerror = () => { thumbImg.src = mediaUrl+"media/images/no-product.png" }; // Use thumbImg instead of img to avoid naming conflict with other code if possible // We will call it 'img' here to match your original code: const img = thumbImg; left.appendChild(img); container.appendChild(left); // Add the visible thumbnail to the main container // 4. Create the full-size image element for hover (new part) const fullImgWrapper = el('div', 'full-image-hover'); // Class for CSS control const fullImg = new Image(); fullImg.src = imageSrc; // Use the same source, or p.full_image_path if available fullImg.alt = `${p.product_name} full view`; fullImg.onerror = () => { fullImg.src = mediaUrl + "media/images/no-product.png" }; // Same fallback fullImgWrapper.appendChild(fullImg); container.appendChild(fullImgWrapper); // Add full image wrapper to the main container // The variable to use when appending this element to the DOM is 'container'. // return container; // If this block is inside a function const mid = el('div', 'meta'); const name = el('div', 'name'); name.textContent = p.product_name || '—'; const desc = el('div', 'desc'); desc.innerHTML = p.product_short_description || ''; const badges = el('div', 'badges'); const sku = el('span', 'pill'); sku.textContent = `SKU: ${p.item_id || '—'}`; badges.appendChild(sku); const type = el('span', 'pill'); type.textContent = p.product_type || '—'; badges.appendChild(type); const long = el('div', 'desc'); long.innerHTML = p.product_long_description || ''; long.style.marginTop = '4px'; mid.appendChild(name); mid.appendChild(desc); mid.appendChild(badges); mid.appendChild(long); // Helper function to create the chip UI for attributes function createAttributeChips(p, attr, initialSelected, isMandatory, updateCallback) { const wrapper = el('div', 'attribute-chip-group'); wrapper.dataset.attrName = attr?.attribute_name; wrapper.innerHTML = `

${attr?.attribute_name} (${isMandatory ? 'Mandatory' : 'Optional'}):

`; const chipContainer = el('div', 'chips-container'); attr?.possible_values.forEach(value => { const chip = el('label', 'attribute-chip'); // Checkbox input is hidden, but drives the selection state const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = value; checkbox.name = `${p.item_id}-${attr?.attribute_name}`; // Set initial state checkbox.checked = initialSelected.includes(value); // The visual part of the chip const span = el('span'); span.textContent = value; chip.appendChild(checkbox); chip.appendChild(span); chipContainer.appendChild(chip); }); // Use event delegation on the container for performance chipContainer.addEventListener('change', updateCallback); wrapper.appendChild(chipContainer); return wrapper; } // --- Main Select Checkbox (Product Selection) --- const right = el('label', 'select'); const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = isProductSelected(p.item_id); const lbl = el('span'); lbl.textContent = 'Select Product'; right.appendChild(cb); right.appendChild(lbl); // --- Dynamic Attribute Selects --- const attrContainer = el('div', 'attribute-selectors'); if(p.product_type_details.length > 0){ // Find all mandatory and non-mandatory attributes for this product const mandatoryAttributes = p.product_type_details?.filter(a => a.is_mandatory === 'Yes') || []; const optionalAttributes = p.product_type_details?.filter(a => a.is_mandatory !== 'Yes') || []; // Helper to update the main state object with all current selections const updateProductState = () => { const isSelected = cb.checked; const currentSelections = {}; if (isSelected) { // Iterate over all attribute groups (Mandatory and Optional) attrContainer.querySelectorAll('.attribute-chip-group').forEach(group => { const attrName = group.dataset.attrName; // Collect selected chip values const selectedOptions = Array.from(group.querySelectorAll('input[type="checkbox"]:checked')) .map(checkbox => checkbox.value); if (selectedOptions.length > 0) { currentSelections[attrName] = selectedOptions; } }); } toggleProductSelection(p.item_id, isSelected, currentSelections); row.classList.toggle('selected', isSelected); }; // Attach listener to main checkbox cb.addEventListener('change', () => { attrContainer.classList.toggle('disabled', !cb.checked); updateProductState(); }); // --- Render Mandatory Attributes --- // if (mandatoryAttributes.length > 0) { // const manTitle = el('p', "pSelectRight mandatory-title"); // manTitle.innerHTML = "Mandatory Attributes:"; // attrContainer.appendChild(manTitle); // mandatoryAttributes.forEach(attr => { // const initialSelected = getSelectedAttributes(p.item_id)[attr?.attribute_name] || attr?.possible_values; // const chipGroup = createAttributeChips(p, attr, initialSelected, true, updateProductState); // attrContainer.appendChild(chipGroup); // }); // } // --- Render Optional Attributes --- if (optionalAttributes.length > 0) { const br = el('br'); const optTitle = el('p', "pSelectRight optional-title"); optTitle.innerHTML = "Additional Attributes:"; attrContainer.appendChild(br); attrContainer.appendChild(optTitle); optionalAttributes.forEach(attr => { const initialSelected = getSelectedAttributes(p.item_id)[attr?.attribute_name] || attr?.possible_values; const chipGroup = createAttributeChips(p, attr, initialSelected, false, updateProductState); attrContainer.appendChild(chipGroup); }); } // Initialize attribute selectors' enabled state and state data attrContainer.classList.toggle('disabled', !cb.checked); // Initial state setup if the product was already selected (e.g., after a re-render) if (cb.checked) { // This is important to set the initial state correctly on load // We defer this until all selects are mounted, or ensure the initial state is correct. // For simplicity, we assume the data from PRODUCT_BASE already includes selected attributes if a selection exists // (which it won't in this case, so they default to all/empty) } } const inline = el('div', 'attr-inline'); inline.dataset.pid = p.item_id; // use item_id for mapping row.appendChild(container); row.appendChild(mid); if(p.product_type_details.length > 0){ console.log("IN "); // row.appendChild(attrContainer); // Append the new attribute selectors container } row.appendChild(right); // if (p.mandatoryAttributes && p.mandatoryAttributes.length > 0) { // const hr = el('hr'); // row.appendChild(hr); // row.appendChild(attri); // row.appendChild(secondRight); // } row.appendChild(inline); return row; } // Cards layout function renderProductsCards(items = getCurrentSlice()) { const cards = document.getElementById('cardsContainer'); cards.innerHTML = ''; if(items.length > 0){ items.forEach(p => cards.appendChild(createProductCard(p))); }else{ cards.innerHTML = "

No Products Found.

" } } // --- Table layout --- // function createMiniThumb(p) { // const mt = el('div', 'mini-thumb'); // const img = new Image(); img.src = p.image_path || p.image || ''; img.alt = `${p.product_name} image`; // // console.log("image path",p.image_path); // // console.log("img",img); // img.onerror = () => { img.src = mediaUrl+"media/images/no-product.png" }; // // img.onerror = () => { img.remove(); const fb = el('div', 'fallback'); fb.textContent = (p.product_name || 'Product').split(' ').map(w => w[0]).slice(0,2).join('').toUpperCase(); mt.appendChild(fb); }; // mt.appendChild(img); // return mt; // } function createMiniThumb(p) { // 1. Create a container for the hover effect const container = document.createElement('div'); container.className = 'mini-thumb-container'; // 2. Create the visible thumbnail (Mini Thumb) const mt = document.createElement('div'); mt.className = 'mini-thumb'; // Get the image source const imageSrc = p.image_path || p.image || ''; // 3. Create the thumbnail image element const thumbImg = new Image(); thumbImg.src = imageSrc; thumbImg.alt = `${p.product_name} thumbnail`; thumbImg.onerror = () => { thumbImg.src = mediaUrl + "media/images/no-product.png" }; mt.appendChild(thumbImg); container.appendChild(mt); // Add thumbnail to container // 4. Create the full-size image element for hover const fullImgWrapper = document.createElement('div'); fullImgWrapper.className = 'full-image-hover'; // Class for CSS control const fullImg = new Image(); // Assuming the same source is used for the full image, but you can change this // to p.full_image_path if your product object has a separate full-size URL. fullImg.src = imageSrc; fullImg.alt = `${p.product_name} full view`; fullImg.onerror = () => { fullImg.src = mediaUrl + "media/images/no-product.png" }; // Same fallback fullImgWrapper.appendChild(fullImg); container.appendChild(fullImgWrapper); // Add full image wrapper to container // Return the main container instead of just the mini-thumb return container; } // function createMiniThumb(p) { // const container = document.createElement('div'); // container.className = 'mini-thumb-container'; // const mt = document.createElement('div'); // mt.className = 'mini-thumb'; // const imageSrc = p.image_path || p.image || ''; // const thumbImg = new Image(); // thumbImg.src = imageSrc; // thumbImg.alt = `${p.product_name} thumbnail`; // thumbImg.onerror = () => { thumbImg.src = mediaUrl + "media/images/no-product.png" }; // mt.appendChild(thumbImg); // container.appendChild(mt); // const fullImgWrapper = document.createElement('div'); // fullImgWrapper.className = 'full-image-hover'; // const fullImg = new Image(); // fullImg.src = imageSrc; // fullImg.alt = `${p.product_name} full view`; // fullImg.onerror = () => { fullImg.src = mediaUrl + "media/images/no-product.png" }; // fullImgWrapper.appendChild(fullImg); // document.body.appendChild(fullImgWrapper); // Append to body, not container // // Hover logic // mt.addEventListener('mouseenter', () => { // fullImgWrapper.style.display = 'block'; // }); // mt.addEventListener('mousemove', (e) => { // fullImgWrapper.style.top = (e.clientY + 20) + 'px'; // 20px offset // fullImgWrapper.style.left = (e.clientX + 20) + 'px'; // }); // mt.addEventListener('mouseleave', () => { // fullImgWrapper.style.display = 'none'; // }); // return container; // } // Create one global hover container // const hoverContainer = document.createElement('div'); // hoverContainer.className = 'full-image-hover'; // const hoverImg = new Image(); // hoverContainer.appendChild(hoverImg); // document.body.appendChild(hoverContainer); // let hoverActive = false; // function createMiniThumb(p) { // const container = document.createElement('div'); // container.className = 'mini-thumb-container'; // const mt = document.createElement('div'); // mt.className = 'mini-thumb'; // const imageSrc = p.image_path || p.image || ''; // const thumbImg = new Image(); // thumbImg.src = imageSrc; // thumbImg.alt = `${p.product_name} thumbnail`; // thumbImg.onerror = () => { thumbImg.src = mediaUrl + "media/images/no-product.png" }; // mt.appendChild(thumbImg); // container.appendChild(mt); // // Hover logic // mt.addEventListener('mouseenter', () => { // hoverImg.src = imageSrc; // hoverImg.alt = `${p.product_name} full view`; // hoverContainer.style.display = 'block'; // }); // mt.addEventListener('mousemove', (e) => { // hoverContainer.style.top = (e.clientY + 20) + 'px'; // hoverContainer.style.left = (e.clientX + 20) + 'px'; // }); // mt.addEventListener('mouseleave', () => { // setTimeout(() => { // if (!hoverActive) hoverContainer.style.display = 'none'; // }, 100); // }); // hoverContainer.addEventListener('mouseenter', () => { // hoverActive = true; // }); // hoverContainer.addEventListener('mouseleave', () => { // hoverActive = false; // hoverContainer.style.display = 'none'; // }); // return container; // } // Table layout // function renderProductsTable(items = getCurrentSlice()) { // const wrap = document.getElementById('tableContainer'); // wrap.innerHTML = ''; // const table = document.createElement('table'); // const thead = document.createElement('thead'); const trh = document.createElement('tr'); // ['Select', 'Image', 'Product', 'SKU', 'Type', 'Short Description'].forEach(h => { // const th = document.createElement('th'); th.textContent = h; trh.appendChild(th); // }); // thead.appendChild(trh); table.appendChild(thead); // const tbody = document.createElement('tbody'); // if(items.length > 0 ){ // items.forEach(p => { // const tr = document.createElement('tr'); tr.id = `row-${p.id}`; // const tdSel = document.createElement('td'); tdSel.className = 'select-cell'; // const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = selectedIds.has(p.item_id); // cb.addEventListener('change', () => { setChecked(p.item_id, cb.checked); tr.classList.toggle('selected', cb.checked); }); // tdSel.appendChild(cb); tr.appendChild(tdSel); // const tdImg = document.createElement('td'); tdImg.className = 'thumb-cell'; tdImg.appendChild(createMiniThumb(p)); tr.appendChild(tdImg); // const tdName = document.createElement('td'); tdName.textContent = p.product_name || '—'; tr.appendChild(tdName); // const tdSku = document.createElement('td'); tdSku.textContent = p.item_id || '—'; tr.appendChild(tdSku); // const tdType = document.createElement('td'); const b = document.createElement('span'); b.className = 'badge'; b.textContent = p.product_type || '—'; tdType.appendChild(b); tr.appendChild(tdType); // const tdDesc = document.createElement('td'); tdDesc.textContent = p.product_short_description || ''; tr.appendChild(tdDesc); // tr.addEventListener('click', (e) => { if (e.target.tagName.toLowerCase() !== 'input') { cb.checked = !cb.checked; cb.dispatchEvent(new Event('change')); } }); // tbody.appendChild(tr); // }); // }else{ // const tr = el('tr'); // // tr.id = `row-${p.id}`; // const tdName = el('td'); // tdName.colSpan = 6; // tdName.innerHTML = "No Products Found." // tr.appendChild(tdName); // // tr.colspan = 6; // // tr.innerHTML // tbody.appendChild(tr); // } // table.appendChild(tbody); // wrap.appendChild(table); // } // NOTE: Ensure getProductStateUpdater and generateAttributeUI functions are defined globally or accessible here. /** * Returns a closure function that updates the global selectedProductsWithAttributes state * based on the current selections (chips) found in the DOM for a specific product. * This is used for both card and table views. * * @param {Object} p - The product object. * @param {HTMLElement} cb - The main product selection checkbox element. * @param {HTMLElement} tr - The main row/card element (used for toggling 'selected' class). * @returns {function} A function to be used as the attribute change handler. */ const getProductStateUpdater = (p, cb, tr) => () => { const isSelected = cb.checked; const currentSelections = {}; // Find the attribute container using its unique ID, which is the same structure // used in both card and table detail views (e.g., 'attr-container-124353498' or just the main card element). // For card view, the container is often the attrContainer element itself. // For table view, we use the explicit ID. const attrContainer = document.getElementById(`attr-container-${p.item_id}`) || tr.querySelector('.attribute-selectors'); if (isSelected && attrContainer) { // Iterate over all attribute groups (Mandatory and Optional) within the container attrContainer.querySelectorAll('.attribute-chip-group').forEach(group => { const attrName = group.dataset.attrName; // Collect selected chip values const selectedOptions = Array.from(group.querySelectorAll('input[type="checkbox"]:checked')) .map(checkbox => checkbox.value); // Only add to the selection if at least one option is selected if (selectedOptions.length > 0) { currentSelections[attrName] = selectedOptions; } }); } // Update the global state array (selectedProductsWithAttributes) toggleProductSelection(p.item_id, isSelected, currentSelections); // Update the visual status of the row/card tr.classList.toggle('selected', isSelected); }; /** * Generates the full attribute selection UI (chips) for a given product. * NOTE: Assumes el(), createAttributeChips(), and getSelectedAttributes() are defined globally. * @param {Object} p - The product object from PRODUCT_BASE. * @param {function} updateProductState - The callback to run on chip changes. * @param {HTMLElement} attrContainer - The container to append the UI to. */ function generateAttributeUI(p, updateProductState, attrContainer) { // Clear the container first, just in case attrContainer.innerHTML = ''; const mandatoryAttributes = p.product_type_details?.filter(a => a.is_mandatory === 'Yes') || []; const optionalAttributes = p.product_type_details?.filter(a => a.is_mandatory !== 'Yes') || []; // --- Render Mandatory Attributes --- if (mandatoryAttributes.length > 0) { // Use a general title for the section header const manTitle = el('p', "pSelectRight mandatory-title"); manTitle.innerHTML = "Mandatory Attributes:"; attrContainer.appendChild(manTitle); mandatoryAttributes.forEach(attr => { const initialSelected = getSelectedAttributes(p.item_id)[attr?.attribute_name] || attr?.possible_values; // The createAttributeChips function must be globally available const chipGroup = createAttributeChips(p, attr, initialSelected, true, updateProductState); attrContainer.appendChild(chipGroup); }); } // --- Render Optional Attributes --- if (optionalAttributes.length > 0) { // Add visual separation using the optional-title class const optTitle = el('p', "pSelectRight optional-title"); optTitle.innerHTML = "Additional Attributes:"; // Append the title for separation attrContainer.appendChild(optTitle); optionalAttributes.forEach(attr => { const initialSelected = getSelectedAttributes(p.item_id)[attr?.attribute_name] || attr?.possible_values; const chipGroup = createAttributeChips(p, attr, initialSelected, false, updateProductState); attrContainer.appendChild(chipGroup); }); } } /** * Creates the HTML structure for a single attribute group using chip/checkbox labels. * Assumes the helper function 'el' is available. * * @param {Object} p - The product object. * @param {Object} attr - The specific attribute detail object. * @param {string[]} initialSelected - Array of values that should be pre-checked. * @param {boolean} isMandatory - True if the attribute is mandatory. * @param {function} updateCallback - The function to call when a chip selection changes. * @returns {HTMLElement} The attribute chip group container (div). */ function createAttributeChips(p, attr, initialSelected, isMandatory, updateCallback) { const wrapper = el('div', 'attribute-chip-group'); wrapper.dataset.attrName = attr?.attribute_name; // Determine the header text based on structure preference (e.g., just the name) const statusText = isMandatory ? ' (Mandatory)' : ' (Optional)'; wrapper.innerHTML = `

${attr?.attribute_name}${statusText}:

`; const chipContainer = el('div', 'chips-container'); attr?.possible_values.forEach(value => { const chip = el('label', 'attribute-chip'); // Checkbox input is hidden, but drives the selection state const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = value; // Ensure the name is unique per product/attribute group checkbox.name = `${p.item_id}-${attr?.attribute_name}`; // Set initial state checkbox.checked = initialSelected.includes(value); // The visual part of the chip const span = el('span'); span.textContent = value; chip.appendChild(checkbox); chip.appendChild(span); chipContainer.appendChild(chip); }); // Attach listener to the container using event delegation chipContainer.addEventListener('change', updateCallback); wrapper.appendChild(chipContainer); return wrapper; } // function renderProductsTable(items = getCurrentSlice()) { // const wrap = document.getElementById('tableContainer'); // wrap.innerHTML = ''; // const table = document.createElement('table'); // table.classList.add('table', 'table-striped', 'table-bordered','table-responsive'); // const thead = document.createElement('thead'); // const trh = document.createElement('tr'); // // Table Headers // ['Select', 'Image', 'Product', 'SKU', 'Type', 'Short Description'].forEach(h => { // const th = document.createElement('th'); th.textContent = h; trh.appendChild(th); // }); // thead.appendChild(trh); table.appendChild(thead); // const tbody = document.createElement('tbody'); // if (items.length > 0) { // items.forEach(p => { // const tr = document.createElement('tr'); // tr.id = `row-${p.id}`; // if (isProductSelected(p.item_id)) tr.classList.add('selected'); // // --- Define Checkbox (cb) and State Updater --- // const cb = document.createElement('input'); // cb.type = 'checkbox'; // cb.checked = isProductSelected(p.item_id); // // console.log("checkkkkk") // // The state updater function is bound to this specific row/checkbox // const updateProductState = getProductStateUpdater(p, cb, tr); // // --- Select Cell --- // const tdSel = document.createElement('td'); // tdSel.className = 'select-cell'; // tdSel.appendChild(cb); // tr.appendChild(tdSel); // // --- Other Cells --- // const tdImg = document.createElement('td'); tdImg.className = 'thumb-cell'; tdImg.appendChild(createMiniThumb(p)); tr.appendChild(tdImg); // const tdName = document.createElement('td'); tdName.textContent = p.product_name || '—'; tr.appendChild(tdName); // const tdSku  = document.createElement('td'); tdSku.textContent = p.item_id || '—'; tr.appendChild(tdSku); // const tdType = document.createElement('td'); const b = document.createElement('span'); b.className = 'badge'; b.textContent = p.product_type || '—'; tdType.appendChild(b); tr.appendChild(tdType); // const tdDesc = document.createElement('td'); tdDesc.innerHTML = p.product_short_description || ''; tr.appendChild(tdDesc); // // --------------------------------------------- // // --- ATTRIBUTE SELECTION IMPLEMENTATION --- // // --------------------------------------------- // // 1. DETAIL ROW STRUCTURE // const detailRow = document.createElement('tr'); // detailRow.classList.add('attribute-detail-row'); // Custom class for styling // detailRow.style.display = 'none'; // Initially hidden // detailRow.id = `detail-row-${p.id}`; // const detailCell = document.createElement('td'); // detailCell.colSpan = 6; // Must span all columns // const attrContainer = document.createElement('div'); // attrContainer.id = `attr-container-${p.item_id}`; // Unique ID for targeting by updateProductState // attrContainer.classList.add('attribute-selectors', 'table-selectors'); // // 2. GENERATE CHIPS UI // generateAttributeUI(p, updateProductState, attrContainer); // // Initially disable the chips if the product is not selected // attrContainer.classList.toggle('disabled', !cb.checked); // detailCell.appendChild(attrContainer); // detailRow.appendChild(detailCell); // if(p.product_type_details.length > 0){ // // 3. TOGGLE BUTTON (in the main row) // const tdAttr = document.createElement('td'); // const toggleButton = document.createElement('button'); // toggleButton.textContent = 'Configure'; // toggleButton.classList.add('btn', 'btn-sm', 'btn-info', 'attribute-toggle-btn'); // tdattr?.appendChild(toggleButton); // // tr.appendChild(tdAttr); // // 4. EVENT LISTENERS // // a) Toggle Button Logic // toggleButton.addEventListener('click', (e) => { // e.stopPropagation(); // Stop row click event // const isHidden = detailRow.style.display === 'none'; // detailRow.style.display = isHidden ? '' : 'none'; // Toggle visibility // toggleButton.textContent = isHidden ? 'Hide Attributes' : 'Configure'; // toggleButton.classList.toggle('btn-info', !isHidden); // toggleButton.classList.toggle('btn-secondary', isHidden); // }); // // b) Main Checkbox Change Logic // cb.addEventListener('change', () => { // // console.log("cheeeeeeeeee"); // updateProductState(); // Update state on check/uncheck // attrContainer.classList.toggle('disabled', !cb.checked); // Enable/Disable chips // }); // // c) Row Click Listener (Updated to ignore button clicks) // tr.addEventListener('click', (e) => { // const tag = e.target.tagName.toLowerCase(); // // console.log("clikk") // if (tag !== 'input' && tag !== 'button') { // cb.checked = !cb.checked; // cb.dispatchEvent(new Event('change')); // } // }); // }else{ // const tdAttr = document.createElement('td'); // tr.appendChild(tdAttr); // // b) Main Checkbox Change Logic // cb.addEventListener('change', () => { // // console.log("cheeeeeeeeee"); // updateProductState(); // Update state on check/uncheck // attrContainer.classList.toggle('disabled', !cb.checked); // Enable/Disable chips // }); // tr.addEventListener('click', (e) => { // const tag = e.target.tagName.toLowerCase(); // // console.log("clikk") // if (tag !== 'input' && tag !== 'button') { // cb.checked = !cb.checked; // cb.dispatchEvent(new Event('change')); // } // }); // } // // 5. Append Rows to TBODY // tbody.appendChild(tr); // tbody.appendChild(detailRow); // Append the detail row right after the main row // }); // } else { // const tr = el('tr'); // const tdName = el('td'); // tdName.colSpan = 6; // tdName.innerHTML = "No Products Found."; // tr.appendChild(tdName); // tbody.appendChild(tr); // } // table.appendChild(tbody); // wrap.appendChild(table); // } function renderProductsTable(items = getCurrentSlice()) { const wrap = document.getElementById('tableContainer'); wrap.innerHTML = ''; const table = document.createElement('table'); table.classList.add('table', 'table-striped', 'table-bordered', 'table-responsive'); table.id = 'productsTable'; // ✅ Add a unique table ID const thead = document.createElement('thead'); const trh = document.createElement('tr'); // --- "Select All" Header with Checkbox --- const thSelect = document.createElement('th'); const selectAllCheckbox = document.createElement('input'); selectAllCheckbox.type = 'checkbox'; selectAllCheckbox.id = 'selectAllCheckbox'; thSelect.appendChild(selectAllCheckbox); trh.appendChild(thSelect); // Other headers ['Image', 'Product', 'SKU', 'Type', 'Description'].forEach(h => { const th = document.createElement('th'); th.textContent = h; trh.appendChild(th); }); thead.appendChild(trh); table.appendChild(thead); const tbody = document.createElement('tbody'); if (items.length > 0) { items.forEach(p => { const tr = document.createElement('tr'); tr.id = `row-${p.id}`; if (isProductSelected(p.item_id)) tr.classList.add('selected'); // Checkbox for each row const cb = document.createElement('input'); cb.type = 'checkbox'; cb.classList.add('checkbox-productlist'); // ✅ correct way to add class cb.checked = isProductSelected(p.item_id); const updateProductState = getProductStateUpdater(p, cb, tr); const tdSel = document.createElement('td'); tdSel.className = 'select-cell'; tdSel.appendChild(cb); tr.appendChild(tdSel); const tdImg = document.createElement('td'); tdImg.className = 'thumb-cell'; tdImg.appendChild(createMiniThumb(p)); tr.appendChild(tdImg); const tdName = document.createElement('td'); tdName.textContent = p.product_name || '—'; tr.appendChild(tdName); const tdSku = document.createElement('td'); tdSku.textContent = p.item_id || '—'; tr.appendChild(tdSku); const tdType = document.createElement('td'); const b = document.createElement('span'); b.className = 'badge'; b.textContent = p.product_type || '—'; tdType.appendChild(b); tr.appendChild(tdType); // const tdDesc = document.createElement('td'); // tdDesc.innerHTML = p.product_short_description || ''; // tr.appendChild(tdDesc); const tdDesc = document.createElement('td'); // Create a container for descriptions const descContainer = document.createElement('div'); descContainer.classList.add('description-cell'); // Short description (always visible) const shortDesc = document.createElement('div'); shortDesc.classList.add('short-desc'); shortDesc.innerHTML = `Short Description: ${p.product_short_description || 'N/A'}`; // Long description (hidden initially) const longDesc = document.createElement('div'); longDesc.classList.add('long-desc'); longDesc.innerHTML = `Long Description: ${p.product_long_description || 'N/A'}`; longDesc.style.display = 'none'; // Button to toggle long description const toggleBtn = document.createElement('button'); toggleBtn.textContent = 'Show Long Description'; toggleBtn.classList.add('btn'); toggleBtn.classList.add('btn-primary'); toggleBtn.classList.add('btn-sm'); toggleBtn.classList.add('btn-toggle-desc'); toggleBtn.style.fontSize = 'smaller'; toggleBtn.style.margin = '5px'; // Toggle logic toggleBtn.addEventListener('click', () => { const isVisible = longDesc.style.display === 'block'; longDesc.style.display = isVisible ? 'none' : 'block'; toggleBtn.textContent = isVisible ? 'Show Long Description' : 'Hide Long Description'; }); // Append elements in order descContainer.appendChild(shortDesc); descContainer.appendChild(toggleBtn); descContainer.appendChild(longDesc); tdDesc.appendChild(descContainer); tr.appendChild(tdDesc); // Handle attribute rows (kept your same logic) const detailRow = document.createElement('tr'); detailRow.classList.add('attribute-detail-row'); detailRow.style.display = 'none'; detailRow.id = `detail-row-${p.id}`; const detailCell = document.createElement('td'); detailCell.colSpan = 6; const attrContainer = document.createElement('div'); attrContainer.id = `attr-container-${p.item_id}`; attrContainer.classList.add('attribute-selectors', 'table-selectors'); generateAttributeUI(p, updateProductState, attrContainer); attrContainer.classList.toggle('disabled', !cb.checked); detailCell.appendChild(attrContainer); detailRow.appendChild(detailCell); // Checkbox behavior cb.addEventListener('change', () => { updateProductState(); attrContainer.classList.toggle('disabled', !cb.checked); }); tr.addEventListener('click', (e) => { const tag = e.target.tagName.toLowerCase(); if (tag !== 'input' && tag !== 'button') { cb.checked = !cb.checked; cb.dispatchEvent(new Event('change')); } }); tbody.appendChild(tr); tbody.appendChild(detailRow); }); } else { const tr = document.createElement('tr'); const td = document.createElement('td'); td.colSpan = 6; td.textContent = 'No Products Found.'; tr.appendChild(td); tbody.appendChild(tr); } table.appendChild(tbody); wrap.appendChild(table); // --- Select All Checkbox Logic --- selectAllCheckbox.addEventListener('change', () => { // ✅ Only get product checkboxes inside *this* table const allCheckboxes = table.querySelectorAll('tbody .checkbox-productlist'); console.log("allCheckboxes", allCheckboxes); allCheckboxes.forEach(cb => { cb.checked = selectAllCheckbox.checked; cb.dispatchEvent(new Event('change')); }); }); // --- Keep "Select All" synced with individual selections --- tbody.addEventListener('change', (e) => { if (e.target && e.target.classList.contains('checkbox-productlist')) { // ✅ Again, limit scope to *this* table const allCheckboxes = table.querySelectorAll('tbody .checkbox-productlist'); const allChecked = Array.from(allCheckboxes).every(cb => cb.checked); const someChecked = Array.from(allCheckboxes).some(cb => cb.checked); selectAllCheckbox.checked = allChecked; selectAllCheckbox.indeterminate = !allChecked && someChecked; } }); } // 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 = 'Mandatory'; // const manChips = el('div', 'chips'); // const addTitle = el('div', 'section-title'); addTitle.innerHTML = 'Additional'; // const addChips = el('div', 'chips'); // const addOcr = el('div', 'section-title'); addOcr.innerHTML = 'Ocr'; // const ocrChips = el('div', 'chips'); // const addVisual = el('div', 'section-title'); addVisual.innerHTML = 'Visual'; // 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 = 'Mandatory'; // const manChips = el('div', 'chips'); // const addTitle = el('div', 'section-title'); addTitle.innerHTML = 'Additional'; // const addChips = el('div', 'chips'); // const addOcr = el('div', 'section-title'); addOcr.innerHTML = 'Ocr'; // const ocrChips = el('div', 'chips'); // const addVisuals = el('div', 'section-title'); addVisuals.innerHTML = 'Visuals'; // 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() { const api = API_RESPONSE_AI; 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); // 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 combinedTitle = el('div', 'section-title'); combinedTitle.innerHTML = '

Additional & AI-Driven Attributes

'; 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 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'; } // 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 = 'Additional'; // 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 = 'Ocr'; // 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 = 'Visual'; // 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 = `

${title} (${attributeEntries.length})

`; // 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, productType) { const section = el('div', 'attribute-section'); // --- 1. Intermediate object for merging values --- let mergedAttributes = {}; // --- Build mergedAttributes --- Object.keys(attributes).forEach(key => { const valuesArray = Array.isArray(attributes[key]) ? attributes[key] : [attributes[key]]; if (!mergedAttributes[key]) { mergedAttributes[key] = { aiValues: new Set(), originalValues: new Set(), sources: new Set(), reasons: new Set(), // Store the first encountered config for possibleValues lookup firstValue: valuesArray[0] }; } valuesArray.forEach(v => { const aiValue = v.value || 'N/A'; const originalValue = v.original_value || 'N/A'; const source = v.source || 'N/A'; const reason = v.reason || 'N/A'; // Add values to sets for unique collection if (aiValue !== 'N/A') mergedAttributes[key].aiValues.add(aiValue); if (originalValue !== 'N/A') mergedAttributes[key].originalValues.add(originalValue); if (source !== 'N/A') mergedAttributes[key].sources.add(source); if (reason !== 'N/A') mergedAttributes[key].reasons.add(reason); }); }); // --- 2. Final attributeEntries from merged data --- let attributeEntries = []; Object.keys(mergedAttributes).forEach(key => { const mergedData = mergedAttributes[key]; // Find possible values for this attribute from full data using the stored firstValue const attrConfig = attributesFullData.find(item => item.attribute_name === key); let possibleValues = []; if (attrConfig && attrConfig.possible_values) { possibleValues = attrConfig.possible_values.split(',').map(s => s.trim()); } // Get merged AI Value const aiValueString = Array.from(mergedData.aiValues).join(', '); // Determine match flag: check if ANY of the AI values are in possibleValues const isFoundInPossible = Array.from(mergedData.aiValues).some(aiVal => possibleValues.includes(aiVal)); const matchFlag = isFoundInPossible ? true : false; // Get merged Original Value, Source, and Reason strings const originalValueString = Array.from(mergedData.originalValues).join(', '); const sourceString = Array.from(mergedData.sources).join(', '); const reasonString = Array.from(mergedData.reasons).join(' | '); attributeEntries.push({ name: key, aiValue: aiValueString || 'N/A', // Use merged string possibleValues: possibleValues, originalValue: originalValueString || 'N/A', source: sourceString || 'N/A', isMatch: matchFlag, reason: reasonString || 'N/A' }); }); // --- Rest of the function (unchanged) --- // ... (Section title, empty check, splitting into two tables, and buildTable helper) ... // --- Section title --- const titleEl = el('div', 'section-title'); titleEl.innerHTML = `

${title} (${attributeEntries.length})

`; 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; } // --- Split attributes in half --- const midIndex = Math.ceil(attributeEntries.length / 2); const leftAttributes = attributeEntries.slice(0, midIndex); const rightAttributes = attributeEntries.slice(midIndex); // --- Create a container for two tables --- const tableContainer = el('div', 'two-column-table-container'); // Helper function to build a table function buildTable(attrArray) { const scrollWrapper = el('div', 'attribute-scroll-wrapper'); const table = el('table', 'attribute-detail-table comparison-table'); const thead = el('thead'); const headerRow = el('tr'); // , 'AI Generated Value' ['Attribute Name', 'Recommended Attribute Value(s)'].forEach(text => { const th = el('th'); th.textContent = text; headerRow.appendChild(th); }); thead.appendChild(headerRow); table.appendChild(thead); const tbody = el('tbody'); attrArray.forEach(attr => { const row = el('tr', attr?.isMatch ? 'match-row' : 'mismatch-row'); // Attribute name const nameTd = el('td', 'attribute-name'); nameTd.textContent = formatString(attr?.name).replace(/_/g, ' '); row.appendChild(nameTd); // Dropdown const aiTd = el('td', 'attribute-source'); // Create a multi-select dropdown const select = document.createElement('select'); select.classList.add('select2-dropdown'); select.name = 'manuallyUpdatedAttributes[]'; select.setAttribute('multiple', 'multiple'); select.setAttribute('data-attribute', attr?.name); // The values selected will be the merged AI values const selectedValues = attr?.aiValue.split(', ').filter(v => v !== 'N/A' && v.trim() !== ''); // Populate options // Add possible values // attr?.possibleValues.forEach(val => { // const option = document.createElement('option'); // option.value = val; // option.textContent = val; // if (selectedValues.includes(val)) option.selected = true; // select.appendChild(option); // }); // attr?.possibleValues.forEach(val => { // // Split by comma if there are multiple values, trim spaces // const values = val.split(',').map(v => v.trim()); // values.forEach(v => { // const option = document.createElement('option'); // option.value = v; // option.textContent = v; // if (selectedValues.includes(v)) option.selected = true; // select.appendChild(option); // }); // }); // Add AI values not found in possibleValues as new selected options // selectedValues.forEach(aiVal => { // if (!attr?.possibleValues.includes(aiVal)) { // const newOpt = document.createElement('option'); // newOpt.value = aiVal; // newOpt.textContent = aiVal; // newOpt.selected = true; // select.appendChild(newOpt); // } // }); // 1. Prepare and clean the selected values first, flattening the comma-separated data. let allSelectedValues = new Set(); selectedValues.forEach(aiVal => { aiVal.split(',') .map(value => value.trim()) .filter(value => value.length > 0) .forEach(singleVal => allSelectedValues.add(singleVal)); }); // Convert the Set back to an array for easy checking, or keep it as a Set for O(1) lookups. // We'll keep it as a Set for efficient checking. // --- // 2. Add all default possible values. attr?.possibleValues.forEach(val => { const option = document.createElement('option'); option.value = val; option.textContent = val; // Check if the possible value is one of the cleaned selected values if (allSelectedValues.has(val)) { option.selected = true; // IMPORTANT: Remove this value from the Set so we know which ones are left over (the custom ones) allSelectedValues.delete(val); } select.appendChild(option); }); // --- // 3. Add any "custom" selected values that weren't in possibleValues. // The allSelectedValues Set now only contains values that are NOT in attr?.possibleValues. allSelectedValues.forEach(singleVal => { const newOpt = document.createElement('option'); newOpt.value = singleVal; newOpt.textContent = singleVal; newOpt.selected = true; // These are guaranteed to be selected values select.appendChild(newOpt); }); aiTd.appendChild(select); row.appendChild(aiTd); tbody.appendChild(row); // Initialize Select2 jQuery(select).select2({ tags: true, width: 'resolve' }); }); table.appendChild(tbody); scrollWrapper.appendChild(table); return scrollWrapper; } // --- Build and append both tables --- const leftTable = buildTable(leftAttributes); const rightTable = buildTable(rightAttributes); tableContainer.appendChild(leftTable); tableContainer.appendChild(rightTable); section.appendChild(tableContainer); return section; } // function renderMandatoryComparisonTable(attributes, title, productType) { // const section = el('div', 'attribute-section'); // let attributeEntries = []; // // --- Build 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'; // const reason = v.reason || 'N/A'; // // Find possible values for this attribute from full data // const attrConfig = attributesFullData.find(item => item.attribute_name === key); // let possibleValues = []; // if (attrConfig && attrConfig.possible_values) { // possibleValues = attrConfig.possible_values.split(',').map(s => s.trim()); // } // // Determine match flag // const isFoundInPossible = possibleValues.includes(aiValue); // const matchFlag = isFoundInPossible ? true : false; // attributeEntries.push({ // name: key, // aiValue: aiValue, // possibleValues: possibleValues, // originalValue: originalValue, // source: source, // isMatch: matchFlag, // reason: reason // }); // }); // }); // // --- Section title --- // const titleEl = el('div', 'section-title'); // titleEl.innerHTML = `

${title} (${attributeEntries.length})

`; // 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; // } // // --- Split attributes in half --- // const midIndex = Math.ceil(attributeEntries.length / 2); // const leftAttributes = attributeEntries.slice(0, midIndex); // const rightAttributes = attributeEntries.slice(midIndex); // // --- Create a container for two tables --- // const tableContainer = el('div', 'two-column-table-container'); // // Helper function to build a table // function buildTable(attrArray) { // const scrollWrapper = el('div', 'attribute-scroll-wrapper'); // const table = el('table', 'attribute-detail-table comparison-table'); // const thead = el('thead'); // const headerRow = el('tr'); // // , 'AI Generated Value' // ['Attribute Name', 'Recommended Attribute Value(s)'].forEach(text => { // const th = el('th'); // th.textContent = text; // headerRow.appendChild(th); // }); // thead.appendChild(headerRow); // table.appendChild(thead); // const tbody = el('tbody'); // attrArray.forEach(attr => { // const row = el('tr', attr?.isMatch ? 'match-row' : 'mismatch-row'); // // Attribute name // const nameTd = el('td', 'attribute-name'); // nameTd.textContent = formatString(attr?.name).replace(/_/g, ' '); // row.appendChild(nameTd); // // AI Value display // // AI Value cell with info icon // // const aiValueTd = el('td', `ai-value ${attr?.isMatch ? '' : 'mismatch-value'}`); // // // Create a wrapper for text and icon // // const aiValueWrapper = document.createElement('div'); // // aiValueWrapper.style.display = 'flex'; // // aiValueWrapper.style.alignItems = 'center'; // // aiValueWrapper.style.gap = '6px'; // // // Text node // // const valueText = document.createElement('span'); // // valueText.textContent = formatString(attr?.aiValue); // // aiValueWrapper.appendChild(valueText); // // // Info icon (Bootstrap Icons) // // if (attr?.reason && attr?.reason.trim() !== '') { // // const infoIcon = document.createElement('i'); // // infoIcon.className = 'bi bi-info-circle'; // // infoIcon.style.cursor = 'pointer'; // // infoIcon.setAttribute('title', attr?.reason); // Tooltip on hover // // infoIcon.style.color = '#0d6efd'; // Bootstrap blue // // aiValueWrapper.appendChild(infoIcon); // // } // // aiValueTd.appendChild(aiValueWrapper); // // row.appendChild(aiValueTd); // // const aiValueTd = el('td', `ai-value ${attr?.isMatch ? '' : 'mismatch-value'}`); // // aiValueTd.textContent = formatString(attr?.aiValue); // // row.appendChild(aiValueTd); // // Dropdown // // const aiTd = el('td', 'attribute-source'); // // const select = document.createElement('select'); // // select.classList.add('select2-dropdown'); // // select.setAttribute('data-attribute', attr?.name); // const aiTd = el('td', 'attribute-source'); // // Create a multi-select dropdown // const select = document.createElement('select'); // select.classList.add('select2-dropdown'); // // select.id = 'manually-attributes'; // You may want to make this unique per row // select.name = 'manuallyUpdatedAttributes[]'; // // select.setAttribute('aria-labelledby', 'select a attribute for which multiple data required'); // select.setAttribute('multiple', 'multiple'); // // select.style.width = '100%'; // select.setAttribute('data-attribute', attr?.name); // // Populate options // attr?.possibleValues.forEach(val => { // const option = document.createElement('option'); // option.value = val; // option.textContent = val; // if (val === attr?.aiValue) option.selected = true; // select.appendChild(option); // }); // // if (!attr?.isMatch && attr?.aiValue !== 'N/A') { // // const newOpt = document.createElement('option'); // // newOpt.value = attr?.aiValue; // // newOpt.textContent = attr?.aiValue; // + " (new)"; // // newOpt.selected = true; // // select.appendChild(newOpt); // // } // aiTd.appendChild(select); // row.appendChild(aiTd); // tbody.appendChild(row); // // Initialize Select2 // jQuery(select).select2({ // tags: true, // width: 'resolve' // }); // }); // table.appendChild(tbody); // scrollWrapper.appendChild(table); // return scrollWrapper; // } // // --- Build and append both tables --- // const leftTable = buildTable(leftAttributes); // const rightTable = buildTable(rightAttributes); // tableContainer.appendChild(leftTable); // tableContainer.appendChild(rightTable); // section.appendChild(tableContainer); // return section; // } // function renderMandatoryComparisonTable(attributes, title, productType) { // 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'; // // Find possible values for this attribute from `a` // const attrConfig = attributesFullData.find(item => // item.attribute_name === key // ); // let possibleValues = []; // if (attrConfig && attrConfig.possible_values) { // possibleValues = attrConfig.possible_values.split(',').map(s => s.trim()); // } // // Determine if AI value exists in possible values // const isFoundInPossible = possibleValues.includes(aiValue); // const matchFlag = isFoundInPossible ? true : false; // attributeEntries.push({ // name: key, // aiValue: aiValue, // possibleValues: possibleValues, // originalValue: originalValue, // source: source, // isMatch: matchFlag // }); // }); // }); // const titleEl = el('div', 'section-title'); // titleEl.innerHTML = `

${title} (${attributeEntries.length})

`; // 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; // } // 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', 'AI Generated Value', 'Action'].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', 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); // // 3. Source Column // const sourceTd = el('td', `ai-value ${attr?.isMatch ? '' : 'mismatch-value'}`); // sourceTd.textContent = formatString(attr?.aiValue); // row.appendChild(sourceTd); // // 2. AI Value Dropdown (Select2) // const aiTd = el('td', 'attribute-source'); // const select = document.createElement('select'); // select.classList.add('select2-dropdown'); // select.setAttribute('data-attribute', attr?.name); // // Populate possible values // attr?.possibleValues.forEach(val => { // const option = document.createElement('option'); // option.value = val; // option.textContent = val; // if (val === attr?.aiValue) option.selected = true; // select.appendChild(option); // }); // // If not found in possible values, add it as a new option // if (!attr?.isMatch && attr?.aiValue !== 'N/A') { // const newOpt = document.createElement('option'); // newOpt.value = attr?.aiValue; // newOpt.textContent = attr?.aiValue + " (new)"; // newOpt.selected = true; // select.appendChild(newOpt); // } // aiTd.appendChild(select); // row.appendChild(aiTd); // tbody.appendChild(row); // // Initialize Select2 after element is added // jQuery(select).select2({ // tags: true, // allows new values // width: 'resolve' // }); // }); // table.appendChild(tbody); // scrollWrapper.appendChild(table); // section.appendChild(scrollWrapper); // return section; // } // 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 = `

${title} (${attributeEntries.length})

`; // 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', 'AI Generated Value', 'Action'].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); // // 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); // 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, mandatoryData = null) { const section = el('div', 'attribute-section'); let attributeEntries = []; // --- STEP 1: Create a Set of all Mandatory Original Values (Normalized) --- // A Set is used for fast lookups. Values are normalized (trimmed, lowercase) const mandatoryOriginalValuesSet = new Set(); if (mandatoryData) { Object.values(mandatoryData).forEach(attrArray => { const originalValue = attrArray[0]?.original_value; if (originalValue) { mandatoryOriginalValuesSet.add(String(originalValue).trim().toLowerCase()); } }); } // Helper to extract attribute entries consistently 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' }); }); }; // Iterate through attributes (OCR/Visual/Additional) and flatten them Object.keys(attributes).forEach(key => { const attribute = attributes[key]; if (Array.isArray(attribute)) { processAttribute(key, attribute); } else if (typeof attribute === 'object' && attribute !== null) { // Handle simple { "key": { "value": "X", "source": "Y" } } structure if (attribute.value !== undefined) { attributeEntries.push({ name: key, value: attribute.value, source: attribute.source || 'N/A' }); } else { // Handle nested objects Object.keys(attribute).forEach(subKey => { const subAttribute = attribute[subKey]; if (Array.isArray(subAttribute)) { processAttribute(`${key} (${subKey.replace(/_/g, ' ')})`, subAttribute); } }); } } }); const titleEl = el('div', 'section-title'); titleEl.innerHTML = `

${title} (${attributeEntries.length})

`; 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; } 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'); // --- CORE COMPARISON LOGIC (Global Check) --- let colorClass = ''; const hasMandatoryBaseline = mandatoryOriginalValuesSet.size > 0; if (hasMandatoryBaseline) { // Normalize the current attribute's value const currentValueNormalized = String(attr?.value).trim().toLowerCase(); // Check if the current value exists ANYWHERE in the mandatory original values set if (mandatoryOriginalValuesSet.has(currentValueNormalized)) { colorClass = 'green-text'; // Found a match in the global mandatory set } else { colorClass = 'red-text'; // Did NOT find a match } } // --- END CORE COMPARISON LOGIC --- const nameTd = el('td', 'attribute-name'); nameTd.textContent = formatString(attr?.name).replace(/_/g, ' '); row.appendChild(nameTd); const valueTd = el('td', 'attribute-value'); const displayValue = formatString(attr?.value) + ' (' + formatString(attr?.source) + ')'; valueTd.textContent = displayValue || 'N/A'; // Apply the determined color class to the value cell if (colorClass) { valueTd.classList.add(colorClass); } row.appendChild(valueTd); tbody.appendChild(row); }); table.appendChild(tbody); scrollWrapper.appendChild(table); section.appendChild(scrollWrapper); return section; } // function renderAttributesAsTable(attributes, title, mandatoryData = null) { // 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 = `

${title} (${attributeEntries.length})

`; // 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() { // 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) => { // if (!isProductSelected(p.item_id)) return; // const res = findApiResultForProduct(p, idx, api); // const tbody = table.querySelector('tbody'); // const baseRow = tbody ? tbody.querySelector(`#row-${p.id}`) : null; // if (!baseRow) return; // // --- Detail Row Construction --- // const detail = el('tr', 'detail-row'); // // 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 = '

Additional & AI-Driven Attributes

'; // 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); // content.appendChild(counts); // // Final assembly and insertion // td.appendChild(content); // detail.appendChild(td); // baseRow.insertAdjacentElement('afterend', detail); // }); // // Update summary statistics // $('#statTotal').textContent = api.total_products ?? 0; // $('#statOk').textContent = api.successful ?? 0; // $('#statKo').textContent = api.failed ?? 0; // const apiSummary = $('#api-summary'); // if (apiSummary) apiSummary.style.display = 'block'; // } // function highlightMatches(text, keywords) { // if (!text) return '—'; // let highlighted = text; // keywords.forEach(keyword => { // const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Escape regex // const regex = new RegExp(`(${escapedKeyword})`, 'gi'); // highlighted = highlighted.replace(regex, '$1'); // }); // return highlighted; // } function highlightMatches(text, keywords) { if (!text) return '—'; // 1. Process Keywords: Flatten the array and handle comma-separated strings const uniqueKeywords = new Set(); keywords.forEach(compositeKeyword => { // Split by comma, trim whitespace, and add non-empty terms to the Set compositeKeyword.split(',') .map(kw => kw.trim()) .filter(kw => kw.length > 0) .forEach(singleKeyword => { // Only add the keyword if it's not just an empty string if (singleKeyword) { uniqueKeywords.add(singleKeyword); } }); }); let highlighted = text; // 2. Apply Highlighting using the clean, unique keywords uniqueKeywords.forEach(keyword => { // We still need to check for length in case something slipped through, // though the filter and Set should handle most cases. if (keyword.length > 0) { const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Escape regex special characters // Use 'g' (global) and 'i' (case-insensitive) flags const regex = new RegExp(`(${escapedKeyword})`, 'gi'); // Use a function as the replacer to ensure we highlight all matches highlighted = highlighted.replace(regex, '$1'); } }); return highlighted; } // WOrking ONe 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) => { if (!isProductSelected(p.item_id)) return; const res = findApiResultForProduct(p, idx, api); const tbody = table.querySelector('tbody'); const baseRow = tbody ? tbody.querySelector(`#row-${p.id}`) : null; if (!baseRow) return; // --- Detail Row Construction --- const detail = el('tr', 'detail-row'); // td.colSpan must match the number of columns in your main table const td = el('td'); td.colSpan = 6; const content = el('div', 'detail-content-tables'); // 1. MANDATORY Attributes Table // 🚨 Note: The color check (Requirement #2) must be implemented inside renderMandatoryComparisonTable. const mandatoryData = res?.mandatory || {}; if (Object.keys(mandatoryData).length > 0) { const mandatoryTable = renderMandatoryComparisonTable( mandatoryData, 'Attributes' ); content.appendChild(mandatoryTable); } // 2. COMBINED Attributes Section (Additional, OCR, Visuals) const additionalData = res?.additional || {}; const ocrData = res?.ocr_results?.extracted_attributes || {}; const visualsData = res?.visual_results?.visual_attributes || {}; const hasCombinedData = Object.keys(additionalData).length > 0 || Object.keys(ocrData).length > 0 || Object.keys(visualsData).length > 0; if (hasCombinedData) { content.appendChild(el('hr', 'section-separator')); const combinedTitle = el('div', 'section-title'); combinedTitle.innerHTML = '

Additional & AI-Driven Attributes

'; content.appendChild(combinedTitle); const combinedAttributesContainer = el('div', 'combined-attributes-container'); // Render Additional Table (Conditional based on data existence) if (Object.keys(additionalData).length > 0) { const additionalTable = renderAttributesAsTable( additionalData, 'Additional Attributes', mandatoryData ); combinedAttributesContainer.appendChild(additionalTable); } // Render OCR Table (Conditional based on data existence) if (Object.keys(ocrData).length > 0) { const ocrTable = renderAttributesAsTable( ocrData, 'OCR Results', mandatoryData ); combinedAttributesContainer.appendChild(ocrTable); } // Render Visuals Table (Conditional based on data existence) if (Object.keys(visualsData).length > 0) { const visualsTable = renderAttributesAsTable( visualsData, 'Visual Results', mandatoryData ); combinedAttributesContainer.appendChild(visualsTable); } content.appendChild(combinedAttributesContainer); } // --- Summary Counts --- const mandCount = Object.keys(mandatoryData).length; const addCount = Object.keys(additionalData).length; const ocrExtractedCount = Object.keys(ocrData).length; const visualExtractedCount = Object.keys(visualsData).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); // content.appendChild(counts); // Final assembly and insertion td.appendChild(content); detail.appendChild(td); baseRow.insertAdjacentElement('afterend', detail); // Extract mandatory values for highlighting // const mandatoryValues = Object.values(mandatoryData) // .map(v => v?.value?.toString()?.trim()) // .filter(Boolean); // const attrConfig = attributesFullData.find(item => item.attribute_name === key); const mandatoryValues = Object.values(mandatoryData) // 1. Use flatMap to iterate over the outer array AND flatten the inner arrays. // 'arr' here is the array like [ { value: 'Pullover', ... } ] .flatMap(arr => // 2. Map the inner array. We safely assume the first element [0] // holds the value we want. arr.map(item => item?.value?.toString()?.trim()) ) // 3. Filter out any potential undefined/null/empty string results. .filter(Boolean); // Highlight in Product Name const nameCell = baseRow.querySelector('td:nth-child(3)'); if (nameCell) { nameCell.innerHTML = highlightMatches(nameCell.textContent, mandatoryValues); } // Highlight in Short Description const descCell = baseRow.querySelector('td:nth-child(6)'); if (descCell) { const shortDescDiv = descCell.querySelector('.short-desc'); if (shortDescDiv) { shortDescDiv.innerHTML = highlightMatches(shortDescDiv.innerHTML, mandatoryValues); } } const desclongCell = baseRow.querySelector('td:nth-child(6)'); if (desclongCell) { const longDescDiv = desclongCell.querySelector('.long-desc'); if (longDescDiv) { longDescDiv.innerHTML = highlightMatches(longDescDiv.innerHTML, mandatoryValues); } } }); // Update summary statistics $('#statTotal').textContent = api.total_products ?? 0; $('#statOk').textContent = api.successful ?? 0; $('#statKo').textContent = api.failed ?? 0; const apiSummary = $('#api-summary'); if (apiSummary) apiSummary.style.display = 'block'; } // working one function applyHighlightingToTableRow(baseRow, mandatoryValues) { const nameCell = baseRow.querySelector('td:nth-child(3)'); if (nameCell) { nameCell.innerHTML = highlightMatches(nameCell.textContent, mandatoryValues); } const descCell = baseRow.querySelector('td:nth-child(6)'); if (descCell) { const shortDescDiv = descCell.querySelector('.short-desc'); if (shortDescDiv) { shortDescDiv.innerHTML = highlightMatches(shortDescDiv.innerHTML, mandatoryValues); } } } function renderInlineAttributes() { if (layoutMode === 'cards') renderInlineForCards(); else renderInlineForTable(); // renderInlineForCards(); // renderInlineForTable(); } // --- Main rendering --- function renderProducts() { if (layoutMode === 'cards') { $('#cardsContainer').style.display = ''; $('#tableContainer').style.display = 'none'; // console.log("PRODUCT_BASE",PRODUCT_BASE); renderProductsCards(); } else { $('#cardsContainer').style.display = 'none'; $('#tableContainer').style.display = ''; renderProductsTable(); } updateSelectionInfo(); renderPagination(); // If there is a selection, re-render inline attributes (persist across toggle) if (selectedProductsWithAttributes.length > 0) renderInlineAttributes(); } // --- Submit & Reset --- function submitAttributes() { // Check the length of the new array if (selectedProductsWithAttributes.length === 0) { alert('Please select at least one product.'); return; } // if (selectedIds.size === 0) { alert('Please select at least one product.'); return; } // console.log("selectedIds",selectedIds); jQuery('#full-page-loader').show(); // let inputArray = { // "product_ids" : [...selectedIds] // } const extractAdditional = document.getElementById('extract_additional').checked; const processImage = document.getElementById('process_image').checked; // const selectedMultiples = document.getElementById('#mandatory-attributes'); // const selectedValues = Array.from(selectedMultiples.selectedOptions).map(option => option.value); const selectElement = document.getElementById('mandatory-attributes'); const selectedValues = Array.from(selectElement.selectedOptions).map(option => option.value); // console.log(selectedValues); // Logs an array of selected values // console.log("thresholdValueDisplay",thresholdValueDisplay.value); const threshold = parseFloat(document.getElementById('thresholdRange').value); // Transform the new state array into the required API format const itemIds = selectedProductsWithAttributes.map(p => p.item_id); // Create the mandatory_attrs map: { item_id: { attr_name: [values] } } // NOTE: The backend API you showed expects a flattened list of "mandatory_attrs" // like: { "color": ["color", "shade"], "size": ["size", "fit"] } // It seems to ignore the selected product-specific values and uses a general list of synonyms. // Assuming the request needs a general map of *all unique* selected attributes across all selected products: let mandatoryAttrsMap = {}; selectedProductsWithAttributes.forEach(product => { // Merge attributes from all selected products Object.assign(mandatoryAttrsMap, product.mandatory_attrs); }); // If the API expects the complex, product-specific payload from your Q1 example: const payloadForQ1 = selectedProductsWithAttributes.map(p => ({ item_id: p.item_id, mandatory_attrs: p.mandatory_attrs })); let inputArray = { "products": payloadForQ1, "model": "llama-3.1-8b-instant", "extract_additional": extractAdditional, "process_image": processImage, "multiple": selectedValues, "threshold_abs": 0.6, // Lower threshold to be more permissive // "margin": 0.3, // Larger margin to include more candidates // "use_adaptive_margin": true, // "use_semantic_clustering": true } let raw = JSON.stringify(inputArray); fetch('/attr/batch-extract/', { method: 'POST', // or 'POST' if your API expects POST headers: { 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || '', 'Content-Type': "application/json" }, body: raw }) .then(response => response.json()) .then(async data => { await delay(5000); // waits for 3 seconds // console.log("response data",data); API_RESPONSE_AI = data; renderInlineAttributes(); jQuery('#full-page-loader').hide(); }); } function resetAll() { selectedProductsWithAttributes = []; // Reset the main array // selectedIds.clear(); lastSeen.clear(); renderProducts(); // Clear summary document.getElementById('statTotal').textContent = '0'; document.getElementById('statOk').textContent = '0'; document.getElementById('statKo').textContent = '0'; $('#api-summary').style.display = 'none'; // ✅ Clear Select2 selections jQuery('#mandatory-attributes').val(null).trigger('change'); // ✅ Reset threshold input (and display) const thresholdInput = document.getElementById('thresholdRange'); const thresholdDisplay = document.getElementById('thresholdValue'); thresholdInput.value = '0.65'; // or any default value you prefer if (thresholdDisplay) { thresholdDisplay.textContent = '0.65'; } } function setLayout(mode) { layoutMode = mode; const btnCards = document.getElementById('btnCards'); const btnTable = document.getElementById('btnTable'); if (mode === 'cards') { btnCards.classList.add('active'); btnCards.setAttribute('aria-selected', 'true'); btnTable.classList.remove('active'); btnTable.setAttribute('aria-selected', 'false'); } else { btnTable.classList.add('active'); btnTable.setAttribute('aria-selected', 'true'); btnCards.classList.remove('active'); btnCards.setAttribute('aria-selected', 'false'); } renderProducts(); } // Upload elements (Bootstrap modal version) const uploadModalEl = document.getElementById('uploadModal'); const dropzone = document.getElementById('dropzone'); const uploadFiles = document.getElementById('uploadFiles'); const fileInfo = document.getElementById('fileInfo'); const uploadBar = document.getElementById('uploadBar'); const uploadStatus = document.getElementById('uploadStatus'); // Reset modal on show uploadModalEl.addEventListener('shown.bs.modal', () => { uploadStatus.textContent = ''; uploadStatus.className = ''; // clear success/error class uploadBar.style.width = '0%'; uploadBar.setAttribute('aria-valuenow', '0'); uploadFiles.value = ''; uploadFiles.setAttribute('accept', ACCEPT_TYPES); fileInfo.textContent = 'No files selected.'; }); function describeFiles(list) { if (!list || list.length === 0) { fileInfo.textContent = 'No files selected.'; return; } const names = Array.from(list).map(f => `${f.name} (${Math.round(f.size/1024)} KB)`); fileInfo.textContent = names.join(', '); } // Drag & drop feedback ['dragenter','dragover'].forEach(evt => { dropzone.addEventListener(evt, e => { e.preventDefault(); e.stopPropagation(); dropzone.classList.add('drag'); }); }); ['dragleave','drop'].forEach(evt => { dropzone.addEventListener(evt, e => { e.preventDefault(); e.stopPropagation(); dropzone.classList.remove('drag'); }); }); // Handle drop dropzone.addEventListener('drop', e => { uploadFiles.files = e.dataTransfer.files; describeFiles(uploadFiles.files); }); // Click to browse // dropzone.addEventListener('click', () => uploadFiles.click()); // Picker change uploadFiles.addEventListener('change', () => describeFiles(uploadFiles.files)); function startUpload() { const files = uploadFiles.files; if (!files || files.length === 0) { alert('Please select file(s) to upload.'); return; } jQuery('#full-page-loader').show(); uploadStatus.textContent = 'Uploading...'; uploadStatus.className = ''; // neutral uploadBar.style.width = '0%'; uploadBar.setAttribute('aria-valuenow', '0'); const form = new FormData(); Array.from(files).forEach(f => form.append('file', f)); // form.append('uploaded_by', 'Vishal'); // example extra field const xhr = new XMLHttpRequest(); xhr.open('POST', UPLOAD_API_URL, true); // If you need auth: // xhr.setRequestHeader('Authorization', 'Bearer '); xhr.upload.onprogress = (e) => { if (e.lengthComputable) { const pct = Math.round((e.loaded / e.total) * 100); uploadBar.style.width = pct + '%'; uploadBar.setAttribute('aria-valuenow', String(pct)); } }; xhr.onreadystatechange = () => { if (xhr.readyState === 4) { const ok = (xhr.status >= 200 && xhr.status < 300); try { const resp = JSON.parse(xhr.responseText || '{}'); uploadStatus.textContent = ok ? (resp.message || 'Upload successful') : (resp.error || `Upload failed (${xhr.status})`); } catch { uploadStatus.textContent = ok ? 'Upload successful' : `Upload failed (${xhr.status})`; } uploadStatus.className = ok ? 'success' : 'error'; // Optional: auto-close the modal on success after 1.2s: // if (ok) setTimeout(() => bootstrap.Modal.getInstance(uploadModalEl).hide(), 1200); } }; xhr.onerror = () => { uploadStatus.textContent = 'Network error during upload.'; uploadStatus.className = 'error'; }; xhr.send(form); setTimeout(()=>{ jQuery('#uploadModal').modal('hide'); window.location.reload(); },3000) jQuery('#full-page-loader').hide(); } // Wire Start button document.getElementById('uploadStart').addEventListener('click', startUpload); // Cancel button already closes the modal via data-bs-dismiss // --- Pagination state --- let page = 1; let pageSize = 50; // default rows per page function totalPages() { return Math.max(1, Math.ceil(PRODUCT_BASE.length / pageSize)); } function clampPage() { page = Math.min(Math.max(1, page), totalPages()); } function getCurrentSlice() { clampPage(); const start = (page - 1) * pageSize; return PRODUCT_BASE.slice(start, start + pageSize); } function renderPagination() { const bar = document.getElementById('paginationBar'); if (!bar) return; const tp = totalPages(); clampPage(); bar.innerHTML = `
Page ${page} of ${tp}
`; // wire events document.getElementById('prevPage')?.addEventListener('click', () => { if (page > 1) { page--; renderProducts(); } }); document.getElementById('nextPage')?.addEventListener('click', () => { if (page < tp) { page++; renderProducts(); } }); const sel = document.getElementById('pageSizeSelect'); if (sel) { sel.addEventListener('change', () => { const val = sel.value; pageSize = (val === 'all') ? PRODUCT_BASE.length : parseInt(val, 10); page = 1; // reset to first page when size changes renderProducts(); }); } } // Function to add/remove product from the state and manage its attributes function toggleProductSelection(itemId, isChecked, attributes = {}) { const index = selectedProductsWithAttributes.findIndex(p => p.item_id === itemId); // console.log("index",index); if (isChecked) { // If selecting, ensure the product object exists in the array if (index === -1) { selectedProductsWithAttributes.push({ item_id: itemId, mandatory_attrs: attributes }); } else { // Update attributes if the product is already selected selectedProductsWithAttributes[index].mandatory_attrs = attributes; } } else { // If deselecting, remove the product object from the array if (index !== -1) { selectedProductsWithAttributes.splice(index, 1); } } updateSelectionInfo(); } // Function to get the current mandatory attributes for a selected item function getSelectedAttributes(itemId) { const productEntry = selectedProductsWithAttributes.find(p => p.item_id === itemId); return productEntry ? productEntry.mandatory_attrs : {}; } // Helper to check if a product is selected function isProductSelected(itemId) { return selectedProductsWithAttributes.some(p => p.item_id === itemId); } // Helper to check if a specific attribute/value is selected function isAttributeValueSelected(itemId, attrName, value) { const attrs = getSelectedAttributes(itemId); const values = attrs[attrName]; return values ? values.includes(value) : false; // Default all selected when first loaded } // $('.attribute-select').select2({ // placeholder: 'Select product attributes' // }); function getAtributeList(){ jQuery('#full-page-loader').show(); try{ fetch('/attr/products/attributes', { method: 'GET', // or 'POST' if your API expects POST headers: { 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || '' } }) .then(response => response.json()) .then(data => { // console.log("data",data); attributesFullData = data; let attributesData = data; // Step 1: Extract unique mandatory attribute names const mandatoryAttributes = [...new Set( attributesData .filter(attr => attr?.is_mandatory === "Yes") .map(attr => attr?.attribute_name) )]; // Step 2: Populate the select element const $select = jQuery('#mandatory-attributes'); $select.append(new Option("Select All", "select_all")); // Add "Select All" option first mandatoryAttributes.forEach(attr => { $select.append(new Option(attr, attr)); }); // Step 3: Initialize Select2 with placeholder // $select.select2({ // placeholder: "Select mandatory attributes", // allowClear: true // }); // Step 4: Handle 'Select All' logic $select.on('select2:select', function (e) { if (e.params.data.id === "select_all") { // Select all real options except "Select All" const allOptions = mandatoryAttributes; $select.val(allOptions).trigger('change'); } }); jQuery('#full-page-loader').hide(); }); }catch(err){ console.log("err",err); jQuery('#full-page-loader').hide(); } } document.addEventListener("DOMContentLoaded", function () { // Update span when range changes thresholdInput.addEventListener('input', function () { // console.log("this.value",this.value); thresholdValueDisplay.textContent = this.value; }); }); // Get threshold value when needed function getThreshold() { // console.log("parseFloat(thresholdInput.value)",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 = `

${title} (${attributeEntries.length})

`; 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 = `
Manually Identified Value
${attr?.originalValue}
`; content.appendChild(originalBox); // AI Value Box const aiBox = el('div', `value-box ai-box ${attr?.isMatch ? 'found-value' : 'mismatch-value'}`); aiBox.innerHTML = `
AI Generated Value (${attr?.source})
${attr?.aiValue}
`; 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 = `

${title} (${attributeEntries.length})

`; // 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 = ` //
Manually Identified Value
//
${attr?.originalValue}
// `; // content.appendChild(originalBox); // // AI Value Box // const aiBox = el('div', `value-box ai-box ${attr?.isMatch ? 'found-value' : 'mismatch-value'}`); // aiBox.innerHTML = ` //
AI Generated Value (${attr?.source})
//
${attr?.aiValue}
// `; // 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; // } 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 = `

${title} (${attributeEntries.length})

`; 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 => { // --- CHANGE 1: Apply 'match-card' or 'mismatch-card' explicitly --- const cardClass = attr?.isMatch ? 'match-card' : 'mismatch-card'; const card = el('div', `comparison-card ${cardClass}`); 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 = `
Manually Identified Value
${attr?.originalValue}
`; content.appendChild(originalBox); // AI Value Box // Removed 'found-value' class here as styling should rely on the parent card class const aiBox = el('div', `value-box ai-box ${attr?.isMatch ? '' : 'mismatch-value'}`); aiBox.innerHTML = `
AI Generated Value (${attr?.source})
${attr?.aiValue}
`; content.appendChild(aiBox); card.appendChild(content); // --- CHANGE 2: Display the indicator for ALL cards, controlling color via CSS --- const indicator = el('div', 'match-status-indicator'); 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 = `

${title} (${attributeEntries.length})

`; 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 = `
Extracted Value (${formatString(attr?.source)})
${displayValue || 'N/A'}
`; content.appendChild(valueBox); card.appendChild(content); cardsContainer.appendChild(card); }); section.appendChild(cardsContainer); return section; } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }