diff --git a/services/web-ui/public/index.html b/services/web-ui/public/index.html index cce9239..c9ebd60 100644 --- a/services/web-ui/public/index.html +++ b/services/web-ui/public/index.html @@ -93,10 +93,11 @@ border-bottom: 1px solid var(--border); flex-shrink: 0; gap: var(--sp-3); + flex-wrap: wrap; } .asset-toolbar-left { display: flex; align-items: center; gap: var(--sp-3); } - .asset-toolbar-right { display: flex; align-items: center; gap: var(--sp-2); } + .asset-toolbar-right { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; } .asset-count { font-size: var(--text-xs); @@ -105,7 +106,7 @@ } .search-input { - width: 220px; + width: 200px; height: 28px; padding: 0 var(--sp-3); background: var(--bg-surface); @@ -119,6 +120,41 @@ .search-input:focus { border-color: var(--accent-border); } .search-input::placeholder { color: var(--text-tertiary); } + /* Filter chips */ + .filter-chips { display: flex; gap: 3px; } + .filter-chip { + padding: 2px 9px; + border-radius: 999px; + font-size: 11px; + font-weight: 500; + border: 1px solid var(--border); + background: transparent; + color: var(--text-tertiary); + cursor: pointer; + transition: all var(--t-fast); + line-height: 20px; + } + .filter-chip:hover { border-color: var(--border-strong); color: var(--text-primary); } + .filter-chip.active { + background: var(--accent-subtle); + border-color: var(--accent-border); + color: var(--accent); + } + + /* Sort select */ + .sort-select { + height: 26px; + font-size: 12px; + padding: 0 20px 0 8px; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--r-md); + color: var(--text-secondary); + outline: none; + cursor: pointer; + } + .sort-select:focus { border-color: var(--accent-border); } + .asset-grid-wrap { flex: 1; overflow-y: auto; @@ -422,6 +458,21 @@ 0 assets
+
+ + + + + +
+
@@ -515,6 +566,8 @@ assets: [], thumbCache: {}, searchTerm: '', + statusFilter: '', + sortBy: 'newest', }; const thumbObserver = new IntersectionObserver((entries) => { @@ -565,6 +618,7 @@ await loadProjects(); setupDrag(); setupSearch(); + setupFilters(); // Multi-select bulk actions if (window.SelectionManager) { @@ -619,6 +673,10 @@ function handleProjectChange() { state.currentProjectId = document.getElementById('projectSelect').value || null; state.currentBinId = null; + state.statusFilter = ''; + state.sortBy = 'newest'; + document.querySelectorAll('.filter-chip').forEach(c => c.classList.toggle('active', c.dataset.status === '')); + document.getElementById('sortSelect').value = 'newest'; loadBinsAndAssets(); } @@ -635,7 +693,6 @@ function renderBins(bins) { const tree = document.getElementById('binTree'); - const allItem = document.getElementById('allAssetsItem'); tree.querySelectorAll('.bin-item:not(#allAssetsItem)').forEach(n => n.remove()); bins.forEach(bin => { const el = document.createElement('a'); @@ -655,6 +712,10 @@ function selectBin(binId) { state.currentBinId = binId; + state.statusFilter = ''; + state.sortBy = 'newest'; + document.querySelectorAll('.filter-chip').forEach(c => c.classList.toggle('active', c.dataset.status === '')); + document.getElementById('sortSelect').value = 'newest'; updateBinActive(); loadAssets(); } @@ -683,12 +744,40 @@ function renderAssets(assets) { const term = state.searchTerm.toLowerCase(); - const filtered = term ? assets.filter(a => a.filename?.toLowerCase().includes(term) || a.display_name?.toLowerCase().includes(term)) : assets; + + // Apply status filter + let filtered = state.statusFilter + ? assets.filter(a => { + if (state.statusFilter === 'processing') return a.status === 'processing' || a.status === 'ingesting'; + return a.status === state.statusFilter; + }) + : assets; + + // Apply text search + if (term) filtered = filtered.filter(a => + a.filename?.toLowerCase().includes(term) || a.display_name?.toLowerCase().includes(term) + ); + + // Sort + filtered = [...filtered].sort((a, b) => { + switch (state.sortBy) { + case 'oldest': return new Date(a.created_at) - new Date(b.created_at); + case 'name': return (a.display_name || a.filename || '').localeCompare(b.display_name || b.filename || ''); + case 'name-desc': return (b.display_name || b.filename || '').localeCompare(a.display_name || a.filename || ''); + case 'duration': return (b.duration_ms || 0) - (a.duration_ms || 0); + case 'size': return (b.file_size || 0) - (a.file_size || 0); + default: return new Date(b.created_at) - new Date(a.created_at); + } + }); + const grid = document.getElementById('assetGrid'); grid.innerHTML = ''; const count = filtered.length; - document.getElementById('assetCount').textContent = `${count} asset${count !== 1 ? 's' : ''}`; + const totalCount = assets.length; + document.getElementById('assetCount').textContent = + count === totalCount ? `${count} asset${count !== 1 ? 's' : ''}` : + `${count} of ${totalCount} asset${totalCount !== 1 ? 's' : ''}`; document.getElementById('assetEmpty').style.display = count === 0 ? 'flex' : 'none'; filtered.forEach(asset => { @@ -729,7 +818,7 @@ else img.style.display = 'none'; card.addEventListener('click', (e) => { - if (e.target.closest('.asset-action-btn')) return; // delete button etc. + if (e.target.closest('.asset-action-btn')) return; if (window.openAssetPreview) window.openAssetPreview(asset.id); }); @@ -781,6 +870,24 @@ }); } + // ── Filter chips + sort ─────────────────── + function setupFilters() { + document.querySelectorAll('.filter-chip').forEach(chip => { + chip.addEventListener('click', () => { + state.statusFilter = chip.dataset.status; + document.querySelectorAll('.filter-chip').forEach(c => + c.classList.toggle('active', c.dataset.status === state.statusFilter) + ); + renderAssets(state.assets); + }); + }); + + document.getElementById('sortSelect').addEventListener('change', e => { + state.sortBy = e.target.value; + renderAssets(state.assets); + }); + } + // ── New project ─────────────────────────── async function saveProject() { const name = document.getElementById('newProjectName').value.trim();