ui: a subscription's windows side by side put every count under its name once one's doesn't fit beside it, not one up by its name and the next a line below (#90); a provider's models can be picked all at once (those the filter shows) or none, and their list scrolls on its own, not the editor with it (#115); DeepSeek Harness's own models are listed under its name, apart from magpie's DeepSeek provider, which read as DeepSeek twice (#115); Hide emails says when it is on — a struck eye, 邮箱已打码 and the accent, an open eye while emails show

This commit is contained in:
yetone committed 2026-09-27 21:38:25 +08:00
1 parent 115f0cbd9d
commit c76af6822f
5 files changed
+55 -8

No files matched your search

+6 -4
View File
@@ -31,11 +31,13 @@ const dshMark = "# magpie"
// dshKeyRef is the credential dsh signs gateway requests with.
const dshKeyRef = "MAGPIE_API_KEY"
// dshModels are the models dsh reaches on its own, as it ships them.
// dshModels are the models dsh reaches on its own, as it ships them — under
// its own name, not DeepSeek: magpie's DeepSeek provider is a group of that
// name too, and two would read as one listed twice (#115).
var dshModels = []Option{
{Value: "deepseek-flash", Label: "DeepSeek-V41-Flash", Icon: "deepseek-color", Group: "DeepSeek"},
{Value: "deepseek-v4-pro", Label: "DeepSeek-V4-Pro", Icon: "deepseek-color", Group: "DeepSeek"},
{Value: "deepseek-v4-flash", Label: "DeepSeek-V4-Flash", Icon: "deepseek-color", Group: "DeepSeek"},
{Value: "deepseek-flash", Label: "DeepSeek-V41-Flash", Icon: "deepseek-color", Group: "DeepSeek Harness"},
{Value: "deepseek-v4-pro", Label: "DeepSeek-V4-Pro", Icon: "deepseek-color", Group: "DeepSeek Harness"},
{Value: "deepseek-v4-flash", Label: "DeepSeek-V4-Flash", Icon: "deepseek-color", Group: "DeepSeek Harness"},
}
func dsh(home string) *Agent {
+5 -2
View File
@@ -413,6 +413,7 @@ body.window .field.solo { grid-column: auto; }
.quota-labels > span { white-space: nowrap; }
.quota-labels .quota-n { max-width: 100%; padding: 0; text-align: left; overflow: hidden; text-overflow: ellipsis; color: var(--fg-2); font-size: inherit; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
.quota-labels .quota-n:hover { color: var(--fg); }
.quota-windows.stacked .quota-n { flex-basis: 100%; }
.quota-track { height: 4px; border-radius: 2px; background: var(--pill); overflow: hidden; }
.quota-track i { display: block; height: 100%; border-radius: inherit; background: var(--accent); }
.subscription-error { font-size: 11px; color: var(--faint); }
@@ -1030,8 +1031,10 @@ body.window .ag-sk-icon { width: 22px; height: 22px; border-radius: 6px; }
/* model chips: click to expose, click again to hide */
.models { display: flex; flex-direction: column; gap: 6px; }
.models > input { max-width: 260px; }
.mchips { display: flex; flex-wrap: wrap; gap: 4px; max-height: 132px; overflow-y: auto; padding: 1px; }
.mbulk { display: flex; align-items: center; gap: 6px; }
.mbulk > input { max-width: 260px; flex: 1; }
/* a long list scrolls on its own, not the editor with it past its ends */
.mchips { display: flex; flex-wrap: wrap; gap: 4px; max-height: 132px; overflow-y: auto; overscroll-behavior: contain; padding: 1px; }
.mchips::-webkit-scrollbar { width: 0; }
.mchip {
display: inline-flex; align-items: center; gap: 5px; height: 24px; padding: 0 9px; border-radius: 999px;
+40 -1
View File
@@ -2936,7 +2936,24 @@ function renderModels(p) {
names.append(row);
}
};
if (q) { q.oninput = draw; box.append(q); }
// every model at once (those the filter shows, when there is one), or none
const bulk = el("div", "mbulk");
const allOn = el("button", "text action", t("Select all"));
allOn.title = t("Pick every model listed (those the filter shows)");
allOn.onclick = () => {
const f = (q?.value || "").trim().toLowerCase();
const ids = p.models.filter((m) => !f || m.id.toLowerCase().includes(f) || (m.name || "").toLowerCase().includes(f) || (m.default || "").toLowerCase().includes(f)).map((m) => m.id);
draft.chosen = [...draft.chosen, ...ids.filter((id) => !draft.chosen.includes(id))];
draw();
};
const allOff = el("button", "text action", t("Select none"));
allOff.title = t("Unpick every model");
allOff.onclick = () => { draft.chosen = []; draw(); };
bulk.append(allOn, allOff);
if (q || p.models.length > 1) {
if (q) { q.oninput = draw; bulk.prepend(q); }
box.append(bulk);
}
box.append(chips, names);
const foot = el("div", "mfoot");
const add = input(draft.typed || "", t("add a model id…"));
@@ -3839,6 +3856,19 @@ function panelQuotaRow(q) {
return row;
}
// quotaFit puts every window's count under its name once one's doesn't fit
// beside it, so windows side by side read alike rather than one count up
// by its name and the next a line below (#90)
const quotaFit = new ResizeObserver((es) => {
for (const { target: g } of es) {
const wraps = [...g.querySelectorAll(".quota-labels")].some((l) => {
const [name, n] = l.children;
return name.scrollWidth + 6 + n.scrollWidth > l.clientWidth;
});
g.classList.toggle("stacked", wraps);
}
});
// quotaWindows: one account's allowance as meters, or why there are none.
function quotaWindows(sub) {
if (sub.balance) {
@@ -3868,6 +3898,7 @@ function quotaWindows(sub) {
if (w.resetsAt) quota.title = t("Resets {when}", { when: new Date(w.resetsAt).toLocaleString() });
windows.append(quota);
}
quotaFit.observe(windows);
return windows;
}
@@ -4924,6 +4955,8 @@ setInterval(renderUpdateBadge, 15 * 60 * 1000); // a window left open still hear
// a tooltip — is swapped for blurred stand-in letters while it's on, as the
// page redraws too; the address itself is kept aside to put back.
(() => {
const EYE = "M2 12s3.5-8 10-8 10 8 10 8-3.5 8-10 8-10-8-10-8zM12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z";
const EYE_OFF = "M9.9 4.2A10.4 10.4 0 0 1 12 4c6.5 0 10 8 10 8a17.6 17.6 0 0 1-2.2 3.2M6.6 6.6C3.9 8.4 2 12 2 12s3.5 8 10 8a9.7 9.7 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2M2 2l20 20";
const EMAIL = /[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g, IS_EMAIL = new RegExp(EMAIL.source);
// stand-in letters of the address's shape, the same each time it's drawn:
// blurred, they read as a name without being one
@@ -4982,6 +5015,12 @@ setInterval(renderUpdateBadge, 15 * 60 * 1000); // a window left open still hear
};
return (on) => {
btn.setAttribute("aria-pressed", String(on));
// what it is now, in its icon and its words: an open eye while the
// addresses show, struck through once they're hidden
btn.querySelector("path").setAttribute("d", on ? EYE_OFF : EYE);
const label = btn.querySelector("[data-t]");
label.dataset.en = on ? "Emails hidden" : "Hide emails";
label.textContent = t(label.dataset.en);
view.classList.toggle("masked", on);
if (on) { mask(); watch.observe(view, OBS); }
else { watch.disconnect(); unmask(); }
+3
View File
@@ -406,6 +406,8 @@ const I18N = {
"Names & levels": "名称与推理档位",
"Rename the models agents see, or offer fewer of their reasoning levels": "重命名 Agent 看到的模型,或只提供部分推理档位",
"Test models": "测试模型",
"Pick every model listed (those the filter shows)": "选中列出的所有模型(有筛选时只选筛出来的)",
"Unpick every model": "取消选中所有模型",
"Send a tiny request to each model agents see, to find the ones that don't answer": "给 Agent 能看到的每个模型发一个极小的请求,找出不通的模型",
"{n} of {all} models didn't answer": "{all} 个模型中有 {n} 个没有响应",
"All {n} models answered": "{n} 个模型全部可用",
@@ -665,6 +667,7 @@ const I18N = {
// Gateway: routing, live
"live, as the gateway decides": "实时 · 网关正在做的决定",
"Hide emails": "邮箱打码",
"Emails hidden": "邮箱已打码",
"Add a model": "添加模型",
"Add another model": "再添加一个模型",
"{api} only · after the others": "仅 {api} · 排在其他之后",
+1 -1
View File
@@ -134,7 +134,7 @@
#view-routing .row-head .rt-livedot + .note { margin-left: 8px; }
.rt-mask { display: inline-flex; align-items: center; gap: 5px; }
.rt-mask svg { width: 13px; height: 13px; flex: none; }
.rt-mask[aria-pressed="true"] { color: var(--fg); background: var(--pill); box-shadow: inset 0 0 0 1px var(--line); }
.rt-mask[aria-pressed="true"] { color: var(--accent); background: color-mix(in srgb, var(--accent) 13%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 38%, transparent); }
/* an email, hidden: stand-in letters of its shape, softly blurred —
coming in out of focus when asked for */
.pii { filter: blur(2.2px); opacity: .5; user-select: none; }