Kaynağa Gözat

UI changes in the attribute extraction

VISHAL BHANUSHALI 3 ay önce
ebeveyn
işleme
7022ed2e23

+ 38 - 1
content_quality_tool_public/static/css/attr-extraction.css

@@ -906,4 +906,41 @@ td{
 /* .red-text {
     color: red;
     font-weight: bold; 
-} */
+} */
+
+
+.two-column-table-container {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 20px;
+}
+
+.two-column-table-container .attribute-scroll-wrapper {
+    flex: 1;
+    min-width: 400px;
+}
+
+.attribute-detail-table {
+    width: 100%;
+    border-collapse: collapse;
+}
+
+.attribute-detail-table th,
+.attribute-detail-table td {
+    border: 1px solid #ddd;
+    padding: 8px;
+    vertical-align: middle;
+}
+
+.match-row {
+    background-color: #f6fff6;
+}
+
+.mismatch-row {
+    background-color: #fff5f5;
+}
+
+.mismatch-value {
+    color: #d9534f;
+    font-weight: bold;
+}

+ 347 - 54
content_quality_tool_public/static/js/attr-extraction.js

@@ -10,6 +10,7 @@ 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' },
@@ -1316,35 +1317,49 @@ function renderInlineForCards() {
  * @param {string} title - The title for the table section.
  * @returns {HTMLElement} A div containing the comparison table.
  */
-function renderMandatoryComparisonTable(attributes, title) {
+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]]; 
+        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);
             
-            // Comparison is case-insensitive and ignores leading/trailing whitespace
-            const isMatch = (String(aiValue).trim().toLowerCase() === String(originalValue).trim().toLowerCase());
+            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: isMatch
+                isMatch: matchFlag,
+                reason: reason
             });
         });
     });
 
+    // --- Section title ---
     const titleEl = el('div', 'section-title');
     titleEl.innerHTML = `<h3>${title} (${attributeEntries.length})</h3>`;
     section.appendChild(titleEl);
