dsh-agy 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -10
- package/lib/{accounts-BN4h0AU4.mjs → accounts-D605JE2i.mjs} +186 -19
- package/lib/accounts-D605JE2i.mjs.map +1 -0
- package/lib/cli/index.d.mts.map +1 -1
- package/lib/cli/index.mjs +9 -7
- package/lib/cli/index.mjs.map +1 -1
- package/lib/client.js +402 -51
- package/lib/{constants-BpyRirvk.mjs → constants-CmA9wXNe.mjs} +3 -3
- package/lib/{constants-BpyRirvk.mjs.map → constants-CmA9wXNe.mjs.map} +1 -1
- package/lib/{exchange-LzEyxve1.mjs → exchange-C-tndv7j.mjs} +3 -3
- package/lib/{exchange-LzEyxve1.mjs.map → exchange-C-tndv7j.mjs.map} +1 -1
- package/lib/{import-CotRHNDk.mjs → import-SxPDjH3u.mjs} +4 -4
- package/lib/{import-CotRHNDk.mjs.map → import-SxPDjH3u.mjs.map} +1 -1
- package/lib/index.mjs +2 -2
- package/lib/{models-Qn0-sh6X.mjs → models-B6bD7zPi.mjs} +3 -3
- package/lib/{models-Qn0-sh6X.mjs.map → models-B6bD7zPi.mjs.map} +1 -1
- package/lib/{plugin-common-N7p9Cm-3.mjs → plugin-common-kSBwIaFi.mjs} +303 -17
- package/lib/plugin-common-kSBwIaFi.mjs.map +1 -0
- package/lib/{proxy-DfaL73mL.mjs → proxy-BbuQX5U9.mjs} +28 -8
- package/lib/proxy-BbuQX5U9.mjs.map +1 -0
- package/lib/{quota-summary-adID82I2.mjs → quota-summary-DEA3P3Zh.mjs} +3 -3
- package/lib/{quota-summary-adID82I2.mjs.map → quota-summary-DEA3P3Zh.mjs.map} +1 -1
- package/lib/web/plugin.mjs +40 -18
- package/lib/web/plugin.mjs.map +1 -1
- package/package.json +1 -4
- package/lib/accounts-BN4h0AU4.mjs.map +0 -1
- package/lib/plugin-common-N7p9Cm-3.mjs.map +0 -1
- package/lib/proxy-DfaL73mL.mjs.map +0 -1
package/lib/client.js
CHANGED
|
@@ -103,17 +103,28 @@ window.__ModuleLoader__.load({
|
|
|
103
103
|
rounded rect inset 2px from the card edge and transparent at rest. A
|
|
104
104
|
full-bleed rectangle reads as a slab and fights the card's own radius. */
|
|
105
105
|
.agy-rows { display: flex; flex-direction: column; gap: 2px; }
|
|
106
|
+
/* The live line: one status strip above the rows, present ONLY while upstream
|
|
107
|
+
requests are in flight — an idle pool renders no strip, so quiet stays quiet.
|
|
108
|
+
The pulsing dot is the host StateDot primitive ('ongoing'), so the animation
|
|
109
|
+
is the platform's; this rule is layout and tone only. Sits OUTSIDE .agy-rows,
|
|
110
|
+
so the master list's scroll cap does not scroll the status away. */
|
|
111
|
+
.agy-live {
|
|
112
|
+
display: flex; align-items: center; gap: 7px;
|
|
113
|
+
margin: 2px 2px 6px; padding: 7px 8px; border-radius: 10px;
|
|
114
|
+
font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-secondary, #61666b);
|
|
115
|
+
background: var(--dsw-alias-bg-layer-2, #f4f5f7);
|
|
116
|
+
}
|
|
106
117
|
/* Master/detail: the account list beside the selected account's detail, so a
|
|
107
|
-
* row and the panel it opens stay in view together
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
118
|
+
* row and the panel it opens stay in view together — but only when the
|
|
119
|
+
* container can host both columns comfortably. The Settings panel gives the
|
|
120
|
+
* wrap ~564px of inline space on desktop (800px modal - 188px nav - 48px
|
|
121
|
+
* padding), so at the panel the split STACKS into one full-width column:
|
|
122
|
+
* 564px cannot host two comfortable columns, and a forced 300px master
|
|
123
|
+
* truncated every email to "a1…" while squeezing the detail to ~280px (the
|
|
124
|
+
* original "panel feels too narrow" report). The breakpoint therefore sits
|
|
125
|
+
* ABOVE the panel width and must stay there; if the host's modal geometry
|
|
126
|
+
* changes, re-measure before moving it. The query is a CONTAINER one because
|
|
127
|
+
* the former viewport @media (max-width: 720px) never fired inside the panel.
|
|
117
128
|
*
|
|
118
129
|
* The containment lives on a dedicated wrapper, NOT on .agy-root:
|
|
119
130
|
* container-type: inline-size applies layout containment, which makes the
|
|
@@ -314,6 +325,12 @@ window.__ModuleLoader__.load({
|
|
|
314
325
|
color: var(--dsw-alias-label-primary, #1f2329); }
|
|
315
326
|
.agy-limit-reset { text-align: right; font: var(--dsw-font-xxxs-11);
|
|
316
327
|
color: var(--dsw-alias-label-tertiary, #8f959e); }
|
|
328
|
+
/* The burn projection: indented to align with the bar (58px label + 10px gap),
|
|
329
|
+
warn-tinted because "this window runs dry before it resets" is the one
|
|
330
|
+
projection that asks the reader to act. */
|
|
331
|
+
.agy-limit-burn { padding: 0 0 4px 68px;
|
|
332
|
+
font: var(--dsw-font-xxxs-11);
|
|
333
|
+
color: var(--dsw-alias-state-warn-primary, #f59e0b); }
|
|
317
334
|
|
|
318
335
|
/* ── Dense breakdown tables (Usage tab only) ─────────────────────────────── */
|
|
319
336
|
.agy-table-wrap { padding: 6px 0 2px; }
|
|
@@ -360,6 +377,19 @@ window.__ModuleLoader__.load({
|
|
|
360
377
|
/* Danger has no primitive variant; keep the ghost skin and tint the label. */
|
|
361
378
|
.agy-btn-danger { color: var(--dsw-alias-state-error-primary, #ec1313) !important; }
|
|
362
379
|
|
|
380
|
+
/* ── Recent activity ring ──────────────────────────────────────────────────
|
|
381
|
+
The "what just happened" list. Only the result cell carries color — ok
|
|
382
|
+
inherits the table's neutral, and a wall of tinted rows would read as an
|
|
383
|
+
alarm rather than a log. */
|
|
384
|
+
.agy-recent-state { font: var(--dsw-font-xxs-12); }
|
|
385
|
+
.agy-recent-state[data-kind="fail"] { color: var(--dsw-alias-state-error-primary, #ec1313); }
|
|
386
|
+
.agy-recent-state[data-kind="limited"] { color: var(--dsw-alias-state-warn-primary, #f59e0b); }
|
|
387
|
+
.agy-recent-state[data-kind="rotation"] { color: var(--dsw-alias-brand-primary-new-colorprimary-new-color, #4176e6); }
|
|
388
|
+
/* The recent list is a standalone disclosure on the tab root, not one block
|
|
389
|
+
inside a card body — the separator border-top the disclosure idiom uses
|
|
390
|
+
between sibling blocks would draw a stray line across nothing here. */
|
|
391
|
+
.agy-recent.agy-disclosure { border-top: 0; }
|
|
392
|
+
|
|
363
393
|
.agy-toolbar { display: flex; align-items: center; gap: 8px; }
|
|
364
394
|
.agy-textarea { width: 100%; min-height: 88px; resize: vertical; outline: none;
|
|
365
395
|
padding: 9px 10px; font: var(--dsw-font-xxs-12); font-family: var(--ds-font-family-code);
|
|
@@ -367,19 +397,24 @@ window.__ModuleLoader__.load({
|
|
|
367
397
|
border: 0.5px solid var(--dsw-alias-border-l2, #eef0f3); border-radius: 8px; }
|
|
368
398
|
.agy-textarea:focus { border-color: var(--dsw-alias-brand-primary-new-colorprimary-new-color, #4176e6); }
|
|
369
399
|
`;
|
|
370
|
-
/** Install the stylesheet once (idempotent across plugin reloads)
|
|
400
|
+
/** Install the stylesheet once (idempotent across plugin reloads): a second
|
|
401
|
+
* install never appends a duplicate, and one whose content has gone stale
|
|
402
|
+
* (a previous bundle's CSS) is refreshed in place. The disposer is
|
|
403
|
+
* deliberately a NO-OP — it runs on every Cordis effect re-evaluation, and
|
|
404
|
+
* removing the element there stripped every .agy-* style mid-session; the
|
|
405
|
+
* element is unique by id and inert once the section is gone. */
|
|
371
406
|
function installAgyStyles() {
|
|
372
407
|
if (typeof document === "undefined") return () => {};
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
408
|
+
const existing = document.getElementById(STYLE_ID);
|
|
409
|
+
if (existing !== null) {
|
|
410
|
+
if (existing.textContent !== CSS) existing.textContent = CSS;
|
|
411
|
+
return () => {};
|
|
412
|
+
}
|
|
376
413
|
const style = document.createElement("style");
|
|
377
414
|
style.id = STYLE_ID;
|
|
378
415
|
style.textContent = CSS;
|
|
379
416
|
document.head.appendChild(style);
|
|
380
|
-
return () => {
|
|
381
|
-
document.getElementById(STYLE_ID)?.remove();
|
|
382
|
-
};
|
|
417
|
+
return () => {};
|
|
383
418
|
}
|
|
384
419
|
//#endregion
|
|
385
420
|
//#region src/client/locales.ts
|
|
@@ -401,16 +436,34 @@ window.__ModuleLoader__.load({
|
|
|
401
436
|
tabModels: "模型",
|
|
402
437
|
tabUsage: "用量",
|
|
403
438
|
tabCredentials: "凭据",
|
|
404
|
-
stateActive: "
|
|
439
|
+
stateActive: "可用",
|
|
405
440
|
stateCooling: "冷却中",
|
|
406
441
|
stateVerificationRequired: "待验证",
|
|
407
442
|
stateDisabled: "已停用",
|
|
408
443
|
coolingUntil: "冷却至",
|
|
444
|
+
verificationRetry: "待验证 · {time} 重试",
|
|
409
445
|
currentAccount: "当前账号",
|
|
446
|
+
fieldDisabled: "停用",
|
|
447
|
+
disabledCredentials: "凭据失效",
|
|
448
|
+
disabledSince: "停用于 {ago}",
|
|
449
|
+
disabledHint: "凭据已被上游拒绝。运行该账号所在行的「验证」尝试恢复;若仍失败,请重新登录导入。",
|
|
410
450
|
noProject: "—",
|
|
411
451
|
valueUnknown: "—",
|
|
412
452
|
colAccount: "账号",
|
|
413
453
|
colRequests: "请求",
|
|
454
|
+
colTime: "时间",
|
|
455
|
+
colResult: "结果",
|
|
456
|
+
colDuration: "耗时",
|
|
457
|
+
recentTitle: "最近请求",
|
|
458
|
+
recentEmpty: "暂无最近记录",
|
|
459
|
+
recentOk: "成功",
|
|
460
|
+
rowRequestsTotal: "累计请求 {n}",
|
|
461
|
+
lastActive: "{ago}活跃",
|
|
462
|
+
liveOne: "正在通过 {email} 生成 · {count} 个并发",
|
|
463
|
+
liveMany: "正在生成 · {count} 个并发 · {accounts} 个账号",
|
|
464
|
+
fieldThroughput: "吞吐",
|
|
465
|
+
throughputValue: "≈ {n} token/s",
|
|
466
|
+
throughputNote: "首 token 后 · 累计平均",
|
|
414
467
|
colActions: "操作",
|
|
415
468
|
emptyAccounts: "还没有账号。切到「凭据」标签导入,或点击上方「登录」。",
|
|
416
469
|
detailTitle: "已选账号",
|
|
@@ -420,6 +473,7 @@ window.__ModuleLoader__.load({
|
|
|
420
473
|
limitsRefreshOk: "已刷新 {measured} 个账号的限额",
|
|
421
474
|
limitsRefreshFresh: "限额仍是新鲜的,无需刷新",
|
|
422
475
|
limitsRefreshFailed: "{failed} 个账号的限额刷新失败",
|
|
476
|
+
limitBurnWarn: "按此速度{value}后耗尽",
|
|
423
477
|
quotaWindow5h: "5 小时",
|
|
424
478
|
quotaWindowWeekly: "每周",
|
|
425
479
|
fieldProject: "项目",
|
|
@@ -447,13 +501,14 @@ window.__ModuleLoader__.load({
|
|
|
447
501
|
quotaResetIn: "重置 {value}",
|
|
448
502
|
relNow: "即将",
|
|
449
503
|
relJustNow: "刚刚",
|
|
504
|
+
relSeconds: "{n} 秒",
|
|
450
505
|
relAgo: "{value}前",
|
|
451
506
|
relMinutes: "{n} 分钟",
|
|
452
507
|
relHours: "{n} 小时",
|
|
453
508
|
relDays: "{n} 天",
|
|
454
509
|
relMonths: "{n} 个月",
|
|
455
510
|
relYears: "{n} 年",
|
|
456
|
-
actionActivate: "
|
|
511
|
+
actionActivate: "设为当前",
|
|
457
512
|
actionVerify: "验证",
|
|
458
513
|
actionDelete: "删除",
|
|
459
514
|
actionTest: "测试调用",
|
|
@@ -494,6 +549,8 @@ window.__ModuleLoader__.load({
|
|
|
494
549
|
fieldRateLimitRotation: "限流 / 轮换",
|
|
495
550
|
byModel: "按模型",
|
|
496
551
|
byAccount: "按账号",
|
|
552
|
+
trendTitle: "近 7 天",
|
|
553
|
+
colDay: "日期",
|
|
497
554
|
colModel: "模型",
|
|
498
555
|
colTokenShare: "Token 占比",
|
|
499
556
|
colOutput: "输出",
|
|
@@ -561,16 +618,34 @@ window.__ModuleLoader__.load({
|
|
|
561
618
|
tabModels: "Models",
|
|
562
619
|
tabUsage: "Usage",
|
|
563
620
|
tabCredentials: "Credentials",
|
|
564
|
-
stateActive: "
|
|
621
|
+
stateActive: "Ready",
|
|
565
622
|
stateCooling: "Cooling down",
|
|
566
623
|
stateVerificationRequired: "Needs verification",
|
|
567
624
|
stateDisabled: "Disabled",
|
|
568
625
|
coolingUntil: "Cooling until",
|
|
569
|
-
|
|
626
|
+
verificationRetry: "Needs verification · retries {time}",
|
|
627
|
+
currentAccount: "Current",
|
|
628
|
+
fieldDisabled: "Disabled",
|
|
629
|
+
disabledCredentials: "Credentials rejected",
|
|
630
|
+
disabledSince: "disabled {ago}",
|
|
631
|
+
disabledHint: "The credentials were rejected by upstream. Run Verify on this account's row to try restoring it; if that fails, sign in again to re-import.",
|
|
570
632
|
noProject: "—",
|
|
571
633
|
valueUnknown: "—",
|
|
572
634
|
colAccount: "Account",
|
|
573
635
|
colRequests: "Requests",
|
|
636
|
+
colTime: "Time",
|
|
637
|
+
colResult: "Result",
|
|
638
|
+
colDuration: "Duration",
|
|
639
|
+
recentTitle: "Recent requests",
|
|
640
|
+
recentEmpty: "No recent activity",
|
|
641
|
+
recentOk: "ok",
|
|
642
|
+
rowRequestsTotal: "{n} requests in total",
|
|
643
|
+
lastActive: "active {ago}",
|
|
644
|
+
liveOne: "Generating via {email} · {count} in flight",
|
|
645
|
+
liveMany: "Generating · {count} in flight across {accounts} account(s)",
|
|
646
|
+
fieldThroughput: "Throughput",
|
|
647
|
+
throughputValue: "≈ {n} token/s",
|
|
648
|
+
throughputNote: "after first token · cumulative",
|
|
574
649
|
colActions: "Actions",
|
|
575
650
|
emptyAccounts: "No accounts yet. Import one from the Credentials tab, or use Sign in above.",
|
|
576
651
|
detailTitle: "Selected account",
|
|
@@ -580,6 +655,7 @@ window.__ModuleLoader__.load({
|
|
|
580
655
|
limitsRefreshOk: "Refreshed limits for {measured} account(s)",
|
|
581
656
|
limitsRefreshFresh: "Limits are already fresh — nothing to refresh",
|
|
582
657
|
limitsRefreshFailed: "Limit refresh failed for {failed} account(s)",
|
|
658
|
+
limitBurnWarn: "at this rate, empty in {value}",
|
|
583
659
|
quotaWindow5h: "5 hours",
|
|
584
660
|
quotaWindowWeekly: "Weekly",
|
|
585
661
|
fieldProject: "Project",
|
|
@@ -607,13 +683,14 @@ window.__ModuleLoader__.load({
|
|
|
607
683
|
quotaResetIn: "resets {value}",
|
|
608
684
|
relNow: "shortly",
|
|
609
685
|
relJustNow: "just now",
|
|
686
|
+
relSeconds: "{n}s",
|
|
610
687
|
relAgo: "{value} ago",
|
|
611
688
|
relMinutes: "{n} min",
|
|
612
689
|
relHours: "{n} h",
|
|
613
690
|
relDays: "{n} d",
|
|
614
691
|
relMonths: "{n} mo",
|
|
615
692
|
relYears: "{n} y",
|
|
616
|
-
actionActivate: "
|
|
693
|
+
actionActivate: "Set as current",
|
|
617
694
|
actionVerify: "Verify",
|
|
618
695
|
actionDelete: "Delete",
|
|
619
696
|
actionTest: "Test call",
|
|
@@ -654,6 +731,8 @@ window.__ModuleLoader__.load({
|
|
|
654
731
|
fieldRateLimitRotation: "Rate limits / rotations",
|
|
655
732
|
byModel: "By model",
|
|
656
733
|
byAccount: "By account",
|
|
734
|
+
trendTitle: "Last 7 days",
|
|
735
|
+
colDay: "Day",
|
|
657
736
|
colModel: "Model",
|
|
658
737
|
colTokenShare: "Token share",
|
|
659
738
|
colOutput: "Output",
|
|
@@ -782,6 +861,14 @@ window.__ModuleLoader__.load({
|
|
|
782
861
|
* it — see `actionError` in `AgySettings`.
|
|
783
862
|
*/
|
|
784
863
|
const ACTION_MESSAGE_TTL_MS = 3500;
|
|
864
|
+
/**
|
|
865
|
+
* How often the section re-reads the pool's in-flight snapshot.
|
|
866
|
+
*
|
|
867
|
+
* The call is a pure in-memory read on the host (no token refresh, no quota
|
|
868
|
+
* probe), so a tight-ish interval is free; 3s makes the live line feel live
|
|
869
|
+
* without a visible request cost.
|
|
870
|
+
*/
|
|
871
|
+
const POOL_POLL_INTERVAL_MS = 3e3;
|
|
785
872
|
function h(tag, props, ...children) {
|
|
786
873
|
return (0, react.createElement)(tag, props ?? null, ...children);
|
|
787
874
|
}
|
|
@@ -854,6 +941,28 @@ window.__ModuleLoader__.load({
|
|
|
854
941
|
return counters.input + counters.output + counters.cacheRead + counters.cacheWrite;
|
|
855
942
|
}
|
|
856
943
|
/**
|
|
944
|
+
* Cumulative average OUTPUT rate for one scope, in tokens per second.
|
|
945
|
+
*
|
|
946
|
+
* Pure and exported for a direct unit test. The denominator is the STREAMING
|
|
947
|
+
* window (average latency minus average time-to-first-token), NOT the wall
|
|
948
|
+
* clock: on this channel the pre-first-token wait (upstream queue, routing,
|
|
949
|
+
* prompt processing, silent thinking) is ~90% of the request — measured 7.5s
|
|
950
|
+
* of an 8.3s average — so dividing by wall time understated the decode rate
|
|
951
|
+
* about 8× (40 tok/s shown where the account really streamed ~300).
|
|
952
|
+
*
|
|
953
|
+
* Per-request AVERAGES, not raw sums: a failed request carries wall time but
|
|
954
|
+
* never a first token (`latencyN` > `ttftN`), so subtracting raw sums would
|
|
955
|
+
* hand its wait to the decode window. Both averages must exist — a scope with
|
|
956
|
+
* no timed request or no reported first token gets null, hiding the row rather
|
|
957
|
+
* than showing a fake rate.
|
|
958
|
+
*/
|
|
959
|
+
function throughputTokenPerSecond(totals) {
|
|
960
|
+
if (totals.latencyN === 0 || totals.ttftN === 0 || totals.output <= 0) return null;
|
|
961
|
+
const decodeMs = totals.latencyMs / totals.latencyN - totals.ttftMs / totals.ttftN;
|
|
962
|
+
if (decodeMs <= 0) return null;
|
|
963
|
+
return Math.round(totals.output / totals.latencyN / (decodeMs / 1e3));
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
857
966
|
* The whole prompt side: everything the model read, cached or not.
|
|
858
967
|
*
|
|
859
968
|
* The LEDGER keeps `input` as the uncached portion alone, because that is DSH's
|
|
@@ -918,28 +1027,58 @@ window.__ModuleLoader__.load({
|
|
|
918
1027
|
}
|
|
919
1028
|
}
|
|
920
1029
|
/**
|
|
1030
|
+
* Humanize a burn horizon (hours until a window runs dry at the sampled rate).
|
|
1031
|
+
*
|
|
1032
|
+
* Reuses the shared `rel*` magnitudes so the phrasing matches every other
|
|
1033
|
+
* duration on the panel; under an hour falls to minutes rather than rounding
|
|
1034
|
+
* to a false zero.
|
|
1035
|
+
*/
|
|
1036
|
+
function burnHorizon(hours, t) {
|
|
1037
|
+
if (hours < 1) return t("relMinutes", { n: Math.max(1, Math.round(hours * 60)) });
|
|
1038
|
+
if (hours < 48) return t("relHours", { n: Math.round(hours) });
|
|
1039
|
+
return t("relDays", { n: Math.round(hours / 24) });
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
921
1042
|
* A wall-clock moment for a state label (a cooldown end).
|
|
922
1043
|
*
|
|
923
1044
|
* Time-of-day alone is enough while the wall is today; past midnight it must
|
|
924
1045
|
* carry the date, or a 24h quota cooldown reads as though it ends in a few
|
|
925
1046
|
* minutes. (`untilText` is the relative form, used where "how long from now" is
|
|
926
1047
|
* the question rather than "when".)
|
|
1048
|
+
*
|
|
1049
|
+
* `lang` is the UI language ('zh' | 'en'), not the browser locale: the words on
|
|
1050
|
+
* this panel follow the host's language setting, so the dates must too — a zh
|
|
1051
|
+
* panel rendering `9/24/2026` (the browser's en-US ordering) was the symptom.
|
|
1052
|
+
* `undefined` degrades to the browser default, the pre-`lang` behaviour.
|
|
927
1053
|
*/
|
|
928
|
-
function clockTime(iso) {
|
|
1054
|
+
function clockTime(iso, lang) {
|
|
929
1055
|
if (iso === null) return "—";
|
|
930
1056
|
const date = new Date(iso);
|
|
931
1057
|
if (Number.isNaN(date.getTime())) return "—";
|
|
932
1058
|
const now = /* @__PURE__ */ new Date();
|
|
933
|
-
return date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate() ? date.toLocaleTimeString(
|
|
1059
|
+
return date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate() ? date.toLocaleTimeString(lang, {
|
|
934
1060
|
hour: "2-digit",
|
|
935
1061
|
minute: "2-digit"
|
|
936
|
-
}) : date.toLocaleString(
|
|
1062
|
+
}) : date.toLocaleString(lang, {
|
|
937
1063
|
month: "short",
|
|
938
1064
|
day: "numeric",
|
|
939
1065
|
hour: "2-digit",
|
|
940
1066
|
minute: "2-digit"
|
|
941
1067
|
});
|
|
942
1068
|
}
|
|
1069
|
+
/**
|
|
1070
|
+
* A ledger day key ('YYYY-MM-DD', local by construction) as a short label.
|
|
1071
|
+
* Parsed as LOCAL date parts — a UTC parse would shift the label a day for
|
|
1072
|
+
* half the planet.
|
|
1073
|
+
*/
|
|
1074
|
+
function dayLabel(day, lang) {
|
|
1075
|
+
const [y, m, d] = day.split("-").map(Number);
|
|
1076
|
+
if (y === void 0 || m === void 0 || d === void 0 || Number.isNaN(y)) return day;
|
|
1077
|
+
return new Date(y, m - 1, d).toLocaleDateString(lang, {
|
|
1078
|
+
month: "short",
|
|
1079
|
+
day: "numeric"
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
943
1082
|
const MINUTE_MS = 6e4;
|
|
944
1083
|
const HOUR_MS = 60 * MINUTE_MS;
|
|
945
1084
|
const DAY_MS = 24 * HOUR_MS;
|
|
@@ -993,6 +1132,34 @@ window.__ModuleLoader__.load({
|
|
|
993
1132
|
return t("relAgo", { value: diff < HOUR_MS ? t("relMinutes", { n: Math.floor(diff / MINUTE_MS) }) : diff < DAY_MS ? t("relHours", { n: Math.floor(diff / HOUR_MS) }) : diff < 30 * DAY_MS ? t("relDays", { n: Math.floor(diff / DAY_MS) }) : diff < 365 * DAY_MS ? t("relMonths", { n: Math.floor(diff / (30 * DAY_MS)) }) : t("relYears", { n: Math.floor(diff / (365 * DAY_MS)) }) });
|
|
994
1133
|
}
|
|
995
1134
|
/**
|
|
1135
|
+
* Ago label with seconds resolution, for the recent list. `agoText` collapses
|
|
1136
|
+
* the whole first minute into "just now", which is too coarse when the reader
|
|
1137
|
+
* is watching live activity — here the seconds carry the information.
|
|
1138
|
+
*/
|
|
1139
|
+
function recentAgo(at, now, t) {
|
|
1140
|
+
const diff = now - at;
|
|
1141
|
+
if (diff < 1e4) return t("relJustNow");
|
|
1142
|
+
if (diff < MINUTE_MS) return t("relSeconds", { n: Math.floor(diff / 1e3) });
|
|
1143
|
+
return agoText(new Date(at).toISOString(), t, now);
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Middle-truncate an identity for the recent list.
|
|
1147
|
+
*
|
|
1148
|
+
* Both ends carry the signal — an email's domain, a model id's tier suffix —
|
|
1149
|
+
* so the cut is taken from the MIDDLE, and bounding the rendered length (not
|
|
1150
|
+
* relying on CSS clipping of a `table-layout: fixed` cell) is what keeps the
|
|
1151
|
+
* columns honest. The full value stays on the cell's title.
|
|
1152
|
+
* @param text - the full identity.
|
|
1153
|
+
* @param max - rendered character budget including the ellipsis.
|
|
1154
|
+
* @returns the truncated display string.
|
|
1155
|
+
*/
|
|
1156
|
+
function truncateIdentity(text, max = 22) {
|
|
1157
|
+
if (text.length <= max) return text;
|
|
1158
|
+
const head = Math.ceil((max - 1) / 2);
|
|
1159
|
+
const tail = max - 1 - head;
|
|
1160
|
+
return `${text.slice(0, head)}…${text.slice(-tail)}`;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
996
1163
|
* A host-styled button.
|
|
997
1164
|
*
|
|
998
1165
|
* Delegates to the host's `Button` so focus rings, disabled states, size tiers
|
|
@@ -1061,36 +1228,44 @@ window.__ModuleLoader__.load({
|
|
|
1061
1228
|
const [proxyDraft, setProxyDraft] = (0, react.useState)("");
|
|
1062
1229
|
const usage = account.usage;
|
|
1063
1230
|
const now = Date.now();
|
|
1231
|
+
const throughput = usage === null ? null : throughputTokenPerSecond(usage.totals);
|
|
1064
1232
|
const identityRows = [
|
|
1065
1233
|
[t("fieldProject"), account.projectId ?? t("noProject")],
|
|
1066
1234
|
[t("fieldProxy"), h("span", { className: "agy-mono" }, account.proxy ?? t("proxyDirect"))],
|
|
1067
1235
|
[t("fieldFingerprint"), account.fingerprint === null ? t("fingerprintNone") : t("fingerprintRegenerated", {
|
|
1068
1236
|
count: account.fingerprintHistory,
|
|
1069
|
-
date: new Date(account.fingerprint.createdAt).toLocaleDateString()
|
|
1237
|
+
date: new Date(account.fingerprint.createdAt).toLocaleDateString(props.lang)
|
|
1070
1238
|
})],
|
|
1071
|
-
|
|
1239
|
+
...account.cooldownReason === null ? [] : [[t("fieldCooldownReason"), `${cooldownReasonLabel(account.cooldownReason, t)} · ${agoText(account.cooldownSetAt, t, now)}`]],
|
|
1072
1240
|
[t("fieldSources"), usage === null ? t("noProject") : t("sourcesSummary", {
|
|
1073
1241
|
chat: usage.sources.chat,
|
|
1074
1242
|
cli: usage.sources.cli,
|
|
1075
1243
|
verify: usage.sources.verify,
|
|
1076
1244
|
test: usage.sources.test
|
|
1077
1245
|
})],
|
|
1078
|
-
[t("fieldLatency"), usage === null ? t("noProject") : `${t("latencyAverage", { value: formatDuration(average(usage.totals.latencyMs, usage.totals.latencyN)) })} · ${t("latencyTtft", { value: formatDuration(average(usage.totals.ttftMs, usage.totals.ttftN)) })}`]
|
|
1246
|
+
[t("fieldLatency"), usage === null ? t("noProject") : `${t("latencyAverage", { value: formatDuration(average(usage.totals.latencyMs, usage.totals.latencyN)) })} · ${t("latencyTtft", { value: formatDuration(average(usage.totals.ttftMs, usage.totals.ttftN)) })}`],
|
|
1247
|
+
...throughput === null ? [] : [[t("fieldThroughput"), `${t("throughputValue", { n: throughput })} · ${t("throughputNote")}`]]
|
|
1079
1248
|
];
|
|
1249
|
+
if (account.state === "disabled") identityRows.push([t("fieldDisabled"), account.disabledAt === null ? t("disabledCredentials") : `${t("disabledCredentials")} · ${t("disabledSince", { ago: agoText(account.disabledAt, t, now) })}`]);
|
|
1080
1250
|
if (account.verificationRequired) identityRows.push([t("fieldVerification"), account.verificationUrl === null ? t("verificationNoUrl") : h("a", {
|
|
1081
1251
|
className: "agy-link",
|
|
1082
1252
|
href: account.verificationUrl,
|
|
1083
1253
|
target: "_blank",
|
|
1084
1254
|
rel: "noreferrer noopener"
|
|
1085
1255
|
}, t("verificationOpen"))]);
|
|
1086
|
-
const identity = card(t("detailTitle"), defs(identityRows), account.email ?? `#${account.index}`);
|
|
1256
|
+
const identity = card(t("detailTitle"), h("div", null, defs(identityRows), account.state === "disabled" ? hint(t("disabledHint")) : null), account.email ?? `#${account.index}`);
|
|
1087
1257
|
const actions = card(t("colActions"), h("div", { className: "agy-actions" }, button(t("actionTest"), () => {
|
|
1088
1258
|
handlers.onTest(account.index);
|
|
1089
1259
|
}, { disabled: busy }), button(t("actionExport"), () => {
|
|
1090
1260
|
handlers.onExport(account.index);
|
|
1091
1261
|
}, { disabled: busy }), button(t("actionRegenerateFingerprint"), () => {
|
|
1092
1262
|
handlers.onRegenerateFingerprint(account.index);
|
|
1093
|
-
}, { disabled: busy })))
|
|
1263
|
+
}, { disabled: busy }), button(t("actionDelete"), () => {
|
|
1264
|
+
handlers.onDelete(account.index);
|
|
1265
|
+
}, {
|
|
1266
|
+
variant: "danger",
|
|
1267
|
+
disabled: busy
|
|
1268
|
+
})));
|
|
1094
1269
|
/**
|
|
1095
1270
|
* The 5-hour / weekly windows, placed ABOVE the cumulative usage card.
|
|
1096
1271
|
*
|
|
@@ -1111,13 +1286,14 @@ window.__ModuleLoader__.load({
|
|
|
1111
1286
|
key: group.name
|
|
1112
1287
|
}, h("div", { className: "agy-limit-group-name" }, group.name), ...group.windows.map((window) => {
|
|
1113
1288
|
const fraction = window.remainingFraction;
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1289
|
+
const burn = account.limitBurn?.[window.bucketId];
|
|
1290
|
+
const hoursLeft = fraction !== null && burn !== void 0 && burn > 0 ? fraction / burn : null;
|
|
1291
|
+
const resetHours = window.resetTime === null ? null : (new Date(window.resetTime).getTime() - now) / HOUR_MS;
|
|
1292
|
+
const exhaustsFirst = hoursLeft !== null && resetHours !== null && hoursLeft < resetHours;
|
|
1293
|
+
return h("div", { key: window.bucketId }, h("div", { className: "agy-limit-row" }, h("span", { className: "agy-limit-k" }, windowLabel(window.window, t)), h("span", { className: "agy-limit-track" }, fraction === null ? null : h("i", { style: {
|
|
1118
1294
|
width: `${Math.round(fraction * 100)}%`,
|
|
1119
1295
|
background: quotaColor(fraction)
|
|
1120
|
-
} })), h("span", { className: "agy-limit-p" }, fraction === null ? t("valueUnknown") : `${Math.round(fraction * 100)}%`), h("span", { className: "agy-limit-reset" }, window.resetTime === null ? null : untilText(window.resetTime, t, now)));
|
|
1296
|
+
} })), h("span", { className: "agy-limit-p" }, fraction === null ? t("valueUnknown") : `${Math.round(fraction * 100)}%`), h("span", { className: "agy-limit-reset" }, window.resetTime === null ? null : untilText(window.resetTime, t, now))), exhaustsFirst ? h("div", { className: "agy-limit-burn" }, t("limitBurnWarn", { value: burnHorizon(hoursLeft, t) })) : null);
|
|
1121
1297
|
})))));
|
|
1122
1298
|
const usageBlock = usage === null ? null : card(t("usageCumulative"), metrics([
|
|
1123
1299
|
metric(t("kpiInput"), promptTokens(usage.totals), t("kpiInputMissed", { tokens: tokenText(usage.totals.input) })),
|
|
@@ -1154,6 +1330,19 @@ window.__ModuleLoader__.load({
|
|
|
1154
1330
|
})())));
|
|
1155
1331
|
}
|
|
1156
1332
|
/**
|
|
1333
|
+
* Whether the row should offer "set as current".
|
|
1334
|
+
*
|
|
1335
|
+
* A pure rule, exported for a direct unit test. Two cases hide the action:
|
|
1336
|
+
* the account is already the pool's preference, or it is DISABLED — `activate`
|
|
1337
|
+
* only writes the preference and cannot re-enable, so on a disabled row the
|
|
1338
|
+
* button was a no-op the user would read as broken. The repair path for a
|
|
1339
|
+
* button was a no-op the user would read as broken. The repair path for a
|
|
1340
|
+
* disabled account is `actionVerify`, which stays on the row.
|
|
1341
|
+
*/
|
|
1342
|
+
function canActivateAccount(account) {
|
|
1343
|
+
return !account.active && account.state !== "disabled";
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1157
1346
|
* Pick the selected account index: defaults to the active account (badge "current")
|
|
1158
1347
|
* when present, otherwise falls back to index 0. Clamps to valid bounds.
|
|
1159
1348
|
*/
|
|
@@ -1171,6 +1360,29 @@ window.__ModuleLoader__.load({
|
|
|
1171
1360
|
const { accounts, busy, handlers, t } = props;
|
|
1172
1361
|
const [selected, setSelected] = (0, react.useState)(null);
|
|
1173
1362
|
const selectedRef = (0, react.useRef)(null);
|
|
1363
|
+
const now = Date.now();
|
|
1364
|
+
/**
|
|
1365
|
+
* The live line: who is serving right now, and how much.
|
|
1366
|
+
*
|
|
1367
|
+
* Present ONLY while something is in flight — an idle pool renders no strip,
|
|
1368
|
+
* so the quiet state stays quiet. The dot is the host `StateDot`'s ongoing
|
|
1369
|
+
* state, so the animation is the platform's.
|
|
1370
|
+
*/
|
|
1371
|
+
const liveLine = props.busyNow.length === 0 ? null : (() => {
|
|
1372
|
+
const total = props.busyNow.reduce((sum, entry) => sum + entry.count, 0);
|
|
1373
|
+
const [first] = props.busyNow;
|
|
1374
|
+
const subject = first === void 0 ? "" : first.email ?? `#${first.index}`;
|
|
1375
|
+
return h("div", { className: "agy-live" }, h(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
|
|
1376
|
+
state: "ongoing",
|
|
1377
|
+
size: 8
|
|
1378
|
+
}), h("span", null, props.busyNow.length === 1 ? t("liveOne", {
|
|
1379
|
+
email: subject,
|
|
1380
|
+
count: total
|
|
1381
|
+
}) : t("liveMany", {
|
|
1382
|
+
count: total,
|
|
1383
|
+
accounts: props.busyNow.length
|
|
1384
|
+
})));
|
|
1385
|
+
})();
|
|
1174
1386
|
const index = resolveSelectedAccountIndex(accounts, selected);
|
|
1175
1387
|
const current = accounts[index];
|
|
1176
1388
|
(0, react.useEffect)(() => {
|
|
@@ -1194,32 +1406,35 @@ window.__ModuleLoader__.load({
|
|
|
1194
1406
|
event.preventDefault();
|
|
1195
1407
|
setSelected(at);
|
|
1196
1408
|
}
|
|
1197
|
-
}, h("div", { className: "agy-rowmain" }, h("div", { className: "agy-rowtitle" }, h("span", { className: "agy-rowname" }, account.email ?? `#${account.index}`), account.active ? h(_deepseek_ai_dsh_client_ui_primitives.Tag, { tone: "info" }, t("currentAccount")) : null), h("div", { className: "agy-rowmeta" }, account.projectId ?? t("noProject"), account.usage === null || account.usage.totals.requests === 0 ? null : ` · ${t("
|
|
1409
|
+
}, h("div", { className: "agy-rowmain" }, h("div", { className: "agy-rowtitle" }, h("span", { className: "agy-rowname" }, account.email ?? `#${account.index}`), account.active ? h(_deepseek_ai_dsh_client_ui_primitives.Tag, { tone: "info" }, t("currentAccount")) : null), h("div", { className: "agy-rowmeta" }, account.projectId ?? t("noProject"), account.usage === null || account.usage.totals.requests === 0 ? null : ` · ${t("rowRequestsTotal", { n: account.usage.totals.requests })}`, account.usage === null || account.usage.lastUsedAt === null ? null : ` · ${t("lastActive", { ago: agoText(new Date(account.usage.lastUsedAt).toISOString(), t, now) })}`)), h("div", { className: "agy-rowactions" }, stateBadge(account.state, account.state === "cooling" ? `${t("coolingUntil")} ${clockTime(account.cooldownUntil, props.lang)}` : account.state === "verification-required" ? t("verificationRetry", { time: clockTime(account.cooldownUntil, props.lang) }) : stateLabel(account.state, t)), canActivateAccount(account) ? button(t("actionActivate"), () => {
|
|
1198
1410
|
setSelected(at);
|
|
1199
1411
|
handlers.onActivate(account.index);
|
|
1200
1412
|
}, {
|
|
1201
1413
|
size: "sm",
|
|
1202
1414
|
disabled: busy
|
|
1203
|
-
}), button(t("actionVerify"), () => {
|
|
1415
|
+
}) : null, button(t("actionVerify"), () => {
|
|
1204
1416
|
handlers.onVerify(account.index);
|
|
1205
1417
|
}, {
|
|
1206
1418
|
size: "sm",
|
|
1207
1419
|
disabled: busy
|
|
1208
|
-
}), button(t("actionDelete"), () => {
|
|
1209
|
-
setSelected(null);
|
|
1210
|
-
handlers.onDelete(account.index);
|
|
1211
|
-
}, {
|
|
1212
|
-
size: "sm",
|
|
1213
|
-
variant: "danger",
|
|
1214
|
-
disabled: busy
|
|
1215
1420
|
}))));
|
|
1216
|
-
return h("div", { className: "agy-root" }, h("div", { className: "agy-split-wrap" }, h("div", { className: "agy-split" }, card(t("colAccount"), h("div", { className: "agy-rows" }, ...rows), `${accounts.length}`), current === void 0 ? null : h(AccountDetail, {
|
|
1421
|
+
return h("div", { className: "agy-root" }, h("div", { className: "agy-split-wrap" }, h("div", { className: "agy-split" }, card(t("colAccount"), h("div", null, liveLine, h("div", { className: "agy-rows" }, ...rows)), `${accounts.length}`), current === void 0 ? null : h(AccountDetail, {
|
|
1217
1422
|
key: String(current.index),
|
|
1218
1423
|
account: current,
|
|
1219
1424
|
busy,
|
|
1220
|
-
handlers
|
|
1425
|
+
handlers: {
|
|
1426
|
+
...handlers,
|
|
1427
|
+
onDelete: (index) => {
|
|
1428
|
+
setSelected(null);
|
|
1429
|
+
handlers.onDelete(index);
|
|
1430
|
+
}
|
|
1431
|
+
},
|
|
1432
|
+
lang: props.lang,
|
|
1221
1433
|
t
|
|
1222
|
-
}))
|
|
1434
|
+
})), h(RecentCard, {
|
|
1435
|
+
rpc: props.rpc,
|
|
1436
|
+
t
|
|
1437
|
+
})));
|
|
1223
1438
|
}
|
|
1224
1439
|
/**
|
|
1225
1440
|
* Models tab: per-model visibility switches, plus the account's model quota in a
|
|
@@ -1577,6 +1792,110 @@ window.__ModuleLoader__.load({
|
|
|
1577
1792
|
}))) : null);
|
|
1578
1793
|
return card(t("thinkingTitle"), block);
|
|
1579
1794
|
}
|
|
1795
|
+
/** Result vocabulary for one recent row; rotation events are their own kind. */
|
|
1796
|
+
function recentResultKind(entry) {
|
|
1797
|
+
if (entry.kind === "rotation") return "rotation";
|
|
1798
|
+
if (entry.rateLimited) return "limited";
|
|
1799
|
+
if (!entry.ok) return "fail";
|
|
1800
|
+
return "ok";
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Localized label for a failure-classification token, falling back to the raw
|
|
1804
|
+
* token so a classification added upstream still reads as something.
|
|
1805
|
+
*/
|
|
1806
|
+
function failureReasonLabel(reason, t) {
|
|
1807
|
+
switch (reason) {
|
|
1808
|
+
case "rate-limit": return t("colRateLimited");
|
|
1809
|
+
case "network-error": return t("cooldownReasonNetworkError");
|
|
1810
|
+
case "auth-failure": return t("disabledCredentials");
|
|
1811
|
+
case "verification-required": return t("cooldownReasonValidationRequired");
|
|
1812
|
+
case "quota-exhausted": return t("cooldownReasonQuotaExhausted");
|
|
1813
|
+
case "project-error": return t("cooldownReasonProjectError");
|
|
1814
|
+
default: return reason;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* The result cell's text. Rotation rows say WHY (`reason` rides the record
|
|
1819
|
+
* since the ring captures the classification); a failed request with a
|
|
1820
|
+
* non-rate-limit classification also names it — the rate-limit case is already
|
|
1821
|
+
* the whole RateLimited label, and doubling it reads as a stutter.
|
|
1822
|
+
*/
|
|
1823
|
+
function recentResultText(entry, t) {
|
|
1824
|
+
const kind = recentResultKind(entry);
|
|
1825
|
+
if (kind === "rotation") return entry.reason === null ? t("colRotations") : `${t("colRotations")} · ${failureReasonLabel(entry.reason, t)}`;
|
|
1826
|
+
if (kind === "limited") return t("colRateLimited");
|
|
1827
|
+
if (kind === "fail") return entry.reason === null || entry.reason === "rate-limit" ? t("colFailed") : `${t("colFailed")} · ${failureReasonLabel(entry.reason, t)}`;
|
|
1828
|
+
return t("recentOk");
|
|
1829
|
+
}
|
|
1830
|
+
/**
|
|
1831
|
+
* The "what just happened" list: the most recent records this process saw,
|
|
1832
|
+
* newest first, collapsed by default and polled while open.
|
|
1833
|
+
*
|
|
1834
|
+
* A component (never called directly) — it owns hooks; see ThinkingSamples for
|
|
1835
|
+
* the hook-order rule this file has been bitten by twice. The poll is the same
|
|
1836
|
+
* 3s cadence as the live line and reads the host's memory ring, so an open
|
|
1837
|
+
* list costs nothing upstream.
|
|
1838
|
+
*/
|
|
1839
|
+
function RecentCard(props) {
|
|
1840
|
+
const { rpc, t } = props;
|
|
1841
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1842
|
+
const [recent, setRecent] = (0, react.useState)(null);
|
|
1843
|
+
const [error, setError] = (0, react.useState)(void 0);
|
|
1844
|
+
const alive = (0, react.useRef)(true);
|
|
1845
|
+
(0, react.useEffect)(() => {
|
|
1846
|
+
alive.current = true;
|
|
1847
|
+
return () => {
|
|
1848
|
+
alive.current = false;
|
|
1849
|
+
};
|
|
1850
|
+
}, []);
|
|
1851
|
+
const load = (0, react.useCallback)(() => {
|
|
1852
|
+
rpc.call("pool.recent", {}).then((result) => {
|
|
1853
|
+
if (alive.current) {
|
|
1854
|
+
setRecent(result.recent);
|
|
1855
|
+
setError(void 0);
|
|
1856
|
+
}
|
|
1857
|
+
}).catch((caught) => {
|
|
1858
|
+
if (alive.current) setError(caught instanceof Error ? caught.message : String(caught));
|
|
1859
|
+
});
|
|
1860
|
+
}, [rpc]);
|
|
1861
|
+
(0, react.useEffect)(() => {
|
|
1862
|
+
if (open && recent === null) load();
|
|
1863
|
+
}, [
|
|
1864
|
+
open,
|
|
1865
|
+
recent,
|
|
1866
|
+
load
|
|
1867
|
+
]);
|
|
1868
|
+
(0, react.useEffect)(() => {
|
|
1869
|
+
if (!open) return;
|
|
1870
|
+
const timer = setInterval(() => {
|
|
1871
|
+
if (!document.hidden) load();
|
|
1872
|
+
}, POOL_POLL_INTERVAL_MS);
|
|
1873
|
+
return () => {
|
|
1874
|
+
clearInterval(timer);
|
|
1875
|
+
};
|
|
1876
|
+
}, [open, load]);
|
|
1877
|
+
const now = Date.now();
|
|
1878
|
+
return h("div", {
|
|
1879
|
+
className: "agy-disclosure agy-recent",
|
|
1880
|
+
"data-open": open
|
|
1881
|
+
}, h("button", {
|
|
1882
|
+
type: "button",
|
|
1883
|
+
className: "agy-disclosure-toggle",
|
|
1884
|
+
"aria-expanded": open,
|
|
1885
|
+
onClick: () => {
|
|
1886
|
+
setOpen(!open);
|
|
1887
|
+
}
|
|
1888
|
+
}, h("span", { className: "agy-caret" }), h("span", null, t("recentTitle")), recent === null ? null : h("span", { className: "agy-disclosure-meta" }, String(recent.length))), open === false ? null : h("div", { className: "agy-disclosure-body" }, error === void 0 ? null : h("div", { className: "agy-error" }, error), recent === null ? h("div", { className: "agy-empty" }, t("loading")) : recent.length === 0 ? h("div", { className: "agy-empty" }, t("recentEmpty")) : h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", { style: { width: "64px" } }, t("colTime")), h("th", { style: { width: "30%" } }, t("colAccount")), h("th", { style: { width: "24%" } }, t("colModel")), h("th", { style: { width: "96px" } }, t("colResult")), h("th", { style: { width: "56px" } }, t("colDuration")), h("th", { style: { width: "48px" } }, t("colOutput"))), recent.map((entry, index) => h("tr", { key: `${entry.at}-${index}` }, h("td", null, recentAgo(entry.at, now, t)), h("td", null, h("span", {
|
|
1889
|
+
className: "agy-mail",
|
|
1890
|
+
title: entry.account ?? void 0
|
|
1891
|
+
}, entry.account === null ? "—" : truncateIdentity(entry.account))), h("td", null, h("span", {
|
|
1892
|
+
className: "agy-mail",
|
|
1893
|
+
title: entry.model ?? void 0
|
|
1894
|
+
}, entry.model === null ? "—" : truncateIdentity(entry.model))), h("td", null, h("span", {
|
|
1895
|
+
className: "agy-recent-state",
|
|
1896
|
+
"data-kind": recentResultKind(entry)
|
|
1897
|
+
}, recentResultText(entry, t))), h("td", null, entry.latencyMs === null ? "—" : formatDuration(entry.latencyMs)), h("td", { className: "agy-num" }, entry.output === null ? "—" : tokenText(entry.output))))))));
|
|
1898
|
+
}
|
|
1580
1899
|
const RANGE_IDS = [
|
|
1581
1900
|
"today",
|
|
1582
1901
|
"week",
|
|
@@ -1688,7 +2007,7 @@ window.__ModuleLoader__.load({
|
|
|
1688
2007
|
onClick: () => {
|
|
1689
2008
|
setRange(id);
|
|
1690
2009
|
}
|
|
1691
|
-
}, rangeLabel(id, t)))), h("span", { className: "agy-grow" }), stats.since === null ? null : h("span", { className: "agy-aside" }, t("since", { date: new Date(stats.since).toLocaleDateString() }))), card(t("usageTitle"), h("div", null, metrics([
|
|
2010
|
+
}, rangeLabel(id, t)))), h("span", { className: "agy-grow" }), stats.since === null ? null : h("span", { className: "agy-aside" }, t("since", { date: new Date(stats.since).toLocaleDateString(props.lang) }))), card(t("usageTitle"), h("div", null, metrics([
|
|
1692
2011
|
metric(t("kpiTotalTokens"), total, t("kpiTotalDetail", {
|
|
1693
2012
|
requests: counters.requests,
|
|
1694
2013
|
failed: counters.failed
|
|
@@ -1701,7 +2020,7 @@ window.__ModuleLoader__.load({
|
|
|
1701
2020
|
[t("fieldRateLimitRotation"), `${counters.rateLimited} / ${counters.rotations}`],
|
|
1702
2021
|
[t("labelLatencyAverage"), formatDuration(average(counters.latencyMs, counters.latencyN))],
|
|
1703
2022
|
[t("labelTtft"), formatDuration(average(counters.ttftMs, counters.ttftN))]
|
|
1704
|
-
])), view.models.length === 0 ? null : card(t("byModel"), h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", null, t("colModel")), numHeader(0, t("colRequests")), numHeader(1, t("kpiInput")), numHeader(2, t("kpiCacheRead")), numHeader(3, t("colOutput")), numHeader(4, t("colTokenShare"))), view.models.map((row) => h("tr", { key: row.model }, h("td", { className: "agy-strong" }, h("span", { className: "agy-mail" }, row.model)), h("td", { className: "agy-num" }, String(row.counters.requests)), h("td", { className: "agy-num" }, tokenText(promptTokens(row.counters))), h("td", { className: "agy-num" }, tokenText(row.counters.cacheRead)), h("td", { className: "agy-num" }, tokenText(row.counters.output)), h("td", { className: "agy-num" }, h("span", { className: "agy-bar" }, h("span", { className: "agy-track" }, h("i", { style: { width: `${Math.round(totalTokens(row.counters) / Math.max(1, total) * 100)}%` } }))))))))), view.accounts.length === 0 ? null : card(t("byAccount"), h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", null, t("colAccount")), numHeader(0, t("colRequests")), numHeader(1, t("colToken")), numHeader(2, t("colFailed")), numHeader(3, t("colRateLimited")), numHeader(4, t("colRotations"))), view.accounts.map((row) => h("tr", { key: row.account }, h("td", { className: "agy-strong" }, h("span", { className: "agy-mail" }, row.account)), h("td", { className: "agy-num" }, String(row.counters.requests)), h("td", { className: "agy-num" }, tokenText(totalTokens(row.counters))), h("td", { className: "agy-num" }, String(row.counters.failed)), h("td", { className: "agy-num" }, String(row.counters.rateLimited)), h("td", { className: "agy-num" }, String(row.counters.rotations))))))));
|
|
2023
|
+
])), card(t("trendTitle"), h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", null, t("colDay")), numHeader(0, t("colRequests")), numHeader(1, t("colFailed")), numHeader(2, t("colRateLimited")), numHeader(3, t("colRotations"))), stats.days.map((row) => h("tr", { key: row.day }, h("td", null, dayLabel(row.day, props.lang)), h("td", { className: "agy-num" }, String(row.requests)), h("td", { className: "agy-num" }, String(row.failed)), h("td", { className: "agy-num" }, String(row.rateLimited)), h("td", { className: "agy-num" }, String(row.rotations))))))), view.models.length === 0 ? null : card(t("byModel"), h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", null, t("colModel")), numHeader(0, t("colRequests")), numHeader(1, t("kpiInput")), numHeader(2, t("kpiCacheRead")), numHeader(3, t("colOutput")), numHeader(4, t("colTokenShare"))), view.models.map((row) => h("tr", { key: row.model }, h("td", { className: "agy-strong" }, h("span", { className: "agy-mail" }, row.model)), h("td", { className: "agy-num" }, String(row.counters.requests)), h("td", { className: "agy-num" }, tokenText(promptTokens(row.counters))), h("td", { className: "agy-num" }, tokenText(row.counters.cacheRead)), h("td", { className: "agy-num" }, tokenText(row.counters.output)), h("td", { className: "agy-num" }, h("span", { className: "agy-bar" }, h("span", { className: "agy-track" }, h("i", { style: { width: `${Math.round(totalTokens(row.counters) / Math.max(1, total) * 100)}%` } }))))))))), view.accounts.length === 0 ? null : card(t("byAccount"), h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", null, t("colAccount")), numHeader(0, t("colRequests")), numHeader(1, t("colToken")), numHeader(2, t("colFailed")), numHeader(3, t("colRateLimited")), numHeader(4, t("colRotations"))), view.accounts.map((row) => h("tr", { key: row.account }, h("td", { className: "agy-strong" }, h("span", { className: "agy-mail" }, row.account)), h("td", { className: "agy-num" }, String(row.counters.requests)), h("td", { className: "agy-num" }, tokenText(totalTokens(row.counters))), h("td", { className: "agy-num" }, String(row.counters.failed)), h("td", { className: "agy-num" }, String(row.counters.rateLimited)), h("td", { className: "agy-num" }, String(row.counters.rotations))))))));
|
|
1705
2024
|
}
|
|
1706
2025
|
function CredentialsTab(props) {
|
|
1707
2026
|
const { t } = props;
|
|
@@ -1741,6 +2060,15 @@ window.__ModuleLoader__.load({
|
|
|
1741
2060
|
/** Model ids whose own test call is in flight (see the per-row test button). */
|
|
1742
2061
|
const [modelTesting, setModelTesting] = (0, react.useState)(() => /* @__PURE__ */ new Set());
|
|
1743
2062
|
const [stats, setStats] = (0, react.useState)(null);
|
|
2063
|
+
/**
|
|
2064
|
+
* Accounts with upstream requests in flight, from the polled `pool.status`.
|
|
2065
|
+
*
|
|
2066
|
+
* A poll, not a push: the section has no push channel, and the call is a pure
|
|
2067
|
+
* in-memory read on the host. A failed poll is display-only and stays silent —
|
|
2068
|
+
* the previous frame (including "idle") holds until the next tick, which is
|
|
2069
|
+
* the honest degradation for a status line.
|
|
2070
|
+
*/
|
|
2071
|
+
const [poolBusy, setPoolBusy] = (0, react.useState)([]);
|
|
1744
2072
|
const [error, setError] = (0, react.useState)(void 0);
|
|
1745
2073
|
/** A non-fatal outcome worth reporting (e.g. a partial credential import). */
|
|
1746
2074
|
const [notice, setNoticeState] = (0, react.useState)(void 0);
|
|
@@ -1842,7 +2170,8 @@ window.__ModuleLoader__.load({
|
|
|
1842
2170
|
return {
|
|
1843
2171
|
...account,
|
|
1844
2172
|
limits: entry.groups,
|
|
1845
|
-
limitsUpdatedAt: entry.updatedAt
|
|
2173
|
+
limitsUpdatedAt: entry.updatedAt,
|
|
2174
|
+
limitBurn: entry.burn ?? account.limitBurn
|
|
1846
2175
|
};
|
|
1847
2176
|
}));
|
|
1848
2177
|
if (report) {
|
|
@@ -1862,6 +2191,19 @@ window.__ModuleLoader__.load({
|
|
|
1862
2191
|
(0, react.useEffect)(() => {
|
|
1863
2192
|
refresh();
|
|
1864
2193
|
}, [refresh]);
|
|
2194
|
+
(0, react.useEffect)(() => {
|
|
2195
|
+
const tick = () => {
|
|
2196
|
+
if (document.hidden) return;
|
|
2197
|
+
rpc.call("pool.status", {}).then((result) => {
|
|
2198
|
+
if (alive.current) setPoolBusy(result.busy);
|
|
2199
|
+
}).catch(() => {});
|
|
2200
|
+
};
|
|
2201
|
+
tick();
|
|
2202
|
+
const timer = setInterval(tick, POOL_POLL_INTERVAL_MS);
|
|
2203
|
+
return () => {
|
|
2204
|
+
clearInterval(timer);
|
|
2205
|
+
};
|
|
2206
|
+
}, [rpc]);
|
|
1865
2207
|
(0, react.useEffect)(() => {
|
|
1866
2208
|
if (tab === "accounts" && accounts.length > 0) loadLimits();
|
|
1867
2209
|
}, [
|
|
@@ -2075,7 +2417,10 @@ window.__ModuleLoader__.load({
|
|
|
2075
2417
|
const body = tab === "accounts" ? h(AccountsTab, {
|
|
2076
2418
|
accounts,
|
|
2077
2419
|
busy,
|
|
2420
|
+
busyNow: poolBusy,
|
|
2078
2421
|
handlers,
|
|
2422
|
+
lang: props.lang,
|
|
2423
|
+
rpc,
|
|
2079
2424
|
t
|
|
2080
2425
|
}) : tab === "models" ? modelError === void 0 ? h(ModelsTab, {
|
|
2081
2426
|
models,
|
|
@@ -2152,6 +2497,7 @@ window.__ModuleLoader__.load({
|
|
|
2152
2497
|
loadModels();
|
|
2153
2498
|
}, { size: "sm" })) : tab === "usage" ? h(UsageTab, {
|
|
2154
2499
|
stats,
|
|
2500
|
+
lang: props.lang,
|
|
2155
2501
|
t
|
|
2156
2502
|
}) : h(CredentialsTab, {
|
|
2157
2503
|
busy,
|
|
@@ -2205,6 +2551,7 @@ window.__ModuleLoader__.load({
|
|
|
2205
2551
|
}
|
|
2206
2552
|
const rpc = createRpc(connection);
|
|
2207
2553
|
const t = ctx.locale.bind(NS);
|
|
2554
|
+
const lang = ctx.locale.getLocale?.().active;
|
|
2208
2555
|
ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
2209
2556
|
name: "settings.section",
|
|
2210
2557
|
id: "agy",
|
|
@@ -2213,16 +2560,20 @@ window.__ModuleLoader__.load({
|
|
|
2213
2560
|
label: () => t("title")
|
|
2214
2561
|
}, () => h(AgySettings, {
|
|
2215
2562
|
rpc,
|
|
2216
|
-
t
|
|
2563
|
+
t,
|
|
2564
|
+
lang
|
|
2217
2565
|
}))), "dsh-agy: Settings section");
|
|
2218
2566
|
}
|
|
2219
2567
|
//#endregion
|
|
2220
2568
|
exports.AgySettings = AgySettings;
|
|
2221
2569
|
exports.apply = apply;
|
|
2570
|
+
exports.canActivateAccount = canActivateAccount;
|
|
2222
2571
|
exports.inject = inject;
|
|
2223
2572
|
exports.orderModels = orderModels;
|
|
2224
2573
|
exports.resolveSelectedAccountIndex = resolveSelectedAccountIndex;
|
|
2574
|
+
exports.throughputTokenPerSecond = throughputTokenPerSecond;
|
|
2225
2575
|
exports.tokenText = tokenText;
|
|
2576
|
+
exports.truncateIdentity = truncateIdentity;
|
|
2226
2577
|
return module.exports;
|
|
2227
2578
|
}
|
|
2228
2579
|
});
|