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:
5 files changed
+55
-8
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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} · 排在其他之后",
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in new issue
Block a user