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
|
||||
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();
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
|
||||
// ============================================================
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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=">">></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 -->
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user