page.html 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. <!doctype html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>sm121 — tested recipes</title>
  6. <style>
  7. :root { color-scheme: light dark; }
  8. body { font: 13px/1.45 system-ui, sans-serif; margin: 16px; }
  9. h1 { font-size: 18px; margin: 0 0 10px; }
  10. #bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 10px; }
  11. #q { flex: 1 1 220px; max-width: 380px; padding: 4px 8px; }
  12. select, button, #cols summary { padding: 3px 8px; cursor: pointer; }
  13. #cols { position: relative; display: inline-block; }
  14. #colbox { position: absolute; z-index: 5; background: canvas; border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
  15. padding: 6px 10px; max-height: 60vh; overflow: auto; white-space: nowrap; }
  16. #colbox label { display: block; }
  17. table { border-collapse: collapse; width: 100%; }
  18. th, td { border: 1px solid color-mix(in srgb, currentColor 20%, transparent); padding: 3px 8px; text-align: left; vertical-align: top; }
  19. th { cursor: pointer; user-select: none; white-space: nowrap; position: sticky; top: 0; background: canvas; }
  20. tr.g1 td { font-weight: 700; background: color-mix(in srgb, currentColor 8%, canvas); cursor: pointer; }
  21. tr.g2 td { font-style: italic; background: color-mix(in srgb, currentColor 4%, canvas); cursor: pointer; }
  22. tr.hist td { font-family: ui-monospace, monospace; font-size: 12px; background: color-mix(in srgb, currentColor 3%, canvas); }
  23. a.open { color: inherit; text-decoration: none; border: 1px solid color-mix(in srgb, currentColor 30%, transparent); border-radius: 3px; padding: 0 4px; }
  24. a.open:hover { background: color-mix(in srgb, currentColor 12%, canvas); }
  25. tr.row { cursor: pointer; }
  26. td.num { text-align: right; white-space: nowrap; }
  27. #count { color: gray; }
  28. .dim { color: gray; }
  29. </style>
  30. </head>
  31. <body>
  32. <h1>sm121 — tested recipes &amp; benchmark history</h1>
  33. <div id="bar">
  34. <input id="q" type="search" placeholder="Search recipes, models, images, labels…">
  35. <label>Group:
  36. <select id="group">
  37. <option value="mq" selected>Model → Quant</option>
  38. <option value="model">Model</option>
  39. <option value="quant">Quant</option>
  40. <option value="engine">Engine</option>
  41. <option value="">None (flat)</option>
  42. </select>
  43. </label>
  44. <details id="cols"><summary>Columns ▾</summary><div id="colbox"></div></details>
  45. <label>Rows/page:
  46. <select id="size"><option>25</option><option selected>50</option><option>100</option><option value="0">All</option></select>
  47. </label>
  48. <button id="prev">‹</button><span id="count"></span><button id="next">›</button>
  49. </div>
  50. <table id="tbl"><thead></thead><tbody></tbody></table>
  51. <script>
  52. "use strict";
  53. const DATA = {{DATA}};
  54. const COLS = [
  55. ["name", "Recipe"], ["title", "Title"], ["model", "Model"], ["quant", "Quant"],
  56. ["engine", "Engine"], ["image", "Image"], ["nodes", "Nodes"], ["tp", "TP"],
  57. ["deploy", "Deploy"], ["port", "Port"], ["n", "Reports"], ["last", "Last bench"],
  58. ["points", "Points"], ["pct", "Score /100"],
  59. ];
  60. const NUM = new Set(["nodes", "tp", "port", "n", "pct"]);
  61. // latest report matching a predicate, for the points / score columns
  62. function latest(r, f) { for (let i = r.reports.length - 1; i >= 0; i--) if (f(r.reports[i])) return r.reports[i]; return null; }
  63. function cell(r, k) {
  64. if (k === "n") return r.reports.length;
  65. if (k === "last") return r.reports.length ? r.reports[r.reports.length - 1].date : "";
  66. if (k === "points") { const e = latest(r, e => e.points); return e ? e.points : ""; }
  67. if (k === "pct") { const e = latest(r, e => e.pct != null); return e ? e.pct : ""; }
  68. return r[k];
  69. }
  70. // deep-link state: the full view (filter, grouping, sort, columns, paging,
  71. // collapsed groups) lives in the URL — survives any reload and is shareable
  72. const P = new URLSearchParams(location.search);
  73. const size0 = +(P.get("size") ?? 50);
  74. const S = {
  75. q: P.get("q") ?? "",
  76. group: P.get("group") ?? "mq",
  77. sort: P.get("sort") ?? "name",
  78. dir: P.get("dir") ? +P.get("dir") : 1,
  79. size: [25, 50, 100, 0].includes(size0) ? size0 : 50,
  80. page: Math.max(0, +(P.get("page") ?? 0)),
  81. hidden: new Set((P.get("hidden") ?? "title,image,port").split(",").filter(Boolean)),
  82. closed: new Set((P.get("closed") ?? "").split("|").map(decodeURIComponent).filter(Boolean)),
  83. openRow: P.get("open") ?? null,
  84. };
  85. document.getElementById("q").value = S.q;
  86. document.getElementById("group").value = S.group;
  87. document.getElementById("size").value = String(S.size);
  88. function syncUrl() {
  89. const p = new URLSearchParams();
  90. if (S.q) p.set("q", S.q);
  91. if (S.group !== "mq") p.set("group", S.group);
  92. if (S.sort !== "name") p.set("sort", S.sort);
  93. if (S.dir !== 1) p.set("dir", String(S.dir));
  94. if (S.size !== 50) p.set("size", String(S.size));
  95. if (S.page) p.set("page", String(S.page));
  96. const hidden = [...S.hidden].sort();
  97. if (hidden.join(",") !== ["title", "image", "port"].sort().join(",")) p.set("hidden", hidden.join(","));
  98. const closed = [...S.closed].map(encodeURIComponent);
  99. if (closed.length) p.set("closed", closed.join("|"));
  100. if (S.openRow) p.set("open", S.openRow);
  101. const qs = p.toString();
  102. history.replaceState(null, "", qs ? "?" + qs : location.pathname);
  103. }
  104. const esc = s => String(s).replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
  105. function filtered() {
  106. const q = S.q.toLowerCase();
  107. return DATA.filter(r => !q || (r.name + " " + r.title + " " + r.model + " " + r.quant + " " +
  108. r.engine + " " + r.image + " " + r.deploy + " " +
  109. r.reports.map(e => e.date + " " + e.label).join(" ")).toLowerCase().includes(q));
  110. }
  111. function sorted(rows) {
  112. const gk = { mq: r => r.model + "\0" + r.quant, model: r => r.model,
  113. quant: r => r.quant, engine: r => r.engine }[S.group];
  114. const k = S.sort, d = S.dir;
  115. const val = r => {
  116. if (k === "points") { const e = latest(r, e => e.ratio != null); return e ? e.ratio : -1; }
  117. const v = cell(r, k); return NUM.has(k) || k === "last" ? v : String(v).toLowerCase();
  118. };
  119. return rows.slice().sort((a, b) => {
  120. if (gk) { const ga = gk(a), gb = gk(b); if (ga !== gb) return ga.localeCompare(gb); }
  121. let x = val(a), y = val(b);
  122. if (x === y) return a.name.localeCompare(b.name);
  123. if (typeof x === "number" && typeof y === "number") return (x - y) * d;
  124. return String(x).localeCompare(String(y)) * d;
  125. });
  126. }
  127. function gkey(r, level) {
  128. if (S.group === "mq") return level === 1 ? r.model : r.model + "\u0001" + r.quant;
  129. const f = { model: "model", quant: "quant", engine: "engine" }[S.group];
  130. return level === 1 ? r[f] : null;
  131. }
  132. function render() {
  133. syncUrl();
  134. // visible columns
  135. const vis = COLS.filter(c => !S.hidden.has(c[0]));
  136. document.querySelector("thead").innerHTML = "<tr>" + vis.map(c => {
  137. const arrow = S.sort === c[0] ? (S.dir > 0 ? " ▲" : " ▼") : "";
  138. return `<th data-k="${c[0]}">${esc(c[1])}${arrow}</th>`;
  139. }).join("") + "</tr>";
  140. const all = sorted(filtered());
  141. const size = +document.getElementById("size").value;
  142. const pages = size ? Math.max(1, Math.ceil(all.length / size)) : 1;
  143. if (S.page >= pages) S.page = pages - 1;
  144. const rows = size ? all.slice(S.page * size, S.page * size + size) : all;
  145. document.getElementById("count").textContent = size
  146. ? `${all.length ? S.page * size + 1 : 0}–${Math.min(all.length, S.page * size + size)} / ${all.length}` : `${all.length}`;
  147. // group tree over the current page slice
  148. const out = [];
  149. let g1 = null, g2 = null;
  150. for (const r of rows) {
  151. if (S.group) {
  152. const k1 = gkey(r, 1);
  153. if (k1 !== g1) {
  154. g1 = k1; g2 = null;
  155. const closed = S.closed.has("1\0" + k1);
  156. out.push(`<tr class="g1" data-g="${encodeURIComponent("1\0" + k1)}"><td colspan="${vis.length}">${closed ? "▶" : "▼"} ${esc(k1)} <span class="dim">(${all.filter(x => gkey(x, 1) === k1).length})</span></td></tr>`);
  157. }
  158. if (S.closed.has("1\0" + k1)) continue;
  159. const k2 = gkey(r, 2);
  160. if (k2 !== null) {
  161. if (k2 !== g2) {
  162. g2 = k2;
  163. const closed = S.closed.has("2\0" + k2);
  164. out.push(`<tr class="g2" data-g="${encodeURIComponent("2\0" + k2)}"><td colspan="${vis.length}">${closed ? "▶" : "▼"} ${esc(r.quant)} <span class="dim">(${all.filter(x => gkey(x, 2) === k2).length})</span></td></tr>`);
  165. }
  166. if (S.closed.has("2\0" + k2)) continue;
  167. }
  168. }
  169. out.push("<tr class=\"row\" data-n=\"" + esc(r.name) + "\">" + vis.map(c => {
  170. const v = cell(r, c[0]);
  171. const lr = r.reports.length ? r.reports[r.reports.length - 1] : null;
  172. const tip = c[0] === "last" && lr
  173. ? ` title="${esc(lr.date + (lr.time ? " " + lr.time : ""))}"` : "";
  174. return `<td${NUM.has(c[0]) ? " class=\"num\"" : ""}${tip}>${v === "" || v == null ? '<span class="dim">—</span>' : esc(v)}</td>`;
  175. }).join("") + "</tr>");
  176. if (S.openRow === r.name)
  177. out.push(`<tr class="hist"><td colspan="${vis.length}">${r.reports.length ? r.reports.map(e => {
  178. const fl = e.files && e.files.length ? e.files : [e.file];
  179. return `<span>${fl.map(f =>
  180. `<a class="open" href="/r/${encodeURIComponent(r.name)}/${encodeURIComponent(f)}" target="_blank" title="open ${esc(f)}">${esc(f.split('.').pop())}</a>`).join(" ")}</span> ` +
  181. `${esc(e.date)}${e.time ? "·" + esc(e.time) : ""} ${esc(e.label)} — ${e.pct != null ? esc(e.pct) + " /100" : '<span class="dim">no score</span>'}${e.points ? " (" + esc(e.points) + ")" : ""}`;
  182. }).join("<br>") : "no reports"}</td></tr>`);
  183. }
  184. document.querySelector("tbody").innerHTML = out.join("");
  185. }
  186. document.querySelector("thead").addEventListener("click", e => {
  187. const th = e.target.closest("th"); if (!th) return;
  188. const k = th.dataset.k;
  189. if (S.sort === k) S.dir = -S.dir; else { S.sort = k; S.dir = 1; }
  190. render();
  191. });
  192. document.querySelector("tbody").addEventListener("click", e => {
  193. const g = e.target.closest("tr.g1,tr.g2");
  194. if (g) { const k = decodeURIComponent(g.dataset.g); S.closed.has(k) ? S.closed.delete(k) : S.closed.add(k); render(); return; }
  195. const row = e.target.closest("tr.row");
  196. if (row) { S.openRow = S.openRow === row.dataset.n ? null : row.dataset.n; render(); }
  197. });
  198. document.getElementById("q").addEventListener("input", e => { S.q = e.target.value; S.page = 0; render(); });
  199. document.getElementById("group").addEventListener("change", e => { S.group = e.target.value; S.closed.clear(); S.page = 0; render(); });
  200. document.getElementById("size").addEventListener("change", () => { S.page = 0; render(); });
  201. document.getElementById("prev").addEventListener("click", () => { if (S.page > 0) { S.page--; render(); } });
  202. document.getElementById("next").addEventListener("click", () => { S.page++; render(); });
  203. // column checkboxes
  204. document.getElementById("colbox").innerHTML = COLS.map(c =>
  205. `<label><input type="checkbox" data-k="${c[0]}" ${S.hidden.has(c[0]) ? "" : "checked"}> ${esc(c[1])}</label>`).join("");
  206. document.getElementById("colbox").addEventListener("change", e => {
  207. const k = e.target.dataset.k; if (!k) return;
  208. e.target.checked ? S.hidden.delete(k) : S.hidden.add(k);
  209. render();
  210. });
  211. render();
  212. </script>
  213. </body>
  214. </html>