dsh-token-chart 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +200 -2
- package/cordis.patch.yml +7 -0
- package/icon.svg +7 -0
- package/lib/buckets.js +115 -0
- package/lib/chunk/echarts.entry.js +25 -0
- package/lib/client.echarts.js +58 -0
- package/lib/client.js +1743 -0
- package/lib/constants.js +22 -0
- package/lib/index.js +640 -0
- package/lib/paths.js +24 -0
- package/lib/prefs.js +54 -0
- package/lib/store.js +273 -0
- package/lib/token-chart.css +568 -0
- package/lib/types/buckets.d.ts +24 -0
- package/lib/types/chunk/echarts.entry.d.ts +11 -0
- package/lib/types/constants.d.ts +22 -0
- package/lib/types/index.d.ts +8 -0
- package/lib/types/paths.d.ts +3 -0
- package/lib/types/prefs.d.ts +8 -0
- package/lib/types/store.d.ts +72 -0
- package/lib/types/types.d.ts +230 -0
- package/lib/types/update.d.ts +24 -0
- package/lib/types.js +10 -0
- package/lib/update.js +110 -0
- package/locale/en.json +4 -0
- package/locale/zh.json +4 -0
- package/package.json +110 -4
- package/src/buckets.ts +120 -0
- package/src/chunk/echarts.entry.ts +27 -0
- package/src/client/api.ts +126 -0
- package/src/client/charts.tsx +255 -0
- package/src/client/chrome.tsx +127 -0
- package/src/client/contract.ts +62 -0
- package/src/client/drawer.tsx +113 -0
- package/src/client/format.ts +36 -0
- package/src/client/globals.d.ts +21 -0
- package/src/client/index.tsx +227 -0
- package/src/client/locales.ts +164 -0
- package/src/client/models.ts +76 -0
- package/src/client/mount.tsx +312 -0
- package/src/client/panel.tsx +152 -0
- package/src/client/pill.ts +21 -0
- package/src/client/react.ts +12 -0
- package/src/client/series.tsx +140 -0
- package/src/client/settings.tsx +347 -0
- package/src/client/styles/components.less +191 -0
- package/src/client/styles/index.less +7 -0
- package/src/client/styles/tailwind.css +21 -0
- package/src/client/styles/tokens.less +25 -0
- package/src/client/table.tsx +153 -0
- package/src/client/ui.tsx +87 -0
- package/src/constants.ts +26 -0
- package/src/index.ts +673 -0
- package/src/paths.ts +28 -0
- package/src/prefs.ts +61 -0
- package/src/store.ts +295 -0
- package/src/types.ts +216 -0
- package/src/update.ts +106 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-wide constants shared by every client module: the API
|
|
3
|
+
* prefix, the entry id, colors, chart vocabulary and preference defaults.
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-token-chart/client/contract
|
|
6
|
+
*/
|
|
7
|
+
import type { Prefs } from '../types.ts'
|
|
8
|
+
|
|
9
|
+
import { CHART_IDS, METRIC_IDS as SHARED_METRIC_IDS } from '../constants.ts'
|
|
10
|
+
import type { ChartId, MetricId } from '../types.ts'
|
|
11
|
+
|
|
12
|
+
import type { Translate } from './models.ts'
|
|
13
|
+
|
|
14
|
+
export const API = '/token-chart/api'
|
|
15
|
+
export const NS = 'token-chart'
|
|
16
|
+
/** Right-sidebar tab type id and kind; also the slot key of its seats. */
|
|
17
|
+
export const KIND = 'dsh-token-chart'
|
|
18
|
+
/** Left sidebar panel id and the key of its `main` seat. */
|
|
19
|
+
export const PANEL_ID = 'token-chart'
|
|
20
|
+
|
|
21
|
+
export const COLOR = {
|
|
22
|
+
total: 'var(--dsw-alias-brand-primary)',
|
|
23
|
+
input: 'var(--dsw-alias-state-warn-primary)',
|
|
24
|
+
output: 'var(--dsw-alias-state-success-primary)',
|
|
25
|
+
cache: 'var(--dsw-alias-state-idle-primary)',
|
|
26
|
+
cacheHitRate: 'var(--dsw-alias-state-error-primary)',
|
|
27
|
+
}
|
|
28
|
+
export const METRIC_COLOR = {
|
|
29
|
+
total: COLOR.total,
|
|
30
|
+
input: COLOR.input,
|
|
31
|
+
output: COLOR.output,
|
|
32
|
+
cache: COLOR.cache,
|
|
33
|
+
cacheHitRate: COLOR.cacheHitRate,
|
|
34
|
+
}
|
|
35
|
+
export const CHART_KINDS: ChartId[] = [...CHART_IDS]
|
|
36
|
+
export const METRIC_IDS: MetricId[] = [...SHARED_METRIC_IDS]
|
|
37
|
+
export const DEFAULT_PREFS: Prefs = {
|
|
38
|
+
sessionChart: 'multi-bar',
|
|
39
|
+
dailyChart: 'multi-bar',
|
|
40
|
+
sessionMetrics: ['input', 'output', 'cache', 'total'],
|
|
41
|
+
dailyMetrics: ['input', 'output', 'cache', 'total'],
|
|
42
|
+
showTable: true,
|
|
43
|
+
showCacheHitRate: true,
|
|
44
|
+
interceptUsageButton: true,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* ------------------------------------------------------------ locale */
|
|
48
|
+
|
|
49
|
+
/** What the shell hands a browser-half plugin when it mounts `apply`. */
|
|
50
|
+
export interface ClientContext {
|
|
51
|
+
effect: (callback: () => unknown, description?: string) => unknown
|
|
52
|
+
inject: (deps: string[], callback: (services: Record<string, any>) => unknown) => unknown
|
|
53
|
+
logger?: { info?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void }
|
|
54
|
+
locale: {
|
|
55
|
+
bind: (namespace: string) => Translate
|
|
56
|
+
register: (namespace: string, dictionaries: { zh: Record<string, string>; en: Record<string, string> }) => unknown
|
|
57
|
+
}
|
|
58
|
+
slots: {
|
|
59
|
+
inject: (name: string, callback: () => unknown) => () => void
|
|
60
|
+
register: (options: Record<string, unknown>, component?: unknown) => unknown
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session drawer body shown in the right sidebar.
|
|
3
|
+
*
|
|
4
|
+
* @module dsh-token-chart/client/drawer
|
|
5
|
+
*/
|
|
6
|
+
import type { SessionReport } from '../types.ts'
|
|
7
|
+
|
|
8
|
+
import type { ChartPoint, TableRow } from './models.ts'
|
|
9
|
+
|
|
10
|
+
import { useEndpoint } from './api.ts'
|
|
11
|
+
|
|
12
|
+
import type { ColumnSpec, Translate } from './models.ts'
|
|
13
|
+
|
|
14
|
+
import { React } from './react.ts'
|
|
15
|
+
|
|
16
|
+
import { usePrefs } from './api.ts'
|
|
17
|
+
import { clockText, exactTokens, fill, percentText } from './format.ts'
|
|
18
|
+
import { ChartBlock } from './series.tsx'
|
|
19
|
+
import { KpiRow, UsageFacts } from './ui.tsx'
|
|
20
|
+
|
|
21
|
+
export function SessionUsageBody({ sessionId, t }: { sessionId: string; t: Translate }) {
|
|
22
|
+
const prefs = usePrefs()
|
|
23
|
+
const [tick, setTick] = React.useState(0)
|
|
24
|
+
const endpoint = useEndpoint(`/session?sessionId=${encodeURIComponent(sessionId ?? '')}`, tick)
|
|
25
|
+
const report = endpoint.value as SessionReport | undefined
|
|
26
|
+
const totals = report?.totals
|
|
27
|
+
const turns: TableRow[] = Array.isArray(report?.turns) ? (report.turns as TableRow[]) : []
|
|
28
|
+
const points: ChartPoint[] = turns.map((turn: TableRow) => ({
|
|
29
|
+
label: `#${turn.turn}`,
|
|
30
|
+
values: {
|
|
31
|
+
input: turn.input,
|
|
32
|
+
output: turn.output,
|
|
33
|
+
cache: turn.cache,
|
|
34
|
+
total: turn.total,
|
|
35
|
+
cacheHitRate: turn.cacheHitRate,
|
|
36
|
+
},
|
|
37
|
+
}))
|
|
38
|
+
const columns: ColumnSpec[] = [
|
|
39
|
+
{ key: 'turn', label: t('table.turn') },
|
|
40
|
+
{ key: 'time', label: t('table.time'), render: (row) => clockText(row.time) },
|
|
41
|
+
{ key: 'total', label: t('metric.total'), align: 'right', render: (row) => exactTokens(row.total) },
|
|
42
|
+
{ key: 'input', label: t('metric.input'), align: 'right', render: (row) => exactTokens(row.input) },
|
|
43
|
+
{ key: 'output', label: t('metric.output'), align: 'right', render: (row) => exactTokens(row.output) },
|
|
44
|
+
{ key: 'cache', label: t('metric.cache'), align: 'right', render: (row) => exactTokens(row.cache) },
|
|
45
|
+
{
|
|
46
|
+
key: 'cacheHitRate',
|
|
47
|
+
label: t('metric.cacheHitRate'),
|
|
48
|
+
align: 'right',
|
|
49
|
+
render: (row) => percentText(row.cacheHitRate),
|
|
50
|
+
},
|
|
51
|
+
]
|
|
52
|
+
const routes: Array<[string, string]> =
|
|
53
|
+
Array.isArray(totals?.routes) && totals.routes.length > 0 ? [[t('usage.route'), totals.routes.join(', ')]] : []
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="dtc-root dtc-drawer">
|
|
57
|
+
{
|
|
58
|
+
<div className="dtc-head">
|
|
59
|
+
<div>
|
|
60
|
+
<h3 className="dtc-title">{t('usage.title')}</h3>
|
|
61
|
+
<p className="dtc-subtitle">{`${t('usage.session')} · ${turns.length} ${t('usage.turns')}`}</p>
|
|
62
|
+
</div>
|
|
63
|
+
{
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
className="dtc-button"
|
|
67
|
+
onClick={() => setTick((value) => value + 1)}
|
|
68
|
+
title={t('usage.refresh')}
|
|
69
|
+
>
|
|
70
|
+
{t('panel.refresh')}
|
|
71
|
+
</button>
|
|
72
|
+
}
|
|
73
|
+
</div>
|
|
74
|
+
}
|
|
75
|
+
{endpoint.loading && report === undefined ? (
|
|
76
|
+
<p className="dtc-empty">{t('panel.loading')}</p>
|
|
77
|
+
) : endpoint.error !== undefined ? (
|
|
78
|
+
<p className="dtc-error">{endpoint.error}</p>
|
|
79
|
+
) : turns.length === 0 ? (
|
|
80
|
+
<p className="dtc-empty">{t('usage.emptySession')}</p>
|
|
81
|
+
) : (
|
|
82
|
+
<React.Fragment>
|
|
83
|
+
<div className="dtc-banner">
|
|
84
|
+
<span className="dtc-banner-label">{t('metric.total')}</span>
|
|
85
|
+
<span className="dtc-banner-value">{exactTokens(totals?.total)}</span>
|
|
86
|
+
</div>
|
|
87
|
+
<UsageFacts totals={totals} t={t} extra={routes} />
|
|
88
|
+
{report?.crossCheck?.matches === false ? (
|
|
89
|
+
<p className="dtc-warn" data-dtc-crosscheck="mismatch">
|
|
90
|
+
{fill(t('usage.crossCheckFailed'), { product: exactTokens(report.crossCheck.total) })}
|
|
91
|
+
</p>
|
|
92
|
+
) : null}
|
|
93
|
+
<p className="dtc-footnote">{t('usage.accounting')}</p>
|
|
94
|
+
<KpiRow totals={totals} t={t} />
|
|
95
|
+
<div className="dtc-section-title">{t('settings.sessionChart')}</div>
|
|
96
|
+
<ChartBlock
|
|
97
|
+
kind={prefs.sessionChart}
|
|
98
|
+
points={points}
|
|
99
|
+
totals={totals}
|
|
100
|
+
metrics={prefs.sessionMetrics}
|
|
101
|
+
columns={columns}
|
|
102
|
+
rows={turns}
|
|
103
|
+
showTable={prefs.showTable}
|
|
104
|
+
showHitRate={prefs.showCacheHitRate}
|
|
105
|
+
t={t}
|
|
106
|
+
/>
|
|
107
|
+
</React.Fragment>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* -------------------------------------------------- daily usage panel */
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Number, rate and time formatting shared by charts, tables and facts.
|
|
3
|
+
*
|
|
4
|
+
* @module dsh-token-chart/client/format
|
|
5
|
+
*/
|
|
6
|
+
export function compactTokens(value: unknown): string {
|
|
7
|
+
const number = Number(value) || 0
|
|
8
|
+
if (Math.abs(number) >= 1e9) return `${(number / 1e9).toFixed(2)}B`
|
|
9
|
+
if (Math.abs(number) >= 1e6) return `${(number / 1e6).toFixed(2)}M`
|
|
10
|
+
if (Math.abs(number) >= 1e4) return `${(number / 1e3).toFixed(1)}K`
|
|
11
|
+
return String(Math.round(number))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function exactTokens(value: unknown): string {
|
|
15
|
+
return (Number(value) || 0).toLocaleString('en-US')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function percentText(rate: unknown): string {
|
|
19
|
+
return `${((Number(rate) || 0) * 100).toFixed(1)}%`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function clockText(time: unknown, withSeconds = false): string {
|
|
23
|
+
const date = new Date(Number(time) || Date.now())
|
|
24
|
+
const pad = (input: number): string => String(input).padStart(2, '0')
|
|
25
|
+
const base = `${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
26
|
+
return withSeconds ? `${base}:${pad(date.getSeconds())}` : base
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Translate one template with `{name}` placeholders. */
|
|
30
|
+
export function fill(template: string, values: Record<string, string | number>): string {
|
|
31
|
+
return String(template).replace(/\{(\w+)\}/g, (_match, key) =>
|
|
32
|
+
values[key] === undefined ? `{${key}}` : String(values[key]),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ------------------------------------------------------- prefs store */
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient declarations for the loader-provided globals a browser half sees.
|
|
3
|
+
*
|
|
4
|
+
* `require` is not Node's: the client module system passes a function whose
|
|
5
|
+
* `async` member loads a declared package-local chunk. `window.__ModuleLoader__`
|
|
6
|
+
* is the registration entry point every client bundle calls.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-token-chart/client/globals
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** The chunk-aware require the loader injects into a client factory. */
|
|
12
|
+
declare const require: {
|
|
13
|
+
(specifier: string): any
|
|
14
|
+
async: (chunk: string) => Promise<any>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Window {
|
|
18
|
+
__ModuleLoader__: {
|
|
19
|
+
load: (registration: { id: string; chunk?: string; factory: (require: any) => unknown }) => void
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-token-chart — browser half entry.
|
|
3
|
+
*
|
|
4
|
+
* Registers the plugin's seats (settings section, sidebar entry + panel, the
|
|
5
|
+
* right-sidebar drawer, the session-header action), intercepts the product's
|
|
6
|
+
* own usage pill, and keeps the preference store warm for every seat.
|
|
7
|
+
*
|
|
8
|
+
* Built by scripts/build-client.mjs into lib/client.js as a
|
|
9
|
+
* `window.__ModuleLoader__` factory — the DSH client-module contract, the same
|
|
10
|
+
* shape the shipped plugins use.
|
|
11
|
+
*
|
|
12
|
+
* @module dsh-token-chart/client
|
|
13
|
+
*/
|
|
14
|
+
import type { Translate } from './models.ts'
|
|
15
|
+
|
|
16
|
+
import { DEFAULT_PREFS, KIND, NS, PANEL_ID } from './contract.ts'
|
|
17
|
+
import { apiGet, prefsStore } from './api.ts'
|
|
18
|
+
import { HeaderUsageButton, installStyles, PanelIcon, registerSettingsNavIcon } from './chrome.tsx'
|
|
19
|
+
import { isUsagePill } from './pill.ts'
|
|
20
|
+
import { en, zh } from './locales.ts'
|
|
21
|
+
import { DailyUsagePanel } from './panel.tsx'
|
|
22
|
+
import { SessionUsageBody } from './drawer.tsx'
|
|
23
|
+
import { TokenChartSettings } from './settings.tsx'
|
|
24
|
+
import type { ClientContext } from './contract.ts'
|
|
25
|
+
|
|
26
|
+
export const inject = ['slots', 'locale']
|
|
27
|
+
|
|
28
|
+
export function apply(ctx: ClientContext): void {
|
|
29
|
+
const t = ctx.locale.bind(NS)
|
|
30
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-token-chart: dictionaries')
|
|
31
|
+
ctx.effect(() => installStyles(), 'dsh-token-chart: styles')
|
|
32
|
+
ctx.effect(() => registerSettingsNavIcon(() => t('settings.nav')), 'dsh-token-chart: settings nav icon')
|
|
33
|
+
|
|
34
|
+
/** Optional services captured as they appear. */
|
|
35
|
+
const services: { sidebarRight: any; sidebarRightTabs: any } = {
|
|
36
|
+
sidebarRight: undefined,
|
|
37
|
+
sidebarRightTabs: undefined,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const openDrawer = (sessionId: string): boolean => {
|
|
41
|
+
const right = services.sidebarRight
|
|
42
|
+
if (right === undefined || typeof sessionId !== 'string' || sessionId === '') return false
|
|
43
|
+
try {
|
|
44
|
+
right.openTab(KIND, { params: { sessionId } })
|
|
45
|
+
return true
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.warn('[dsh-token-chart] could not open the usage drawer:', error)
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const mountedSessionId = () => {
|
|
53
|
+
const right = services.sidebarRight
|
|
54
|
+
if (right === undefined || right.mounted === undefined) return undefined
|
|
55
|
+
try {
|
|
56
|
+
return right.mounted.getSnapshot()
|
|
57
|
+
} catch {
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Warm the preference store for every seat. */
|
|
63
|
+
ctx.effect(() => {
|
|
64
|
+
let live = true
|
|
65
|
+
apiGet('/prefs')
|
|
66
|
+
.then((payload) => {
|
|
67
|
+
if (live) prefsStore.set({ ...DEFAULT_PREFS, ...payload.value })
|
|
68
|
+
})
|
|
69
|
+
.catch(() => {
|
|
70
|
+
/* defaults */
|
|
71
|
+
})
|
|
72
|
+
return () => {
|
|
73
|
+
live = false
|
|
74
|
+
}
|
|
75
|
+
}, 'dsh-token-chart: preferences')
|
|
76
|
+
|
|
77
|
+
/* ---- settings page ------------------------------------------- */
|
|
78
|
+
|
|
79
|
+
ctx.slots.inject('settings.section', () =>
|
|
80
|
+
ctx.slots.register(
|
|
81
|
+
{
|
|
82
|
+
name: 'settings.section',
|
|
83
|
+
id: 'token-chart',
|
|
84
|
+
order: 30,
|
|
85
|
+
label: () => t('settings.nav'),
|
|
86
|
+
locale: NS,
|
|
87
|
+
},
|
|
88
|
+
TokenChartSettings,
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
/* ---- left sidebar entry + panel ------------------------------ */
|
|
93
|
+
|
|
94
|
+
ctx.slots.inject('sidebar.panellist', () =>
|
|
95
|
+
ctx.slots.register(
|
|
96
|
+
{
|
|
97
|
+
name: 'sidebar.panellist',
|
|
98
|
+
id: PANEL_ID,
|
|
99
|
+
order: 12,
|
|
100
|
+
label: () => t('panel.title'),
|
|
101
|
+
locale: NS,
|
|
102
|
+
},
|
|
103
|
+
PanelIcon,
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
ctx.slots.inject('main', () =>
|
|
108
|
+
ctx.slots.register(
|
|
109
|
+
{
|
|
110
|
+
name: 'main',
|
|
111
|
+
key: PANEL_ID,
|
|
112
|
+
locale: NS,
|
|
113
|
+
},
|
|
114
|
+
DailyUsagePanel,
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
/* ---- session header shortcut --------------------------------- */
|
|
119
|
+
|
|
120
|
+
ctx.slots.inject('conversation.session.header.utilities', () =>
|
|
121
|
+
ctx.slots.register(
|
|
122
|
+
{
|
|
123
|
+
name: 'conversation.session.header.utilities',
|
|
124
|
+
id: 'token-chart',
|
|
125
|
+
order: 6,
|
|
126
|
+
locale: NS,
|
|
127
|
+
inject: (sessionId: string) => ({ sessionId, open: openDrawer }),
|
|
128
|
+
},
|
|
129
|
+
HeaderUsageButton,
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
/* ---- right sidebar tab type + seats -------------------------- */
|
|
134
|
+
|
|
135
|
+
ctx.inject(['sidebarRightTabs'], (injected: Record<string, any>) => {
|
|
136
|
+
const tabs = injected.get('sidebarRightTabs')
|
|
137
|
+
if (tabs === undefined) return undefined
|
|
138
|
+
services.sidebarRightTabs = tabs
|
|
139
|
+
const disposers: Array<() => void> = []
|
|
140
|
+
try {
|
|
141
|
+
disposers.push(
|
|
142
|
+
tabs.register({
|
|
143
|
+
id: KIND,
|
|
144
|
+
kind: KIND,
|
|
145
|
+
priority: 'extension',
|
|
146
|
+
title: () => t('usage.title'),
|
|
147
|
+
guide: [
|
|
148
|
+
{
|
|
149
|
+
id: KIND,
|
|
150
|
+
order: 40,
|
|
151
|
+
title: () => t('usage.title'),
|
|
152
|
+
description: () => t('usage.session'),
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
}),
|
|
156
|
+
)
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.warn('[dsh-token-chart] tab type registration failed:', error)
|
|
159
|
+
return undefined
|
|
160
|
+
}
|
|
161
|
+
disposers.push(
|
|
162
|
+
ctx.slots.inject('sidebar.right.pane.tab', () =>
|
|
163
|
+
ctx.slots.register(
|
|
164
|
+
{
|
|
165
|
+
name: 'sidebar.right.pane.tab',
|
|
166
|
+
key: KIND,
|
|
167
|
+
locale: NS,
|
|
168
|
+
inject: (sessionId: string) => ({ sessionId }),
|
|
169
|
+
},
|
|
170
|
+
SessionUsageBody,
|
|
171
|
+
),
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
disposers.push(
|
|
175
|
+
ctx.slots.inject('sidebar.right.pane.tab.title', () =>
|
|
176
|
+
ctx.slots.register(
|
|
177
|
+
{
|
|
178
|
+
name: 'sidebar.right.pane.tab.title',
|
|
179
|
+
key: KIND,
|
|
180
|
+
locale: NS,
|
|
181
|
+
},
|
|
182
|
+
({ t: translate }: { t: Translate }) => <span className="dtc-tab-title">{translate('usage.title')}</span>,
|
|
183
|
+
),
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
return () => {
|
|
187
|
+
services.sidebarRightTabs = undefined
|
|
188
|
+
for (const dispose of disposers) {
|
|
189
|
+
try {
|
|
190
|
+
dispose()
|
|
191
|
+
} catch {
|
|
192
|
+
/* a seat already collapsed is not a failure */
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
ctx.inject(['sidebarRight'], (injected: Record<string, any>) => {
|
|
199
|
+
services.sidebarRight = injected.get('sidebarRight')
|
|
200
|
+
return () => {
|
|
201
|
+
services.sidebarRight = undefined
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
/* ---- usage pill interception --------------------------------- */
|
|
206
|
+
|
|
207
|
+
ctx.effect(() => {
|
|
208
|
+
const onClick = (event: MouseEvent): void => {
|
|
209
|
+
if (prefsStore.get().interceptUsageButton !== true) return
|
|
210
|
+
if (isUsagePill(event.target) === false) return
|
|
211
|
+
const sessionId = mountedSessionId()
|
|
212
|
+
if (sessionId === undefined) return
|
|
213
|
+
if (openDrawer(sessionId) === false) return
|
|
214
|
+
event.preventDefault()
|
|
215
|
+
event.stopPropagation()
|
|
216
|
+
}
|
|
217
|
+
document.addEventListener('click', onClick, true)
|
|
218
|
+
return () => document.removeEventListener('click', onClick, true)
|
|
219
|
+
}, 'dsh-token-chart: usage pill interception')
|
|
220
|
+
|
|
221
|
+
ctx.effect(
|
|
222
|
+
() => () => {
|
|
223
|
+
/* listeners and registrations are disposed with this fiber */
|
|
224
|
+
},
|
|
225
|
+
'dsh-token-chart: teardown',
|
|
226
|
+
)
|
|
227
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin's own dictionaries (zh / en) for its namespace.
|
|
3
|
+
*
|
|
4
|
+
* @module dsh-token-chart/client/locales
|
|
5
|
+
*/
|
|
6
|
+
export const zh = {
|
|
7
|
+
'panel.title': 'Token 用量',
|
|
8
|
+
'panel.subtitle': '统计今天、近 3 天、近 7 天的 Token 用量',
|
|
9
|
+
'panel.range.today': '今天',
|
|
10
|
+
'panel.range.3d': '近 3 天',
|
|
11
|
+
'panel.range.7d': '近 7 天',
|
|
12
|
+
'panel.refresh': '刷新',
|
|
13
|
+
'panel.loading': '正在统计…',
|
|
14
|
+
'panel.empty': '所选区间内还没有 Token 用量记录',
|
|
15
|
+
'table.total': '共 {count} 条',
|
|
16
|
+
'table.pagination': '分页',
|
|
17
|
+
'table.prev': '上一页',
|
|
18
|
+
'table.next': '下一页',
|
|
19
|
+
'table.pageSizeLabel': '每页条数',
|
|
20
|
+
'table.page': '第 {page} 页',
|
|
21
|
+
'table.pageSize': '{size} 条/页',
|
|
22
|
+
'panel.updated': '更新于 {time}',
|
|
23
|
+
'panel.retention': '仅保留近 {days} 天的数据',
|
|
24
|
+
'settings.nav': 'Token 用量',
|
|
25
|
+
'settings.title': 'Token 用量可视化',
|
|
26
|
+
'settings.description': '选择各场景使用的图表形式与展示指标,保存后立即生效。',
|
|
27
|
+
'settings.sessionChart': '会话用量抽屉',
|
|
28
|
+
'settings.sessionChartHint': '点击会话中的「用量」按钮后,在右侧抽屉里展示的图形。',
|
|
29
|
+
'settings.dailyChart': '每日用量面板',
|
|
30
|
+
'settings.dailyChartHint': '侧边栏「Token 用量」菜单中「今天 / 近 3 天 / 近 7 天」的图形。',
|
|
31
|
+
'settings.metrics': '展示指标',
|
|
32
|
+
'settings.showTable': '图表下方附带数据表格',
|
|
33
|
+
'settings.showCacheHitRate': '叠加缓存命中率折线',
|
|
34
|
+
'settings.showCacheHitRateHint': '在柱状图、折线图与组合图中叠加缓存命中率折线(右轴)',
|
|
35
|
+
'settings.interceptUsageButton': '点击「用量」按钮打开侧边抽屉',
|
|
36
|
+
'settings.interceptUsageButtonHint': '关闭后恢复 DSH 默认的用量弹窗。',
|
|
37
|
+
'settings.saved': '已保存',
|
|
38
|
+
'settings.saving': '保存中…',
|
|
39
|
+
'settings.saveFailed': '保存失败:{error}',
|
|
40
|
+
'settings.preview': '预览',
|
|
41
|
+
'settings.reset': '恢复默认',
|
|
42
|
+
'chart.bar': '柱状图',
|
|
43
|
+
'chart.line': '折线图',
|
|
44
|
+
'chart.pie': '饼图',
|
|
45
|
+
'chart.table': '表格',
|
|
46
|
+
'chart.multi-bar': '多柱状图',
|
|
47
|
+
'chart.multi-line': '多折线图',
|
|
48
|
+
'chart.bar-line': '柱状折线组合图',
|
|
49
|
+
'chart.loading': '正在加载图表…',
|
|
50
|
+
'settings.update': '更新版本',
|
|
51
|
+
'settings.update.checking': '正在检查更新…',
|
|
52
|
+
'settings.update.upToDate': '已是最新版本',
|
|
53
|
+
'settings.update.unpublished': '未发布到 registry,无法自动更新',
|
|
54
|
+
'settings.update.running': '正在更新…',
|
|
55
|
+
'settings.update.done': '更新完成:{target}',
|
|
56
|
+
'settings.update.failed': '更新失败:{error}',
|
|
57
|
+
'settings.update.local': '本地安装,请在源码目录重建并重新加载',
|
|
58
|
+
'settings.update.check': '检查更新',
|
|
59
|
+
'metric.total': '总 Token',
|
|
60
|
+
'metric.input': '输入 Token',
|
|
61
|
+
'metric.output': '输出 Token',
|
|
62
|
+
'metric.cache': '缓存 Token',
|
|
63
|
+
'metric.cacheHitRate': '缓存命中率',
|
|
64
|
+
'usage.title': 'Token 用量',
|
|
65
|
+
'usage.session': '会话 Token 用量',
|
|
66
|
+
'usage.turns': '对话轮次',
|
|
67
|
+
'usage.sessions': '活跃会话',
|
|
68
|
+
'usage.cacheRead': '缓存读取',
|
|
69
|
+
'usage.cacheWrite': '缓存写入',
|
|
70
|
+
'usage.route': '模型',
|
|
71
|
+
'usage.openDrawer': '打开 Token 用量抽屉',
|
|
72
|
+
'usage.refresh': '刷新用量',
|
|
73
|
+
'table.day': '日期',
|
|
74
|
+
'table.turn': '轮次',
|
|
75
|
+
'table.time': '时间',
|
|
76
|
+
'table.metric': '指标',
|
|
77
|
+
'table.value': '数值',
|
|
78
|
+
'table.share': '占比',
|
|
79
|
+
'table.session': '会话',
|
|
80
|
+
'usage.emptySession': '该会话还没有可统计的 Token 用量',
|
|
81
|
+
'usage.accounting': '总量按计费口径累计:每次请求的上下文(含缓存重读)都计入,与 DSH 产品内「用量」一致',
|
|
82
|
+
'usage.crossCheckFailed': '与产品内「用量」投影不一致(产品 {product})——请反馈此差异',
|
|
83
|
+
}
|
|
84
|
+
export const en = {
|
|
85
|
+
'panel.title': 'Token usage',
|
|
86
|
+
'panel.subtitle': 'Today, last 3 days, and last 7 days of token usage',
|
|
87
|
+
'panel.range.today': 'Today',
|
|
88
|
+
'panel.range.3d': 'Last 3 days',
|
|
89
|
+
'panel.range.7d': 'Last 7 days',
|
|
90
|
+
'panel.refresh': 'Refresh',
|
|
91
|
+
'panel.loading': 'Collecting…',
|
|
92
|
+
'panel.empty': 'No token usage recorded in this window',
|
|
93
|
+
'table.total': '{count} rows',
|
|
94
|
+
'table.pagination': 'Pagination',
|
|
95
|
+
'table.prev': 'Previous page',
|
|
96
|
+
'table.next': 'Next page',
|
|
97
|
+
'table.page': 'Page {page}',
|
|
98
|
+
'table.pageSize': '{size} / page',
|
|
99
|
+
'table.pageSizeLabel': 'Rows per page',
|
|
100
|
+
'panel.updated': 'Updated {time}',
|
|
101
|
+
'panel.retention': 'Only the last {days} days are kept',
|
|
102
|
+
'settings.nav': 'Token usage',
|
|
103
|
+
'settings.title': 'Token usage charts',
|
|
104
|
+
'settings.description': 'Choose the chart form and the metrics for each surface; changes apply immediately.',
|
|
105
|
+
'settings.sessionChart': 'Session usage drawer',
|
|
106
|
+
'settings.sessionChartHint': 'The chart shown in the right drawer after clicking the usage button.',
|
|
107
|
+
'settings.dailyChart': 'Daily usage panel',
|
|
108
|
+
'settings.dailyChartHint': 'The chart for Today / last 3 days / last 7 days in the sidebar panel.',
|
|
109
|
+
'settings.metrics': 'Metrics',
|
|
110
|
+
'settings.showTable': 'Show the data table under the chart',
|
|
111
|
+
'settings.showCacheHitRate': 'Overlay the cache-hit-rate line',
|
|
112
|
+
'settings.showCacheHitRateHint': 'Overlay the cache-hit-rate line (right axis) on bar, line and combo charts',
|
|
113
|
+
'settings.interceptUsageButton': 'Open the drawer from the usage button',
|
|
114
|
+
'settings.interceptUsageButtonHint': 'Turn off to restore the shipped usage dialog.',
|
|
115
|
+
'settings.saved': 'Saved',
|
|
116
|
+
'settings.saving': 'Saving…',
|
|
117
|
+
'settings.saveFailed': 'Save failed: {error}',
|
|
118
|
+
'settings.preview': 'Preview',
|
|
119
|
+
'settings.reset': 'Restore defaults',
|
|
120
|
+
'chart.bar': 'Bar',
|
|
121
|
+
'chart.line': 'Line',
|
|
122
|
+
'chart.pie': 'Pie',
|
|
123
|
+
'chart.table': 'Table',
|
|
124
|
+
'chart.multi-bar': 'Grouped bars',
|
|
125
|
+
'chart.multi-line': 'Multi line',
|
|
126
|
+
'chart.bar-line': 'Bar + line',
|
|
127
|
+
'chart.loading': 'Loading chart…',
|
|
128
|
+
'settings.update': 'Update',
|
|
129
|
+
'settings.update.checking': 'Checking for updates…',
|
|
130
|
+
'settings.update.upToDate': 'Up to date',
|
|
131
|
+
'settings.update.unpublished': 'Not published to a registry',
|
|
132
|
+
'settings.update.running': 'Updating…',
|
|
133
|
+
'settings.update.done': 'Updated: {target}',
|
|
134
|
+
'settings.update.failed': 'Update failed: {error}',
|
|
135
|
+
'settings.update.local': 'Local install — rebuild and reload from the source directory',
|
|
136
|
+
'settings.update.check': 'Check for updates',
|
|
137
|
+
'metric.total': 'Total tokens',
|
|
138
|
+
'metric.input': 'Input tokens',
|
|
139
|
+
'metric.output': 'Output tokens',
|
|
140
|
+
'metric.cache': 'Cache tokens',
|
|
141
|
+
'metric.cacheHitRate': 'Cache hit rate',
|
|
142
|
+
'usage.title': 'Token usage',
|
|
143
|
+
'usage.session': 'Session token usage',
|
|
144
|
+
'usage.turns': 'Turns',
|
|
145
|
+
'usage.sessions': 'Active sessions',
|
|
146
|
+
'usage.cacheRead': 'Cache read',
|
|
147
|
+
'usage.cacheWrite': 'Cache write',
|
|
148
|
+
'usage.route': 'Model',
|
|
149
|
+
'usage.openDrawer': 'Open the token usage drawer',
|
|
150
|
+
'usage.refresh': 'Refresh usage',
|
|
151
|
+
'table.day': 'Day',
|
|
152
|
+
'table.turn': 'Turn',
|
|
153
|
+
'table.time': 'Time',
|
|
154
|
+
'table.metric': 'Metric',
|
|
155
|
+
'table.value': 'Value',
|
|
156
|
+
'table.share': 'Share',
|
|
157
|
+
'table.session': 'Session',
|
|
158
|
+
'usage.emptySession': 'This session has no token usage yet',
|
|
159
|
+
'usage.accounting':
|
|
160
|
+
'Totals follow the billing rule: every request re-reads the context (cache included) and counts — same as the product usage pill',
|
|
161
|
+
'usage.crossCheckFailed': 'Differs from the product usage projection ({product}) — please report this',
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* -------------------------------------------------------------- util */
|