From 332c9f26128507697ef90376e744ba30a20282cd Mon Sep 17 00:00:00 2001 From: ai_coder Date: Fri, 16 Oct 2026 15:46:14 +0000 Subject: [PATCH] feat: click-to-sort table headers with toggle direction --- app/static/index.html | 65 ++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/app/static/index.html b/app/static/index.html index a2c11ff..6cb0edb 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -168,12 +168,6 @@ - @@ -280,27 +274,23 @@ function applyFilters() { const prov = document.getElementById('filter-provider').value; const arenaOnly = document.getElementById('filter-arena-only').checked; const search = document.getElementById('search').value.toLowerCase(); - const sortCol = document.getElementById('filter-sort').value; + const sortCol = currentSort.col; + const asc = currentSort.asc; let data = [...allData]; if (prov) data = data.filter(m => m.provider === prov); if (arenaOnly) data = data.filter(m => m.arena_text_score); if (search) data = data.filter(m => m.id.toLowerCase().includes(search)); - // sort + // sort (null всегда внизу независимо от направления) + const dir = asc ? 1 : -1; data.sort((a, b) => { - if (sortCol === 'arena_text_rank' || sortCol === 'arena_agent_rank') { - const av = a[sortCol], bv = b[sortCol]; - if (av == null && bv == null) return 0; - if (av == null) return 1; - if (bv == null) return -1; - return av - bv; - } - if (sortCol === 'uptime_pct') { - const av = a[sortCol] ?? -1, bv = b[sortCol] ?? -1; - return bv - av; - } - return (a[sortCol] || '').localeCompare(b[sortCol] || ''); + const av = a[sortCol], bv = b[sortCol]; + if (av == null && bv == null) return 0; + if (av == null) return 1; + if (bv == null) return -1; + if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * dir; + return String(av).localeCompare(String(bv)) * dir; }); const tbody = document.getElementById('table-body'); @@ -316,21 +306,39 @@ function applyFilters() { tbody.innerHTML = data.map(renderRow).join(''); } + updateSortHeaders(); updateStats(data); } +function updateSortHeaders() { + document.querySelectorAll('#main-table thead th[data-col]').forEach(th => { + const icon = th.querySelector('.sort-icon'); + if (th.dataset.col === currentSort.col) { + th.classList.add('active'); + icon.textContent = currentSort.asc ? '↑' : '↓'; + } else { + th.classList.remove('active'); + icon.textContent = '↕'; + } + }); +} + +document.querySelectorAll('#main-table thead th[data-col]').forEach(th => { + th.addEventListener('click', () => { + const col = th.dataset.col; + if (currentSort.col === col) { + currentSort.asc = !currentSort.asc; // повторный клик — инверсия + } else { + currentSort = { col, asc: true }; + } + applyFilters(); + }); +}); + async function loadData() { document.getElementById('loading').style.display = 'block'; document.getElementById('main-table').style.display = 'none'; - const sort = document.getElementById('filter-sort').value; - const prov = document.getElementById('filter-provider').value; - const arenaOnly = document.getElementById('filter-arena-only').checked; - - let url = `/api/models?sort=${sort}`; - if (prov) url += `&provider=${encodeURIComponent(prov)}`; - if (arenaOnly) url += `&arena_only=true`; - const res = await fetch('/api/models'); allData = await res.json(); @@ -386,7 +394,6 @@ document.getElementById('btn-sync').addEventListener('click', async () => { }); document.getElementById('filter-provider').addEventListener('change', applyFilters); document.getElementById('filter-arena-only').addEventListener('change', applyFilters); -document.getElementById('filter-sort').addEventListener('change', applyFilters); document.getElementById('search').addEventListener('input', applyFilters); (async () => {