[fix(web)] localize copied task-stats line instead of hardcoded Chinese (#8)
This commit is contained in:
@@ -71,7 +71,16 @@ export function TaskStatsLine({
|
||||
|
||||
const copy = () => {
|
||||
const text =
|
||||
assistantText?.trim() || stats === null ? (assistantText ?? "") : formatTaskStats(stats);
|
||||
assistantText?.trim() || stats === null
|
||||
? (assistantText ?? "")
|
||||
: formatTaskStats(stats, {
|
||||
stats: S.chat.statsLabel,
|
||||
input: S.chat.statInput,
|
||||
cached: S.chat.statCached,
|
||||
output: S.chat.statOutput,
|
||||
parenOpen: S.chat.statParenOpen,
|
||||
parenClose: S.chat.statParenClose,
|
||||
});
|
||||
void navigator.clipboard?.writeText(text).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
|
||||
@@ -287,17 +287,34 @@ export function endTask(t: TaskStatsTracker, elapsedMs: number): TaskStats | nul
|
||||
return stats;
|
||||
}
|
||||
|
||||
/** Localized labels for {@link formatTaskStats} (supplied by the view layer's active dictionary). */
|
||||
export interface TaskStatsLabels {
|
||||
/** Row prefix, e.g. "Stats" / "统计信息". */
|
||||
stats: string;
|
||||
/** Input-tokens label, e.g. "Input tokens" / "输入 tokens". */
|
||||
input: string;
|
||||
/** Cached-amount label, e.g. "cached" / "已缓存". */
|
||||
cached: string;
|
||||
/** Output-tokens label, e.g. "Output tokens" / "输出 tokens". */
|
||||
output: string;
|
||||
/** Opening wrapper before the cached amount — keeps per-locale typography (e.g. " (" / "("). */
|
||||
parenOpen: string;
|
||||
/** Closing wrapper after the cached amount (e.g. ")" / ")"). */
|
||||
parenClose: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stats row text (copy fallback / used when there's no body text): same basis as the stats row —
|
||||
* this round's usage "input tokens (including cached amount) · output tokens · output TPS", e.g.
|
||||
* `[统计信息] 输入 tokens 4k(已缓存 3k) · 输出 tokens 1.2k · 42.5 tok/s`; label can be overridden
|
||||
* per UI locale.
|
||||
* `[Stats] Input tokens 4k (cached 3k) · Output tokens 1.2k · 42.5 tok/s`. Labels (including the
|
||||
* parenthesis wrappers) come from the caller's active locale dictionary, so the copied text keeps
|
||||
* per-locale typography — ASCII `(…)` in English, fullwidth `(…)` in Chinese.
|
||||
*/
|
||||
export function formatTaskStats(s: TaskStats, label = "统计信息"): string {
|
||||
export function formatTaskStats(s: TaskStats, labels: TaskStatsLabels): string {
|
||||
const b = s.tokensByBucket;
|
||||
return (
|
||||
`[${label}] 输入 tokens ${humanizeTokens(b.cacheRead + b.cacheWrite)}(已缓存 ${humanizeTokens(b.cacheRead)})` +
|
||||
` · 输出 tokens ${humanizeTokens(b.output)}` +
|
||||
`[${labels.stats}] ${labels.input} ${humanizeTokens(b.cacheRead + b.cacheWrite)}${labels.parenOpen}${labels.cached} ${humanizeTokens(b.cacheRead)}${labels.parenClose}` +
|
||||
` · ${labels.output} ${humanizeTokens(b.output)}` +
|
||||
` · ${formatTps(s.outputTps)}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -609,8 +609,12 @@ When done, open index.html in a browser and self-test once.`,
|
||||
statCost: "Cost",
|
||||
statElapsed: "Elapsed",
|
||||
statInput: "Input tokens",
|
||||
statCached: "cached",
|
||||
statOutput: "Output tokens",
|
||||
statTps: "Output TPS",
|
||||
/** Copied-stats-line parenthesis wrappers around the cached amount (ASCII with a leading space for en). */
|
||||
statParenOpen: " (",
|
||||
statParenClose: ")",
|
||||
noSessions: "No Sessions yet",
|
||||
emptyStream: "Send a message to start the conversation",
|
||||
historyLoadFailed: "Failed to load history",
|
||||
|
||||
@@ -589,8 +589,12 @@ Penguin 视觉风格(见 web-design 技能),深色/浅色主题(<html da
|
||||
statCost: "成本",
|
||||
statElapsed: "用时",
|
||||
statInput: "输入 tokens",
|
||||
statCached: "已缓存",
|
||||
statOutput: "输出 tokens",
|
||||
statTps: "输出 TPS",
|
||||
/** Copied-stats-line parenthesis wrappers around the cached amount (fullwidth for zh typography). */
|
||||
statParenOpen: "(",
|
||||
statParenClose: ")",
|
||||
noSessions: "还没有 Session",
|
||||
emptyStream: "发送一条消息开始对话",
|
||||
historyLoadFailed: "历史消息加载失败",
|
||||
|
||||
@@ -195,33 +195,74 @@ describe("TaskStatsTracker", () => {
|
||||
});
|
||||
|
||||
describe("formatTaskStats", () => {
|
||||
it("the convention is this round's usage \"输入(已缓存)· 输出 · 输出 TPS\", taking this Task's three-bucket usage + this Task's TPS", () => {
|
||||
const EN_LABELS = {
|
||||
stats: "Stats",
|
||||
input: "Input tokens",
|
||||
cached: "cached",
|
||||
output: "Output tokens",
|
||||
parenOpen: " (",
|
||||
parenClose: ")",
|
||||
};
|
||||
const ZH_LABELS = {
|
||||
stats: "统计信息",
|
||||
input: "输入 tokens",
|
||||
cached: "已缓存",
|
||||
output: "输出 tokens",
|
||||
parenOpen: "(",
|
||||
parenClose: ")",
|
||||
};
|
||||
|
||||
it("the convention is this round's usage \"input (cached) · output · output TPS\", taking this Task's three-bucket usage + this Task's TPS", () => {
|
||||
expect(
|
||||
formatTaskStats({
|
||||
context: 40000, // context usage isn't in the stats row (shown by the input-box ring instead)
|
||||
contextDelta: 1000,
|
||||
tokens: 60000,
|
||||
tokensDelta: 1200,
|
||||
elapsedMs: 5100,
|
||||
elapsedDeltaMs: 2300,
|
||||
tokensByBucket: { cacheRead: 3000, cacheWrite: 1000, output: 1200 },
|
||||
outputTps: 42.5,
|
||||
}),
|
||||
).toBe("[统计信息] 输入 tokens 4k(已缓存 3k) · 输出 tokens 1.2k · 42.5 tok/s");
|
||||
formatTaskStats(
|
||||
{
|
||||
context: 40000, // context usage isn't in the stats row (shown by the input-box ring instead)
|
||||
contextDelta: 1000,
|
||||
tokens: 60000,
|
||||
tokensDelta: 1200,
|
||||
elapsedMs: 5100,
|
||||
elapsedDeltaMs: 2300,
|
||||
tokensByBucket: { cacheRead: 3000, cacheWrite: 1000, output: 1200 },
|
||||
outputTps: 42.5,
|
||||
},
|
||||
EN_LABELS,
|
||||
),
|
||||
).toBe("[Stats] Input tokens 4k (cached 3k) · Output tokens 1.2k · 42.5 tok/s");
|
||||
});
|
||||
|
||||
it("TPS shows — without LLM timing", () => {
|
||||
expect(
|
||||
formatTaskStats({
|
||||
context: 500,
|
||||
contextDelta: 0,
|
||||
tokens: 500,
|
||||
tokensDelta: 500,
|
||||
elapsedMs: 900,
|
||||
elapsedDeltaMs: 900,
|
||||
tokensByBucket: { cacheRead: 500, cacheWrite: 0, output: 0 },
|
||||
outputTps: null,
|
||||
}),
|
||||
).toBe("[统计信息] 输入 tokens 500(已缓存 500) · 输出 tokens 0 · —");
|
||||
formatTaskStats(
|
||||
{
|
||||
context: 500,
|
||||
contextDelta: 0,
|
||||
tokens: 500,
|
||||
tokensDelta: 500,
|
||||
elapsedMs: 900,
|
||||
elapsedDeltaMs: 900,
|
||||
tokensByBucket: { cacheRead: 500, cacheWrite: 0, output: 0 },
|
||||
outputTps: null,
|
||||
},
|
||||
EN_LABELS,
|
||||
),
|
||||
).toBe("[Stats] Input tokens 500 (cached 500) · Output tokens 0 · —");
|
||||
});
|
||||
|
||||
it("renders with the supplied locale labels (zh)", () => {
|
||||
expect(
|
||||
formatTaskStats(
|
||||
{
|
||||
context: 40000,
|
||||
contextDelta: 1000,
|
||||
tokens: 60000,
|
||||
tokensDelta: 1200,
|
||||
elapsedMs: 5100,
|
||||
elapsedDeltaMs: 2300,
|
||||
tokensByBucket: { cacheRead: 3000, cacheWrite: 1000, output: 1200 },
|
||||
outputTps: 42.5,
|
||||
},
|
||||
ZH_LABELS,
|
||||
),
|
||||
).toBe("[统计信息] 输入 tokens 4k(已缓存 3k) · 输出 tokens 1.2k · 42.5 tok/s");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user