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:
Jason Hoku
2026-05-07 13:11:58 -07:00
co-authored by Claude Opus 4.7
parent b95d2d0cfc
commit bcadb7e4e3
6 changed files with 217 additions and 1 deletions
+3
View File
@@ -58,6 +58,9 @@ function init() {
// Initialize search filter UI
if (typeof renderSearchFilters === 'function') renderSearchFilters();
// Initialize logic filter UI
if (typeof renderLogicFilters === 'function') renderLogicFilters();
// IMPORTANT: Calculate grid dimensions BEFORE running pipeline
if (typeof updateGridDimensions === 'function') {
updateGridDimensions();
+71 -1
View File
@@ -16,6 +16,69 @@ function refreshIndices() {
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
function getFilterKey() {
const parts = [
@@ -37,7 +100,8 @@ function getFilterKey() {
[...filters.cfg].sort().join(','),
[...filters.upscaleMethod].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('|');
}
@@ -280,6 +344,9 @@ function incrementalFilter(newItems) {
if (!matchesSearchFilters(d)) return false;
}
// Apply logic filters (numeric comparisons)
if (!matchesLogicFilters(d)) return false;
return true;
});
}
@@ -371,6 +438,9 @@ function executePipeline() {
if (hasSearchFilters && !matchesSearchFilters(d)) return false;
// Apply logic filters (numeric comparisons)
if (!matchesLogicFilters(d)) return false;
return true;
});
+5
View File
@@ -60,6 +60,11 @@ const sortOptionsList = [
// --- SEARCH FILTER STATE ---
// 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 ---
let visibleSlice = { start: -1, end: -1 };
+102
View File
@@ -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
// ============================================================
+5
View File
@@ -115,6 +115,11 @@ async function loadSession() {
renderSearchFilters();
}
// Initialize logic filter UI
if (typeof renderLogicFilters === 'function') {
renderLogicFilters();
}
// 8. CRITICAL FIX: Force pipeline and render
// Wait a tick for the DOM to update, then process
await new Promise(resolve => setTimeout(resolve, 10));
+31
View File
@@ -502,6 +502,37 @@
</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=">">&gt;</option>
<option value="<">&lt;</option>
<option value=">=">&gt;=</option>
<option value="<=">&lt;=</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 -->