routing: the Requests and Accounts columns read in order (the user: 这里排版是不是有问题啊?感觉好乱) — side by side, the accounts ran far on down the page beside an empty column under the requests; they now end where the requests do and scroll like them. An account's models were wrapped in among its numbers (tried, answered, last answered), a code chip mid-line; they now take a line of their own under them. A provider's own row said its model twice (heading and chip) and the provider's name twice (card and row); the row is now its model alone. The two-column rule had never matched at all: .rt-col is a container too, so an unnamed @container query was sized against the column (~446px), not the section; the section's container is now named. The requests list is sized in the frame after a resize, since the accounts beside it now follow its height (WebKit reported a ResizeObserver loop otherwise). routing-columns.test.cjs (Chromium, WebKit, en/zh: bottoms level, accounts scroll, models on their own line, a provider's row its model alone, one column below 860px) fails without the change. routing-steady, -busy, -compact, -shape, -flood, -effort-row, -kind, -served, panel-routing, click-scroll, old-webkit pass; go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/gui pass.
This commit is contained in:
3 files changed
+141
-8
No files matched your search
@@ -146,9 +146,17 @@
|
||||
.masking .pii { animation: rtpii .35s ease-out both; }
|
||||
@keyframes rtpii { from { filter: blur(0); opacity: 1; } }
|
||||
#view-routing .rt { margin: 0; }
|
||||
.rt-more { container-type: inline-size; padding-bottom: 12px; }
|
||||
.rt-more { container: rt-more / inline-size; padding-bottom: 12px; }
|
||||
.rt-cols { display: grid; grid-template-columns: minmax(0, 1fr); column-gap: 16px; }
|
||||
@container (min-width: 860px) { .rt-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; } }
|
||||
/* side by side, the accounts end where the requests do and scroll like
|
||||
them, not run on down the page beside an empty column: of no height of
|
||||
their own, they take what the requests give the row, and no less than a
|
||||
few accounts' */
|
||||
@container rt-more (min-width: 860px) {
|
||||
.rt-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.rt-cols:not(.solo) .rt-col:last-child { display: flex; flex-direction: column; }
|
||||
.rt-cols:not(.solo) .rt-acts { flex: 1 1 auto; height: 0; min-height: 216px; overflow-y: auto; overscroll-behavior: contain; }
|
||||
}
|
||||
/* each column its own container: a request's row lays itself out by the
|
||||
list's width, not the page's (#273: in two columns of ~470px the rows
|
||||
kept to one line, and the reasoning was drawn over the numbers) */
|
||||
@@ -216,6 +224,7 @@
|
||||
.rt-act .st.left { color: var(--muted); }
|
||||
.rt-act .tally { display: flex; flex-wrap: wrap; gap: 2px 12px; color: var(--muted); font-size: 11px; font-variant-numeric: tabular-nums; }
|
||||
.rt-act .tally .bad { color: var(--red); }
|
||||
.rt-act .mdls { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
.rt-act .fix { display: flex; flex-wrap: wrap; gap: 2px 12px; font-size: 11px; }
|
||||
.rt-act .bar { height: 3px; border-radius: 2px; background: var(--card-2); overflow: hidden; }
|
||||
.rt-act .bar i { display: block; height: 100%; border-radius: 2px; background: var(--muted); }
|
||||
|
||||
@@ -1240,7 +1240,9 @@
|
||||
}
|
||||
const row = el("div", "rt-act");
|
||||
const name = el("div", "nm");
|
||||
name.append(el("b", "", who(w)), el("span", "", w.kind === "provider" ? w.model : w.plan || (w.kind === "key" ? t("API key") : "")));
|
||||
// a provider's own row: its model, the provider's name is the heading
|
||||
if (w.kind === "provider") name.append(el("b", "", w.model || w.name));
|
||||
else name.append(el("b", "", who(w)), el("span", "", w.plan || (w.kind === "key" ? t("API key") : "")));
|
||||
let st, cls = "";
|
||||
const resting = a.rest && at(a.rest.until) > n;
|
||||
if (resting) { st = `${failWord(a.rest.why)} · ${restWhen(a.rest)}`; cls = "rest"; }
|
||||
@@ -1257,9 +1259,16 @@
|
||||
el("span", "", t("tried {n}", { n: a.tried })),
|
||||
el("span", "ok", t("answered {n}", { n: a.ok })),
|
||||
...(fails.length ? [el("span", "bad", fails.join(", "))] : []),
|
||||
...(a.last ? [el("span", "", t("last answered {time}", { time: clock(a.last) }))] : []),
|
||||
...[...a.models].map((m) => el("code", "mdl", m)));
|
||||
...(a.last ? [el("span", "", t("last answered {time}", { time: clock(a.last) }))] : []));
|
||||
row.append(name, el("div", "st " + cls, st), tally);
|
||||
// the models it answered, on a line of their own, not wrapped in
|
||||
// among the numbers; a provider's one model is already its name
|
||||
const mdls = [...a.models].filter((m) => !(w.kind === "provider" && m === w.model));
|
||||
if (mdls.length) {
|
||||
const ms = el("div", "mdls");
|
||||
ms.append(...mdls.map((m) => el("code", "mdl", m)));
|
||||
row.append(ms);
|
||||
}
|
||||
if (resting && a.rest.why === "verify") {
|
||||
// the vendor wants the account verified (#152): where, and a way to
|
||||
// stop its rest once it is
|
||||
@@ -2648,10 +2657,16 @@
|
||||
if (v.hidden || !reqs.offsetParent) return;
|
||||
const above = reqs.getBoundingClientRect().top - box.getBoundingClientRect().top;
|
||||
const room = v.clientHeight - above - 28;
|
||||
reqs.style.maxHeight = Math.round(Math.max(216, Math.min(420, room))) + "px";
|
||||
const h = Math.round(Math.max(216, Math.min(420, room))) + "px";
|
||||
if (reqs.style.maxHeight !== h) reqs.style.maxHeight = h;
|
||||
}
|
||||
new ResizeObserver(fitReqs).observe($("#view-routing"));
|
||||
new ResizeObserver(fitReqs).observe(box);
|
||||
// the accounts beside the requests end where they do, so a new height
|
||||
// resizes what the other observers have just been told of: size the
|
||||
// list in the next frame, not inside this round of them
|
||||
let fitting = 0;
|
||||
const fitSoon = () => { if (!fitting) fitting = requestAnimationFrame(() => { fitting = 0; fitReqs(); }); };
|
||||
new ResizeObserver(fitSoon).observe($("#view-routing"));
|
||||
new ResizeObserver(fitSoon).observe(box);
|
||||
words();
|
||||
requestAnimationFrame(frame);
|
||||
poll();
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// Run with Node's test runner and Playwright on the module path; see README.md.
|
||||
// The Routing page's Requests and Accounts and keys, side by side (a report
|
||||
// with a screenshot: "这里排版是不是有问题啊?感觉好乱"): the two lists end on one
|
||||
// line, the accounts scrolling like the requests instead of running on down
|
||||
// the page beside an empty column; the models an account answered sit on a
|
||||
// line of their own, not wrapped in among its numbers; a provider's row
|
||||
// says its model once, under the provider's name, not the name again and the
|
||||
// model twice. One column, the accounts list is as long as it is.
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs/promises");
|
||||
const path = require("node:path");
|
||||
const { test } = require("node:test");
|
||||
const { chromium, webkit } = require("playwright");
|
||||
|
||||
const now = new Date();
|
||||
const at = (i) => new Date(now.getTime() - (i + 1) * 60e3).toISOString();
|
||||
const renew = new Date(now.getTime() + 3 * 864e5).toISOString();
|
||||
const W = {
|
||||
aihub: { id: "aihubmix", provider: "aihubmix", name: "AiHubMix", kind: "provider", model: "glm-5.3-flash", icon: "aihubmix-color", routing: "smart" },
|
||||
c1: { id: "codex:a", provider: "codex", name: "Codex", who: "ann@example.com", plan: "plus", kind: "account", model: "gpt-5.6-terra", known: true, used: 9, renews: [renew], icon: "codex-color", routing: "smart" },
|
||||
c2: { id: "codex:b", provider: "codex", name: "Codex", who: "bob@example.com", plan: "pro", kind: "account", model: "gpt-5.6-terra", known: true, used: 4, renews: [renew], icon: "codex-color", routing: "smart" },
|
||||
cop: { id: "copilot:a", provider: "copilot", name: "Copilot", who: "ann", kind: "account", model: "gpt-6-sol", known: true, used: 1, renews: [renew], icon: "githubcopilot", routing: "smart" },
|
||||
ds: { id: "deepseek", provider: "deepseek", name: "DeepSeek", kind: "provider", model: "deepseek-flash", icon: "deepseek-color", routing: "smart" },
|
||||
or: { id: "openrouter", provider: "openrouter", name: "OpenRouter", kind: "provider", model: "openai/gpt-6-sol", icon: "openrouter", routing: "smart" },
|
||||
wb: { id: "wb:a", provider: "workbuddy", name: "WorkBuddy", who: "ann", kind: "account", model: "glm", known: false, icon: "generic", routing: "smart" },
|
||||
};
|
||||
const mk = (i, w, model, extra = {}) => ({
|
||||
id: 200 - i, seq: 200 - i, time: at(i), agent: i % 2 ? "codex" : "curl", model, provider: w.provider,
|
||||
order: Object.values(W).filter((x) => x.provider === w.provider), done: true, status: 200, ms: 5000 + i * 900, tokens: 90000 + i * 700,
|
||||
tries: [{ id: w.id, model: w.model || model.split("/").pop(), start: at(i), done: true, status: 200, ms: 5000 }], ...extra,
|
||||
});
|
||||
const routes = [
|
||||
mk(0, W.ds, "deepseek/deepseek-flash"), mk(1, W.ds, "deepseek/deepseek-flash"),
|
||||
...[2, 3, 4, 5].map((i) => ({ ...mk(i, W.c1, "codex/gpt-5.6-terra"), tries: [{ id: W.c1.id, model: "gpt-5.6-terra", start: at(i), done: true, status: 200, ms: 8000, effort: "medium" }] })),
|
||||
{ ...mk(6, W.c1, "codex/gpt-6-luna"), tries: [{ id: W.c1.id, model: "gpt-6-luna", start: at(6), done: true, status: 200, ms: 8000 }] },
|
||||
{ ...mk(7, W.c1, "codex/gpt-5.6-luna"), tries: [{ id: W.c1.id, model: "gpt-5.6-luna", start: at(7), done: true, status: 200, ms: 8000 }] },
|
||||
mk(8, W.cop, "copilot/gpt-6-sol"), mk(9, W.aihub, "aihubmix/glm-5.3-flash"), mk(10, W.or, "openrouter/openai/gpt-6-sol"), mk(11, W.wb, "workbuddy/glm"),
|
||||
];
|
||||
function serve(lang) {
|
||||
const state = { agents: [{ id: "codex", name: "Codex", path: "/t", fields: [] }], profiles: [], settings: { lang, theme: "light" } };
|
||||
const assets = path.resolve(__dirname, "../assets");
|
||||
return async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
const json = (data) => route.fulfill({ json: data });
|
||||
if (url.pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"light",web:true};` });
|
||||
if (url.pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
|
||||
if (url.pathname === "/api/state") return json(state);
|
||||
if (url.pathname === "/api/gateway/trace") {
|
||||
if (url.searchParams.get("wait")) await new Promise((r) => setTimeout(r, 20e3));
|
||||
return json({ mine: true, now: now.toISOString(), seq: 200, totals: { requests: routes.length, rerouted: 0, errors: 0 }, routes });
|
||||
}
|
||||
if (url.pathname === "/api/gateway/history") return json({ cut: false, days: [], routes: [] });
|
||||
if (url.pathname === "/api/groups") return json({ groups: [] });
|
||||
if (url.pathname === "/api/providers") return json({ providers: [], presets: [], excluded: [], gateway: { running: true, window: true } });
|
||||
if (url.pathname.startsWith("/api/")) return json({});
|
||||
const file = path.join(assets, url.pathname === "/" ? "index.html" : url.pathname);
|
||||
const contentType = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png" }[path.extname(file)];
|
||||
await route.fulfill({ body: await fs.readFile(file).catch(() => ""), contentType });
|
||||
};
|
||||
}
|
||||
|
||||
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
|
||||
for (const lang of ["en", "zh"]) {
|
||||
test(`${engine} ${lang}: requests and accounts side by side`, async (t) => {
|
||||
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
|
||||
const page = await browser.newPage({ viewport: { width: 940, height: 1400 } });
|
||||
page.setDefaultTimeout(5000);
|
||||
const errors = [];
|
||||
page.on("pageerror", (e) => errors.push(e.message));
|
||||
await page.route("**/*", serve(lang));
|
||||
t.after(async () => {
|
||||
if (process.env.ARTIFACT_DIR) {
|
||||
await fs.mkdir(process.env.ARTIFACT_DIR, { recursive: true });
|
||||
await page.locator(".rt-cols").screenshot({ path: path.join(process.env.ARTIFACT_DIR, `${engine}-${lang}-columns.png`) });
|
||||
}
|
||||
await browser.close();
|
||||
});
|
||||
await page.goto("http://magpie.test/?view=routing");
|
||||
await page.locator(".rt-act").first().waitFor();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
const box = (s) => page.locator(s).evaluate((e) => { const b = e.getBoundingClientRect(); return { left: b.left, right: b.right, bottom: b.bottom, scroll: e.scrollHeight > e.clientHeight + 1 }; });
|
||||
const reqs = await box(".rt-reqs"), acts = await box(".rt-acts");
|
||||
assert(acts.left >= reqs.right, "side by side");
|
||||
assert(Math.abs(acts.bottom - reqs.bottom) <= 1, `the lists end on one line: ${reqs.bottom} / ${acts.bottom}`);
|
||||
assert(acts.scroll, "the rest of the accounts scroll");
|
||||
|
||||
// the Codex account's three models on their own line, none in the tally
|
||||
const codex = page.locator(".rt-act", { hasText: "ann@example.com" });
|
||||
assert.deepEqual((await codex.locator(".mdls code").allInnerTexts()).sort(), ["gpt-5.6-luna", "gpt-5.6-terra", "gpt-6-luna"]);
|
||||
assert.equal(await codex.locator(".tally code").count(), 0);
|
||||
const tally = await codex.locator(".tally").boundingBox(), mdls = await codex.locator(".mdls").boundingBox();
|
||||
assert(mdls.y >= tally.y + tally.height - 0.5, "the models under the numbers");
|
||||
|
||||
// a provider's row: its model once, not its name again
|
||||
const ai = page.locator(".rt-act").first();
|
||||
assert.equal((await ai.locator(".nm").innerText()).trim(), "glm-5.3-flash");
|
||||
assert.equal(await ai.locator("code").count(), 0);
|
||||
|
||||
// one column: the accounts as long as they are
|
||||
await page.setViewportSize({ width: 700, height: 1400 });
|
||||
await page.waitForTimeout(300);
|
||||
const one = await box(".rt-acts");
|
||||
assert(!one.scroll, "one column: nothing hidden in the accounts");
|
||||
assert.equal(await page.locator(".rt-acts").evaluate((e) => e.style.maxHeight), "");
|
||||
assert.deepEqual(errors, []);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user