dsh-tokenlens 1.1.0 → 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 +8 -0
- package/PROTOCOL.md +23 -3
- package/README.en.md +4 -4
- package/README.md +5 -5
- package/client.js +482 -107
- package/index.js +1 -1
- 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}
|
|
@@ -3555,7 +3609,7 @@ window.__ModuleLoader__.load({
|
|
|
3555
3609
|
'data-scope': scope,
|
|
3556
3610
|
'data-active': effective === scope ? 'true' : 'false',
|
|
3557
3611
|
disabled: stats === null,
|
|
3558
|
-
onClick: () => store.set({ statsScope: scope, chartHover: null }),
|
|
3612
|
+
onClick: () => store.set({ statsScope: scope, chartHover: null, chartDay: null }),
|
|
3559
3613
|
},
|
|
3560
3614
|
label,
|
|
3561
3615
|
)
|
|
@@ -3581,7 +3635,7 @@ window.__ModuleLoader__.load({
|
|
|
3581
3635
|
'data-active': active === view ? 'true' : 'false',
|
|
3582
3636
|
'aria-pressed': active === view ? 'true' : 'false',
|
|
3583
3637
|
onClick: () => {
|
|
3584
|
-
if (active !== view) store.set({ statsView: view, chartHover: null })
|
|
3638
|
+
if (active !== view) store.set({ statsView: view, chartHover: null, chartDay: null })
|
|
3585
3639
|
},
|
|
3586
3640
|
},
|
|
3587
3641
|
Icon(iconName, 12),
|
|
@@ -3601,37 +3655,88 @@ window.__ModuleLoader__.load({
|
|
|
3601
3655
|
}
|
|
3602
3656
|
|
|
3603
3657
|
/**
|
|
3604
|
-
* 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).
|
|
3605
3718
|
*
|
|
3606
3719
|
* Provider scope reads the selected provider's daily bucket and limits the
|
|
3607
3720
|
* window to the provider's effective day when it is subscription-backed
|
|
3608
3721
|
* (fallback: the last 30 days). The all-harness scope uses the day totals of
|
|
3609
|
-
* 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.
|
|
3610
3724
|
*/
|
|
3611
3725
|
function chartSeriesOf(ctx) {
|
|
3612
3726
|
const stats = ctx.stats
|
|
3613
3727
|
if (stats === null) return []
|
|
3614
3728
|
const daily = stats.daily
|
|
3615
3729
|
if (ctx.scope === 'all') {
|
|
3616
|
-
return daily.slice(-30).map((entry) => ({
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
if (ctx.currentRow !== null && typeof ctx.currentRow.providerId === 'string' && ctx.currentRow.providerId !== '') {
|
|
3622
|
-
wanted.push(ctx.currentRow.providerId)
|
|
3623
|
-
}
|
|
3624
|
-
if (typeof ctx.selectedProviderId === 'string' && ctx.selectedProviderId !== '') {
|
|
3625
|
-
wanted.push(ctx.selectedProviderId)
|
|
3626
|
-
}
|
|
3627
|
-
const canonicalWanted = new Set(wanted.map((id) => canonicalProviderId(id)))
|
|
3628
|
-
const valueOf = (entry) => {
|
|
3629
|
-
for (const [key, bucket] of Object.entries(entry.providers)) {
|
|
3630
|
-
if (wanted.includes(key) || canonicalWanted.has(canonicalProviderId(key))) return bucket?.total ?? 0
|
|
3631
|
-
}
|
|
3632
|
-
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
|
+
}))
|
|
3633
3735
|
}
|
|
3634
|
-
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
|
+
})
|
|
3635
3740
|
const effectiveDay = ctx.currentRow !== null ? ctx.currentRow.effectiveDay : ''
|
|
3636
3741
|
if (ctx.subscribed && typeof effectiveDay === 'string' && effectiveDay !== '') {
|
|
3637
3742
|
return series.filter((point) => point.day >= effectiveDay)
|
|
@@ -3710,7 +3815,6 @@ window.__ModuleLoader__.load({
|
|
|
3710
3815
|
if (store.get().chartHover !== next) store.set({ chartHover: next })
|
|
3711
3816
|
}
|
|
3712
3817
|
|
|
3713
|
-
/** The pure-CSS daily bar chart: axes + live pointer readout (v0.25.0; no deps). */
|
|
3714
3818
|
/** The active today bucket of the current stats scope (v0.27.2). */
|
|
3715
3819
|
function todayBucketOf(ctx) {
|
|
3716
3820
|
return ctx.providerScoped ? ctx.currentRow?.today ?? null : ctx.stats?.today ?? null
|
|
@@ -3727,37 +3831,25 @@ window.__ModuleLoader__.load({
|
|
|
3727
3831
|
return ctx.providerScoped ? ctx.currentRow?.last7Days ?? null : ctx.stats?.last7Days ?? null
|
|
3728
3832
|
}
|
|
3729
3833
|
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
h(
|
|
3742
|
-
'span',
|
|
3743
|
-
{ key: 'hint', className: 'dsh-tokenlens-hint dsh-tokenlens-chart-empty-hint' },
|
|
3744
|
-
t('statsChartEmptyHint'),
|
|
3745
|
-
),
|
|
3746
|
-
),
|
|
3747
|
-
)
|
|
3748
|
-
}
|
|
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) {
|
|
3749
3845
|
const max = Math.max(...points.map((point) => point.total), 1)
|
|
3750
|
-
|
|
3751
|
-
for (const point of points) {
|
|
3752
|
-
if (point.total > peak.total) peak = point
|
|
3753
|
-
}
|
|
3846
|
+
const total = points.reduce((sum, point) => sum + point.total, 0)
|
|
3754
3847
|
const hoverIndex =
|
|
3755
3848
|
Number.isInteger(state.chartHover) && state.chartHover >= 0 && state.chartHover < points.length
|
|
3756
3849
|
? state.chartHover
|
|
3757
3850
|
: null
|
|
3758
3851
|
const hovered = hoverIndex === null ? null : points[hoverIndex]
|
|
3759
|
-
const hoverShare =
|
|
3760
|
-
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
|
|
3761
3853
|
const handleMove = (event) => {
|
|
3762
3854
|
const index = chartIndexFromEvent(event, points.length)
|
|
3763
3855
|
if (index !== null) setChartHover(index)
|
|
@@ -3783,32 +3875,10 @@ window.__ModuleLoader__.load({
|
|
|
3783
3875
|
tick.text,
|
|
3784
3876
|
),
|
|
3785
3877
|
)
|
|
3786
|
-
const bars = points.map((point, index) => {
|
|
3787
|
-
const height = point.total > 0 ? Math.max(2, Math.round((point.total / max) * 1000) / 10) : 0
|
|
3788
|
-
const isHovered = index === hoverIndex
|
|
3789
|
-
return h(
|
|
3790
|
-
'div',
|
|
3791
|
-
{
|
|
3792
|
-
key: point.day,
|
|
3793
|
-
className: 'dsh-tokenlens-chart-bar-wrap',
|
|
3794
|
-
'data-chart-day': point.day,
|
|
3795
|
-
'data-chart-total': String(point.total),
|
|
3796
|
-
'data-chart-hover': isHovered ? 'true' : 'false',
|
|
3797
|
-
title: t('statsChartBarTitle', {
|
|
3798
|
-
date: point.day,
|
|
3799
|
-
tokens: formatTokensPrecise(point.total),
|
|
3800
|
-
}),
|
|
3801
|
-
},
|
|
3802
|
-
h('div', {
|
|
3803
|
-
className: 'dsh-tokenlens-chart-bar',
|
|
3804
|
-
'data-empty': point.total > 0 ? 'false' : 'true',
|
|
3805
|
-
'data-height': String(height),
|
|
3806
|
-
style: { height: `${height}%` },
|
|
3807
|
-
}),
|
|
3808
|
-
)
|
|
3809
|
-
})
|
|
3878
|
+
const bars = points.map((point, index) => spec.bar(point, index, { max, hovered: index === hoverIndex }))
|
|
3810
3879
|
const xLabels = chartXLabelIndices(points.length).map((index) => {
|
|
3811
3880
|
const point = points[index]
|
|
3881
|
+
const label = spec.xLabel(point, index)
|
|
3812
3882
|
const first = index === 0
|
|
3813
3883
|
const last = index === points.length - 1
|
|
3814
3884
|
const style = first
|
|
@@ -3819,12 +3889,12 @@ window.__ModuleLoader__.load({
|
|
|
3819
3889
|
return h(
|
|
3820
3890
|
'span',
|
|
3821
3891
|
{
|
|
3822
|
-
key: `x-${
|
|
3892
|
+
key: `x-${index}`,
|
|
3823
3893
|
className: 'dsh-tokenlens-chart-x-label',
|
|
3824
|
-
|
|
3894
|
+
...(label.attrs === null || label.attrs === undefined ? {} : label.attrs),
|
|
3825
3895
|
style,
|
|
3826
3896
|
},
|
|
3827
|
-
|
|
3897
|
+
label.text,
|
|
3828
3898
|
)
|
|
3829
3899
|
})
|
|
3830
3900
|
const crosshair =
|
|
@@ -3857,37 +3927,249 @@ window.__ModuleLoader__.load({
|
|
|
3857
3927
|
role: 'status',
|
|
3858
3928
|
style: readoutStyle,
|
|
3859
3929
|
},
|
|
3860
|
-
|
|
3861
|
-
date: hovered.day,
|
|
3862
|
-
value: formatTokensPrecise(hovered.total),
|
|
3863
|
-
share: String(hoverShare),
|
|
3864
|
-
}),
|
|
3930
|
+
spec.readout(hovered, hoverShare),
|
|
3865
3931
|
)
|
|
3866
3932
|
return h(
|
|
3867
3933
|
'div',
|
|
3868
|
-
{ key: '
|
|
3934
|
+
{ key: 'body', className: 'dsh-tokenlens-chart-body' },
|
|
3935
|
+
h('div', { key: 'y-axis', className: 'dsh-tokenlens-chart-yaxis', 'aria-hidden': 'true' }, yTicks),
|
|
3869
3936
|
h(
|
|
3870
3937
|
'div',
|
|
3871
|
-
{
|
|
3872
|
-
|
|
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,
|
|
3873
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(
|
|
3874
4069
|
'div',
|
|
3875
4070
|
{
|
|
3876
|
-
key:
|
|
3877
|
-
className: 'dsh-tokenlens-chart-
|
|
3878
|
-
'data-chart-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
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
|
+
}),
|
|
3883
4081
|
},
|
|
3884
|
-
h('div', {
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
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) }),
|
|
3889
4118
|
),
|
|
3890
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,
|
|
3891
4173
|
h(
|
|
3892
4174
|
'div',
|
|
3893
4175
|
{ key: 'summary', className: 'dsh-tokenlens-chart-summary' },
|
|
@@ -4086,6 +4368,83 @@ window.__ModuleLoader__.load({
|
|
|
4086
4368
|
return t('statsLast30Days')
|
|
4087
4369
|
}
|
|
4088
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
|
+
|
|
4089
4448
|
/**
|
|
4090
4449
|
* The compact 明细 table columns of one group (v0.30.2). Harness/provider
|
|
4091
4450
|
* rows start at 今日; the all-harness top-model table starts at the
|
|
@@ -4758,10 +5117,18 @@ window.__ModuleLoader__.load({
|
|
|
4758
5117
|
)
|
|
4759
5118
|
}
|
|
4760
5119
|
|
|
4761
|
-
/**
|
|
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
|
+
*/
|
|
4762
5128
|
function buildStatsDetailsCharts(state, ctx, t) {
|
|
4763
|
-
const { stats
|
|
5129
|
+
const { stats } = ctx
|
|
4764
5130
|
if (stats === null) return null
|
|
5131
|
+
const windowKey = detailsWindowOf(state, ctx)
|
|
4765
5132
|
const children = [
|
|
4766
5133
|
h(
|
|
4767
5134
|
'div',
|
|
@@ -4773,17 +5140,18 @@ window.__ModuleLoader__.load({
|
|
|
4773
5140
|
h('span', { key: 'label' }, t('statsDetailsTitle')),
|
|
4774
5141
|
),
|
|
4775
5142
|
),
|
|
5143
|
+
buildDetailsWindowSwitch(state, ctx, t),
|
|
4776
5144
|
]
|
|
5145
|
+
const groups = []
|
|
4777
5146
|
const harnessRows = scopedHarnessRows(ctx).map((row) => ({
|
|
4778
5147
|
label: row.label,
|
|
4779
|
-
value:
|
|
5148
|
+
value: detailsWindowTotalOf(windowKey, row.buckets, ctx.secondBucketOf),
|
|
4780
5149
|
}))
|
|
4781
|
-
const groups = []
|
|
4782
5150
|
const harnessChart = buildBreakdownDonutGroup('harness-chart', t('statsByHarness'), 'list', harnessRows, t)
|
|
4783
5151
|
if (harnessChart !== null) groups.push(harnessChart)
|
|
4784
5152
|
const providerRows = stats.providers.map((provider) => ({
|
|
4785
5153
|
label: provider.label,
|
|
4786
|
-
value: ctx.secondBucketOf
|
|
5154
|
+
value: detailsWindowTotalOf(windowKey, provider, ctx.secondBucketOf),
|
|
4787
5155
|
}))
|
|
4788
5156
|
const providerChart = buildBreakdownDonutGroup('provider-chart', t('statsByProvider'), 'server', providerRows, t)
|
|
4789
5157
|
if (providerChart !== null) groups.push(providerChart)
|
|
@@ -4792,18 +5160,25 @@ window.__ModuleLoader__.load({
|
|
|
4792
5160
|
// models from other providers silently disappear from the donut (v0.27.1).
|
|
4793
5161
|
const modelRows = stats.models.map((model) => ({
|
|
4794
5162
|
label: model.modelId,
|
|
4795
|
-
value: ctx.secondModelOf
|
|
5163
|
+
value: detailsWindowTotalOf(windowKey, model, ctx.secondModelOf),
|
|
4796
5164
|
}))
|
|
4797
5165
|
const modelChart = buildBreakdownDonutGroup('model-chart', t('statsTopModels'), 'cube', modelRows, t)
|
|
4798
5166
|
if (modelChart !== null) groups.push(modelChart)
|
|
4799
5167
|
if (groups.length > 0) {
|
|
4800
5168
|
children.push(h('div', { key: 'charts', className: 'dsh-tokenlens-details-charts-grid' }, groups))
|
|
4801
|
-
}
|
|
4802
|
-
|
|
5169
|
+
} else {
|
|
5170
|
+
// An all-zero window renders no donut at all: keep the readable hint.
|
|
4803
5171
|
children.push(h('div', { key: 'empty', className: 'dsh-tokenlens-hint' }, t('statsEmpty')))
|
|
4804
5172
|
}
|
|
4805
|
-
|
|
4806
|
-
|
|
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
|
+
)
|
|
4807
5182
|
}
|
|
4808
5183
|
|
|
4809
5184
|
function buildStatsDetails(state, ctx, t) {
|