Files
cortiq-gateway/web/js/ui.js
T
infosave2007 b1e0dc76ec fix(benchmark): fair to reasoning models, fair to router degradation, real sub-cent costs
Three false signals in benchmark results, all seen on a live run:

- Reasoning models were scored ❌ because the per-test max_tokens cap cut
  them mid-<think> before the actual answer. Every run now gets +256 tokens
  of headroom; non-thinking models stop at EOS as before, thinking ones get
  to finish (math-mult on nanbeige: false 0/1 → honest 1/1).
- When the router degraded (timeout → fallback chain) the table showed
  "✗ degraded" and counted it against classification accuracy. There was no
  classification to judge — such rows now show a muted "⚠ router degraded"
  with an explanatory tooltip and are excluded from the accuracy KPI.
- Costs below $0.0001 (cheap flash models) collapsed to "$0.0000". money
  formatting now grows precision until two significant digits survive
  ($0.000035 instead of $0.0000).

2 new strings in all 7 languages.
2026-08-07 19:48:03 +03:00

133 lines
4.4 KiB
JavaScript
Vendored

// Tiny DOM helpers — no framework. `h` builds elements; `mount` swaps children.
import { t } from "./i18n.js";
export function h(tag, attrs = {}, ...children) {
const el = document.createElement(tag);
for (const [k, v] of Object.entries(attrs || {})) {
if (v == null || v === false) continue;
if (k === "class") el.className = v;
else if (k === "html") el.innerHTML = v;
else if (k === "dataset") Object.assign(el.dataset, v);
else if (k.startsWith("on") && typeof v === "function") el.addEventListener(k.slice(2).toLowerCase(), v);
else if (v === true) el.setAttribute(k, "");
else el.setAttribute(k, v);
}
for (const c of children.flat()) {
if (c == null || c === false) continue;
el.appendChild(typeof c === "object" ? c : document.createTextNode(String(c)));
}
return el;
}
export function clear(node) {
while (node && node.firstChild) node.removeChild(node.firstChild);
}
export function opt(v, label, sel) {
return h("option", { value: v, selected: sel ? true : null }, label || v);
}
export function mount(node, ...children) {
clear(node);
for (const c of children.flat())
if (c != null && c !== false)
node.appendChild(typeof c === "object" ? c : document.createTextNode(String(c)));
return node;
}
export function toast(msg, kind = "info", ms = 3200) {
const box = document.getElementById("toasts");
if (!box) return;
const el = h("div", { class: `toast ${kind}` }, msg);
box.appendChild(el);
setTimeout(() => {
el.style.opacity = "0";
el.style.transition = "opacity .2s";
setTimeout(() => el.remove(), 220);
}, ms);
}
// Modal dialog. `body` is a DOM node; `onSave` returns a Promise; returns nothing.
export function modal(title, body, onSave, saveLabel) {
const backdrop = h("div", { class: "modal-backdrop" });
const onKeydown = (e) => {
if (e.key === "Escape") close();
};
const close = () => {
backdrop.remove();
document.body.classList.remove("modal-open");
document.removeEventListener("keydown", onKeydown);
};
backdrop.addEventListener("click", (e) => {
if (e.target === backdrop) close();
});
const saveBtn = h("button", { class: "btn primary" }, saveLabel || t("common.save"));
saveBtn.addEventListener("click", async () => {
saveBtn.disabled = true;
try {
const keep = await onSave();
if (keep !== false) close();
} finally {
saveBtn.disabled = false;
}
});
const card = h(
"div",
{ class: "modal" },
h("h3", {}, title),
body,
h(
"div",
{ class: "modal-foot" },
h("button", { class: "btn ghost", onClick: close }, t("common.cancel")),
onSave ? saveBtn : null
)
);
backdrop.appendChild(card);
document.body.classList.add("modal-open");
document.addEventListener("keydown", onKeydown);
document.body.appendChild(backdrop);
return { close };
}
export function confirmDialog(message) {
return Promise.resolve(window.confirm(message));
}
// number formatting
// Money as a DOM node: 4 decimals, the last two rendered smaller — tiny
// per-request prices stay readable without shouting six zeros.
export function moneyEl(n) {
if (n == null) return document.createTextNode("—");
if (n === 0) return document.createTextNode("$0");
// sub-0.0001 costs (cheap flash models) would collapse to $0.0000 — grow
// precision until two significant digits survive
const s = Math.abs(n) >= 0.0001 ? n.toFixed(4) : n.toFixed(Math.min(8, 1 - Math.floor(Math.log10(Math.abs(n)))));
return h("span", { class: "money" }, "$" + s.slice(0, -2), h("small", { class: "money-tail" }, s.slice(-2)));
}
export function money(n) {
if (n == null) return "—";
if (n === 0) return "$0";
if (Math.abs(n) > 0 && Math.abs(n) < 0.0001) return "$" + n.toFixed(Math.min(8, 1 - Math.floor(Math.log10(Math.abs(n)))));
// up to 4 decimals, trailing zeros trimmed (but keep money-like 2 minimum)
let s = n.toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
if (/\.\d$/.test(s)) s += "0";
return "$" + s;
}
export function ms(n) {
return n == null ? "—" : `${Math.round(Number(n))} ms`;
}
export function num(n) {
if (n == null) return "—";
return Number(n).toLocaleString();
}
export function pct(n) {
if (n == null) return "—";
return (n * 100).toFixed(1) + "%";
}
export function timeAgo(unixSecs) {
const d = new Date(unixSecs * 1000);
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
}