diff --git a/internal/gui/assets/app.css b/internal/gui/assets/app.css index edbb16c6..eb2bcacd 100644 --- a/internal/gui/assets/app.css +++ b/internal/gui/assets/app.css @@ -527,33 +527,60 @@ body.window .field.solo { grid-column: auto; } } /* ---- the panel's tabs: agents, usage, profiles ---------------------------- */ -.ptabs { display: flex; flex: none; gap: 2px; margin: 0 0 8px; padding: 2px; border-radius: 9px; background: color-mix(in srgb, var(--fg) 6%, transparent); } +/* outside the scrolling page, so they stay put under the header */ +.ptabs { display: flex; flex: none; margin: 0 12px 10px; padding: 2px; border-radius: 9px; background: color-mix(in srgb, var(--fg) 6%, transparent); } .ptabs[hidden] { display: none; } -.ptabs button { flex: 1; height: 24px; border-radius: 7px; font-size: 12px; color: var(--muted); transition: color .15s, background-color .15s; } +.ptabs button { flex: 1; display: flex; align-items: center; justify-content: center; gap: 3px; height: 26px; border-radius: 7px; font-size: 12px; color: var(--muted); transition: color .15s, background-color .15s; } .ptabs button:hover:not(.on) { color: var(--fg-2); } -.ptabs button.on { color: var(--fg); font-weight: 550; background: var(--card); box-shadow: var(--card-edge); } +.ptabs button.on { color: var(--fg); font-weight: 550; background: var(--card); box-shadow: 0 1px 2px rgba(0, 0, 0, .08), var(--card-edge); } .ptabs button[hidden] { display: none; } +.ptabs em { font-style: normal; font-weight: 400; color: var(--faint); font-variant-numeric: tabular-nums; } body.panel[data-ptab="usage"] :is(#agents, .profiles), body.panel[data-ptab="profiles"] :is(#agents, #panelQuota), body.panel[data-ptab="agents"] :is(.profiles, #panelQuota) { display: none; } -/* ---- the panel's agents: what's set in words, controls once opened ------- */ -body.panel .row.agent { flex-wrap: wrap; row-gap: 7px; cursor: default; } -body.panel .row.agent .fields { display: none; } +/* ---- the panel's agents: one quiet line each, opened in place ------------ */ +/* icon · name · model · effort · chevron, the last three in columns of their + own so the bars and chevrons line up down the list */ +body.panel .row.agent { display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; grid-template-rows: 36px; align-items: center; column-gap: 10px; min-height: 36px; padding: 0 12px; cursor: default; } +body.panel .row.agent .ag-handle { width: 20px; height: 20px; margin: 0; } +body.panel .row.agent .ic { width: 18px; height: 18px; } +body.panel .row.agent .who { display: flex; align-items: center; gap: 6px; min-width: 0; } body.panel .row.agent.open { background: var(--card-2); } -body.panel .row.agent.open .fields, body.panel #agents.extras .row.agent.open .fields { display: grid; order: 3; flex: 0 0 100%; width: 100%; padding: 0 0 3px 27px; } -body.panel .row.agent.open .field.main, body.panel .row.agent.open .field.solo { justify-self: stretch; } -.ag-sum { display: flex; align-items: center; gap: 7px; min-width: 0; max-width: 62%; margin-left: auto; height: 24px; padding: 0 2px 0 6px; border-radius: 6px; font-size: 12px; color: var(--muted); } -.ag-sum .v { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.ag-sum .v.empty { color: var(--faint); } -.ag-sum .effort-ic { flex: none; width: 13px; height: 13px; color: var(--fg-2); } -.ag-sum .chev { flex: none; display: inline-flex; color: var(--faint); transform: rotate(-90deg); transition: transform .2s var(--ease); } -.row.agent:hover .ag-sum, .row.agent.open .ag-sum { color: var(--fg-2); } +.ag-sum { display: grid; grid-template-columns: auto 12px 10px; align-items: center; gap: 10px; min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted); } +.ag-sum .v { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: right; } +.ag-sum .v.empty { color: var(--muted); } +.ag-sum .chev { display: inline-flex; width: 10px; color: var(--faint); transform: rotate(-90deg); transition: transform .2s var(--ease); } .row.agent.open .ag-sum .chev { transform: none; } -body.panel .row.agent .ag-fix { top: 21px; } +.eff { display: inline-flex; align-items: flex-end; gap: 1.5px; height: 10px; } +.eff i { width: 2.5px; border-radius: 1px; background: var(--faint); opacity: .45; } +.eff i:nth-child(1) { height: 4px; } +.eff i:nth-child(2) { height: 7px; } +.eff i:nth-child(3) { height: 10px; } +.eff[data-l="1"] i:nth-child(-n+1), .eff[data-l="2"] i:nth-child(-n+2), .eff[data-l="3"] i { background: var(--fg-2); opacity: 1; } +.ag-open { display: none; } +.row.agent.open .ag-open { grid-column: 1 / -1; display: flex; flex-direction: column; gap: 8px; padding: 0 0 12px 30px; } +.ag-open .field { display: flex; justify-content: flex-start; width: 100%; height: 30px; gap: 8px; padding: 0 10px; border-radius: 8px; background: var(--card); font-size: 12.5px; box-shadow: inset 0 0 0 1px var(--pill-line); } +body.panel .ag-open .field:hover { background: var(--card); box-shadow: inset 0 0 0 1px var(--pill-line), var(--pill-shadow); } +body.panel .ag-open .field.open { box-shadow: inset 0 0 0 1px var(--accent); } +.ag-open .field .k { order: 0; width: 44px; font-size: 11px; color: var(--faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.ag-open .field .v { order: 1; flex: 1 1 auto; font-weight: 400; text-align: left; } +.ag-open .field > .ic, .ag-open .field > .ic-stack { order: 2; flex: none; } +.row.agent .ag-open .field .ic { width: 14px; height: 14px; } +.ag-open .field .chev { order: 3; margin-left: 0; transform: none; } +.ag-seg { display: flex; padding: 2px; border-radius: 8px; background: color-mix(in srgb, var(--fg) 6%, transparent); } +.ag-seg button { flex: 1; min-width: 0; padding: 4px 0; border-radius: 6px; font-size: 11.5px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: color .15s, background-color .15s; } +.ag-seg button:hover:not(.on) { color: var(--fg-2); } +.ag-seg button.on { background: var(--card); color: var(--fg); box-shadow: 0 1px 2px rgba(0, 0, 0, .08), var(--card-edge); } +.ag-open .extras-cell { display: flex; justify-content: flex-start; gap: 6px; } +.ag-open .extras-cell .field { width: 30px; padding: 0; justify-content: center; } +body.panel .row.agent .ag-fix { left: 21px; top: 22px; } +body.panel .agent-fold-cap { padding-left: 12px; } +/* "Show 9 more", the list's last line, where the names start */ +body.panel .agent-more { justify-content: flex-start; gap: 6px; padding: 9px 14px; } /* ---- the panel's profiles tab ---------------------------------------------- */ -body.panel .profiles { flex-direction: column; align-items: stretch; gap: 8px; padding: 10px 0 2px; } +body.panel .profiles { flex-direction: column; align-items: stretch; gap: 8px; padding: 2px 0; } body.panel .profiles > .label { display: none; } body.panel .profiles .chips { flex: none; flex-wrap: wrap; } body.panel .profiles .chips .hint { white-space: normal; line-height: 1.4; padding: 0 4px; color: var(--muted); } @@ -561,32 +588,33 @@ body.panel .profiles .chips:has(.chip-input) .hint { display: none; } body.panel .profiles #save { margin: 0; align-self: flex-start; } /* ---- the panel's usage tab: accounts under their vendor, windows as rings -- */ -.panel-quota { flex: none; display: flex; flex-direction: column; gap: 12px; padding: 0 0 4px; } -.pq-group { display: flex; flex-direction: column; gap: 6px; } -.pq-gh { display: flex; align-items: center; gap: 6px; padding: 0 2px; font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--faint); } -.pq-gh .ic { width: 13px; height: 13px; } -.pq-gc { font-weight: 500; letter-spacing: 0; } -.pq-card { display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 8px 12px; border-radius: 10px; background: var(--card); box-shadow: var(--card-edge); } -.pq-who { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; } -.pq-who > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.pq-user { font-size: 12.5px; color: var(--fg); } -.pq-sub { font-size: 11px; color: var(--faint); font-variant-numeric: tabular-nums; } -.pq-sub.ends { color: var(--muted); } -.pq-sub.err { color: var(--muted); white-space: normal !important; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; } -.pq-rings { flex: none; display: flex; gap: 10px; } -.pq-ring { display: flex; flex-direction: column; align-items: center; gap: 3px; width: 44px; cursor: pointer; } +.panel-quota { flex: none; display: flex; flex-direction: column; gap: 14px; padding: 2px 2px 6px; } +.pq-group { display: flex; flex-direction: column; gap: 8px; } +.pq-gh { display: flex; align-items: center; gap: 8px; font-size: 10.5px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; color: var(--faint); } +.pq-gh .ic { width: 14px; height: 14px; flex: none; } +.pq-gn { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.pq-gnote { flex: none; font-size: 12px; font-weight: 400; letter-spacing: 0; text-transform: none; font-variant-numeric: tabular-nums; } +.pq-gnote.ends { color: var(--amber); } +.pq-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 4px 12px; padding: 10px 12px; border-radius: 11px; background: var(--card); box-shadow: var(--card-edge); } +.pq-user { font-size: 12.5px; color: var(--fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.pq-sub { font-size: 11px; color: var(--faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; } +.pq-sub.err { color: var(--muted); white-space: normal; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; } +.pq-rings { grid-row: 1 / 3; grid-column: 2; display: flex; gap: 12px; } +.pq-ring { display: flex; flex-direction: column; align-items: center; gap: 3px; cursor: pointer; } .pq-dial { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: conic-gradient(var(--fg-2) calc(var(--p) * 1%), color-mix(in srgb, var(--fg) 9%, transparent) 0); } -.pq-dial b { width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center; background: var(--card); font-size: 9.5px; font-weight: 600; color: var(--fg-2); font-variant-numeric: tabular-nums; } +.pq-dial b { width: 27px; height: 27px; border-radius: 50%; display: grid; place-items: center; background: var(--card); font-size: 9.5px; font-weight: 600; color: var(--fg-2); font-variant-numeric: tabular-nums; } .pq-ring.full .pq-dial { background: conic-gradient(var(--amber) calc(var(--p) * 1%), color-mix(in srgb, var(--amber) 18%, transparent) 0); } .pq-ring.full b { color: var(--fg); } -.pq-rn { max-width: 100%; font-size: 9.5px; line-height: 1.2; color: var(--faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.pq-bals { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; } -.pq-card.bal { flex-direction: column; align-items: stretch; gap: 4px; min-height: 0; } -.pq-bn { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--muted); } -.pq-bn .ic { width: 13px; height: 13px; flex: none; } +.pq-rn { max-width: 44px; font-size: 9.5px; line-height: 1.2; color: var(--faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.pq-bals { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; } +.pq-card.bal { grid-template-columns: minmax(0, 1fr); } .pq-amt { font-size: 15px; font-weight: 600; letter-spacing: -.01em; color: var(--fg); font-variant-numeric: tabular-nums; } -.pq-card .skeleton.sk-aq { width: 130px; height: 9px; border-radius: 4px; margin-right: auto; } +.pq-card .skeleton.sk-aq { width: 130px; height: 9px; border-radius: 4px; } .pq-card .skeleton.sk-ring { width: 34px; height: 34px; border-radius: 50%; } +/* the panel's footer: a hairline over it, as much room above as below */ +body.panel .foot { min-height: 0; padding: 6px 8px 6px 14px; border-top: 1px solid var(--line-2); } +.pq-age { font-size: 12px; color: var(--muted); white-space: nowrap; } +.pq-age:not([hidden]) + .status:empty { display: none; } #view-agents.loading .panel-quota { visibility: hidden; } /* ---- profiles ------------------------------------------------------------- */ diff --git a/internal/gui/assets/app.js b/internal/gui/assets/app.js index 423294b4..d73ef913 100644 --- a/internal/gui/assets/app.js +++ b/internal/gui/assets/app.js @@ -180,6 +180,7 @@ function renderAgents() { list.append(e); } const { shown: used, folded } = arrangeAgents(); + if (mode === "panel") $("#ptabN").textContent = state.agents.length || ""; const agentRow = (a, inFold) => { const row = el("div", "row agent"); row.dataset.id = a.id; @@ -224,37 +225,42 @@ function renderAgents() { if (extras.childNodes.length) fields.append(extras); // the panel shows what's set as words, and a row's controls only // once it's opened: one row at a time, in place - let sum = null; - if (mode === "panel" && fields.childNodes.length) { - sum = el("button", "ag-sum"); - sum.type = "button"; - const main = sorted.find((f) => !extra(f) && !f.menu); - const effortOf = (f) => f.key === "effort" || f.label === "effort" || f.label === "thinking"; - if (main && !effortOf(main)) { - const opt = optionFor(main, main.value); - sum.append(el("span", "v" + (main.value ? "" : " empty"), opt?.label || main.value || t("default"))); - } + let sum = null, openBox = null; + const effortOf = (f) => f.key === "effort" || f.label === "effort" || f.label === "thinking"; + if (mode === "panel") { + sum = el("span", "ag-sum"); + const main = sorted.find((f) => !extra(f) && !f.menu && !effortOf(f)); + const opt = main && optionFor(main, main.value); + sum.append(el("span", "v" + (main?.value ? "" : " empty"), main ? (opt?.label || main.value || t("default")) : "")); + // how much effort as three bars, in a column of its own down the list: + // none lit for the default, or for an agent that has no such setting const ef = a.fields.find(effortOf); - if (ef) { - const e = effortIcon(ef); - e.title = t("{label}: {value}", { label: t(ef.label), value: effortName(optionFor(ef, ef.value) || { value: ef.value }) }); - sum.append(e); - } + sum.append(effortBars(ef)); const c = el("span", "chev"); - c.append(svg(CHEV, 11, 1.7)); + c.append(svg(CHEV, 10, 1.6)); sum.append(c); - sum.setAttribute("aria-expanded", String(panelOpenAgent === a.id)); + // opened: each setting on a line of its own, named, and the effort as + // its levels side by side + openBox = el("div", "ag-open"); + for (const b of [...fields.querySelectorAll(":scope > .field")]) { + const f = sorted.find((x) => x.key === b.dataset.key) || (b.dataset.key === "tiers" ? tiers : null); + if (f && effortOf(f)) { openBox.append(effortSeg(a, f)); continue; } + if (f && !b.querySelector(":scope > .k")) b.prepend(el("span", "k", t(f.label))); + openBox.append(b); + } + if (extras.childNodes.length) openBox.append(extras); row.classList.toggle("open", panelOpenAgent === a.id); + row.setAttribute("aria-expanded", String(panelOpenAgent === a.id)); row.onclick = (ev) => { - if (ev.target.closest(".fields, .ag-handle, .ag-fix, .ag-show")) return; + if (ev.target.closest(".ag-open, .ag-handle, .ag-fix, .ag-show")) return; const open = panelOpenAgent !== a.id; panelOpenAgent = open ? a.id : null; for (const r of $("#agents").querySelectorAll(".row.agent.open")) { r.classList.remove("open"); - r.querySelector(".ag-sum")?.setAttribute("aria-expanded", "false"); + r.setAttribute("aria-expanded", "false"); } row.classList.toggle("open", open); - sum.setAttribute("aria-expanded", String(open)); + row.setAttribute("aria-expanded", String(open)); fit(); }; } @@ -277,8 +283,8 @@ function renderAgents() { who.append(driftFix(a)); } row.append(agentHandle(a, row, inFold), who); - if (sum) row.append(sum); - row.append(fields); + if (sum) row.append(sum, openBox); + else row.append(fields); return row; }; // the extras column is there for every row once any agent has one, so the @@ -955,6 +961,43 @@ function effortIcon(f) { return e; } +// effortBars: the panel's effort at a glance, three bars lit up to it. +function effortBars(f) { + const levels = (f?.options || []).filter((o) => o.value && o.value !== "off"); + const at = f ? levels.findIndex((o) => o.value === f.value) : -1; + const e = el("span", "eff"); + e.dataset.l = at < 0 ? 0 : Math.max(1, Math.round(((at + 1) / levels.length) * 3)); + e.append(el("i"), el("i"), el("i")); + if (f) e.title = t("{label}: {value}", { label: t(f.label), value: effortName(optionFor(f, f.value) || { value: f.value }) }); + return e; +} + +// effortSeg: an opened panel row's effort, every level side by side. +function effortSeg(a, f) { + const seg = el("div", "ag-seg"); + seg.setAttribute("role", "radiogroup"); + seg.setAttribute("aria-label", t(f.label)); + for (const o of f.options) { + const b = el("button", o.value === f.value ? "on" : "", effortName(o)); + b.type = "button"; + b.setAttribute("role", "radio"); + b.setAttribute("aria-checked", String(o.value === f.value)); + if (o.note) b.title = o.note; + b.onclick = async () => { + if (o.value === f.value) return; + try { + state = await api("set", { agent: a.id, field: f.key, value: o.value }); + renderAgents(); + status(`${a.name} ${t(f.label)} → ${effortName(o)}`, "ok"); + } catch (e) { + status(e.message, "err"); + } + }; + seg.append(b); + } + return seg; +} + function renderEffortPicker() { const range = $("#effortRange"); const options = pick.options; @@ -3889,6 +3932,7 @@ const PERIODS = [["today", "Today"], ["7d", "7 days"], ["30d", "30 days"], ["all // never come without a proxy), so they load on their own and the local log // never waits for them. They don't depend on the period either. let quotas = null; +let quotasAt = 0; // when they came in async function loadUsage() { renderUsageTab(); if (usageTab === "sessions") return loadSessions(); @@ -3902,7 +3946,7 @@ let quotasLoading = null; function loadQuotas() { if (quotasLoading) return quotasLoading; quotasLoading = api("usage/quotas") - .then((q) => { quotas = q || []; }, () => { quotas = quotas || []; }) + .then((q) => { quotas = q || []; quotasAt = Date.now(); }, () => { quotas = quotas || []; }) .finally(() => { quotasLoading = null; renderQuotas(); }); return quotasLoading; } @@ -4037,6 +4081,7 @@ function setPanelTab(tab) { b.classList.toggle("on", b.dataset.ptab === tab); b.setAttribute("aria-selected", String(b.dataset.ptab === tab)); } + panelAge(); fit(); } if (mode === "panel") { @@ -4082,53 +4127,54 @@ function renderPanelQuota() { for (const [name, qs] of groups) { const g = el("div", "pq-group"); const head = el("div", "pq-gh"); - head.append(icon(qs[0].icon), el("span", "pq-gn", name)); - if (qs.length > 1) head.append(el("span", "pq-gc", String(qs.length))); + // how many accounts, or the one account's plan and until when + const note = qs.length > 1 ? t("{n} accounts", { n: qs.length }) : [qs[0].plan, qs[0].until ? planTerm(qs[0]) : ""].filter(Boolean).join(" · "); + head.append(icon(qs[0].icon), el("span", "pq-gn", name), el("span", "pq-gnote" + (qs.length === 1 && qs[0].renew === "off" ? " ends" : ""), note)); g.append(head); for (const q of qs) g.append(panelQuotaCard(q)); box.append(g); } if (bals.length) { const g = el("div", "pq-group"); - g.append(el("div", "pq-gh", t("Balance"))); + const head = el("div", "pq-gh"); + head.append(el("span", "pq-gn", t("Balances"))); + g.append(head); const grid = el("div", "pq-bals"); for (const q of bals) { const card = el("div", "pq-card bal"); - const who = el("span", "pq-who"); - const name = el("span", "pq-bn"); - name.append(icon(q.icon), el("span", "", q.name)); - who.append(name); - if (q.user && q.user !== q.name) who.append(el("span", "pq-sub", q.user)); card.title = [q.name, q.user].filter(Boolean).join(" · "); - card.append(who, el("b", "pq-amt", q.balance)); + card.append(el("span", "pq-sub", q.name), el("b", "pq-amt", q.balance)); grid.append(card); } g.append(grid); box.append(g); } + panelAge(); fit(); } +// shortWindow: "5 hours" as 5h, "7 days" as 7d; any other name as it is. +function shortWindow(name) { + const m = /^(\d+)\s*(minute|hour|day|week|month)s?$/i.exec(name || ""); + return m ? m[1] + { minute: "m", hour: "h", day: "d", week: "w", month: "mo" }[m[2].toLowerCase()] : t(name); +} + function panelQuotaCard(q) { const card = el("div", "pq-card"); - const who = el("span", "pq-who"); - who.append(el("span", "pq-user", q.user || q.name)); - card.title = [q.name, q.user, q.plan, planTerm(q)].filter(Boolean).join(" · "); - const sub = []; - if (q.plan) sub.push(q.plan); - if (q.until) sub.push(planTerm(q)); - card.append(who); + card.append(el("span", "pq-user", q.user || q.name)); + card.title = [q.name, q.user, q.plan, q.until ? planTerm(q) : ""].filter(Boolean).join(" · "); if (q.error) { card.classList.add("err"); - who.append(el("span", "pq-sub err", quotaError(q.error))); + card.append(el("span", "pq-sub err", quotaError(q.error))); card.title += "\n" + q.error; - if (sub.length) who.append(el("span", "pq-sub", sub.join(" · "))); return card; } const ws = q.windows.slice(0, 3); - // when each window starts again, the ones that have begun - const resets = ws.filter((w) => w.resetsAt && w.used > 0).map((w) => (ws.length > 1 ? t(w.name) + " " : "") + "↻ " + resetClock(new Date(w.resetsAt))); - if (sub.length || resets.length) who.append(el("span", "pq-sub" + (q.renew === "off" ? " ends" : ""), [...sub, ...resets].join(" · "))); + // when the windows begun start again: the first bare, the others by name + const begun = ws.filter((w) => w.resetsAt && w.used > 0); + card.append(el("span", "pq-sub", begun.length + ? begun.map((w, i) => (i ? shortWindow(w.name) + " " : "↻ ") + resetClock(new Date(w.resetsAt))).join(" · ") + : t("Not used yet"))); const rings = el("span", "pq-rings"); for (const w of ws) { const used = Math.max(0, Math.min(100, w.used)); @@ -4136,7 +4182,7 @@ function panelQuotaCard(q) { const dial = el("span", "pq-dial"); dial.style.setProperty("--p", quotaFill(w)); dial.append(el("b", "", quotaFill(w) + "%")); - r.append(dial, el("span", "pq-rn", t(w.name))); + r.append(dial, el("span", "pq-rn", shortWindow(w.name))); r.title = t(w.name) + " · " + quotaText(w) + (w.resetsAt ? "\n" + t("Resets {when}", { when: new Date(w.resetsAt).toLocaleString() }) + " · " + untilText(new Date(w.resetsAt)) : "") + "\n" + t(quotaLeft ? "Show how much of each window is used" : "Show how much of each window is left"); // used or left turns here too, as on the Usage page (#124) @@ -4147,6 +4193,15 @@ function panelQuotaCard(q) { return card; } +// panelAge: on the Usage tab, the footer says how old what it shows is. +function panelAge() { + const a = $("#pqAge"); + if (!a) return; + a.hidden = mode !== "panel" || panelTab !== "usage" || !quotasAt; + if (!a.hidden) a.textContent = t("Updated {when}", { when: ago(quotasAt) }); +} +if (mode === "panel") setInterval(panelAge, 30000); + // 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) diff --git a/internal/gui/assets/i18n.js b/internal/gui/assets/i18n.js index f8af3910..a27f91e4 100644 --- a/internal/gui/assets/i18n.js +++ b/internal/gui/assets/i18n.js @@ -381,6 +381,9 @@ const I18N = { "Balance field": "余额字段", "Where the amount is in the reply, e.g. data.balance; it can be a sum with + - * / and brackets, e.g. data.total / 500000 or (1 - credits.used / 70) %; \"$\" in front adds the sign, \"%\" after it shows a percent; several, each with a label, go apart by \";\", e.g. 5h: a.used / a.cap %; $credits.left": "金额在返回 JSON 中的位置,例如 data.balance;可以用 + - * / 和括号算,例如 data.total / 500000 或 (1 - credits.used / 70) %;前面加 \"$\" 显示币种符号,后面加 \"%\" 显示为百分比;多个金额用 \";\" 隔开,各自可加标签,例如 5h: a.used / a.cap %; $credits.left", "Balance": "余额", + "Balances": "余额", + "Updated {when}": "{when}更新", + "Not used yet": "还没用过", "Allowances": "额度", "What is left on the account: the vendor tells only this, so Used / Left leaves it as it is": "账户里剩下的金额:供应商只提供这个数,所以「已用 / 剩余」不会改变它", "Headers": "请求头", diff --git a/internal/gui/assets/index.html b/internal/gui/assets/index.html index 2f827f0b..70819fe1 100644 --- a/internal/gui/assets/index.html +++ b/internal/gui/assets/index.html @@ -43,12 +43,13 @@ + +
-
Profiles @@ -186,6 +187,7 @@