feat(ui): searchable popup pickers for PR scope and model visibility
Replace the cramped PR multi-select with a modal: every repo#pr as a checkbox (with model coverage), a search box, and all/none that apply to the search results. The model hider moves to the same popup style — the per-row × and the hidden-chips bar are gone; both pickers live as buttons in the filter row showing their current state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -138,10 +138,11 @@ ungraded, points, **points-per-minute**, points-per-run, by-severity — with **
|
|||||||
(date range, repo, provider, model, lens, grade/severity), free-text search, and a click-to-scope
|
(date range, repo, provider, model, lens, grade/severity), free-text search, and a click-to-scope
|
||||||
findings detail table.
|
findings detail table.
|
||||||
|
|
||||||
Comparisons can be scoped to **specific PRs**: a multi-select lists every `repo#pr` with how many
|
Comparisons can be scoped to **specific PRs**: the **PRs** button opens a searchable checkbox popup
|
||||||
models ran it (`steve/x#12 · 3/5 models`) — pick the PRs you want and the entire table (runs,
|
listing every `repo#pr` with how many models ran it (`steve/x#12 · 3/5 models`) — tick the PRs you
|
||||||
minutes, findings, points) counts only those, so a model with 2 runs can be compared against one
|
want and the entire table (runs, minutes, findings, points) counts only those, so a model with 2
|
||||||
with 60 on exactly the work you choose.
|
runs can be compared against one with 60 on exactly the work you choose. **all**/**none** apply to
|
||||||
|
the current search, so you can filter to a repo and select all its PRs in one click.
|
||||||
|
|
||||||
True to the store's "no points" rule, **scoring lives in the browser**: the page has an editable
|
True to the store's "no points" rule, **scoring lives in the browser**: the page has an editable
|
||||||
points curve (default `trivial=1, small=3, medium=5, high=8, critical=20`) and computes
|
points curve (default `trivial=1, small=3, medium=5, high=8, critical=20`) and computes
|
||||||
@@ -164,11 +165,11 @@ Its mirror, **solo-error penalty ×** (default `1.5`), multiplies the FP penalty
|
|||||||
was made by **only that model** — a unique wrong claim is noisier than a shared mistake. So a
|
was made by **only that model** — a unique wrong claim is noisier than a shared mistake. So a
|
||||||
Blocking-claimed solo FP costs `high(8) × -0.5 × 1.5 = -6` vs `-4` for a shared one. Set to `1` to disable.
|
Blocking-claimed solo FP costs `high(8) × -0.5 × 1.5 = -6` vs `-4` for a shared one. Set to `1` to disable.
|
||||||
|
|
||||||
**Hiding models.** Each scoreboard row has a small **×** to hide that model — handy for retired ones
|
**Hiding models.** The **models shown** button opens the same style of popup with a checkbox per
|
||||||
(e.g. `m1`) you no longer want cluttering the view. Hidden models drop out of the table, the totals,
|
model — untick to hide one (handy for retired ones, e.g. `m1`), re-tick to restore. Hidden models
|
||||||
and the findings drill-down (but **not** from solo-ness, which stays computed against all models — hiding
|
drop out of the table, the totals, and the findings drill-down (but **not** from solo-ness, which
|
||||||
is a view filter, not a rescoring). The hidden set persists in `localStorage` across reloads; a
|
stays computed against all models — hiding is a view filter, not a rescoring). The hidden set
|
||||||
**hidden (N): …** bar lists them as click-to-restore chips, with a **show all** to clear.
|
persists in `localStorage` across reloads; unlike the other filters, **reset** doesn't touch it.
|
||||||
|
|
||||||
Auth: the `/ui` shell is public (it holds no data); paste the store token into its **connect** box,
|
Auth: the `/ui` shell is public (it holds no data); paste the store token into its **connect** box,
|
||||||
or open `/ui?token=<token>` once (remembered in `localStorage`). Prefer your own dashboard? Point
|
or open `/ui?token=<token>` once (remembered in `localStorage`). Prefer your own dashboard? Point
|
||||||
|
|||||||
@@ -21,8 +21,19 @@
|
|||||||
input, select, button { background:#0c0e12; color:var(--fg); border:1px solid var(--line); border-radius:6px; padding:6px 8px; font:inherit; }
|
input, select, button { background:#0c0e12; color:var(--fg); border:1px solid var(--line); border-radius:6px; padding:6px 8px; font:inherit; }
|
||||||
input[type=number] { width:64px; }
|
input[type=number] { width:64px; }
|
||||||
input[type=date] { width:140px; }
|
input[type=date] { width:140px; }
|
||||||
select[multiple] { min-width:200px; }
|
|
||||||
input.search { width:220px; }
|
input.search { width:220px; }
|
||||||
|
#modalback { position:fixed; inset:0; background:rgba(0,0,0,.55); display:none; z-index:40; }
|
||||||
|
#modal { position:fixed; top:12vh; left:50%; transform:translateX(-50%); width:min(560px,92vw); max-height:72vh;
|
||||||
|
background:var(--panel); border:1px solid var(--line); border-radius:10px; display:none; flex-direction:column;
|
||||||
|
z-index:41; box-shadow:0 12px 40px rgba(0,0,0,.5); }
|
||||||
|
#modal .mhead { display:flex; gap:8px; align-items:center; padding:10px 12px; border-bottom:1px solid var(--line); }
|
||||||
|
#modal .mhead b { margin-right:auto; white-space:nowrap; }
|
||||||
|
#modal .mhead input { flex:1; min-width:80px; width:auto; }
|
||||||
|
#mlist { overflow:auto; padding:6px 0; }
|
||||||
|
#mlist label.item { display:flex; gap:10px; align-items:center; padding:6px 14px; cursor:pointer; font-size:13px; }
|
||||||
|
#mlist label.item:hover { background:#1d212b; }
|
||||||
|
#mlist .note { margin-left:auto; color:var(--mut); font-size:12px; }
|
||||||
|
#modal .mfoot { padding:8px 12px; border-top:1px solid var(--line); color:var(--mut); font-size:12px; }
|
||||||
button { cursor:pointer; }
|
button { cursor:pointer; }
|
||||||
button.primary { background:var(--acc); color:#0c0e12; border-color:var(--acc); font-weight:600; }
|
button.primary { background:var(--acc); color:#0c0e12; border-color:var(--acc); font-weight:600; }
|
||||||
button.link { background:none; border:none; color:var(--acc); padding:0; text-decoration:underline; }
|
button.link { background:none; border:none; color:var(--acc); padding:0; text-decoration:underline; }
|
||||||
@@ -65,7 +76,8 @@
|
|||||||
<div class="f"><label>from</label><input type="date" id="from"></div>
|
<div class="f"><label>from</label><input type="date" id="from"></div>
|
||||||
<div class="f"><label>to</label><input type="date" id="to"></div>
|
<div class="f"><label>to</label><input type="date" id="to"></div>
|
||||||
<div class="f"><label>repo</label><select id="repo"></select></div>
|
<div class="f"><label>repo</label><select id="repo"></select></div>
|
||||||
<div class="f"><label>PRs (⌘/ctrl-click for several)</label><select id="pr" multiple size="4" title="Limit the whole comparison to these PRs — every model is scored only on runs/findings from them. The option label shows how many models ran each PR."></select></div>
|
<div class="f"><label>PRs</label><button id="prbtn" onclick="openModal('pr')" title="Limit the whole comparison to a set of PRs — every model is scored only on runs/findings from them.">all PRs ▾</button></div>
|
||||||
|
<div class="f"><label>models shown</label><button id="modelsbtn" onclick="openModal('models')" title="Hide models from the scoreboard entirely (e.g. retired ones). Persists in this browser.">all ▾</button></div>
|
||||||
<div class="f"><label>provider</label><select id="provider"></select></div>
|
<div class="f"><label>provider</label><select id="provider"></select></div>
|
||||||
<div class="f"><label>model</label><select id="model"></select></div>
|
<div class="f"><label>model</label><select id="model"></select></div>
|
||||||
<div class="f"><label>lens</label><select id="lens"></select></div>
|
<div class="f"><label>lens</label><select id="lens"></select></div>
|
||||||
@@ -90,7 +102,6 @@
|
|||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div id="summary" class="small mut" style="margin-bottom:8px"></div>
|
<div id="summary" class="small mut" style="margin-bottom:8px"></div>
|
||||||
<div id="hidden" class="small mut" style="margin-bottom:8px;display:none"></div>
|
|
||||||
<table id="models">
|
<table id="models">
|
||||||
<thead><tr id="mhead"></tr></thead>
|
<thead><tr id="mhead"></tr></thead>
|
||||||
<tbody id="mbody"></tbody>
|
<tbody id="mbody"></tbody>
|
||||||
@@ -112,6 +123,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- shared picker modal: PR scope + model visibility (outside <main> so its
|
||||||
|
search box doesn't trigger the global re-render listener) -->
|
||||||
|
<div id="modalback" onclick="closeModal()"></div>
|
||||||
|
<div id="modal">
|
||||||
|
<div class="mhead">
|
||||||
|
<b id="mtitle"></b>
|
||||||
|
<input id="msearch" placeholder="filter…" oninput="fillModal()">
|
||||||
|
<button onclick="modalSetAll(true)">all</button>
|
||||||
|
<button onclick="modalSetAll(false)">none</button>
|
||||||
|
<button class="primary" onclick="closeModal()">done</button>
|
||||||
|
</div>
|
||||||
|
<div id="mlist"></div>
|
||||||
|
<div class="mfoot" id="mfoot"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const SEVS = ["trivial","small","medium","high","critical"];
|
const SEVS = ["trivial","small","medium","high","critical"];
|
||||||
const SEVCOLOR = { trivial:"#3b4252", small:"#2e4d3a", medium:"#4d4a2e", high:"#5a3b2e", critical:"#5a2e3a" };
|
const SEVCOLOR = { trivial:"#3b4252", small:"#2e4d3a", medium:"#4d4a2e", high:"#5a3b2e", critical:"#5a2e3a" };
|
||||||
@@ -125,9 +151,82 @@ let sortKey = "ptsPerMin", sortAsc = false, selModel = null;
|
|||||||
function loadHidden(){ try { return new Set(JSON.parse(localStorage.getItem("grt-hidden") || "[]")); } catch { return new Set(); } }
|
function loadHidden(){ try { return new Set(JSON.parse(localStorage.getItem("grt-hidden") || "[]")); } catch { return new Set(); } }
|
||||||
let HIDDEN = loadHidden();
|
let HIDDEN = loadHidden();
|
||||||
function saveHidden(){ localStorage.setItem("grt-hidden", JSON.stringify([...HIDDEN].sort())); }
|
function saveHidden(){ localStorage.setItem("grt-hidden", JSON.stringify([...HIDDEN].sort())); }
|
||||||
function hideModel(m){ HIDDEN.add(m); if (selModel===m) selModel=null; saveHidden(); render(); }
|
|
||||||
function showModel(m){ HIDDEN.delete(m); saveHidden(); render(); }
|
// PRs the comparison is scoped to (repo#pr keys). Empty = every PR counts.
|
||||||
function showAllModels(){ HIDDEN.clear(); saveHidden(); render(); }
|
let SELPRS = new Set();
|
||||||
|
|
||||||
|
// ---- picker modal (shared by the PR scope and the model hider) ----
|
||||||
|
let modalKind = null;
|
||||||
|
function openModal(kind){
|
||||||
|
modalKind = kind;
|
||||||
|
document.getElementById("mtitle").textContent = kind==="pr" ? "compare only these PRs" : "models shown";
|
||||||
|
document.getElementById("msearch").value = "";
|
||||||
|
document.getElementById("modalback").style.display = "block";
|
||||||
|
document.getElementById("modal").style.display = "flex";
|
||||||
|
fillModal();
|
||||||
|
document.getElementById("msearch").focus();
|
||||||
|
}
|
||||||
|
function closeModal(){
|
||||||
|
modalKind = null;
|
||||||
|
document.getElementById("modalback").style.display = "none";
|
||||||
|
document.getElementById("modal").style.display = "none";
|
||||||
|
}
|
||||||
|
// The full item list, rebuilt from the raw data on every open/refresh so it
|
||||||
|
// always has every PR / model. checked = in the comparison.
|
||||||
|
function modalItems(){
|
||||||
|
const allModels = uniq([...RUNS.map(r=>r.model), ...ROWS.map(r=>r.model)]);
|
||||||
|
if (modalKind === "pr"){
|
||||||
|
const byPR = new Map();
|
||||||
|
for (const r of [...RUNS, ...ROWS]){
|
||||||
|
const k = prKey(r);
|
||||||
|
if (!byPR.has(k)) byPR.set(k, new Set());
|
||||||
|
if (r.model) byPR.get(k).add(r.model);
|
||||||
|
}
|
||||||
|
return [...byPR.keys()].sort((a,b)=>{
|
||||||
|
const [ra,pa] = splitPR(a), [rb,pb] = splitPR(b);
|
||||||
|
return ra===rb ? pb-pa : ra.localeCompare(rb); // newest PR first within a repo
|
||||||
|
}).map(k => ({ value:k, label:k, note:`${byPR.get(k).size}/${allModels.length} models`, checked:SELPRS.has(k) }));
|
||||||
|
}
|
||||||
|
return allModels.map(m => ({ value:m, label:m, note:HIDDEN.has(m)?"hidden":"", checked:!HIDDEN.has(m) }));
|
||||||
|
}
|
||||||
|
function visibleModalItems(){
|
||||||
|
const q = document.getElementById("msearch").value.trim().toLowerCase();
|
||||||
|
return modalItems().filter(it => !q || it.label.toLowerCase().includes(q));
|
||||||
|
}
|
||||||
|
function fillModal(){
|
||||||
|
const list = document.getElementById("mlist"); list.innerHTML = "";
|
||||||
|
for (const it of visibleModalItems()){
|
||||||
|
const lab = document.createElement("label"); lab.className = "item";
|
||||||
|
const cb = document.createElement("input"); cb.type = "checkbox"; cb.checked = it.checked;
|
||||||
|
cb.onchange = ()=> modalToggle(it.value, cb.checked);
|
||||||
|
const name = document.createElement("span"); name.textContent = it.label;
|
||||||
|
const note = document.createElement("span"); note.className = "note"; note.textContent = it.note;
|
||||||
|
lab.append(cb, name, note);
|
||||||
|
list.appendChild(lab);
|
||||||
|
}
|
||||||
|
document.getElementById("mfoot").textContent = modalKind==="pr"
|
||||||
|
? (SELPRS.size ? `${SELPRS.size} PR${SELPRS.size===1?"":"s"} selected — only these are compared` : "none selected — every PR counts")
|
||||||
|
: (HIDDEN.size ? `${HIDDEN.size} hidden — excluded from the scoreboard (persists in this browser)` : "all models shown");
|
||||||
|
}
|
||||||
|
function modalToggle(v, on){
|
||||||
|
if (modalKind === "pr"){ if (on) SELPRS.add(v); else SELPRS.delete(v); }
|
||||||
|
else {
|
||||||
|
if (on) HIDDEN.delete(v); else { HIDDEN.add(v); if (selModel===v) selModel=null; }
|
||||||
|
saveHidden();
|
||||||
|
}
|
||||||
|
fillModal(); render();
|
||||||
|
}
|
||||||
|
// all/none apply to the search-filtered items, so you can e.g. type a repo
|
||||||
|
// name and select all its PRs at once.
|
||||||
|
function modalSetAll(on){
|
||||||
|
for (const it of visibleModalItems()){
|
||||||
|
if (modalKind === "pr"){ if (on) SELPRS.add(it.value); else SELPRS.delete(it.value); }
|
||||||
|
else if (on) HIDDEN.delete(it.value);
|
||||||
|
else { HIDDEN.add(it.value); if (selModel===it.value) selModel=null; }
|
||||||
|
}
|
||||||
|
if (modalKind !== "pr") saveHidden();
|
||||||
|
fillModal(); render();
|
||||||
|
}
|
||||||
|
|
||||||
function token(){
|
function token(){
|
||||||
const q = new URL(location.href).searchParams.get("token");
|
const q = new URL(location.href).searchParams.get("token");
|
||||||
@@ -170,34 +269,8 @@ function opt(sel, vals, label){
|
|||||||
if (vals.includes(cur)) sel.value = cur;
|
if (vals.includes(cur)) sel.value = cur;
|
||||||
}
|
}
|
||||||
function prKey(o){ return o.repo + "#" + o.pr; }
|
function prKey(o){ return o.repo + "#" + o.pr; }
|
||||||
// The PR facet lists every repo#pr with how many models ran it, so it's obvious
|
|
||||||
// which PRs are a fair head-to-head (e.g. "steve/x#12 · 5/5 models").
|
|
||||||
function buildPRFacet(){
|
|
||||||
const allModels = uniq(RUNS.map(r=>r.model));
|
|
||||||
const byPR = new Map();
|
|
||||||
for (const r of [...RUNS, ...ROWS]){
|
|
||||||
const k = prKey(r);
|
|
||||||
if (!byPR.has(k)) byPR.set(k, new Set());
|
|
||||||
if (r.model) byPR.get(k).add(r.model);
|
|
||||||
}
|
|
||||||
const sel = document.getElementById("pr");
|
|
||||||
const cur = new Set([...sel.selectedOptions].map(o=>o.value));
|
|
||||||
sel.innerHTML = "";
|
|
||||||
const keys = [...byPR.keys()].sort((a,b)=>{
|
|
||||||
const [ra,pa] = splitPR(a), [rb,pb] = splitPR(b);
|
|
||||||
return ra===rb ? pb-pa : ra.localeCompare(rb); // newest PR first within a repo
|
|
||||||
});
|
|
||||||
for (const k of keys){
|
|
||||||
const o = document.createElement("option");
|
|
||||||
o.value = k;
|
|
||||||
o.textContent = `${k} · ${byPR.get(k).size}/${allModels.length} models`;
|
|
||||||
if (cur.has(k)) o.selected = true;
|
|
||||||
sel.appendChild(o);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function splitPR(k){ const i = k.lastIndexOf("#"); return [k.slice(0,i), +k.slice(i+1)]; }
|
function splitPR(k){ const i = k.lastIndexOf("#"); return [k.slice(0,i), +k.slice(i+1)]; }
|
||||||
function buildFacets(){
|
function buildFacets(){
|
||||||
buildPRFacet();
|
|
||||||
opt(document.getElementById("repo"), uniq([...RUNS.map(r=>r.repo), ...ROWS.map(r=>r.repo)]), "all repos");
|
opt(document.getElementById("repo"), uniq([...RUNS.map(r=>r.repo), ...ROWS.map(r=>r.repo)]), "all repos");
|
||||||
opt(document.getElementById("provider"), uniq([...RUNS.map(r=>r.provider), ...ROWS.map(r=>r.provider)]), "all providers");
|
opt(document.getElementById("provider"), uniq([...RUNS.map(r=>r.provider), ...ROWS.map(r=>r.provider)]), "all providers");
|
||||||
opt(document.getElementById("model"), uniq([...RUNS.map(r=>r.model), ...ROWS.map(r=>r.model)]), "all models");
|
opt(document.getElementById("model"), uniq([...RUNS.map(r=>r.model), ...ROWS.map(r=>r.model)]), "all models");
|
||||||
@@ -226,7 +299,7 @@ function filters(){
|
|||||||
return {
|
return {
|
||||||
from: document.getElementById("from").value,
|
from: document.getElementById("from").value,
|
||||||
to: document.getElementById("to").value,
|
to: document.getElementById("to").value,
|
||||||
prs: new Set([...document.getElementById("pr").selectedOptions].map(o=>o.value)),
|
prs: SELPRS,
|
||||||
repo: document.getElementById("repo").value,
|
repo: document.getElementById("repo").value,
|
||||||
provider: document.getElementById("provider").value,
|
provider: document.getElementById("provider").value,
|
||||||
model: document.getElementById("model").value,
|
model: document.getElementById("model").value,
|
||||||
@@ -236,7 +309,7 @@ function filters(){
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
function dateOK(ts, f){ const d = (ts||"").slice(0,10); return (!f.from || d >= f.from) && (!f.to || d <= f.to); }
|
function dateOK(ts, f){ const d = (ts||"").slice(0,10); return (!f.from || d >= f.from) && (!f.to || d <= f.to); }
|
||||||
// prOK gates a run/row on the PR multi-select: no selection = every PR counts,
|
// prOK gates a run/row on the PR picker: no selection = every PR counts,
|
||||||
// regardless of which models ran it.
|
// regardless of which models ran it.
|
||||||
function prOK(o, f){ return !f.prs.size || f.prs.has(prKey(o)); }
|
function prOK(o, f){ return !f.prs.size || f.prs.has(prKey(o)); }
|
||||||
// run-level filters only (date/repo/provider/model/pr) — severity/lens/search are finding-level.
|
// run-level filters only (date/repo/provider/model/pr) — severity/lens/search are finding-level.
|
||||||
@@ -345,16 +418,7 @@ function render(){
|
|||||||
for (const col of COLS){
|
for (const col of COLS){
|
||||||
const td = document.createElement("td"); if (col.l) td.className="l";
|
const td = document.createElement("td"); if (col.l) td.className="l";
|
||||||
const v = m[col.k];
|
const v = m[col.k];
|
||||||
if (col.k==="model"){
|
td.innerHTML = col.fmt ? col.fmt(v) : (v==null?"—":v);
|
||||||
// model name + a hide control (× pill) — injection-safe via JS handler.
|
|
||||||
td.textContent = (v==null?"—":v) + " ";
|
|
||||||
const x = document.createElement("span");
|
|
||||||
x.className = "pill"; x.textContent = "×"; x.title = "hide this model (persists)";
|
|
||||||
x.onclick = (e)=>{ e.stopPropagation(); hideModel(m.model); };
|
|
||||||
td.appendChild(x);
|
|
||||||
} else {
|
|
||||||
td.innerHTML = col.fmt ? col.fmt(v) : (v==null?"—":v);
|
|
||||||
}
|
|
||||||
if ((col.k==="ptsPerMin" || col.k==="ptsPerRun" || col.k==="points") && v!=null) td.classList.add(v<0 ? "bad" : "good");
|
if ((col.k==="ptsPerMin" || col.k==="ptsPerRun" || col.k==="points") && v!=null) td.classList.add(v<0 ? "bad" : "good");
|
||||||
if (col.k==="fpPen" && v<0) td.classList.add("bad");
|
if (col.k==="fpPen" && v<0) td.classList.add("bad");
|
||||||
if (col.k==="solo" && v>0) td.classList.add("good");
|
if (col.k==="solo" && v>0) td.classList.add("good");
|
||||||
@@ -363,23 +427,11 @@ function render(){
|
|||||||
}
|
}
|
||||||
mb.appendChild(tr);
|
mb.appendChild(tr);
|
||||||
}
|
}
|
||||||
// hidden-models panel: click a model to restore it
|
// picker buttons reflect current scope
|
||||||
const hid = document.getElementById("hidden");
|
document.getElementById("prbtn").textContent =
|
||||||
if (HIDDEN.size){
|
SELPRS.size ? `${SELPRS.size} PR${SELPRS.size===1?"":"s"} ▾` : "all PRs ▾";
|
||||||
hid.innerHTML = "";
|
document.getElementById("modelsbtn").textContent =
|
||||||
const lab = document.createElement("span"); lab.textContent = "hidden ("+HIDDEN.size+"): "; hid.appendChild(lab);
|
HIDDEN.size ? `${HIDDEN.size} hidden ▾` : "all ▾";
|
||||||
for (const m of [...HIDDEN].sort()){
|
|
||||||
const p = document.createElement("span"); p.className="pill"; p.textContent = "+ "+m;
|
|
||||||
p.title = "show this model again"; p.style.marginRight="6px";
|
|
||||||
p.onclick = ()=> showModel(m);
|
|
||||||
hid.appendChild(p);
|
|
||||||
}
|
|
||||||
const all = document.createElement("button"); all.className="link"; all.textContent="show all";
|
|
||||||
all.onclick = showAllModels; hid.appendChild(all);
|
|
||||||
hid.style.display = "";
|
|
||||||
} else {
|
|
||||||
hid.style.display = "none"; hid.innerHTML = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const tot = models.reduce((a,m)=>({runs:a.runs+m.runs, min:a.min+m.minutes, find:a.find+m.findings, conf:a.conf+m.confirmed, pts:a.pts+m.points}), {runs:0,min:0,find:0,conf:0,pts:0});
|
const tot = models.reduce((a,m)=>({runs:a.runs+m.runs, min:a.min+m.minutes, find:a.find+m.findings, conf:a.conf+m.confirmed, pts:a.pts+m.points}), {runs:0,min:0,find:0,conf:0,pts:0});
|
||||||
const prNote = f.prs.size
|
const prNote = f.prs.size
|
||||||
@@ -412,13 +464,14 @@ function esc(s){ return (s==null?"":String(s)).replace(/[&<>]/g, m=>({"&":"&
|
|||||||
function resetFilters(){
|
function resetFilters(){
|
||||||
for (const id of ["from","to","q"]) document.getElementById(id).value="";
|
for (const id of ["from","to","q"]) document.getElementById(id).value="";
|
||||||
for (const id of ["repo","provider","model","lens","grade"]) document.getElementById(id).value="";
|
for (const id of ["repo","provider","model","lens","grade"]) document.getElementById(id).value="";
|
||||||
for (const o of document.getElementById("pr").options) o.selected = false;
|
SELPRS.clear(); // hidden models are a persistent preference, not a filter — reset leaves them
|
||||||
selModel = null; render();
|
selModel = null; render();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("input", e=>{
|
document.addEventListener("input", e=>{
|
||||||
if (e.target.closest("main")) render();
|
if (e.target.closest("main")) render();
|
||||||
});
|
});
|
||||||
|
document.addEventListener("keydown", e=>{ if (e.key === "Escape") closeModal(); });
|
||||||
load();
|
load();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user