dsh-tokenlens 1.0.9 → 1.2.0
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/CHANGELOG.md +15 -0
- package/PROTOCOL.md +28 -3
- package/README.en.md +8 -6
- package/README.md +14 -7
- package/client.js +523 -115
- package/index.js +44 -1
- package/lib/providers.mjs +164 -4
- package/lib/usage-jsonl.mjs +9 -6
- package/lib/usage-opencode.mjs +10 -4
- package/lib/usage-stats.mjs +108 -25
- package/lib/usage-zcode.mjs +23 -8
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -208,6 +208,12 @@ window.__ModuleLoader__.load({
|
|
|
208
208
|
statsChartPeak: '峰值 {value}({day})',
|
|
209
209
|
statsChartBarTitle: '{date}: {tokens}',
|
|
210
210
|
statsChartReadout: '{date} · {value}({share}%)',
|
|
211
|
+
statsChartBack: '← 返回日趋势',
|
|
212
|
+
statsChartBackHint: '返回每日趋势图',
|
|
213
|
+
statsChartHourTitle: '按小时 · {date}',
|
|
214
|
+
statsChartHourBarTitle: '{date} {hour} · {tokens}',
|
|
215
|
+
statsChartHourEmpty: '该日没有小时级数据',
|
|
216
|
+
statsWindowLabel: '明细统计窗口',
|
|
211
217
|
statsByHarness: '按 Harness',
|
|
212
218
|
statsHarnessUnavailable: '不可用',
|
|
213
219
|
statsHarnessRetryHint: '检查本机 Harness 数据源后点刷新重试',
|
|
@@ -429,6 +435,12 @@ window.__ModuleLoader__.load({
|
|
|
429
435
|
statsChartPeak: 'Peak {value} ({day})',
|
|
430
436
|
statsChartBarTitle: '{date}: {tokens}',
|
|
431
437
|
statsChartReadout: '{date} · {value} ({share}%)',
|
|
438
|
+
statsChartBack: '← Back to daily',
|
|
439
|
+
statsChartBackHint: 'Back to the daily trend chart',
|
|
440
|
+
statsChartHourTitle: 'By hour · {date}',
|
|
441
|
+
statsChartHourBarTitle: '{date} {hour} · {tokens}',
|
|
442
|
+
statsChartHourEmpty: 'No hourly data for this day',
|
|
443
|
+
statsWindowLabel: 'Details window',
|
|
432
444
|
statsByHarness: 'By harness',
|
|
433
445
|
statsHarnessUnavailable: 'Unavailable',
|
|
434
446
|
statsHarnessRetryHint: 'Check the harness data source, then click Refresh to retry',
|
|
@@ -1238,6 +1250,17 @@ window.__ModuleLoader__.load({
|
|
|
1238
1250
|
}
|
|
1239
1251
|
}
|
|
1240
1252
|
|
|
1253
|
+
/**
|
|
1254
|
+
* The local 0..23 hour histogram of one daily entry (v1.2.0), or null when
|
|
1255
|
+
* the wire omits it (old Hosts). Anything but a length-24 array is treated
|
|
1256
|
+
* as unsupported; a non-finite / negative slot degrades to 0 so hostile data
|
|
1257
|
+
* can never poison the chart.
|
|
1258
|
+
*/
|
|
1259
|
+
function normalizeHourHistogram(raw) {
|
|
1260
|
+
if (!Array.isArray(raw) || raw.length !== 24) return null
|
|
1261
|
+
return raw.map((value) => (typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0))
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1241
1264
|
/** One provider attribution row (stats v3 aggregates DSH + ZCode per provider). */
|
|
1242
1265
|
function normalizeProviderRow(entry) {
|
|
1243
1266
|
const providerId = stringOrEmpty(entry.providerId)
|
|
@@ -1358,6 +1381,8 @@ window.__ModuleLoader__.load({
|
|
|
1358
1381
|
: []
|
|
1359
1382
|
// v0.24.0: the Host reports a per-day token series (ascending, <=120 days)
|
|
1360
1383
|
// for the chart mode. Every field is normalized defensively.
|
|
1384
|
+
// v1.2.0: each day (and each provider split of that day) may also carry
|
|
1385
|
+
// the local 0..23 hour histogram used by the chart drill-down.
|
|
1361
1386
|
const daily = Array.isArray(raw.daily)
|
|
1362
1387
|
? raw.daily
|
|
1363
1388
|
.filter((entry) => entry !== null && typeof entry === 'object')
|
|
@@ -1369,10 +1394,18 @@ window.__ModuleLoader__.load({
|
|
|
1369
1394
|
const dailyProviders = {}
|
|
1370
1395
|
for (const [providerId, bucketRaw] of Object.entries(providerRaw)) {
|
|
1371
1396
|
if (providerId === '' || bucketRaw === null || typeof bucketRaw !== 'object') continue
|
|
1372
|
-
dailyProviders[providerId] =
|
|
1397
|
+
dailyProviders[providerId] = {
|
|
1398
|
+
...normalizeBucket(bucketRaw),
|
|
1399
|
+
hours: normalizeHourHistogram(bucketRaw.hours),
|
|
1400
|
+
}
|
|
1373
1401
|
}
|
|
1374
1402
|
const day = stringOrEmpty(entry.day).trim()
|
|
1375
|
-
return {
|
|
1403
|
+
return {
|
|
1404
|
+
day,
|
|
1405
|
+
total: normalizeOptionalBucket(entry.total),
|
|
1406
|
+
hours: normalizeHourHistogram(entry.hours),
|
|
1407
|
+
providers: dailyProviders,
|
|
1408
|
+
}
|
|
1376
1409
|
})
|
|
1377
1410
|
.filter((entry) => entry.day !== '')
|
|
1378
1411
|
.sort((a, b) => (a.day < b.day ? -1 : a.day > b.day ? 1 : 0))
|
|
@@ -1706,6 +1739,12 @@ window.__ModuleLoader__.load({
|
|
|
1706
1739
|
// v0.25.0 chart hover: the nearest bar index under the pointer, or null.
|
|
1707
1740
|
// Reset on mouse leave and whenever the scope/view/provider changes.
|
|
1708
1741
|
chartHover: null,
|
|
1742
|
+
// v1.2.0 chart drill-down: the clicked day whose 0..23 hour histogram
|
|
1743
|
+
// is charted instead of the daily trend, or null (daily trend).
|
|
1744
|
+
chartDay: null,
|
|
1745
|
+
// v1.2.0 明细 donut window: 'today' | 'last7Days' | 'second' | 'total'.
|
|
1746
|
+
// Presentation state only (never persisted to the plugin settings).
|
|
1747
|
+
detailsWindow: 'total',
|
|
1709
1748
|
// Provider management page (v0.9.0).
|
|
1710
1749
|
view: 'usage', // 'usage' | 'management'
|
|
1711
1750
|
management: defaultManagementState(),
|
|
@@ -1787,6 +1826,8 @@ window.__ModuleLoader__.load({
|
|
|
1787
1826
|
statsScope: settings.defaultScope,
|
|
1788
1827
|
showDetails: showDetailsFor(settings.detailsExpanded),
|
|
1789
1828
|
chartHover: null,
|
|
1829
|
+
chartDay: null,
|
|
1830
|
+
detailsWindow: 'total',
|
|
1790
1831
|
}
|
|
1791
1832
|
}
|
|
1792
1833
|
|
|
@@ -2857,6 +2898,19 @@ window.__ModuleLoader__.load({
|
|
|
2857
2898
|
.dsh-tokenlens-chart-summary>span{font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
|
|
2858
2899
|
.dsh-tokenlens-chart-empty{box-sizing:border-box;min-height:140px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;border:1px dashed var(--dsw-alias-border-subtle,rgba(127,127,127,.28));border-radius:8px;padding:10px;text-align:center;min-width:0}
|
|
2859
2900
|
.dsh-tokenlens-chart-empty-hint{opacity:.8;font-size:11px}
|
|
2901
|
+
/* Hourly drill-down of the daily chart (v1.2.0): clickable/focusable day bars,
|
|
2902
|
+
a selected-day ring, and the back + title row above the hour bars. */
|
|
2903
|
+
.dsh-tokenlens-chart-bar-wrap-clickable{cursor:pointer}
|
|
2904
|
+
.dsh-tokenlens-chart-bar-wrap-clickable:focus-visible{outline:2px solid #2e9e5b;outline-offset:1px;border-radius:3px}
|
|
2905
|
+
.dsh-tokenlens-chart-bar-wrap[data-chart-selected=true] .dsh-tokenlens-chart-bar{background:#1d6b3d;box-shadow:0 0 0 1px rgba(64,200,119,.9)}
|
|
2906
|
+
.dsh-tokenlens-chart-drilldown{gap:4px}
|
|
2907
|
+
.dsh-tokenlens-chart-drill-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
|
|
2908
|
+
.dsh-tokenlens-chart-back{font:inherit;font-size:var(--dsw-alias-font-size-sm,12px);line-height:1.4;padding:1px 8px;border:1px solid var(--dsw-alias-border-subtle,rgba(127,127,127,.24));border-radius:999px;background:0 0;color:var(--dsw-alias-label-tertiary);cursor:pointer;flex:none}
|
|
2909
|
+
.dsh-tokenlens-chart-back:hover{color:var(--dsw-alias-label-secondary)}
|
|
2910
|
+
.dsh-tokenlens-chart-back:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-alias-border-strong,rgba(127,127,127,.5))}
|
|
2911
|
+
.dsh-tokenlens-chart-drill-title{color:var(--dsw-alias-label-secondary);font-size:var(--dsw-alias-font-size-sm,12px);line-height:1.4;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
2912
|
+
/* 明细 donut window switch (v1.2.0): one pill row under the details title. */
|
|
2913
|
+
.dsh-tokenlens-details-window{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}
|
|
2860
2914
|
.dsh-tokenlens-version{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;text-align:right;opacity:.75}
|
|
2861
2915
|
.dsh-tokenlens-cards{gap:6px;flex-wrap:wrap;display:flex}
|
|
2862
2916
|
.dsh-tokenlens-cards-caption{color:var(--dsw-alias-label-tertiary);font-size:var(--dsw-alias-font-size-sm,12px);line-height:1.4;margin-bottom:-3px}
|
|
@@ -3145,7 +3199,13 @@ window.__ModuleLoader__.load({
|
|
|
3145
3199
|
return `${providerLabel} · ${parts.join(' · ')}`
|
|
3146
3200
|
}
|
|
3147
3201
|
if (snapshot !== null && snapshot.provider.kind === 'windows') {
|
|
3148
|
-
|
|
3202
|
+
const summary = `${providerLabel} · ${windowsSummaryText(snapshot.windows, t)}`
|
|
3203
|
+
// A windows provider may also carry a wallet balance (OpenDesign): keep the
|
|
3204
|
+
// chip body unchanged and append the money to the hover detail only.
|
|
3205
|
+
if (snapshot.balance !== null) {
|
|
3206
|
+
return `${summary} · ${formatMoney(snapshot.balance.amount, snapshot.balance.currency)}`
|
|
3207
|
+
}
|
|
3208
|
+
return summary
|
|
3149
3209
|
}
|
|
3150
3210
|
return `${providerLabel} · ${t('chipNoUsage')}`
|
|
3151
3211
|
}
|
|
@@ -3347,6 +3407,32 @@ window.__ModuleLoader__.load({
|
|
|
3347
3407
|
)
|
|
3348
3408
|
}
|
|
3349
3409
|
|
|
3410
|
+
/**
|
|
3411
|
+
* The wallet-balance row of a windows provider (OpenDesign).
|
|
3412
|
+
*
|
|
3413
|
+
* Some providers report allowance windows AND an independent account balance:
|
|
3414
|
+
* a `windows` snapshot may therefore carry a non-null `balance`. The row reuses
|
|
3415
|
+
* the window row's label/value layout so it reads as part of the same card, and
|
|
3416
|
+
* the shared narrow-screen wrapping of `.dsh-tokenlens-row-head` keeps it in view.
|
|
3417
|
+
*/
|
|
3418
|
+
function buildCapacityBalanceRow(balance, t) {
|
|
3419
|
+
if (balance === null) return null
|
|
3420
|
+
return h(
|
|
3421
|
+
'div',
|
|
3422
|
+
{ key: 'balance', className: 'dsh-tokenlens-row', 'data-capacity-balance': 'true' },
|
|
3423
|
+
h(
|
|
3424
|
+
'div',
|
|
3425
|
+
{ className: 'dsh-tokenlens-row-head' },
|
|
3426
|
+
h('span', { className: 'dsh-tokenlens-row-label' }, t('balanceLabel')),
|
|
3427
|
+
h(
|
|
3428
|
+
'span',
|
|
3429
|
+
{ className: 'dsh-tokenlens-percent dsh-tokenlens-capacity-money' },
|
|
3430
|
+
formatMoney(balance.amount, balance.currency),
|
|
3431
|
+
),
|
|
3432
|
+
),
|
|
3433
|
+
)
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3350
3436
|
/** The provider/plan/account header line, or null when there is nothing to say. */
|
|
3351
3437
|
function buildProviderHeader(snapshot, t) {
|
|
3352
3438
|
if (snapshot === null) return null
|
|
@@ -3523,7 +3609,7 @@ window.__ModuleLoader__.load({
|
|
|
3523
3609
|
'data-scope': scope,
|
|
3524
3610
|
'data-active': effective === scope ? 'true' : 'false',
|
|
3525
3611
|
disabled: stats === null,
|
|
3526
|
-
onClick: () => store.set({ statsScope: scope, chartHover: null }),
|
|
3612
|
+
onClick: () => store.set({ statsScope: scope, chartHover: null, chartDay: null }),
|
|
3527
3613
|
},
|
|
3528
3614
|
label,
|
|
3529
3615
|
)
|
|
@@ -3549,7 +3635,7 @@ window.__ModuleLoader__.load({
|
|
|
3549
3635
|
'data-active': active === view ? 'true' : 'false',
|
|
3550
3636
|
'aria-pressed': active === view ? 'true' : 'false',
|
|
3551
3637
|
onClick: () => {
|
|
3552
|
-
if (active !== view) store.set({ statsView: view, chartHover: null })
|
|
3638
|
+
if (active !== view) store.set({ statsView: view, chartHover: null, chartDay: null })
|
|
3553
3639
|
},
|
|
3554
3640
|
},
|
|
3555
3641
|
Icon(iconName, 12),
|
|
@@ -3569,37 +3655,88 @@ window.__ModuleLoader__.load({
|
|
|
3569
3655
|
}
|
|
3570
3656
|
|
|
3571
3657
|
/**
|
|
3572
|
-
* The daily
|
|
3658
|
+
* The daily provider ids the chart's provider scope matches (v0.24.0).
|
|
3659
|
+
*
|
|
3660
|
+
* Both the stats row id and the session-selected id are matched, in exact and
|
|
3661
|
+
* canonical form, so `deepseek-official` and `deepseek` find the same bucket.
|
|
3662
|
+
*/
|
|
3663
|
+
function chartDailyProviderIds(ctx) {
|
|
3664
|
+
const wanted = []
|
|
3665
|
+
if (ctx.currentRow !== null && typeof ctx.currentRow.providerId === 'string' && ctx.currentRow.providerId !== '') {
|
|
3666
|
+
wanted.push(ctx.currentRow.providerId)
|
|
3667
|
+
}
|
|
3668
|
+
if (typeof ctx.selectedProviderId === 'string' && ctx.selectedProviderId !== '') {
|
|
3669
|
+
wanted.push(ctx.selectedProviderId)
|
|
3670
|
+
}
|
|
3671
|
+
return { wanted, canonicalWanted: new Set(wanted.map((id) => canonicalProviderId(id))) }
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
/** The daily provider bucket of one day in provider scope, or null. */
|
|
3675
|
+
function chartDailyProviderBucket(ctx, entry) {
|
|
3676
|
+
const { wanted, canonicalWanted } = chartDailyProviderIds(ctx)
|
|
3677
|
+
for (const [key, bucket] of Object.entries(entry.providers)) {
|
|
3678
|
+
if (wanted.includes(key) || canonicalWanted.has(canonicalProviderId(key))) return bucket ?? null
|
|
3679
|
+
}
|
|
3680
|
+
return null
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
/** The 24-hour histogram of one chart point, or null when unsupported. */
|
|
3684
|
+
function chartHoursOfPoint(point) {
|
|
3685
|
+
return point !== null && point !== undefined && Array.isArray(point.hours) && point.hours.length === 24
|
|
3686
|
+
? point.hours
|
|
3687
|
+
: null
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
/** The `HH:00` label of one local hour of the drill-down chart (v1.2.0). */
|
|
3691
|
+
function chartHourLabel(hour) {
|
|
3692
|
+
return `${String(hour).padStart(2, '0')}:00`
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
/** Enter / Space activate a focusable chart bar (v1.2.0). */
|
|
3696
|
+
function isChartActivationKey(event) {
|
|
3697
|
+
const key = typeof event?.key === 'string' ? event.key : ''
|
|
3698
|
+
return key === 'Enter' || key === ' ' || key === 'Spacebar'
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
/**
|
|
3702
|
+
* Toggle the chart drill-down (v1.2.0): clicking a day charts its 0..23 hour
|
|
3703
|
+
* histogram; clicking the SAME day again returns to the daily trend.
|
|
3704
|
+
*/
|
|
3705
|
+
function activateChartDay(day) {
|
|
3706
|
+
if (typeof day !== 'string' || day === '') return
|
|
3707
|
+
const next = store.get().chartDay === day ? null : day
|
|
3708
|
+
store.set({ chartDay: next, chartHover: null })
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
/** Leave the hourly drill-down and chart the daily trend again (v1.2.0). */
|
|
3712
|
+
function clearChartDay() {
|
|
3713
|
+
if (store.get().chartDay !== null) store.set({ chartDay: null, chartHover: null })
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
/**
|
|
3717
|
+
* The daily series of the chart (v0.24.0; v1.2.0 adds the day histograms).
|
|
3573
3718
|
*
|
|
3574
3719
|
* Provider scope reads the selected provider's daily bucket and limits the
|
|
3575
3720
|
* window to the provider's effective day when it is subscription-backed
|
|
3576
3721
|
* (fallback: the last 30 days). The all-harness scope uses the day totals of
|
|
3577
|
-
* the last 30 days.
|
|
3722
|
+
* the last 30 days. Every point also carries its 0..23 hour histogram
|
|
3723
|
+
* (`hours`, or null on old Hosts) so the chart can drill into one day.
|
|
3578
3724
|
*/
|
|
3579
3725
|
function chartSeriesOf(ctx) {
|
|
3580
3726
|
const stats = ctx.stats
|
|
3581
3727
|
if (stats === null) return []
|
|
3582
3728
|
const daily = stats.daily
|
|
3583
3729
|
if (ctx.scope === 'all') {
|
|
3584
|
-
return daily.slice(-30).map((entry) => ({
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
if (ctx.currentRow !== null && typeof ctx.currentRow.providerId === 'string' && ctx.currentRow.providerId !== '') {
|
|
3590
|
-
wanted.push(ctx.currentRow.providerId)
|
|
3591
|
-
}
|
|
3592
|
-
if (typeof ctx.selectedProviderId === 'string' && ctx.selectedProviderId !== '') {
|
|
3593
|
-
wanted.push(ctx.selectedProviderId)
|
|
3594
|
-
}
|
|
3595
|
-
const canonicalWanted = new Set(wanted.map((id) => canonicalProviderId(id)))
|
|
3596
|
-
const valueOf = (entry) => {
|
|
3597
|
-
for (const [key, bucket] of Object.entries(entry.providers)) {
|
|
3598
|
-
if (wanted.includes(key) || canonicalWanted.has(canonicalProviderId(key))) return bucket?.total ?? 0
|
|
3599
|
-
}
|
|
3600
|
-
return 0
|
|
3730
|
+
return daily.slice(-30).map((entry) => ({
|
|
3731
|
+
day: entry.day,
|
|
3732
|
+
total: entry.total?.total ?? 0,
|
|
3733
|
+
hours: entry.hours ?? null,
|
|
3734
|
+
}))
|
|
3601
3735
|
}
|
|
3602
|
-
const series = daily.map((entry) =>
|
|
3736
|
+
const series = daily.map((entry) => {
|
|
3737
|
+
const bucket = chartDailyProviderBucket(ctx, entry)
|
|
3738
|
+
return { day: entry.day, total: bucket?.total ?? 0, hours: bucket?.hours ?? null }
|
|
3739
|
+
})
|
|
3603
3740
|
const effectiveDay = ctx.currentRow !== null ? ctx.currentRow.effectiveDay : ''
|
|
3604
3741
|
if (ctx.subscribed && typeof effectiveDay === 'string' && effectiveDay !== '') {
|
|
3605
3742
|
return series.filter((point) => point.day >= effectiveDay)
|
|
@@ -3678,7 +3815,6 @@ window.__ModuleLoader__.load({
|
|
|
3678
3815
|
if (store.get().chartHover !== next) store.set({ chartHover: next })
|
|
3679
3816
|
}
|
|
3680
3817
|
|
|
3681
|
-
/** The pure-CSS daily bar chart: axes + live pointer readout (v0.25.0; no deps). */
|
|
3682
3818
|
/** The active today bucket of the current stats scope (v0.27.2). */
|
|
3683
3819
|
function todayBucketOf(ctx) {
|
|
3684
3820
|
return ctx.providerScoped ? ctx.currentRow?.today ?? null : ctx.stats?.today ?? null
|
|
@@ -3695,37 +3831,25 @@ window.__ModuleLoader__.load({
|
|
|
3695
3831
|
return ctx.providerScoped ? ctx.currentRow?.last7Days ?? null : ctx.stats?.last7Days ?? null
|
|
3696
3832
|
}
|
|
3697
3833
|
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
h(
|
|
3710
|
-
'span',
|
|
3711
|
-
{ key: 'hint', className: 'dsh-tokenlens-hint dsh-tokenlens-chart-empty-hint' },
|
|
3712
|
-
t('statsChartEmptyHint'),
|
|
3713
|
-
),
|
|
3714
|
-
),
|
|
3715
|
-
)
|
|
3716
|
-
}
|
|
3834
|
+
/**
|
|
3835
|
+
* The shared pure-CSS bar plot of the chart modes (v0.25.0; v1.2.0 reuses
|
|
3836
|
+
* it for the hourly drill-down): Y-axis ticks, gridlines, bars, the X-axis
|
|
3837
|
+
* labels, the crosshair and the live pointer readout. Pure DOM, no deps.
|
|
3838
|
+
*
|
|
3839
|
+
* `spec.bar(point, index, meta)` returns the wrapper element of one bar
|
|
3840
|
+
* (`meta.max` / `meta.hovered` drive its height and highlight),
|
|
3841
|
+
* `spec.xLabel(point, index)` returns `{text, attrs}` for the X axis, and
|
|
3842
|
+
* `spec.readout(hovered, share)` returns the live readout text.
|
|
3843
|
+
*/
|
|
3844
|
+
function buildChartPlot(state, points, spec) {
|
|
3717
3845
|
const max = Math.max(...points.map((point) => point.total), 1)
|
|
3718
|
-
|
|
3719
|
-
for (const point of points) {
|
|
3720
|
-
if (point.total > peak.total) peak = point
|
|
3721
|
-
}
|
|
3846
|
+
const total = points.reduce((sum, point) => sum + point.total, 0)
|
|
3722
3847
|
const hoverIndex =
|
|
3723
3848
|
Number.isInteger(state.chartHover) && state.chartHover >= 0 && state.chartHover < points.length
|
|
3724
3849
|
? state.chartHover
|
|
3725
3850
|
: null
|
|
3726
3851
|
const hovered = hoverIndex === null ? null : points[hoverIndex]
|
|
3727
|
-
const hoverShare =
|
|
3728
|
-
hovered === null || total <= 0 ? 0 : Math.round((hovered.total / total) * 1000) / 10
|
|
3852
|
+
const hoverShare = hovered === null || total <= 0 ? 0 : Math.round((hovered.total / total) * 1000) / 10
|
|
3729
3853
|
const handleMove = (event) => {
|
|
3730
3854
|
const index = chartIndexFromEvent(event, points.length)
|
|
3731
3855
|
if (index !== null) setChartHover(index)
|
|
@@ -3751,32 +3875,10 @@ window.__ModuleLoader__.load({
|
|
|
3751
3875
|
tick.text,
|
|
3752
3876
|
),
|
|
3753
3877
|
)
|
|
3754
|
-
const bars = points.map((point, index) => {
|
|
3755
|
-
const height = point.total > 0 ? Math.max(2, Math.round((point.total / max) * 1000) / 10) : 0
|
|
3756
|
-
const isHovered = index === hoverIndex
|
|
3757
|
-
return h(
|
|
3758
|
-
'div',
|
|
3759
|
-
{
|
|
3760
|
-
key: point.day,
|
|
3761
|
-
className: 'dsh-tokenlens-chart-bar-wrap',
|
|
3762
|
-
'data-chart-day': point.day,
|
|
3763
|
-
'data-chart-total': String(point.total),
|
|
3764
|
-
'data-chart-hover': isHovered ? 'true' : 'false',
|
|
3765
|
-
title: t('statsChartBarTitle', {
|
|
3766
|
-
date: point.day,
|
|
3767
|
-
tokens: formatTokensPrecise(point.total),
|
|
3768
|
-
}),
|
|
3769
|
-
},
|
|
3770
|
-
h('div', {
|
|
3771
|
-
className: 'dsh-tokenlens-chart-bar',
|
|
3772
|
-
'data-empty': point.total > 0 ? 'false' : 'true',
|
|
3773
|
-
'data-height': String(height),
|
|
3774
|
-
style: { height: `${height}%` },
|
|
3775
|
-
}),
|
|
3776
|
-
)
|
|
3777
|
-
})
|
|
3878
|
+
const bars = points.map((point, index) => spec.bar(point, index, { max, hovered: index === hoverIndex }))
|
|
3778
3879
|
const xLabels = chartXLabelIndices(points.length).map((index) => {
|
|
3779
3880
|
const point = points[index]
|
|
3881
|
+
const label = spec.xLabel(point, index)
|
|
3780
3882
|
const first = index === 0
|
|
3781
3883
|
const last = index === points.length - 1
|
|
3782
3884
|
const style = first
|
|
@@ -3787,12 +3889,12 @@ window.__ModuleLoader__.load({
|
|
|
3787
3889
|
return h(
|
|
3788
3890
|
'span',
|
|
3789
3891
|
{
|
|
3790
|
-
key: `x-${
|
|
3892
|
+
key: `x-${index}`,
|
|
3791
3893
|
className: 'dsh-tokenlens-chart-x-label',
|
|
3792
|
-
|
|
3894
|
+
...(label.attrs === null || label.attrs === undefined ? {} : label.attrs),
|
|
3793
3895
|
style,
|
|
3794
3896
|
},
|
|
3795
|
-
|
|
3897
|
+
label.text,
|
|
3796
3898
|
)
|
|
3797
3899
|
})
|
|
3798
3900
|
const crosshair =
|
|
@@ -3825,37 +3927,249 @@ window.__ModuleLoader__.load({
|
|
|
3825
3927
|
role: 'status',
|
|
3826
3928
|
style: readoutStyle,
|
|
3827
3929
|
},
|
|
3828
|
-
|
|
3829
|
-
date: hovered.day,
|
|
3830
|
-
value: formatTokensPrecise(hovered.total),
|
|
3831
|
-
share: String(hoverShare),
|
|
3832
|
-
}),
|
|
3930
|
+
spec.readout(hovered, hoverShare),
|
|
3833
3931
|
)
|
|
3834
3932
|
return h(
|
|
3835
3933
|
'div',
|
|
3836
|
-
{ key: '
|
|
3934
|
+
{ key: 'body', className: 'dsh-tokenlens-chart-body' },
|
|
3935
|
+
h('div', { key: 'y-axis', className: 'dsh-tokenlens-chart-yaxis', 'aria-hidden': 'true' }, yTicks),
|
|
3837
3936
|
h(
|
|
3838
3937
|
'div',
|
|
3839
|
-
{
|
|
3840
|
-
|
|
3938
|
+
{
|
|
3939
|
+
key: 'plot',
|
|
3940
|
+
className: 'dsh-tokenlens-chart-plot',
|
|
3941
|
+
'data-chart-plot': 'true',
|
|
3942
|
+
onMouseMove: handleMove,
|
|
3943
|
+
onMouseLeave: handleLeave,
|
|
3944
|
+
onPointerMove: handleMove,
|
|
3945
|
+
onPointerLeave: handleLeave,
|
|
3946
|
+
},
|
|
3947
|
+
h('div', { key: 'grid', className: 'dsh-tokenlens-chart-grid', 'aria-hidden': 'true' }, gridLines),
|
|
3948
|
+
h('div', { key: 'bars', className: 'dsh-tokenlens-chart-bars' }, bars),
|
|
3949
|
+
crosshair,
|
|
3950
|
+
readout,
|
|
3951
|
+
h('div', { key: 'xaxis', className: 'dsh-tokenlens-chart-xaxis', 'aria-hidden': 'true' }, xLabels),
|
|
3952
|
+
),
|
|
3953
|
+
)
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
/**
|
|
3957
|
+
* One bar of the daily trend chart (v0.25.0). A day whose entry carries the
|
|
3958
|
+
* 0..23 hour histogram is ALSO a real button (v1.2.0): focusable, Enter /
|
|
3959
|
+
* Space activate it, `aria-pressed` marks the drilled day. A day without
|
|
3960
|
+
* hours is not drillable and keeps the old non-interactive bar.
|
|
3961
|
+
*/
|
|
3962
|
+
function buildDailyChartBar(state, t, point, meta) {
|
|
3963
|
+
const height = point.total > 0 ? Math.max(2, Math.round((point.total / meta.max) * 1000) / 10) : 0
|
|
3964
|
+
const drillable = chartHoursOfPoint(point) !== null
|
|
3965
|
+
const selected = drillable && state.chartDay === point.day
|
|
3966
|
+
const attrs = {
|
|
3967
|
+
key: point.day,
|
|
3968
|
+
className: drillable
|
|
3969
|
+
? 'dsh-tokenlens-chart-bar-wrap dsh-tokenlens-chart-bar-wrap-clickable'
|
|
3970
|
+
: 'dsh-tokenlens-chart-bar-wrap',
|
|
3971
|
+
'data-chart-day': point.day,
|
|
3972
|
+
'data-chart-total': String(point.total),
|
|
3973
|
+
'data-chart-hover': meta.hovered ? 'true' : 'false',
|
|
3974
|
+
'data-chart-drillable': drillable ? 'true' : 'false',
|
|
3975
|
+
'data-chart-selected': selected ? 'true' : 'false',
|
|
3976
|
+
title: t('statsChartBarTitle', {
|
|
3977
|
+
date: point.day,
|
|
3978
|
+
tokens: formatTokensPrecise(point.total),
|
|
3979
|
+
}),
|
|
3980
|
+
}
|
|
3981
|
+
if (drillable) {
|
|
3982
|
+
attrs.role = 'button'
|
|
3983
|
+
attrs.tabIndex = 0
|
|
3984
|
+
attrs['aria-pressed'] = selected ? 'true' : 'false'
|
|
3985
|
+
attrs['aria-label'] = t('statsChartBarTitle', {
|
|
3986
|
+
date: point.day,
|
|
3987
|
+
tokens: formatTokensPrecise(point.total),
|
|
3988
|
+
})
|
|
3989
|
+
attrs.onClick = () => activateChartDay(point.day)
|
|
3990
|
+
attrs.onKeyDown = (event) => {
|
|
3991
|
+
if (!isChartActivationKey(event)) return
|
|
3992
|
+
if (typeof event?.preventDefault === 'function') event.preventDefault()
|
|
3993
|
+
activateChartDay(point.day)
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
return h(
|
|
3997
|
+
'div',
|
|
3998
|
+
attrs,
|
|
3999
|
+
h('div', {
|
|
4000
|
+
className: 'dsh-tokenlens-chart-bar',
|
|
4001
|
+
'data-empty': point.total > 0 ? 'false' : 'true',
|
|
4002
|
+
'data-height': String(height),
|
|
4003
|
+
style: { height: `${height}%` },
|
|
4004
|
+
}),
|
|
4005
|
+
)
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
/**
|
|
4009
|
+
* The drilled day view (v1.2.0): one day's 00:00–23:00 token bars, with a
|
|
4010
|
+
* back button to the daily trend. It reuses the shared plot (axes, pointer
|
|
4011
|
+
* readout, tooltips) and the existing summary formatters.
|
|
4012
|
+
*/
|
|
4013
|
+
function buildHourChart(state, point, hours, t) {
|
|
4014
|
+
const dayLabel = chartDayLabel(point.day)
|
|
4015
|
+
const hourPoints = hours.map((value, hour) => ({ hour, total: value }))
|
|
4016
|
+
const total = hourPoints.reduce((sum, hourPoint) => sum + hourPoint.total, 0)
|
|
4017
|
+
const head = h(
|
|
4018
|
+
'div',
|
|
4019
|
+
{ key: 'drill-head', className: 'dsh-tokenlens-chart-drill-head', 'data-chart-drill-head': 'true' },
|
|
4020
|
+
h(
|
|
4021
|
+
'button',
|
|
4022
|
+
{
|
|
4023
|
+
key: 'back',
|
|
4024
|
+
type: 'button',
|
|
4025
|
+
className: 'dsh-tokenlens-chart-back',
|
|
4026
|
+
'data-chart-back': point.day,
|
|
4027
|
+
title: t('statsChartBackHint'),
|
|
4028
|
+
'aria-label': t('statsChartBack'),
|
|
4029
|
+
onClick: () => clearChartDay(),
|
|
4030
|
+
},
|
|
4031
|
+
t('statsChartBack'),
|
|
4032
|
+
),
|
|
4033
|
+
h(
|
|
4034
|
+
'span',
|
|
4035
|
+
{ key: 'title', className: 'dsh-tokenlens-chart-drill-title', 'data-chart-hour-title': point.day },
|
|
4036
|
+
t('statsChartHourTitle', { date: point.day }),
|
|
4037
|
+
),
|
|
4038
|
+
)
|
|
4039
|
+
const containerAttrs = {
|
|
4040
|
+
key: 'chart',
|
|
4041
|
+
className: 'dsh-tokenlens-chart dsh-tokenlens-chart-drilldown',
|
|
4042
|
+
'data-chart': 'true',
|
|
4043
|
+
'data-chart-drilldown': 'true',
|
|
4044
|
+
'data-chart-drill-day': point.day,
|
|
4045
|
+
}
|
|
4046
|
+
if (total <= 0) {
|
|
4047
|
+
// A hosted histogram that is all zeros carries no data: say so instead of
|
|
4048
|
+
// inventing an empty 24-bar chart (old Hosts never get this far: their
|
|
4049
|
+
// bars are not drillable at all).
|
|
4050
|
+
return h(
|
|
4051
|
+
'div',
|
|
4052
|
+
containerAttrs,
|
|
4053
|
+
head,
|
|
3841
4054
|
h(
|
|
4055
|
+
'div',
|
|
4056
|
+
{ key: 'empty', className: 'dsh-tokenlens-chart-empty', 'data-chart-hour-empty': 'true' },
|
|
4057
|
+
h('span', { key: 'title', className: 'dsh-tokenlens-hint' }, t('statsChartHourEmpty')),
|
|
4058
|
+
),
|
|
4059
|
+
)
|
|
4060
|
+
}
|
|
4061
|
+
let peak = hourPoints[0]
|
|
4062
|
+
for (const hourPoint of hourPoints) {
|
|
4063
|
+
if (hourPoint.total > peak.total) peak = hourPoint
|
|
4064
|
+
}
|
|
4065
|
+
const body = buildChartPlot(state, hourPoints, {
|
|
4066
|
+
bar: (hourPoint, index, meta) => {
|
|
4067
|
+
const height = hourPoint.total > 0 ? Math.max(2, Math.round((hourPoint.total / meta.max) * 1000) / 10) : 0
|
|
4068
|
+
return h(
|
|
3842
4069
|
'div',
|
|
3843
4070
|
{
|
|
3844
|
-
key:
|
|
3845
|
-
className: 'dsh-tokenlens-chart-
|
|
3846
|
-
'data-chart-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
4071
|
+
key: `hour-${hourPoint.hour}`,
|
|
4072
|
+
className: 'dsh-tokenlens-chart-bar-wrap',
|
|
4073
|
+
'data-chart-hour': String(hourPoint.hour),
|
|
4074
|
+
'data-chart-total': String(hourPoint.total),
|
|
4075
|
+
'data-chart-hover': meta.hovered ? 'true' : 'false',
|
|
4076
|
+
title: t('statsChartHourBarTitle', {
|
|
4077
|
+
date: dayLabel,
|
|
4078
|
+
hour: chartHourLabel(hourPoint.hour),
|
|
4079
|
+
tokens: formatTokensPrecise(hourPoint.total),
|
|
4080
|
+
}),
|
|
3851
4081
|
},
|
|
3852
|
-
h('div', {
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
4082
|
+
h('div', {
|
|
4083
|
+
className: 'dsh-tokenlens-chart-bar',
|
|
4084
|
+
'data-empty': hourPoint.total > 0 ? 'false' : 'true',
|
|
4085
|
+
'data-height': String(height),
|
|
4086
|
+
style: { height: `${height}%` },
|
|
4087
|
+
}),
|
|
4088
|
+
)
|
|
4089
|
+
},
|
|
4090
|
+
xLabel: (hourPoint) => ({
|
|
4091
|
+
text: chartHourLabel(hourPoint.hour),
|
|
4092
|
+
attrs: { 'data-chart-axis-hour': String(hourPoint.hour) },
|
|
4093
|
+
}),
|
|
4094
|
+
readout: (hovered, share) =>
|
|
4095
|
+
t('statsChartReadout', {
|
|
4096
|
+
date: `${dayLabel} ${chartHourLabel(hovered.hour)}`,
|
|
4097
|
+
value: formatTokensPrecise(hovered.total),
|
|
4098
|
+
share: String(share),
|
|
4099
|
+
}),
|
|
4100
|
+
})
|
|
4101
|
+
return h(
|
|
4102
|
+
'div',
|
|
4103
|
+
containerAttrs,
|
|
4104
|
+
head,
|
|
4105
|
+
body,
|
|
4106
|
+
h(
|
|
4107
|
+
'div',
|
|
4108
|
+
{ key: 'summary', className: 'dsh-tokenlens-chart-summary' },
|
|
4109
|
+
h(
|
|
4110
|
+
'span',
|
|
4111
|
+
{ key: 'total', 'data-chart-summary': 'hour-total' },
|
|
4112
|
+
t('statsChartTotal', { value: formatTokensPrecise(total) }),
|
|
4113
|
+
),
|
|
4114
|
+
h(
|
|
4115
|
+
'span',
|
|
4116
|
+
{ key: 'peak', 'data-chart-summary': 'hour-peak' },
|
|
4117
|
+
t('statsChartPeak', { value: formatTokensPrecise(peak.total), day: chartHourLabel(peak.hour) }),
|
|
3857
4118
|
),
|
|
3858
4119
|
),
|
|
4120
|
+
)
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
/** The pure-CSS daily bar chart: axes + live pointer readout (v0.25.0; no deps). */
|
|
4124
|
+
function buildDailyChart(state, ctx, t) {
|
|
4125
|
+
const points = chartSeriesOf(ctx)
|
|
4126
|
+
const total = points.reduce((sum, point) => sum + point.total, 0)
|
|
4127
|
+
if (points.length === 0 || total === 0) {
|
|
4128
|
+
return h(
|
|
4129
|
+
'div',
|
|
4130
|
+
{ key: 'chart', className: 'dsh-tokenlens-chart', 'data-chart': 'true', 'data-chart-empty-series': 'true' },
|
|
4131
|
+
h(
|
|
4132
|
+
'div',
|
|
4133
|
+
{ key: 'empty', className: 'dsh-tokenlens-chart-empty', 'data-chart-empty': 'true' },
|
|
4134
|
+
h('span', { key: 'title', className: 'dsh-tokenlens-hint' }, t('statsChartEmpty')),
|
|
4135
|
+
h(
|
|
4136
|
+
'span',
|
|
4137
|
+
{ key: 'hint', className: 'dsh-tokenlens-hint dsh-tokenlens-chart-empty-hint' },
|
|
4138
|
+
t('statsChartEmptyHint'),
|
|
4139
|
+
),
|
|
4140
|
+
),
|
|
4141
|
+
)
|
|
4142
|
+
}
|
|
4143
|
+
// v1.2.0: a drilled day replaces the daily trend with its 24-hour
|
|
4144
|
+
// histogram. A stale selection (day gone / no hours) silently falls back to
|
|
4145
|
+
// the daily trend instead of charting a fabricated hour series.
|
|
4146
|
+
const drillPoint =
|
|
4147
|
+
typeof state.chartDay === 'string' && state.chartDay !== ''
|
|
4148
|
+
? (points.find((point) => point.day === state.chartDay) ?? null)
|
|
4149
|
+
: null
|
|
4150
|
+
const drillHours = drillPoint === null ? null : chartHoursOfPoint(drillPoint)
|
|
4151
|
+
if (drillPoint !== null && drillHours !== null) return buildHourChart(state, drillPoint, drillHours, t)
|
|
4152
|
+
let peak = points[0]
|
|
4153
|
+
for (const point of points) {
|
|
4154
|
+
if (point.total > peak.total) peak = point
|
|
4155
|
+
}
|
|
4156
|
+
const body = buildChartPlot(state, points, {
|
|
4157
|
+
bar: (point, index, meta) => buildDailyChartBar(state, t, point, meta),
|
|
4158
|
+
xLabel: (point) => ({
|
|
4159
|
+
text: chartDayLabel(point.day),
|
|
4160
|
+
attrs: { 'data-chart-axis-day': point.day },
|
|
4161
|
+
}),
|
|
4162
|
+
readout: (hovered, share) =>
|
|
4163
|
+
t('statsChartReadout', {
|
|
4164
|
+
date: hovered.day,
|
|
4165
|
+
value: formatTokensPrecise(hovered.total),
|
|
4166
|
+
share: String(share),
|
|
4167
|
+
}),
|
|
4168
|
+
})
|
|
4169
|
+
return h(
|
|
4170
|
+
'div',
|
|
4171
|
+
{ key: 'chart', className: 'dsh-tokenlens-chart', 'data-chart': 'true' },
|
|
4172
|
+
body,
|
|
3859
4173
|
h(
|
|
3860
4174
|
'div',
|
|
3861
4175
|
{ key: 'summary', className: 'dsh-tokenlens-chart-summary' },
|
|
@@ -4054,6 +4368,83 @@ window.__ModuleLoader__.load({
|
|
|
4054
4368
|
return t('statsLast30Days')
|
|
4055
4369
|
}
|
|
4056
4370
|
|
|
4371
|
+
/**
|
|
4372
|
+
* The 明细 donut windows in display order (v1.2.0): 今日 → 最近7天 →
|
|
4373
|
+
* 最近X天/最近30天 → 总计. Old Hosts omit the 7-day / second windows, so their
|
|
4374
|
+
* pills are hidden (never disabled ghosts of a window that cannot exist).
|
|
4375
|
+
*/
|
|
4376
|
+
function detailsWindowChoices(ctx, t) {
|
|
4377
|
+
const choices = [{ key: 'today', label: t('statsTodayCol') }]
|
|
4378
|
+
if (ctx.hasWeek) choices.push({ key: 'last7Days', label: t('statsLast7Days') })
|
|
4379
|
+
if (ctx.hasMonth) choices.push({ key: 'second', label: detailsSecondLabel(ctx, t) })
|
|
4380
|
+
choices.push({ key: 'total', label: t('statsAllTimeCol') })
|
|
4381
|
+
return choices
|
|
4382
|
+
}
|
|
4383
|
+
|
|
4384
|
+
/**
|
|
4385
|
+
* The active 明细 donut window (v1.2.0): the stored choice while that window
|
|
4386
|
+
* still exists, else 总计. The state lives in the store next to statsView /
|
|
4387
|
+
* statsScope and is never persisted to the plugin settings.
|
|
4388
|
+
*/
|
|
4389
|
+
function detailsWindowOf(state, ctx) {
|
|
4390
|
+
const key = typeof state.detailsWindow === 'string' ? state.detailsWindow : 'total'
|
|
4391
|
+
if (key === 'today') return 'today'
|
|
4392
|
+
if (key === 'last7Days') return ctx.hasWeek ? 'last7Days' : 'total'
|
|
4393
|
+
if (key === 'second') return ctx.hasMonth ? 'second' : 'total'
|
|
4394
|
+
return 'total'
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
/**
|
|
4398
|
+
* The bucket of one 明细 row for a donut window (v1.2.0). Harness/provider
|
|
4399
|
+
* rows carry the four stats windows directly; model rows use the caller's
|
|
4400
|
+
* second-window accessor (`secondModelOf`) for the effective-window column.
|
|
4401
|
+
*/
|
|
4402
|
+
function detailsWindowBucketOf(windowKey, buckets, secondBucketOf) {
|
|
4403
|
+
if (buckets === null || buckets === undefined) return null
|
|
4404
|
+
if (windowKey === 'today') return buckets.today ?? null
|
|
4405
|
+
if (windowKey === 'last7Days') return buckets.last7Days ?? null
|
|
4406
|
+
if (windowKey === 'second') {
|
|
4407
|
+
return typeof secondBucketOf === 'function' ? secondBucketOf(buckets) : buckets.last30Days ?? null
|
|
4408
|
+
}
|
|
4409
|
+
return buckets.allTime ?? null
|
|
4410
|
+
}
|
|
4411
|
+
|
|
4412
|
+
/** The token total of one 明细 row for a donut window (v1.2.0). */
|
|
4413
|
+
function detailsWindowTotalOf(windowKey, buckets, secondBucketOf) {
|
|
4414
|
+
const bucket = detailsWindowBucketOf(windowKey, buckets, secondBucketOf)
|
|
4415
|
+
return bucket === null ? 0 : bucket.total ?? 0
|
|
4416
|
+
}
|
|
4417
|
+
|
|
4418
|
+
/** The window pill row under the 明细 donut title (v1.2.0). */
|
|
4419
|
+
function buildDetailsWindowSwitch(state, ctx, t) {
|
|
4420
|
+
const active = detailsWindowOf(state, ctx)
|
|
4421
|
+
const option = (key, label) =>
|
|
4422
|
+
h(
|
|
4423
|
+
'button',
|
|
4424
|
+
{
|
|
4425
|
+
key,
|
|
4426
|
+
type: 'button',
|
|
4427
|
+
className: 'dsh-tokenlens-scope-option',
|
|
4428
|
+
'data-details-window': key,
|
|
4429
|
+
'data-active': active === key ? 'true' : 'false',
|
|
4430
|
+
'aria-pressed': active === key ? 'true' : 'false',
|
|
4431
|
+
onClick: () => {
|
|
4432
|
+
if (active !== key) store.set({ detailsWindow: key })
|
|
4433
|
+
},
|
|
4434
|
+
},
|
|
4435
|
+
label,
|
|
4436
|
+
)
|
|
4437
|
+
return h(
|
|
4438
|
+
'div',
|
|
4439
|
+
{ key: 'details-window', className: 'dsh-tokenlens-details-window', 'data-details-window-switch': 'true' },
|
|
4440
|
+
h(
|
|
4441
|
+
'div',
|
|
4442
|
+
{ key: 'scope', className: 'dsh-tokenlens-scope', role: 'group', 'aria-label': t('statsWindowLabel') },
|
|
4443
|
+
detailsWindowChoices(ctx, t).map((choice) => option(choice.key, choice.label)),
|
|
4444
|
+
),
|
|
4445
|
+
)
|
|
4446
|
+
}
|
|
4447
|
+
|
|
4057
4448
|
/**
|
|
4058
4449
|
* The compact 明细 table columns of one group (v0.30.2). Harness/provider
|
|
4059
4450
|
* rows start at 今日; the all-harness top-model table starts at the
|
|
@@ -4365,16 +4756,17 @@ window.__ModuleLoader__.load({
|
|
|
4365
4756
|
if (card !== null) section.push(card)
|
|
4366
4757
|
else section.push(h('div', { key: 'no-balance', className: 'dsh-tokenlens-hint' }, t('chipNoUsage')))
|
|
4367
4758
|
} else if (snapshot.provider.kind === 'windows') {
|
|
4759
|
+
// A windows provider may ALSO report an independent wallet balance
|
|
4760
|
+
// (OpenDesign): show it as one extra row of the same capacity card,
|
|
4761
|
+
// after the allowance windows (or after the "no usage" hint).
|
|
4762
|
+
const balanceRow = buildCapacityBalanceRow(snapshot.balance, t)
|
|
4368
4763
|
if (snapshot.windows !== null) {
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
{ key: 'rows', className: 'dsh-tokenlens-rows' },
|
|
4373
|
-
WINDOW_ORDER.map((id) => buildWindowRow(id, snapshot.windows[id], state, now, t, statsRow)),
|
|
4374
|
-
),
|
|
4375
|
-
)
|
|
4764
|
+
const rows = WINDOW_ORDER.map((id) => buildWindowRow(id, snapshot.windows[id], state, now, t, statsRow))
|
|
4765
|
+
if (balanceRow !== null) rows.push(balanceRow)
|
|
4766
|
+
section.push(h('div', { key: 'rows', className: 'dsh-tokenlens-rows' }, rows))
|
|
4376
4767
|
} else {
|
|
4377
4768
|
section.push(h('div', { key: 'no-windows', className: 'dsh-tokenlens-hint' }, t('chipNoUsage')))
|
|
4769
|
+
if (balanceRow !== null) section.push(balanceRow)
|
|
4378
4770
|
}
|
|
4379
4771
|
} else {
|
|
4380
4772
|
section.push(h('div', { key: 'no-kind', className: 'dsh-tokenlens-hint' }, t('chipNoUsage')))
|
|
@@ -4725,10 +5117,18 @@ window.__ModuleLoader__.load({
|
|
|
4725
5117
|
)
|
|
4726
5118
|
}
|
|
4727
5119
|
|
|
4728
|
-
/**
|
|
5120
|
+
/**
|
|
5121
|
+
* The 明细 column in chart mode: donut share charts + ranked legends.
|
|
5122
|
+
*
|
|
5123
|
+
* v1.2.0: a 今日 / 最近7天 / 最近X天 / 总计 window switch under the title picks
|
|
5124
|
+
* which stats window the three donuts aggregate (default 总计). The pills of
|
|
5125
|
+
* windows an old Host does not report are hidden; the state lives in the store
|
|
5126
|
+
* (never persisted to the plugin settings).
|
|
5127
|
+
*/
|
|
4729
5128
|
function buildStatsDetailsCharts(state, ctx, t) {
|
|
4730
|
-
const { stats
|
|
5129
|
+
const { stats } = ctx
|
|
4731
5130
|
if (stats === null) return null
|
|
5131
|
+
const windowKey = detailsWindowOf(state, ctx)
|
|
4732
5132
|
const children = [
|
|
4733
5133
|
h(
|
|
4734
5134
|
'div',
|
|
@@ -4740,17 +5140,18 @@ window.__ModuleLoader__.load({
|
|
|
4740
5140
|
h('span', { key: 'label' }, t('statsDetailsTitle')),
|
|
4741
5141
|
),
|
|
4742
5142
|
),
|
|
5143
|
+
buildDetailsWindowSwitch(state, ctx, t),
|
|
4743
5144
|
]
|
|
5145
|
+
const groups = []
|
|
4744
5146
|
const harnessRows = scopedHarnessRows(ctx).map((row) => ({
|
|
4745
5147
|
label: row.label,
|
|
4746
|
-
value:
|
|
5148
|
+
value: detailsWindowTotalOf(windowKey, row.buckets, ctx.secondBucketOf),
|
|
4747
5149
|
}))
|
|
4748
|
-
const groups = []
|
|
4749
5150
|
const harnessChart = buildBreakdownDonutGroup('harness-chart', t('statsByHarness'), 'list', harnessRows, t)
|
|
4750
5151
|
if (harnessChart !== null) groups.push(harnessChart)
|
|
4751
5152
|
const providerRows = stats.providers.map((provider) => ({
|
|
4752
5153
|
label: provider.label,
|
|
4753
|
-
value: ctx.secondBucketOf
|
|
5154
|
+
value: detailsWindowTotalOf(windowKey, provider, ctx.secondBucketOf),
|
|
4754
5155
|
}))
|
|
4755
5156
|
const providerChart = buildBreakdownDonutGroup('provider-chart', t('statsByProvider'), 'server', providerRows, t)
|
|
4756
5157
|
if (providerChart !== null) groups.push(providerChart)
|
|
@@ -4759,18 +5160,25 @@ window.__ModuleLoader__.load({
|
|
|
4759
5160
|
// models from other providers silently disappear from the donut (v0.27.1).
|
|
4760
5161
|
const modelRows = stats.models.map((model) => ({
|
|
4761
5162
|
label: model.modelId,
|
|
4762
|
-
value: ctx.secondModelOf
|
|
5163
|
+
value: detailsWindowTotalOf(windowKey, model, ctx.secondModelOf),
|
|
4763
5164
|
}))
|
|
4764
5165
|
const modelChart = buildBreakdownDonutGroup('model-chart', t('statsTopModels'), 'cube', modelRows, t)
|
|
4765
5166
|
if (modelChart !== null) groups.push(modelChart)
|
|
4766
5167
|
if (groups.length > 0) {
|
|
4767
5168
|
children.push(h('div', { key: 'charts', className: 'dsh-tokenlens-details-charts-grid' }, groups))
|
|
4768
|
-
}
|
|
4769
|
-
|
|
5169
|
+
} else {
|
|
5170
|
+
// An all-zero window renders no donut at all: keep the readable hint.
|
|
4770
5171
|
children.push(h('div', { key: 'empty', className: 'dsh-tokenlens-hint' }, t('statsEmpty')))
|
|
4771
5172
|
}
|
|
4772
|
-
|
|
4773
|
-
|
|
5173
|
+
return h(
|
|
5174
|
+
'div',
|
|
5175
|
+
{
|
|
5176
|
+
key: 'details',
|
|
5177
|
+
className: 'dsh-tokenlens-section dsh-tokenlens-details',
|
|
5178
|
+
'data-details-window': windowKey,
|
|
5179
|
+
},
|
|
5180
|
+
children,
|
|
5181
|
+
)
|
|
4774
5182
|
}
|
|
4775
5183
|
|
|
4776
5184
|
function buildStatsDetails(state, ctx, t) {
|