feat: dashboard - Logic Filter (numeric comparison filtering)
Adds a Logic Filter panel in the filter modal with field/operator/value dropdowns (supports lora_strength, cfg, steps, denoise, width, height, seed). Multiple filters apply with AND logic. Chips display active filters with one-click removal. Integrates into data pipeline via matchesLogicFilters() helper. Reset All Filters also clears logic filters. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
b95d2d0cfc
commit
bcadb7e4e3
@@ -58,6 +58,9 @@ function init() {
|
|||||||
// Initialize search filter UI
|
// Initialize search filter UI
|
||||||
if (typeof renderSearchFilters === 'function') renderSearchFilters();
|
if (typeof renderSearchFilters === 'function') renderSearchFilters();
|
||||||
|
|
||||||
|
// Initialize logic filter UI
|
||||||
|
if (typeof renderLogicFilters === 'function') renderLogicFilters();
|
||||||
|
|
||||||
// IMPORTANT: Calculate grid dimensions BEFORE running pipeline
|
// IMPORTANT: Calculate grid dimensions BEFORE running pipeline
|
||||||
if (typeof updateGridDimensions === 'function') {
|
if (typeof updateGridDimensions === 'function') {
|
||||||
updateGridDimensions();
|
updateGridDimensions();
|
||||||
|
|||||||
@@ -16,6 +16,69 @@ function refreshIndices() {
|
|||||||
idToIndexMap = new Map(sorted.map((item, index) => [item.id, index + 1]));
|
idToIndexMap = new Map(sorted.map((item, index) => [item.id, index + 1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// LOGIC FILTER HELPERS (numeric field extraction + comparison)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a numeric field value from a manifest item.
|
||||||
|
* Returns null if the field is missing or not parseable as a number.
|
||||||
|
*/
|
||||||
|
function _extractFieldValue(item, field) {
|
||||||
|
if (field === 'lora_strength') {
|
||||||
|
// Parse first lora's model strength from "name:0.5:0.7" format
|
||||||
|
const lora = item.lora;
|
||||||
|
if (!lora || lora === 'None') return null;
|
||||||
|
const firstPart = String(lora).split(' + ')[0];
|
||||||
|
const parts = firstPart.split(':');
|
||||||
|
if (parts.length < 2) return null;
|
||||||
|
const v = parseFloat(parts[1]);
|
||||||
|
return isNaN(v) ? null : v;
|
||||||
|
}
|
||||||
|
// Simple top-level numeric fields
|
||||||
|
const v = item[field];
|
||||||
|
if (typeof v === 'number') return v;
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
const n = parseFloat(v);
|
||||||
|
return isNaN(n) ? null : n;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate a comparison: itemValue op filterValue.
|
||||||
|
* Returns true if the item passes the comparison, false otherwise.
|
||||||
|
*/
|
||||||
|
function _evalOp(itemValue, op, filterValue) {
|
||||||
|
const a = parseFloat(itemValue);
|
||||||
|
const b = parseFloat(filterValue);
|
||||||
|
if (isNaN(a) || isNaN(b)) return false;
|
||||||
|
switch (op) {
|
||||||
|
case '>': return a > b;
|
||||||
|
case '<': return a < b;
|
||||||
|
case '>=': return a >= b;
|
||||||
|
case '<=': return a <= b;
|
||||||
|
case '=': return a === b;
|
||||||
|
case '!=': return a !== b;
|
||||||
|
default: return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an item passes all active logic filters (AND logic).
|
||||||
|
* Returns true if no logic filters are active or all pass.
|
||||||
|
*/
|
||||||
|
function matchesLogicFilters(item) {
|
||||||
|
if (typeof logicFilters === 'undefined' || logicFilters.length === 0) return true;
|
||||||
|
for (const f of logicFilters) {
|
||||||
|
const itemValue = _extractFieldValue(item, f.field);
|
||||||
|
// If field is missing/null on the item, skip this filter (don't exclude)
|
||||||
|
if (itemValue === null || itemValue === undefined) continue;
|
||||||
|
if (!_evalOp(itemValue, f.op, f.value)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Generate cache key from current filters
|
// Generate cache key from current filters
|
||||||
function getFilterKey() {
|
function getFilterKey() {
|
||||||
const parts = [
|
const parts = [
|
||||||
@@ -37,7 +100,8 @@ function getFilterKey() {
|
|||||||
[...filters.cfg].sort().join(','),
|
[...filters.cfg].sort().join(','),
|
||||||
[...filters.upscaleMethod].sort().join(','),
|
[...filters.upscaleMethod].sort().join(','),
|
||||||
[...filters.mediaType].sort().join(','),
|
[...filters.mediaType].sort().join(','),
|
||||||
searchFilters.map(f => `${f.type}:${f.term}`).join('|')
|
searchFilters.map(f => `${f.type}:${f.term}`).join('|'),
|
||||||
|
(typeof logicFilters !== 'undefined' ? logicFilters.map(f => `${f.field}${f.op}${f.value}`).join('|') : '')
|
||||||
];
|
];
|
||||||
return parts.join('|');
|
return parts.join('|');
|
||||||
}
|
}
|
||||||
@@ -280,6 +344,9 @@ function incrementalFilter(newItems) {
|
|||||||
if (!matchesSearchFilters(d)) return false;
|
if (!matchesSearchFilters(d)) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply logic filters (numeric comparisons)
|
||||||
|
if (!matchesLogicFilters(d)) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -371,6 +438,9 @@ function executePipeline() {
|
|||||||
|
|
||||||
if (hasSearchFilters && !matchesSearchFilters(d)) return false;
|
if (hasSearchFilters && !matchesSearchFilters(d)) return false;
|
||||||
|
|
||||||
|
// Apply logic filters (numeric comparisons)
|
||||||
|
if (!matchesLogicFilters(d)) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ const sortOptionsList = [
|
|||||||
// --- SEARCH FILTER STATE ---
|
// --- SEARCH FILTER STATE ---
|
||||||
// Array of search filter objects: { type: 'model', term: 'mountain' }
|
// Array of search filter objects: { type: 'model', term: 'mountain' }
|
||||||
|
|
||||||
|
// --- LOGIC FILTER STATE ---
|
||||||
|
// Array of numeric comparison filter objects: { field: 'cfg', op: '>', value: 7 }
|
||||||
|
// Multiple filters are applied with AND logic. OR is a future extension.
|
||||||
|
var logicFilters = logicFilters || [];
|
||||||
|
|
||||||
|
|
||||||
// --- VIRTUALIZATION & PIPELINE ---
|
// --- VIRTUALIZATION & PIPELINE ---
|
||||||
let visibleSlice = { start: -1, end: -1 };
|
let visibleSlice = { start: -1, end: -1 };
|
||||||
|
|||||||
@@ -1766,6 +1766,108 @@ function matchesSearchFilters(item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// LOGIC FILTER FUNCTIONS (numeric comparison filters)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Add a logic filter
|
||||||
|
function addLogicFilter() {
|
||||||
|
const fieldSelect = document.getElementById('logic-filter-field');
|
||||||
|
const opSelect = document.getElementById('logic-filter-op');
|
||||||
|
const valueInput = document.getElementById('logic-filter-value');
|
||||||
|
|
||||||
|
if (!fieldSelect || !opSelect || !valueInput) return;
|
||||||
|
|
||||||
|
const field = fieldSelect.value;
|
||||||
|
const op = opSelect.value;
|
||||||
|
const rawValue = valueInput.value.trim();
|
||||||
|
|
||||||
|
if (!field) { alert('Please select a field'); return; }
|
||||||
|
if (rawValue === '') { alert('Please enter a value'); return; }
|
||||||
|
|
||||||
|
const value = parseFloat(rawValue);
|
||||||
|
if (isNaN(value)) { alert('Value must be a number'); return; }
|
||||||
|
|
||||||
|
// Add to logicFilters array
|
||||||
|
logicFilters.push({ field, op, value });
|
||||||
|
|
||||||
|
// Clear inputs
|
||||||
|
fieldSelect.value = '';
|
||||||
|
valueInput.value = '';
|
||||||
|
|
||||||
|
renderLogicFilters();
|
||||||
|
updateDataPipeline();
|
||||||
|
|
||||||
|
console.log(`[Logic Filter] Added: ${field} ${op} ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a logic filter by index
|
||||||
|
function removeLogicFilter(index) {
|
||||||
|
if (index >= 0 && index < logicFilters.length) {
|
||||||
|
const removed = logicFilters.splice(index, 1)[0];
|
||||||
|
console.log(`[Logic Filter] Removed: ${removed.field} ${removed.op} ${removed.value}`);
|
||||||
|
renderLogicFilters();
|
||||||
|
updateDataPipeline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all logic filters
|
||||||
|
function clearAllLogicFilters() {
|
||||||
|
if (logicFilters.length === 0) return;
|
||||||
|
logicFilters = [];
|
||||||
|
renderLogicFilters();
|
||||||
|
updateDataPipeline();
|
||||||
|
console.log('[Logic Filter] All logic filters cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render logic filter chips
|
||||||
|
function renderLogicFilters() {
|
||||||
|
const container = document.getElementById('active-logic-filters');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
if (logicFilters.length === 0) {
|
||||||
|
container.innerHTML = '<div style="color: #555; font-size: 10px; padding: 4px 0;">No active logic filters</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldLabels = {
|
||||||
|
'lora_strength': 'LoRA Str',
|
||||||
|
'cfg': 'CFG',
|
||||||
|
'steps': 'Steps',
|
||||||
|
'denoise': 'Denoise',
|
||||||
|
'width': 'Width',
|
||||||
|
'height': 'Height',
|
||||||
|
'seed': 'Seed'
|
||||||
|
};
|
||||||
|
|
||||||
|
logicFilters.forEach((filter, index) => {
|
||||||
|
const tag = document.createElement('div');
|
||||||
|
tag.className = 'search-filter-tag';
|
||||||
|
|
||||||
|
const label = fieldLabels[filter.field] || filter.field;
|
||||||
|
tag.innerHTML = `
|
||||||
|
<span class="filter-type" style="background: #1a4a2a; color: #4ecf7a;">${label}</span>
|
||||||
|
<span class="filter-term">${filter.op} ${filter.value}</span>
|
||||||
|
<button class="remove-btn" onclick="removeLogicFilter(${index})" title="Remove filter">✕</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.appendChild(tag);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add clear all button if there are multiple filters
|
||||||
|
if (logicFilters.length > 1) {
|
||||||
|
const clearBtn = document.createElement('button');
|
||||||
|
clearBtn.className = 'search-filter-add-btn';
|
||||||
|
clearBtn.style.background = '#ff3860';
|
||||||
|
clearBtn.style.padding = '4px 10px';
|
||||||
|
clearBtn.innerText = 'CLEAR ALL';
|
||||||
|
clearBtn.onclick = clearAllLogicFilters;
|
||||||
|
container.appendChild(clearBtn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// MANIFEST ANALYTICS
|
// MANIFEST ANALYTICS
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -115,6 +115,11 @@ async function loadSession() {
|
|||||||
renderSearchFilters();
|
renderSearchFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize logic filter UI
|
||||||
|
if (typeof renderLogicFilters === 'function') {
|
||||||
|
renderLogicFilters();
|
||||||
|
}
|
||||||
|
|
||||||
// 8. CRITICAL FIX: Force pipeline and render
|
// 8. CRITICAL FIX: Force pipeline and render
|
||||||
// Wait a tick for the DOM to update, then process
|
// Wait a tick for the DOM to update, then process
|
||||||
await new Promise(resolve => setTimeout(resolve, 10));
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
|
|||||||
@@ -502,6 +502,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- LOGIC FILTER SECTION -->
|
||||||
|
<div class="popup-section">
|
||||||
|
<div class="popup-section-title">LOGIC FILTER <span style="font-size: 9px; color: #555; font-weight: normal;">(numeric comparisons, AND logic)</span></div>
|
||||||
|
<div class="search-filter-container">
|
||||||
|
<div class="search-filter-row">
|
||||||
|
<select id="logic-filter-field" class="search-filter-select">
|
||||||
|
<option value="">Field...</option>
|
||||||
|
<option value="lora_strength">LoRA Strength</option>
|
||||||
|
<option value="cfg">CFG</option>
|
||||||
|
<option value="steps">Steps</option>
|
||||||
|
<option value="denoise">Denoise</option>
|
||||||
|
<option value="width">Width</option>
|
||||||
|
<option value="height">Height</option>
|
||||||
|
<option value="seed">Seed</option>
|
||||||
|
</select>
|
||||||
|
<select id="logic-filter-op" class="search-filter-select" style="min-width: 52px; flex: 0 0 52px;">
|
||||||
|
<option value=">">></option>
|
||||||
|
<option value="<"><</option>
|
||||||
|
<option value=">=">>=</option>
|
||||||
|
<option value="<="><=</option>
|
||||||
|
<option value="=">=</option>
|
||||||
|
<option value="!=">!=</option>
|
||||||
|
</select>
|
||||||
|
<input type="number" id="logic-filter-value" class="search-filter-input" step="0.01"
|
||||||
|
placeholder="Value..." onkeypress="if(event.key==='Enter') addLogicFilter()" style="min-width: 70px;">
|
||||||
|
<button class="search-filter-add-btn" onclick="addLogicFilter()">ADD</button>
|
||||||
|
</div>
|
||||||
|
<div id="active-logic-filters" class="active-search-filters"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Meta Info Section -->
|
<!-- Meta Info Section -->
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user