-    
+
     if (attributeEntries.length === 0) {
         const msg = el('p', 'no-attributes-message');
         msg.textContent = `No ${title.toLowerCase()} found.`;
@@ -1352,63 +1367,340 @@ function renderMandatoryComparisonTable(attributes, title) {
         return section;
     }
 
-    // --- SCROLL WRAPPER ADDITION ---
-    const scrollWrapper = el('div', 'attribute-scroll-wrapper');
-    const table = el('table', 'attribute-detail-table comparison-table');
+    // --- 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');
+        ['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');
+
+        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 = `<h3>${title} (${attributeEntries.length})</h3>`;
+//     section.appendChild(titleEl);
+
+//     if (attributeEntries.length === 0) {
+//         const msg = el('p', 'no-attributes-message');
+//         msg.textContent = `No ${title.toLowerCase()} found.`;
+//         section.appendChild(msg);
+//         return section;
+//     }
+
+//     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');
     
-    const thead = el('thead');
-    const headerRow = el('tr');
+//     let attributeEntries = [];
+
+//     Object.keys(attributes).forEach(key => {
+//         const valuesArray = Array.isArray(attributes[key]) ? attributes[key] : [attributes[key]]; 
+//         valuesArray.forEach(v => {
+//             const aiValue = v.value || 'N/A';
+//             const originalValue = v.original_value || 'N/A';
+//             const source = v.source || 'N/A';
+            
+//             // Comparison is case-insensitive and ignores leading/trailing whitespace
+//             const isMatch = (String(aiValue).trim().toLowerCase() === String(originalValue).trim().toLowerCase());
+
+//             attributeEntries.push({
+//                 name: key,
+//                 aiValue: aiValue,
+//                 originalValue: originalValue,
+//                 source: source,
+//                 isMatch: isMatch
+//             });
+//         });
+//     });
+
+//     const titleEl = el('div', 'section-title');
+//     titleEl.innerHTML = `<h3>${title} (${attributeEntries.length})</h3>`;
+//     section.appendChild(titleEl);
     
-    // Updated Headers for Comparison Table
-    ['Attribute Name', 'Manually Identified Value', 'AI Generated Value', 'Source', 'Match'].forEach(text => {
-        const th = el('th');
-        th.textContent = text;
-        headerRow.appendChild(th);
-    });
-    thead.appendChild(headerRow);
-    table.appendChild(thead);
+//     if (attributeEntries.length === 0) {
+//         const msg = el('p', 'no-attributes-message');
+//         msg.textContent = `No ${title.toLowerCase()} found.`;
+//         section.appendChild(msg);
+//         return section;
+//     }
 
-    const tbody = el('tbody');
+//     // --- 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);
 
-    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'); 
+//     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);
+//         // 1. Attribute Name
+//         const nameTd = el('td', 'attribute-name');
+//         nameTd.textContent = formatString(attr.name).replace(/_/g, ' '); 
+//         row.appendChild(nameTd);
 
-        // 2. Existing Value (Manually added value)
-        const originalTd = el('td', 'original-value');
-        originalTd.textContent = formatString(attr.originalValue);
-        row.appendChild(originalTd);
 
-        // 3. AI Extracted Value
-        const aiTd = el('td', `ai-value ${attr.isMatch ? '' : 'mismatch-value'}`);
-        aiTd.textContent = attr.aiValue;
-        row.appendChild(aiTd);
+//         // 3. AI Extracted Value
+//         const aiTd = el('td', `ai-value ${attr.isMatch ? '' : 'mismatch-value'}`);
+//         aiTd.textContent = attr.aiValue;
+//         row.appendChild(aiTd);
         
-        // 4. Source
-        const sourceTd = el('td', 'attribute-source');
-        sourceTd.textContent = formatString(attr.source);
-        row.appendChild(sourceTd);
-
-        // 5. Match Status
-        const matchTd = el('td', 'match-status');
-        const matchPill = el('span', `pill status-pill status-${attr.isMatch ? 'match' : 'mismatch'}`);
-        matchPill.textContent = attr.isMatch ? '✅ MATCH' : '❌ MISMATCH';
-        matchTd.appendChild(matchPill);
-        row.appendChild(matchTd);
+//         // 4. Source
+//         const sourceTd = el('td', 'attribute-source');
+//         sourceTd.textContent = formatString(attr.source);
+//         row.appendChild(sourceTd);
 
-        tbody.appendChild(row);
-    });
+//         tbody.appendChild(row);
+//     });
 
-    table.appendChild(tbody);
-    scrollWrapper.appendChild(table); // Append table to wrapper
-    section.appendChild(scrollWrapper); // Append wrapper to section
-    return section;
-}
+//     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).
@@ -2205,6 +2497,7 @@ function getAtributeList(){
             .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(

+ 1 - 1
content_quality_tool_public/templates/attr-extraction.html

@@ -80,7 +80,7 @@
 
                         <!-- Checkbox: Process Image -->
                         <div class="form-check me-3" style="padding: 10px;">
-                            <input class="form-check-input" checked type="checkbox" id="process_image" name="process_image">
+                            <input class="form-check-input" type="checkbox" id="process_image" name="process_image">
                             <label class="form-check-label" for="process_image">
                                &nbsp; Process Image
                             </label>

+ 1 - 1
content_quality_tool_public/templates/sidebar.html

@@ -3,7 +3,7 @@
     <div class="sidebar-brand"> <!--begin::Brand Link--> <a href="{% url 'file-upload' %}" class="brand-link">
             <!--begin::Brand Image--> <img src="{% static './images/logo-mini.png' %}" alt="Lumina Datamatics"
                 class="brand-image logo-mini"> <!--end::Brand Image--> <!--begin::Brand Text--> <span
-                class="brand-text fw-light"><img style="position:relative; left: -40px;"
+                class="brand-text fw-light"><img style="position:relative; "
                     src="{% static './images/logo.png' %}" alt="Lumina Datamatics" class="brand-image"></span>
             <!--end::Brand Text--> </a>
     </div> <!--end::Sidebar Brand--> <!--begin::Sidebar Wrapper-->