@linxiraos/pi-stats 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/client/index.css +1 -1
- package/dist/client/index.js +72 -72
- package/dist/types/client/app/LanguageToggle.d.ts +7 -0
- package/dist/types/client/i18n/index.d.ts +11 -3
- package/dist/types/client/i18n/messages/common.d.ts +4 -0
- package/dist/types/client/i18n/messages/en.d.ts +4 -0
- package/dist/types/client/i18n/messages/zh-CN.d.ts +1 -1
- package/dist/types/client/i18n/useLocale.d.ts +7 -0
- package/package.json +4 -4
- package/src/client/App.tsx +4 -0
- package/src/client/app/LanguageToggle.tsx +30 -0
- package/src/client/app/Shell.tsx +12 -7
- package/src/client/app/ThemeToggle.tsx +11 -8
- package/src/client/i18n/index.ts +49 -6
- package/src/client/i18n/messages/common.ts +4 -0
- package/src/client/i18n/useLocale.ts +11 -0
- package/src/client/routes/ProvidersRoute.tsx +2 -4
- package/src/client/routes/ToolsRoute.tsx +7 -9
- package/src/client/styles.css +13 -0
- package/src/index.ts +10 -4
- package/src/server.ts +2 -2
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topbar language switch: EN ⇄ 中文. The label shows the language the click
|
|
3
|
+
* will install (the dashboard is already in the other one), and the choice is
|
|
4
|
+
* remembered in localStorage so a reload keeps it over the host's injected
|
|
5
|
+
* locale.
|
|
6
|
+
*/
|
|
7
|
+
export declare function LanguageToggle(): import("react").JSX.Element;
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { type MessageKey } from "./messages/en.js";
|
|
2
2
|
import type { StatsLocale, TranslationParams } from "./types.js";
|
|
3
|
-
/** The locale the dashboard renders in.
|
|
3
|
+
/** The locale the dashboard renders in. */
|
|
4
4
|
export declare function getLocale(): StatsLocale;
|
|
5
|
+
/** Subscribe to runtime switches (the topbar toggle); returns the unsubscribe. */
|
|
6
|
+
export declare function subscribeLocale(listener: () => void): () => void;
|
|
5
7
|
/** Install a locale by tag (`zh`, `zh-CN`, `zh-Hans`; anything else is English). */
|
|
6
8
|
export declare function setLocale(tag: string | undefined | null): StatsLocale;
|
|
7
9
|
/**
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
10
|
+
* The user's explicit pick from the topbar: install it, remember it, and
|
|
11
|
+
* re-render every subscriber.
|
|
12
|
+
*/
|
|
13
|
+
export declare function setUserLocale(tag: string): StatsLocale;
|
|
14
|
+
/**
|
|
15
|
+
* The locale to boot with: an explicit topbar choice, then the host's language
|
|
16
|
+
* when `zetacode stats` injected it, then the browser's own preference, then
|
|
17
|
+
* English.
|
|
10
18
|
*/
|
|
11
19
|
export declare function resolveInitialLocale(): StatsLocale;
|
|
12
20
|
/**
|
|
@@ -77,6 +77,8 @@ export declare const common: {
|
|
|
77
77
|
readonly "shell.light-theme": "Light theme";
|
|
78
78
|
readonly "shell.range-suffix": "range";
|
|
79
79
|
readonly "shell.jump-hint": "then letter to jump";
|
|
80
|
+
readonly "shell.language": "Language";
|
|
81
|
+
readonly "shell.switch-language": "Switch dashboard language";
|
|
80
82
|
};
|
|
81
83
|
readonly "zh-CN": {
|
|
82
84
|
readonly "common.provider": "服务商";
|
|
@@ -149,5 +151,7 @@ export declare const common: {
|
|
|
149
151
|
readonly "shell.light-theme": "浅色主题";
|
|
150
152
|
readonly "shell.range-suffix": "范围";
|
|
151
153
|
readonly "shell.jump-hint": "再按字母跳转";
|
|
154
|
+
readonly "shell.language": "语言";
|
|
155
|
+
readonly "shell.switch-language": "切换仪表盘语言";
|
|
152
156
|
};
|
|
153
157
|
};
|
|
@@ -88,6 +88,8 @@ export declare const enMessages: {
|
|
|
88
88
|
readonly "shell.light-theme": "Light theme";
|
|
89
89
|
readonly "shell.range-suffix": "range";
|
|
90
90
|
readonly "shell.jump-hint": "then letter to jump";
|
|
91
|
+
readonly "shell.language": "Language";
|
|
92
|
+
readonly "shell.switch-language": "Switch dashboard language";
|
|
91
93
|
readonly "overview.title": "Overview";
|
|
92
94
|
readonly "overview.description": "Everything zeta did across your sessions in {window}.";
|
|
93
95
|
readonly "overview.api-equivalent-cost": "API-equivalent cost";
|
|
@@ -753,6 +755,8 @@ export declare const enLocale: {
|
|
|
753
755
|
readonly "shell.light-theme": "Light theme";
|
|
754
756
|
readonly "shell.range-suffix": "range";
|
|
755
757
|
readonly "shell.jump-hint": "then letter to jump";
|
|
758
|
+
readonly "shell.language": "Language";
|
|
759
|
+
readonly "shell.switch-language": "Switch dashboard language";
|
|
756
760
|
readonly "overview.title": "Overview";
|
|
757
761
|
readonly "overview.description": "Everything zeta did across your sessions in {window}.";
|
|
758
762
|
readonly "overview.api-equivalent-cost": "API-equivalent cost";
|
|
@@ -9,5 +9,5 @@ export declare const zhCNMessages: Partial<Record<MessageKey, string>>;
|
|
|
9
9
|
/** The Simplified Chinese locale pack. */
|
|
10
10
|
export declare const zhCNLocale: {
|
|
11
11
|
readonly id: "zh-CN";
|
|
12
|
-
readonly messages: Partial<Record<"common.aborted" | "common.average" | "common.cache-read" | "common.cache-write" | "common.cancel" | "common.close" | "common.copied" | "common.copy" | "common.cost" | "common.duration" | "common.entry-id" | "common.error" | "common.error-rate" | "common.failed" | "common.filter" | "common.input" | "common.model" | "common.models" | "common.no-data-in-range" | "common.nothing-here-yet" | "common.other-n" | "common.output" | "common.output-message" | "common.output-tokens-per-second" | "common.pages" | "common.premium" | "common.premium-requests" | "common.project" | "common.provider" | "common.providers" | "common.range-1h" | "common.range-24h" | "common.range-30d" | "common.range-7d" | "common.range-90d" | "common.range-all" | "common.request" | "common.request-id" | "common.requests" | "common.root" | "common.session-entry" | "common.share" | "common.show-more" | "common.started" | "common.time-range" | "common.tokens" | "common.tokens-per-second" | "common.total" | "common.unknown" | "common.window-1h" | "common.window-24h" | "common.window-30d" | "common.window-7d" | "common.window-90d" | "common.window-all" | "costs.active-days-n" | "costs.active-days-one" | "costs.all-priced" | "costs.api-equivalent-estimate" | "costs.api-estimate-title" | "costs.average-per-day" | "costs.average-per-day-title" | "costs.by-component" | "costs.by-model" | "costs.by-model-description" | "costs.daily-by-component" | "costs.daily-by-model" | "costs.daily-estimate" | "costs.description" | "costs.empty-hint" | "costs.estimate" | "costs.estimate-by-component" | "costs.estimate-title" | "costs.no-priced-usage" | "costs.no-usage-in-range" | "costs.nothing-priced-yet" | "costs.per-priced-request" | "costs.per-priced-request-title" | "costs.per-request" | "costs.per-request-title" | "costs.priced-n" | "costs.requests-n" | "costs.split" | "costs.split-by" | "costs.split-title" | "costs.title" | "costs.top-model" | "costs.top-model-hint" | "costs.unpriced" | "costs.unpriced-column-title" | "costs.unpriced-hint" | "costs.unpriced-note-n" | "costs.unpriced-note-one" | "costs.unpriced-requests" | "costs.unpriced-title" | "costs.where-it-went" | "errors.affected-models" | "errors.by-model" | "errors.by-model-description" | "errors.cost-hint" | "errors.description" | "errors.empty-all-succeeded" | "errors.empty-no-affected-models" | "errors.empty-no-failures" | "errors.error-signatures" | "errors.failures" | "errors.filtered-of-total" | "errors.footer" | "errors.group-meta" | "errors.hint-in-window" | "errors.hint-latest-loaded" | "errors.last-failure" | "errors.last-seen" | "errors.load-latest" | "errors.loading" | "errors.loading-failures" | "errors.more-models" | "errors.no-match" | "errors.no-match-hint" | "errors.none-in-window" | "errors.open-latest" | "errors.search-placeholder" | "errors.signature" | "errors.signatures" | "errors.signatures-card-description" | "errors.signatures-hint" | "errors.signatures-top" | "errors.title" | "errors.unknown-error" | "errors.when" | "frustration.all-classes" | "frustration.all-judged" | "frustration.angry" | "frustration.angry-title" | "frustration.annoyed" | "frustration.annoyed-title" | "frustration.at-assistant" | "frustration.at-assistant-title" | "frustration.by-model" | "frustration.by-model-description" | "frustration.cancel-run" | "frustration.cancelled" | "frustration.cancelling" | "frustration.classify" | "frustration.column-judged" | "frustration.column-mean" | "frustration.column-messages" | "frustration.column-model-ids" | "frustration.column-session" | "frustration.column-tool-errors" | "frustration.column-turns" | "frustration.cost-so-far" | "frustration.coverage-hint" | "frustration.coverage-unscored" | "frustration.description" | "frustration.done" | "frustration.elapsed" | "frustration.estimate-note" | "frustration.estimated-cost" | "frustration.estimating" | "frustration.every-model-class" | "frustration.failed" | "frustration.failed-n" | "frustration.failed-suffix" | "frustration.hatch-title" | "frustration.hide-regex-title" | "frustration.in-flight" | "frustration.in-time" | "frustration.include-small" | "frustration.input-tokens" | "frustration.judge-coverage" | "frustration.judge-coverage-title" | "frustration.judge-description" | "frustration.judge-model" | "frustration.judge-panel" | "frustration.judge-running" | "frustration.judge-with" | "frustration.last-run-cancelled" | "frustration.last-run-done" | "frustration.last-run-failed" | "frustration.legend-angry" | "frustration.legend-annoyed-any" | "frustration.legend-assistant" | "frustration.legend-hatched" | "frustration.legend-hatched-run" | "frustration.legend-hue" | "frustration.legend-other" | "frustration.legend-trend" | "frustration.messages-n" | "frustration.messages-progress" | "frustration.model-breakdown" | "frustration.model-breakdown-title" | "frustration.model-by-model" | "frustration.model-by-model-description" | "frustration.model-class" | "frustration.model-hedge" | "frustration.model-hedge-title" | "frustration.model-hint-mean" | "frustration.model-legend-breakdown" | "frustration.model-mean" | "frustration.model-mean-title" | "frustration.model-no-turns" | "frustration.model-over-time" | "frustration.model-over-time-description" | "frustration.model-rambling" | "frustration.model-rambling-title" | "frustration.model-restatement" | "frustration.model-restatement-title" | "frustration.model-self-blame" | "frustration.model-self-blame-title" | "frustration.model-sessions" | "frustration.model-sessions-description" | "frustration.model-side-description" | "frustration.model-side-title" | "frustration.model-spiral" | "frustration.model-spiral-title" | "frustration.model-table-description" | "frustration.model-table-title" | "frustration.model-try-longer-range" | "frustration.model-turns" | "frustration.model-turns-title" | "frustration.model-unscored-line" | "frustration.model-versions" | "frustration.mostly-regex" | "frustration.no-messages" | "frustration.no-model-match" | "frustration.no-run-yet" | "frustration.not-run-yet" | "frustration.proceed" | "frustration.prose-chars" | "frustration.quote-title" | "frustration.regex-badge" | "frustration.regex-only" | "frustration.row-mostly-regex" | "frustration.run-cancelled-after" | "frustration.run-cancelled-line" | "frustration.run-done-line" | "frustration.run-failed-after" | "frustration.run-failed-line" | "frustration.run-judged" | "frustration.running" | "frustration.small-models-title" | "frustration.stack-relation" | "frustration.starting" | "frustration.table-description" | "frustration.title" | "frustration.try-longer-range" | "frustration.turns-n" | "frustration.unclassified" | "frustration.unjudged-messages" | "frustration.unknown-error" | "frustration.unscored-line" | "frustration.user-messages" | "frustration.user-messages-title" | "gain.all-projects" | "gain.by-source" | "gain.by-source-description" | "gain.cumulative" | "gain.description" | "gain.description-scoped" | "gain.empty-hint-all" | "gain.empty-hint-longer-range" | "gain.empty-title" | "gain.empty-title-scoped" | "gain.hits" | "gain.reduction" | "gain.reduction-title" | "gain.reduction-unknown" | "gain.saved-bytes" | "gain.saved-per-day" | "gain.saved-per-hit" | "gain.saved-tokens" | "gain.savings-over-time" | "gain.savings-over-time-description" | "gain.source" | "gain.title" | "models.across-providers" | "models.across-providers-one" | "models.all-models" | "models.api-cost" | "models.api-cost-title" | "models.avg-duration" | "models.avg-ttft" | "models.bucket-5m" | "models.bucket-day" | "models.bucket-hour" | "models.cache-rate" | "models.cache-savings" | "models.cache-title" | "models.cost-title" | "models.count-desc" | "models.efficiency" | "models.empty-hint" | "models.empty-title" | "models.errors" | "models.failed-n" | "models.first-last-seen" | "models.latency" | "models.most-used" | "models.most-used-hint" | "models.no-samples-hint" | "models.no-samples-title" | "models.performance-per-bucket" | "models.request-share" | "models.share-desc" | "models.share-mode-aria" | "models.subtitle" | "models.table-desc" | "models.tok-per-second" | "models.tokens-title" | "models.tps-title" | "models.trend" | "models.trend-title" | "models.ttft" | "models.ttft-title" | "models.uncached-input" | "models.unpriced-n" | "models.used" | "nav.activity" | "nav.breadcrumb-stats" | "nav.costs" | "nav.dashboard" | "nav.errors" | "nav.frustration" | "nav.gain" | "nav.insights" | "nav.models" | "nav.open-navigation" | "nav.overview" | "nav.pages" | "nav.projects" | "nav.providers" | "nav.requests" | "nav.tools" | "nav.traces" | "nav.usage" | "overview.activity" | "overview.advisor" | "overview.all-requests" | "overview.api-cost-title" | "overview.api-equivalent" | "overview.api-equivalent-cost" | "overview.avg-latency" | "overview.avg-ttft" | "overview.by-agent" | "overview.cache-rate" | "overview.cache-rate-title" | "overview.cache-saved" | "overview.conversation-tokens" | "overview.description" | "overview.failed-n" | "overview.latest-requests" | "overview.latest-requests-description" | "overview.main-agent" | "overview.ok" | "overview.output-n" | "overview.per-5m" | "overview.per-day" | "overview.per-hour" | "overview.requests-short" | "overview.status" | "overview.subagents" | "overview.succeeded" | "overview.succeeded-n" | "overview.title" | "overview.token-mix" | "overview.token-mix-description" | "overview.tokens-title" | "overview.uncached-input" | "overview.unpriced-n" | "overview.when" | "projects.api-equivalent-cost" | "projects.api-equivalent-cost-hint" | "projects.api-equivalent-cost-rates" | "projects.avg-duration" | "projects.cache-rate" | "projects.cache-rate-hint" | "projects.cache-rate-hint-all" | "projects.cache-savings" | "projects.cache-savings-hint" | "projects.conversation-tokens" | "projects.conversation-tokens-hint" | "projects.description" | "projects.empty-no-folders" | "projects.empty-no-match" | "projects.empty-only-temporary" | "projects.errors" | "projects.failed-n" | "projects.filter-placeholder" | "projects.folder" | "projects.folders" | "projects.folders-n" | "projects.folders-of-n" | "projects.hide-temporary" | "projects.hide-temporary-hint" | "projects.hint-longer-range" | "projects.hint-temporary-hidden" | "projects.last-active" | "projects.share-of-cost" | "projects.share-of-requests" | "projects.temp" | "projects.temporary-n" | "projects.title" | "projects.top-by-cost" | "projects.top-by-cost-description" | "projects.top-by-requests" | "projects.top-by-requests-description" | "projects.unpriced-n" | "providers.account" | "providers.accounts" | "providers.accounts-needed" | "providers.accounts-needed-title" | "providers.accounts-title" | "providers.all-providers" | "providers.api-cost-title" | "providers.api-equivalent-cost" | "providers.bucket-5m" | "providers.bucket-day" | "providers.bucket-hour" | "providers.burn" | "providers.burn-description" | "providers.burn-estimate-description" | "providers.cost-title" | "providers.description" | "providers.exhausted" | "providers.exhausted-n" | "providers.exhaustions" | "providers.exhaustions-title" | "providers.failed-n" | "providers.high" | "providers.hours-description" | "providers.ideal-accounts" | "providers.latest-used" | "providers.models-title" | "providers.most-tokens" | "providers.no-activity-in-range" | "providers.no-provider-activity" | "providers.no-reading" | "providers.no-requests-in-range" | "providers.no-snapshots-in-range" | "providers.no-utilization-readings" | "providers.peak-burn-hours" | "providers.peak-hours-description" | "providers.peak-in-range" | "providers.peak-utilization" | "providers.peak-utilization-title" | "providers.premium-title" | "providers.recorded" | "providers.resets" | "providers.resets-title" | "providers.share-title" | "providers.snapshot-hint" | "providers.snapshots" | "providers.status" | "providers.subscription-windows" | "providers.subscription-windows-description" | "providers.succeeded-n" | "providers.tokens-peak-hour" | "providers.tokens-per-window" | "providers.tokens-per-window-title" | "providers.tokens-title" | "providers.too-little-consumed" | "providers.totals" | "providers.totals-description" | "providers.totals-description-unpriced-n" | "providers.totals-description-unpriced-one" | "providers.tps-title" | "providers.unpriced-n" | "providers.utilization" | "providers.utilization-description" | "providers.utilization-description-plain" | "providers.window" | "providers.windows-burned" | "providers.windows-burned-title" | "requests.api-equivalent-cost" | "requests.cache-read-title" | "requests.cache-write" | "requests.cost-estimate" | "requests.cost-hint" | "requests.description" | "requests.empty-in-window" | "requests.empty-no-match" | "requests.empty-no-match-hint" | "requests.failed-hint" | "requests.footer" | "requests.load-latest" | "requests.loading" | "requests.log-count-in-window" | "requests.log-count-latest" | "requests.log-loading" | "requests.log-title" | "requests.median-duration" | "requests.median-ttft" | "requests.none-in-window" | "requests.p95" | "requests.search-placeholder" | "requests.since" | "requests.status" | "requests.status-all" | "requests.tokens-hint" | "requests.ttft" | "requests.ttft-hint" | "requests.uncached-input" | "requests.unpriced-n" | "requests.when" | "shell.building-rollups" | "shell.dark-theme" | "shell.indexing" | "shell.ingesting" | "shell.jump-hint" | "shell.just-now" | "shell.light-theme" | "shell.live" | "shell.lost-connection" | "shell.range-suffix" | "shell.reconnecting" | "shell.sync-failed" | "shell.sync-now" | "shell.syncing" | "shell.system-theme" | "tools.all-tools" | "tools.api-equivalent" | "tools.args" | "tools.args-chars-title" | "tools.attr-cost" | "tools.attr-tokens" | "tools.attributed-cost" | "tools.attributed-tokens" | "tools.attribution-estimate" | "tools.attribution-note" | "tools.avg-args-per-call" | "tools.avg-result-per-call" | "tools.bucket-5m" | "tools.bucket-day" | "tools.bucket-hour" | "tools.by-tool" | "tools.by-tool-and-model" | "tools.by-tool-and-model-description" | "tools.by-tool-description" | "tools.call-arguments" | "tools.calls" | "tools.calls-over-time" | "tools.chars" | "tools.chart-description" | "tools.clear-tool-filter" | "tools.description" | "tools.distinct-tools" | "tools.error-rate-title" | "tools.errors" | "tools.errors-over-time" | "tools.errors-title" | "tools.failed-n" | "tools.filter-by-tool" | "tools.last-used" | "tools.most-used" | "tools.no-calls-to-tool" | "tools.no-tool-calls" | "tools.no-tool-errors" | "tools.result" | "tools.result-chars-title" | "tools.result-per-call" | "tools.result-per-call-title" | "tools.result-text" | "tools.share-of-calls" | "tools.share-of-cost" | "tools.share-of-tokens" | "tools.succeeded-n" | "tools.title" | "tools.tokens-output" | "tools.tool" | "tools.tool-calls" | "tools.trend" | "tools.trend-title" | "tools.unpriced-requests-n" | "trace.agent" | "trace.agents" | "trace.args" | "trace.avg" | "trace.axis-mode" | "trace.background" | "trace.bg" | "trace.calls" | "trace.canvas-aria" | "trace.collapse-all" | "trace.collapse-track" | "trace.compress-idle" | "trace.copy-json" | "trace.description" | "trace.empty-hint" | "trace.empty-title" | "trace.error-message" | "trace.errors" | "trace.expand-all" | "trace.expand-track" | "trace.failed-n" | "trace.filter-placeholder" | "trace.idle" | "trace.io-cached" | "trace.load-failed" | "trace.max" | "trace.minimap-aria" | "trace.mode-calls-title" | "trace.mode-time-title" | "trace.mode-turns-title" | "trace.model-time" | "trace.most-recent" | "trace.most-recent-filtered" | "trace.next-match" | "trace.no-matching-events" | "trace.no-matching-hint" | "trace.no-matching-title" | "trace.of-wall" | "trace.open-child-track" | "trace.previous-match" | "trace.raw-entry" | "trace.refresh" | "trace.search-placeholder" | "trace.session" | "trace.sessions" | "trace.start" | "trace.stop-reason" | "trace.task" | "trace.text" | "trace.thinking" | "trace.timeline" | "trace.timeline-hint" | "trace.title" | "trace.tok" | "trace.tool" | "trace.tool-aggregates" | "trace.tool-aggregates-desc" | "trace.tool-calls-n" | "trace.tool-time" | "trace.tools" | "trace.track" | "trace.transcript" | "trace.transcript-description" | "trace.ttft" | "trace.turns" | "trace.unterminated" | "trace.wall-time" | "ui.api" | "ui.delta-new" | "ui.json" | "ui.open-trace" | "ui.retry" | "ui.section-cost" | "ui.section-identity" | "ui.section-timing" | "ui.session-file" | "ui.show-fewer" | "ui.stats-row" | "ui.status-ok" | "ui.stop-reason" | "ui.table-shown-of" | "ui.time-to-first-token" | "ui.trace" | "ui.uncached-input", string>>;
|
|
12
|
+
readonly messages: Partial<Record<"common.aborted" | "common.average" | "common.cache-read" | "common.cache-write" | "common.cancel" | "common.close" | "common.copied" | "common.copy" | "common.cost" | "common.duration" | "common.entry-id" | "common.error" | "common.error-rate" | "common.failed" | "common.filter" | "common.input" | "common.model" | "common.models" | "common.no-data-in-range" | "common.nothing-here-yet" | "common.other-n" | "common.output" | "common.output-message" | "common.output-tokens-per-second" | "common.pages" | "common.premium" | "common.premium-requests" | "common.project" | "common.provider" | "common.providers" | "common.range-1h" | "common.range-24h" | "common.range-30d" | "common.range-7d" | "common.range-90d" | "common.range-all" | "common.request" | "common.request-id" | "common.requests" | "common.root" | "common.session-entry" | "common.share" | "common.show-more" | "common.started" | "common.time-range" | "common.tokens" | "common.tokens-per-second" | "common.total" | "common.unknown" | "common.window-1h" | "common.window-24h" | "common.window-30d" | "common.window-7d" | "common.window-90d" | "common.window-all" | "costs.active-days-n" | "costs.active-days-one" | "costs.all-priced" | "costs.api-equivalent-estimate" | "costs.api-estimate-title" | "costs.average-per-day" | "costs.average-per-day-title" | "costs.by-component" | "costs.by-model" | "costs.by-model-description" | "costs.daily-by-component" | "costs.daily-by-model" | "costs.daily-estimate" | "costs.description" | "costs.empty-hint" | "costs.estimate" | "costs.estimate-by-component" | "costs.estimate-title" | "costs.no-priced-usage" | "costs.no-usage-in-range" | "costs.nothing-priced-yet" | "costs.per-priced-request" | "costs.per-priced-request-title" | "costs.per-request" | "costs.per-request-title" | "costs.priced-n" | "costs.requests-n" | "costs.split" | "costs.split-by" | "costs.split-title" | "costs.title" | "costs.top-model" | "costs.top-model-hint" | "costs.unpriced" | "costs.unpriced-column-title" | "costs.unpriced-hint" | "costs.unpriced-note-n" | "costs.unpriced-note-one" | "costs.unpriced-requests" | "costs.unpriced-title" | "costs.where-it-went" | "errors.affected-models" | "errors.by-model" | "errors.by-model-description" | "errors.cost-hint" | "errors.description" | "errors.empty-all-succeeded" | "errors.empty-no-affected-models" | "errors.empty-no-failures" | "errors.error-signatures" | "errors.failures" | "errors.filtered-of-total" | "errors.footer" | "errors.group-meta" | "errors.hint-in-window" | "errors.hint-latest-loaded" | "errors.last-failure" | "errors.last-seen" | "errors.load-latest" | "errors.loading" | "errors.loading-failures" | "errors.more-models" | "errors.no-match" | "errors.no-match-hint" | "errors.none-in-window" | "errors.open-latest" | "errors.search-placeholder" | "errors.signature" | "errors.signatures" | "errors.signatures-card-description" | "errors.signatures-hint" | "errors.signatures-top" | "errors.title" | "errors.unknown-error" | "errors.when" | "frustration.all-classes" | "frustration.all-judged" | "frustration.angry" | "frustration.angry-title" | "frustration.annoyed" | "frustration.annoyed-title" | "frustration.at-assistant" | "frustration.at-assistant-title" | "frustration.by-model" | "frustration.by-model-description" | "frustration.cancel-run" | "frustration.cancelled" | "frustration.cancelling" | "frustration.classify" | "frustration.column-judged" | "frustration.column-mean" | "frustration.column-messages" | "frustration.column-model-ids" | "frustration.column-session" | "frustration.column-tool-errors" | "frustration.column-turns" | "frustration.cost-so-far" | "frustration.coverage-hint" | "frustration.coverage-unscored" | "frustration.description" | "frustration.done" | "frustration.elapsed" | "frustration.estimate-note" | "frustration.estimated-cost" | "frustration.estimating" | "frustration.every-model-class" | "frustration.failed" | "frustration.failed-n" | "frustration.failed-suffix" | "frustration.hatch-title" | "frustration.hide-regex-title" | "frustration.in-flight" | "frustration.in-time" | "frustration.include-small" | "frustration.input-tokens" | "frustration.judge-coverage" | "frustration.judge-coverage-title" | "frustration.judge-description" | "frustration.judge-model" | "frustration.judge-panel" | "frustration.judge-running" | "frustration.judge-with" | "frustration.last-run-cancelled" | "frustration.last-run-done" | "frustration.last-run-failed" | "frustration.legend-angry" | "frustration.legend-annoyed-any" | "frustration.legend-assistant" | "frustration.legend-hatched" | "frustration.legend-hatched-run" | "frustration.legend-hue" | "frustration.legend-other" | "frustration.legend-trend" | "frustration.messages-n" | "frustration.messages-progress" | "frustration.model-breakdown" | "frustration.model-breakdown-title" | "frustration.model-by-model" | "frustration.model-by-model-description" | "frustration.model-class" | "frustration.model-hedge" | "frustration.model-hedge-title" | "frustration.model-hint-mean" | "frustration.model-legend-breakdown" | "frustration.model-mean" | "frustration.model-mean-title" | "frustration.model-no-turns" | "frustration.model-over-time" | "frustration.model-over-time-description" | "frustration.model-rambling" | "frustration.model-rambling-title" | "frustration.model-restatement" | "frustration.model-restatement-title" | "frustration.model-self-blame" | "frustration.model-self-blame-title" | "frustration.model-sessions" | "frustration.model-sessions-description" | "frustration.model-side-description" | "frustration.model-side-title" | "frustration.model-spiral" | "frustration.model-spiral-title" | "frustration.model-table-description" | "frustration.model-table-title" | "frustration.model-try-longer-range" | "frustration.model-turns" | "frustration.model-turns-title" | "frustration.model-unscored-line" | "frustration.model-versions" | "frustration.mostly-regex" | "frustration.no-messages" | "frustration.no-model-match" | "frustration.no-run-yet" | "frustration.not-run-yet" | "frustration.proceed" | "frustration.prose-chars" | "frustration.quote-title" | "frustration.regex-badge" | "frustration.regex-only" | "frustration.row-mostly-regex" | "frustration.run-cancelled-after" | "frustration.run-cancelled-line" | "frustration.run-done-line" | "frustration.run-failed-after" | "frustration.run-failed-line" | "frustration.run-judged" | "frustration.running" | "frustration.small-models-title" | "frustration.stack-relation" | "frustration.starting" | "frustration.table-description" | "frustration.title" | "frustration.try-longer-range" | "frustration.turns-n" | "frustration.unclassified" | "frustration.unjudged-messages" | "frustration.unknown-error" | "frustration.unscored-line" | "frustration.user-messages" | "frustration.user-messages-title" | "gain.all-projects" | "gain.by-source" | "gain.by-source-description" | "gain.cumulative" | "gain.description" | "gain.description-scoped" | "gain.empty-hint-all" | "gain.empty-hint-longer-range" | "gain.empty-title" | "gain.empty-title-scoped" | "gain.hits" | "gain.reduction" | "gain.reduction-title" | "gain.reduction-unknown" | "gain.saved-bytes" | "gain.saved-per-day" | "gain.saved-per-hit" | "gain.saved-tokens" | "gain.savings-over-time" | "gain.savings-over-time-description" | "gain.source" | "gain.title" | "models.across-providers" | "models.across-providers-one" | "models.all-models" | "models.api-cost" | "models.api-cost-title" | "models.avg-duration" | "models.avg-ttft" | "models.bucket-5m" | "models.bucket-day" | "models.bucket-hour" | "models.cache-rate" | "models.cache-savings" | "models.cache-title" | "models.cost-title" | "models.count-desc" | "models.efficiency" | "models.empty-hint" | "models.empty-title" | "models.errors" | "models.failed-n" | "models.first-last-seen" | "models.latency" | "models.most-used" | "models.most-used-hint" | "models.no-samples-hint" | "models.no-samples-title" | "models.performance-per-bucket" | "models.request-share" | "models.share-desc" | "models.share-mode-aria" | "models.subtitle" | "models.table-desc" | "models.tok-per-second" | "models.tokens-title" | "models.tps-title" | "models.trend" | "models.trend-title" | "models.ttft" | "models.ttft-title" | "models.uncached-input" | "models.unpriced-n" | "models.used" | "nav.activity" | "nav.breadcrumb-stats" | "nav.costs" | "nav.dashboard" | "nav.errors" | "nav.frustration" | "nav.gain" | "nav.insights" | "nav.models" | "nav.open-navigation" | "nav.overview" | "nav.pages" | "nav.projects" | "nav.providers" | "nav.requests" | "nav.tools" | "nav.traces" | "nav.usage" | "overview.activity" | "overview.advisor" | "overview.all-requests" | "overview.api-cost-title" | "overview.api-equivalent" | "overview.api-equivalent-cost" | "overview.avg-latency" | "overview.avg-ttft" | "overview.by-agent" | "overview.cache-rate" | "overview.cache-rate-title" | "overview.cache-saved" | "overview.conversation-tokens" | "overview.description" | "overview.failed-n" | "overview.latest-requests" | "overview.latest-requests-description" | "overview.main-agent" | "overview.ok" | "overview.output-n" | "overview.per-5m" | "overview.per-day" | "overview.per-hour" | "overview.requests-short" | "overview.status" | "overview.subagents" | "overview.succeeded" | "overview.succeeded-n" | "overview.title" | "overview.token-mix" | "overview.token-mix-description" | "overview.tokens-title" | "overview.uncached-input" | "overview.unpriced-n" | "overview.when" | "projects.api-equivalent-cost" | "projects.api-equivalent-cost-hint" | "projects.api-equivalent-cost-rates" | "projects.avg-duration" | "projects.cache-rate" | "projects.cache-rate-hint" | "projects.cache-rate-hint-all" | "projects.cache-savings" | "projects.cache-savings-hint" | "projects.conversation-tokens" | "projects.conversation-tokens-hint" | "projects.description" | "projects.empty-no-folders" | "projects.empty-no-match" | "projects.empty-only-temporary" | "projects.errors" | "projects.failed-n" | "projects.filter-placeholder" | "projects.folder" | "projects.folders" | "projects.folders-n" | "projects.folders-of-n" | "projects.hide-temporary" | "projects.hide-temporary-hint" | "projects.hint-longer-range" | "projects.hint-temporary-hidden" | "projects.last-active" | "projects.share-of-cost" | "projects.share-of-requests" | "projects.temp" | "projects.temporary-n" | "projects.title" | "projects.top-by-cost" | "projects.top-by-cost-description" | "projects.top-by-requests" | "projects.top-by-requests-description" | "projects.unpriced-n" | "providers.account" | "providers.accounts" | "providers.accounts-needed" | "providers.accounts-needed-title" | "providers.accounts-title" | "providers.all-providers" | "providers.api-cost-title" | "providers.api-equivalent-cost" | "providers.bucket-5m" | "providers.bucket-day" | "providers.bucket-hour" | "providers.burn" | "providers.burn-description" | "providers.burn-estimate-description" | "providers.cost-title" | "providers.description" | "providers.exhausted" | "providers.exhausted-n" | "providers.exhaustions" | "providers.exhaustions-title" | "providers.failed-n" | "providers.high" | "providers.hours-description" | "providers.ideal-accounts" | "providers.latest-used" | "providers.models-title" | "providers.most-tokens" | "providers.no-activity-in-range" | "providers.no-provider-activity" | "providers.no-reading" | "providers.no-requests-in-range" | "providers.no-snapshots-in-range" | "providers.no-utilization-readings" | "providers.peak-burn-hours" | "providers.peak-hours-description" | "providers.peak-in-range" | "providers.peak-utilization" | "providers.peak-utilization-title" | "providers.premium-title" | "providers.recorded" | "providers.resets" | "providers.resets-title" | "providers.share-title" | "providers.snapshot-hint" | "providers.snapshots" | "providers.status" | "providers.subscription-windows" | "providers.subscription-windows-description" | "providers.succeeded-n" | "providers.tokens-peak-hour" | "providers.tokens-per-window" | "providers.tokens-per-window-title" | "providers.tokens-title" | "providers.too-little-consumed" | "providers.totals" | "providers.totals-description" | "providers.totals-description-unpriced-n" | "providers.totals-description-unpriced-one" | "providers.tps-title" | "providers.unpriced-n" | "providers.utilization" | "providers.utilization-description" | "providers.utilization-description-plain" | "providers.window" | "providers.windows-burned" | "providers.windows-burned-title" | "requests.api-equivalent-cost" | "requests.cache-read-title" | "requests.cache-write" | "requests.cost-estimate" | "requests.cost-hint" | "requests.description" | "requests.empty-in-window" | "requests.empty-no-match" | "requests.empty-no-match-hint" | "requests.failed-hint" | "requests.footer" | "requests.load-latest" | "requests.loading" | "requests.log-count-in-window" | "requests.log-count-latest" | "requests.log-loading" | "requests.log-title" | "requests.median-duration" | "requests.median-ttft" | "requests.none-in-window" | "requests.p95" | "requests.search-placeholder" | "requests.since" | "requests.status" | "requests.status-all" | "requests.tokens-hint" | "requests.ttft" | "requests.ttft-hint" | "requests.uncached-input" | "requests.unpriced-n" | "requests.when" | "shell.building-rollups" | "shell.dark-theme" | "shell.indexing" | "shell.ingesting" | "shell.jump-hint" | "shell.just-now" | "shell.language" | "shell.light-theme" | "shell.live" | "shell.lost-connection" | "shell.range-suffix" | "shell.reconnecting" | "shell.switch-language" | "shell.sync-failed" | "shell.sync-now" | "shell.syncing" | "shell.system-theme" | "tools.all-tools" | "tools.api-equivalent" | "tools.args" | "tools.args-chars-title" | "tools.attr-cost" | "tools.attr-tokens" | "tools.attributed-cost" | "tools.attributed-tokens" | "tools.attribution-estimate" | "tools.attribution-note" | "tools.avg-args-per-call" | "tools.avg-result-per-call" | "tools.bucket-5m" | "tools.bucket-day" | "tools.bucket-hour" | "tools.by-tool" | "tools.by-tool-and-model" | "tools.by-tool-and-model-description" | "tools.by-tool-description" | "tools.call-arguments" | "tools.calls" | "tools.calls-over-time" | "tools.chars" | "tools.chart-description" | "tools.clear-tool-filter" | "tools.description" | "tools.distinct-tools" | "tools.error-rate-title" | "tools.errors" | "tools.errors-over-time" | "tools.errors-title" | "tools.failed-n" | "tools.filter-by-tool" | "tools.last-used" | "tools.most-used" | "tools.no-calls-to-tool" | "tools.no-tool-calls" | "tools.no-tool-errors" | "tools.result" | "tools.result-chars-title" | "tools.result-per-call" | "tools.result-per-call-title" | "tools.result-text" | "tools.share-of-calls" | "tools.share-of-cost" | "tools.share-of-tokens" | "tools.succeeded-n" | "tools.title" | "tools.tokens-output" | "tools.tool" | "tools.tool-calls" | "tools.trend" | "tools.trend-title" | "tools.unpriced-requests-n" | "trace.agent" | "trace.agents" | "trace.args" | "trace.avg" | "trace.axis-mode" | "trace.background" | "trace.bg" | "trace.calls" | "trace.canvas-aria" | "trace.collapse-all" | "trace.collapse-track" | "trace.compress-idle" | "trace.copy-json" | "trace.description" | "trace.empty-hint" | "trace.empty-title" | "trace.error-message" | "trace.errors" | "trace.expand-all" | "trace.expand-track" | "trace.failed-n" | "trace.filter-placeholder" | "trace.idle" | "trace.io-cached" | "trace.load-failed" | "trace.max" | "trace.minimap-aria" | "trace.mode-calls-title" | "trace.mode-time-title" | "trace.mode-turns-title" | "trace.model-time" | "trace.most-recent" | "trace.most-recent-filtered" | "trace.next-match" | "trace.no-matching-events" | "trace.no-matching-hint" | "trace.no-matching-title" | "trace.of-wall" | "trace.open-child-track" | "trace.previous-match" | "trace.raw-entry" | "trace.refresh" | "trace.search-placeholder" | "trace.session" | "trace.sessions" | "trace.start" | "trace.stop-reason" | "trace.task" | "trace.text" | "trace.thinking" | "trace.timeline" | "trace.timeline-hint" | "trace.title" | "trace.tok" | "trace.tool" | "trace.tool-aggregates" | "trace.tool-aggregates-desc" | "trace.tool-calls-n" | "trace.tool-time" | "trace.tools" | "trace.track" | "trace.transcript" | "trace.transcript-description" | "trace.ttft" | "trace.turns" | "trace.unterminated" | "trace.wall-time" | "ui.api" | "ui.delta-new" | "ui.json" | "ui.open-trace" | "ui.retry" | "ui.section-cost" | "ui.section-identity" | "ui.section-timing" | "ui.session-file" | "ui.show-fewer" | "ui.stats-row" | "ui.status-ok" | "ui.stop-reason" | "ui.table-shown-of" | "ui.time-to-first-token" | "ui.trace" | "ui.uncached-input", string>>;
|
|
13
13
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type StatsLocale } from "./index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Subscribe a component to the active locale. `t()` reads the module-level
|
|
4
|
+
* catalogue, so a component that renders translated copy must call this —
|
|
5
|
+
* the topbar switch re-renders subscribers, and everything under them.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useLocale(): StatsLocale;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@linxiraos/pi-stats",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.2",
|
|
5
5
|
"description": "Local observability dashboard for omp AI usage statistics",
|
|
6
6
|
"homepage": "https://linxira-os.github.io/zeta/",
|
|
7
7
|
"author": "Linxira-OS",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@linxiraos/pi-ai": "1.4.
|
|
43
|
-
"@linxiraos/pi-catalog": "1.4.
|
|
44
|
-
"@linxiraos/pi-utils": "1.4.
|
|
42
|
+
"@linxiraos/pi-ai": "1.4.2",
|
|
43
|
+
"@linxiraos/pi-catalog": "1.4.2",
|
|
44
|
+
"@linxiraos/pi-utils": "1.4.2",
|
|
45
45
|
"lucide-react": "^1.24.0",
|
|
46
46
|
"react": "19.2.7",
|
|
47
47
|
"react-dom": "19.2.7"
|
package/src/client/App.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useRef, useState } from "react";
|
|
2
2
|
import type { DashboardSection } from "./app/nav";
|
|
3
3
|
import { Shell } from "./app/Shell";
|
|
4
|
+
import { useLocale } from "./i18n/useLocale";
|
|
4
5
|
import { LiveProvider } from "./data/live";
|
|
5
6
|
import { useHashRoute } from "./data/useHashRoute";
|
|
6
7
|
import {
|
|
@@ -27,6 +28,9 @@ export default function App() {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
function Dashboard() {
|
|
31
|
+
// A topbar language switch re-renders the whole tree: translated copy is
|
|
32
|
+
// read through `t()` at render time, including the mounted-but-hidden pages.
|
|
33
|
+
useLocale();
|
|
30
34
|
const { section, setSection, range, setRange, session, setSession } = useHashRoute();
|
|
31
35
|
const [selectedRequestId, setSelectedRequestId] = useState<number | null>(null);
|
|
32
36
|
// Stable identity so the drawer's effects don't tear down on every render.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Languages } from "lucide-react";
|
|
2
|
+
import { setUserLocale, t } from "../i18n";
|
|
3
|
+
import { useLocale } from "../i18n/useLocale";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Topbar language switch: EN ⇄ 中文. The label shows the language the click
|
|
7
|
+
* will install (the dashboard is already in the other one), and the choice is
|
|
8
|
+
* remembered in localStorage so a reload keeps it over the host's injected
|
|
9
|
+
* locale.
|
|
10
|
+
*/
|
|
11
|
+
export function LanguageToggle() {
|
|
12
|
+
const locale = useLocale();
|
|
13
|
+
const next = locale === "en" ? "zh-CN" : "en";
|
|
14
|
+
const label = locale === "en" ? "中文" : "EN";
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<button
|
|
18
|
+
type="button"
|
|
19
|
+
className="btn"
|
|
20
|
+
data-variant="ghost"
|
|
21
|
+
data-locale-toggle={locale}
|
|
22
|
+
onClick={() => setUserLocale(next)}
|
|
23
|
+
aria-label={t("shell.switch-language")}
|
|
24
|
+
title={`${t("shell.language")}: ${label}`}
|
|
25
|
+
>
|
|
26
|
+
<Languages size={15} />
|
|
27
|
+
<span className="topbar-locale-label">{label}</span>
|
|
28
|
+
</button>
|
|
29
|
+
);
|
|
30
|
+
}
|
package/src/client/app/Shell.tsx
CHANGED
|
@@ -2,9 +2,11 @@ import { Menu } from "lucide-react";
|
|
|
2
2
|
import { type ReactNode, useEffect, useState } from "react";
|
|
3
3
|
import { rangeMeta, TIME_RANGES } from "../data/range";
|
|
4
4
|
import { t } from "../i18n";
|
|
5
|
+
import { useLocale } from "../i18n/useLocale";
|
|
5
6
|
import { useLiveStatus } from "../data/live";
|
|
6
7
|
import type { TimeRange } from "../types";
|
|
7
8
|
import { Segmented } from "../ui";
|
|
9
|
+
import { LanguageToggle } from "./LanguageToggle";
|
|
8
10
|
import { LiveChip } from "./LiveChip";
|
|
9
11
|
import { type DashboardSection, NAV } from "./nav";
|
|
10
12
|
import { ThemeToggle } from "./ThemeToggle";
|
|
@@ -17,18 +19,20 @@ export interface ShellProps {
|
|
|
17
19
|
children: ReactNode;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
const RANGE_OPTIONS = TIME_RANGES.map((range, i) => ({
|
|
21
|
-
value: range,
|
|
22
|
-
label: t(rangeMeta(range).labelKey),
|
|
23
|
-
title: `${t(rangeMeta(range).windowKey)} (${i + 1})`,
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
22
|
/**
|
|
27
23
|
* Fixed glass topbar + naked sidebar framing one inset content panel. Owns
|
|
28
24
|
* the global keyboard shortcuts: `1`–`6` pick a range, `g` then a letter jumps
|
|
29
25
|
* to a page.
|
|
30
26
|
*/
|
|
31
27
|
export function Shell({ section, onSectionChange, range, onRangeChange, children }: ShellProps) {
|
|
28
|
+
// Range labels are translated copy, so they are rebuilt on a locale switch
|
|
29
|
+
// instead of being frozen in a module-level table.
|
|
30
|
+
useLocale();
|
|
31
|
+
const rangeOptions = TIME_RANGES.map((range, i) => ({
|
|
32
|
+
value: range,
|
|
33
|
+
label: t(rangeMeta(range).labelKey),
|
|
34
|
+
title: `${t(rangeMeta(range).windowKey)} (${i + 1})`,
|
|
35
|
+
}));
|
|
32
36
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
33
37
|
const scrolled = useScrolled();
|
|
34
38
|
const { sync } = useLiveStatus();
|
|
@@ -87,11 +91,12 @@ export function Shell({ section, onSectionChange, range, onRangeChange, children
|
|
|
87
91
|
<LiveChip />
|
|
88
92
|
</span>
|
|
89
93
|
<Segmented
|
|
90
|
-
options={
|
|
94
|
+
options={rangeOptions}
|
|
91
95
|
value={range}
|
|
92
96
|
onChange={onRangeChange}
|
|
93
97
|
aria-label={t("common.time-range")}
|
|
94
98
|
/>
|
|
99
|
+
<LanguageToggle />
|
|
95
100
|
<ThemeToggle />
|
|
96
101
|
</div>
|
|
97
102
|
{progress !== undefined && (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type LucideIcon, Monitor, Moon, Sun } from "lucide-react";
|
|
2
2
|
import { type ThemePreference, useThemePreference } from "../useSystemTheme";
|
|
3
3
|
import { t } from "../i18n";
|
|
4
|
+
import { useLocale } from "../i18n/useLocale";
|
|
4
5
|
|
|
5
6
|
const NEXT_PREFERENCE: Record<ThemePreference, ThemePreference> = {
|
|
6
7
|
system: "light",
|
|
@@ -8,22 +9,24 @@ const NEXT_PREFERENCE: Record<ThemePreference, ThemePreference> = {
|
|
|
8
9
|
dark: "system",
|
|
9
10
|
};
|
|
10
11
|
|
|
12
|
+
const PREFERENCE_LABEL_KEY = {
|
|
13
|
+
system: "shell.system-theme",
|
|
14
|
+
light: "shell.light-theme",
|
|
15
|
+
dark: "shell.dark-theme",
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
11
18
|
const PREFERENCE_ICON: Record<ThemePreference, LucideIcon> = {
|
|
12
19
|
system: Monitor,
|
|
13
20
|
light: Sun,
|
|
14
21
|
dark: Moon,
|
|
15
22
|
};
|
|
16
23
|
|
|
17
|
-
const PREFERENCE_LABEL: Record<ThemePreference, string> = {
|
|
18
|
-
system: t("shell.system-theme"),
|
|
19
|
-
light: t("shell.light-theme"),
|
|
20
|
-
dark: t("shell.dark-theme"),
|
|
21
|
-
};
|
|
22
|
-
|
|
23
24
|
/** Cycles system → light → dark. */
|
|
24
25
|
export function ThemeToggle() {
|
|
26
|
+
useLocale();
|
|
25
27
|
const { preference, setPreference } = useThemePreference();
|
|
26
28
|
const Icon = PREFERENCE_ICON[preference];
|
|
29
|
+
const label = t(PREFERENCE_LABEL_KEY[preference]);
|
|
27
30
|
|
|
28
31
|
return (
|
|
29
32
|
<button
|
|
@@ -32,8 +35,8 @@ export function ThemeToggle() {
|
|
|
32
35
|
data-variant="ghost"
|
|
33
36
|
data-icon="true"
|
|
34
37
|
onClick={() => setPreference(NEXT_PREFERENCE[preference])}
|
|
35
|
-
aria-label={`${
|
|
36
|
-
title={`${
|
|
38
|
+
aria-label={`${label} (click to switch)`}
|
|
39
|
+
title={`${label} — click to switch`}
|
|
37
40
|
>
|
|
38
41
|
<Icon size={15} />
|
|
39
42
|
</button>
|
package/src/client/i18n/index.ts
CHANGED
|
@@ -11,19 +11,32 @@ function injectedLocale(): string | undefined {
|
|
|
11
11
|
|
|
12
12
|
const messages: MessagesByLocale = { en: enLocale.messages, "zh-CN": zhCNLocale.messages };
|
|
13
13
|
|
|
14
|
+
/** Local override written by the in-page switcher; survives reloads. */
|
|
15
|
+
const LOCALE_STORAGE_KEY = "zeta.stats.locale";
|
|
16
|
+
|
|
17
|
+
const localeListeners = new Set<() => void>();
|
|
18
|
+
|
|
14
19
|
let activeLocale: StatsLocale = "en";
|
|
15
20
|
|
|
16
21
|
// Resolve once at module load. Every `t()` caller imports this module first, so
|
|
17
|
-
// the host language the server injected into `<head
|
|
18
|
-
// preference
|
|
19
|
-
//
|
|
22
|
+
// the stored choice, the host language the server injected into `<head>`, or
|
|
23
|
+
// the browser's own preference is already in effect by the time a component
|
|
24
|
+
// module is evaluated.
|
|
20
25
|
resolveInitialLocale();
|
|
21
26
|
|
|
22
|
-
/** The locale the dashboard renders in.
|
|
27
|
+
/** The locale the dashboard renders in. */
|
|
23
28
|
export function getLocale(): StatsLocale {
|
|
24
29
|
return activeLocale;
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
/** Subscribe to runtime switches (the topbar toggle); returns the unsubscribe. */
|
|
33
|
+
export function subscribeLocale(listener: () => void): () => void {
|
|
34
|
+
localeListeners.add(listener);
|
|
35
|
+
return () => {
|
|
36
|
+
localeListeners.delete(listener);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
/** Install a locale by tag (`zh`, `zh-CN`, `zh-Hans`; anything else is English). */
|
|
28
41
|
export function setLocale(tag: string | undefined | null): StatsLocale {
|
|
29
42
|
activeLocale = resolveLocaleTag(tag);
|
|
@@ -31,10 +44,40 @@ export function setLocale(tag: string | undefined | null): StatsLocale {
|
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
/**
|
|
34
|
-
* The
|
|
35
|
-
*
|
|
47
|
+
* The user's explicit pick from the topbar: install it, remember it, and
|
|
48
|
+
* re-render every subscriber.
|
|
49
|
+
*/
|
|
50
|
+
export function setUserLocale(tag: string): StatsLocale {
|
|
51
|
+
const next = setLocale(tag);
|
|
52
|
+
try {
|
|
53
|
+
globalThis.localStorage?.setItem(LOCALE_STORAGE_KEY, next);
|
|
54
|
+
} catch {
|
|
55
|
+
// Private-mode storage denial only costs the choice at next reload.
|
|
56
|
+
}
|
|
57
|
+
for (const listener of [...localeListeners]) listener();
|
|
58
|
+
return next;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A stored topbar choice, when it still parses as a supported locale. */
|
|
62
|
+
function storedLocale(): StatsLocale | undefined {
|
|
63
|
+
try {
|
|
64
|
+
const raw = globalThis.localStorage?.getItem(LOCALE_STORAGE_KEY);
|
|
65
|
+
if (!raw) return undefined;
|
|
66
|
+
const resolved = resolveLocaleTag(raw);
|
|
67
|
+
return resolved === "en" || resolved === "zh-CN" ? resolved : undefined;
|
|
68
|
+
} catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The locale to boot with: an explicit topbar choice, then the host's language
|
|
75
|
+
* when `zetacode stats` injected it, then the browser's own preference, then
|
|
76
|
+
* English.
|
|
36
77
|
*/
|
|
37
78
|
export function resolveInitialLocale(): StatsLocale {
|
|
79
|
+
const stored = storedLocale();
|
|
80
|
+
if (stored) return setLocale(stored);
|
|
38
81
|
const injected = injectedLocale();
|
|
39
82
|
if (injected) return setLocale(injected);
|
|
40
83
|
if (typeof navigator === "undefined") return "en";
|
|
@@ -81,6 +81,8 @@ export const common = {
|
|
|
81
81
|
"shell.light-theme": "Light theme",
|
|
82
82
|
"shell.range-suffix": "range",
|
|
83
83
|
"shell.jump-hint": "then letter to jump",
|
|
84
|
+
"shell.language": "Language",
|
|
85
|
+
"shell.switch-language": "Switch dashboard language",
|
|
84
86
|
},
|
|
85
87
|
"zh-CN": {
|
|
86
88
|
"common.provider": "服务商",
|
|
@@ -153,5 +155,7 @@ export const common = {
|
|
|
153
155
|
"shell.light-theme": "浅色主题",
|
|
154
156
|
"shell.range-suffix": "范围",
|
|
155
157
|
"shell.jump-hint": "再按字母跳转",
|
|
158
|
+
"shell.language": "语言",
|
|
159
|
+
"shell.switch-language": "切换仪表盘语言",
|
|
156
160
|
},
|
|
157
161
|
} as const;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
import { getLocale, subscribeLocale, type StatsLocale } from "./index";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Subscribe a component to the active locale. `t()` reads the module-level
|
|
6
|
+
* catalogue, so a component that renders translated copy must call this —
|
|
7
|
+
* the topbar switch re-renders subscribers, and everything under them.
|
|
8
|
+
*/
|
|
9
|
+
export function useLocale(): StatsLocale {
|
|
10
|
+
return useSyncExternalStore(subscribeLocale, getLocale, getLocale);
|
|
11
|
+
}
|
|
@@ -68,8 +68,6 @@ const TOKEN_MIX = [
|
|
|
68
68
|
{ key: "output", label: t("common.output"), color: "var(--chart-secondary)" },
|
|
69
69
|
] as const;
|
|
70
70
|
|
|
71
|
-
const SNAPSHOT_HINT = t("providers.snapshot-hint");
|
|
72
|
-
|
|
73
71
|
interface WindowRef {
|
|
74
72
|
provider: string;
|
|
75
73
|
windowKey: string;
|
|
@@ -608,7 +606,7 @@ function WindowInsightsTable({
|
|
|
608
606
|
limit={12}
|
|
609
607
|
selectedKey={selected ? `${selected.provider}::${selected.windowKey}` : null}
|
|
610
608
|
onRowClick={i => onSelect({ provider: i.provider, windowKey: i.windowKey })}
|
|
611
|
-
empty={<EmptyState title={t("providers.no-snapshots-in-range")} hint={
|
|
609
|
+
empty={<EmptyState title={t("providers.no-snapshots-in-range")} hint={t("providers.snapshot-hint")} />}
|
|
612
610
|
/>
|
|
613
611
|
);
|
|
614
612
|
}
|
|
@@ -882,7 +880,7 @@ function WindowUtilizationCard({
|
|
|
882
880
|
{insights === null ? (
|
|
883
881
|
<ChartSkeleton height={260} />
|
|
884
882
|
) : selected === null ? (
|
|
885
|
-
<EmptyState title={t("providers.no-snapshots-in-range")} hint={
|
|
883
|
+
<EmptyState title={t("providers.no-snapshots-in-range")} hint={t("providers.snapshot-hint")} />
|
|
886
884
|
) : (
|
|
887
885
|
<QueryView query={accounts} skeleton={<ChartSkeleton height={260} />}>
|
|
888
886
|
{() =>
|
|
@@ -52,8 +52,6 @@ const METRIC_OPTIONS = [
|
|
|
52
52
|
/** Tools stacked individually in the calls chart; the rest fold into "Other". */
|
|
53
53
|
const TOP_TOOLS = 6;
|
|
54
54
|
|
|
55
|
-
const ATTRIBUTION_NOTE = t("tools.attribution-note");
|
|
56
|
-
|
|
57
55
|
/** Bucket granularity label for the calls chart description. */
|
|
58
56
|
function bucketUnit(bucketMs: number): string {
|
|
59
57
|
if (bucketMs < 3_600_000) return t("tools.bucket-5m");
|
|
@@ -108,13 +106,13 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
|
108
106
|
/>
|
|
109
107
|
<Stat
|
|
110
108
|
label={t("tools.attributed-tokens")}
|
|
111
|
-
title={
|
|
109
|
+
title={t("tools.attribution-note")}
|
|
112
110
|
value={formatCompact(Math.round(totals.tokens))}
|
|
113
111
|
hint={t("tools.tokens-output", { tokens: formatCompact(Math.round(totals.output)) })}
|
|
114
112
|
/>
|
|
115
113
|
<Stat
|
|
116
114
|
label={t("tools.attributed-cost")}
|
|
117
|
-
title={t("tools.attribution-estimate", { note:
|
|
115
|
+
title={t("tools.attribution-estimate", { note: t("tools.attribution-note") })}
|
|
118
116
|
value={formatEstimatedCost(totals.cost, totals.unpriced)}
|
|
119
117
|
hint={
|
|
120
118
|
totals.unpriced > 0
|
|
@@ -195,7 +193,7 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
|
195
193
|
<Card
|
|
196
194
|
index={2}
|
|
197
195
|
title={t("tools.by-tool")}
|
|
198
|
-
description={t("tools.by-tool-description", { note:
|
|
196
|
+
description={t("tools.by-tool-description", { note: t("tools.attribution-note") })}
|
|
199
197
|
flush
|
|
200
198
|
stale={tools.stale}
|
|
201
199
|
>
|
|
@@ -432,7 +430,7 @@ function buildToolColumns(
|
|
|
432
430
|
{
|
|
433
431
|
key: "tokens",
|
|
434
432
|
header: t("tools.attr-tokens"),
|
|
435
|
-
title:
|
|
433
|
+
title: t("tools.attribution-note"),
|
|
436
434
|
align: "right",
|
|
437
435
|
sort: row => row.totalTokensShare,
|
|
438
436
|
render: row => (
|
|
@@ -445,7 +443,7 @@ function buildToolColumns(
|
|
|
445
443
|
{
|
|
446
444
|
key: "cost",
|
|
447
445
|
header: t("tools.attr-cost"),
|
|
448
|
-
title: t("tools.attribution-estimate", { note:
|
|
446
|
+
title: t("tools.attribution-estimate", { note: t("tools.attribution-note") }),
|
|
449
447
|
align: "right",
|
|
450
448
|
sort: row => row.costShare,
|
|
451
449
|
render: row => (
|
|
@@ -549,7 +547,7 @@ function ToolModelTable({
|
|
|
549
547
|
{
|
|
550
548
|
key: "tokens",
|
|
551
549
|
header: t("tools.attr-tokens"),
|
|
552
|
-
title:
|
|
550
|
+
title: t("tools.attribution-note"),
|
|
553
551
|
align: "right",
|
|
554
552
|
sort: row => row.totalTokensShare,
|
|
555
553
|
render: row => <span className="num">{formatCompact(Math.round(row.totalTokensShare))}</span>,
|
|
@@ -557,7 +555,7 @@ function ToolModelTable({
|
|
|
557
555
|
{
|
|
558
556
|
key: "cost",
|
|
559
557
|
header: t("tools.attr-cost"),
|
|
560
|
-
title: t("tools.attribution-estimate", { note:
|
|
558
|
+
title: t("tools.attribution-estimate", { note: t("tools.attribution-note") }),
|
|
561
559
|
align: "right",
|
|
562
560
|
sort: row => row.costShare,
|
|
563
561
|
render: row => <span className="num">{formatEstimatedCost(row.costShare, row.unpricedRequestsShare)}</span>,
|
package/src/client/styles.css
CHANGED
|
@@ -460,6 +460,19 @@ kbd {
|
|
|
460
460
|
gap: 8px;
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
/* Language switch: icon plus the language a click installs. */
|
|
464
|
+
.btn .topbar-locale-label {
|
|
465
|
+
font-size: 12px;
|
|
466
|
+
font-weight: 600;
|
|
467
|
+
letter-spacing: 0.02em;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
@media (max-width: 640px) {
|
|
471
|
+
.btn .topbar-locale-label {
|
|
472
|
+
display: none;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
463
476
|
@media (min-width: 901px) {
|
|
464
477
|
.btn.topbar-menu {
|
|
465
478
|
display: none;
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
formatDuration,
|
|
6
|
+
formatNumber,
|
|
7
|
+
formatPercent,
|
|
8
|
+
normalizePremiumRequests,
|
|
9
|
+
ZETA_STATS_PORT,
|
|
10
|
+
} from "@linxiraos/pi-utils";
|
|
5
11
|
import chalk from "@linxiraos/pi-utils/chalk";
|
|
6
12
|
import { formatErrorRate } from "./client/data/formatters";
|
|
7
13
|
import { getDashboardStats, getTotalMessageCount, syncAllSessions } from "./aggregator";
|
|
@@ -108,7 +114,7 @@ export function parseStandaloneStatsArgs(args: string[]): StandaloneStatsArgs {
|
|
|
108
114
|
const { values } = parseArgs({
|
|
109
115
|
args,
|
|
110
116
|
options: {
|
|
111
|
-
port: { type: "string", short: "p", default:
|
|
117
|
+
port: { type: "string", short: "p", default: String(ZETA_STATS_PORT) },
|
|
112
118
|
host: { type: "string", default: "127.0.0.1" },
|
|
113
119
|
json: { type: "boolean", short: "j", default: false },
|
|
114
120
|
sync: { type: "boolean", short: "s", default: false },
|
|
@@ -117,7 +123,7 @@ export function parseStandaloneStatsArgs(args: string[]): StandaloneStatsArgs {
|
|
|
117
123
|
allowPositionals: true,
|
|
118
124
|
});
|
|
119
125
|
return {
|
|
120
|
-
port: parseInt(values.port ||
|
|
126
|
+
port: parseInt(values.port || String(ZETA_STATS_PORT), 10),
|
|
121
127
|
host: values.host || "127.0.0.1",
|
|
122
128
|
json: values.json ?? false,
|
|
123
129
|
sync: values.sync ?? false,
|
|
@@ -139,7 +145,7 @@ Usage:
|
|
|
139
145
|
zeta-stats [options]
|
|
140
146
|
|
|
141
147
|
Options:
|
|
142
|
-
-p, --port <port> Port for the dashboard server (default:
|
|
148
|
+
-p, --port <port> Port for the dashboard server (default: ${ZETA_STATS_PORT})
|
|
143
149
|
--host <host> Host to bind (default: 127.0.0.1)
|
|
144
150
|
-j, --json Output stats as JSON and exit
|
|
145
151
|
-s, --sync Sync session files and show summary
|