@linxiraos/pi-stats 1.3.2 → 1.4.1
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 +14 -0
- package/README.md +2 -0
- package/dist/client/index.css +1 -1
- package/dist/client/index.js +91 -91
- 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 +76 -0
- package/dist/types/client/i18n/messages/frustration.d.ts +72 -0
- package/dist/types/client/i18n/messages/zh-CN.d.ts +1 -1
- package/dist/types/client/i18n/useLocale.d.ts +7 -0
- package/dist/types/db.d.ts +34 -1
- package/dist/types/model-identity.d.ts +29 -0
- package/dist/types/model-metrics.d.ts +53 -0
- package/dist/types/model-side.d.ts +6 -0
- package/dist/types/parser.d.ts +2 -1
- package/dist/types/shared-types.d.ts +88 -0
- package/dist/types/types.d.ts +47 -1
- package/dist/types/user-metrics.d.ts +22 -2
- package/package.json +4 -4
- package/src/aggregator.ts +1 -0
- 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/messages/frustration.ts +77 -0
- package/src/client/i18n/useLocale.ts +11 -0
- package/src/client/routes/FrustrationRoute.tsx +439 -4
- package/src/client/routes/ProvidersRoute.tsx +2 -4
- package/src/client/routes/ToolsRoute.tsx +7 -9
- package/src/client/routes/frustration.css +13 -0
- package/src/client/styles.css +13 -0
- package/src/db.ts +321 -2
- package/src/frustration.ts +8 -33
- package/src/model-identity.ts +61 -0
- package/src/model-metrics.ts +153 -0
- package/src/model-side.ts +77 -0
- package/src/parser.ts +104 -11
- package/src/shared-types.ts +93 -0
- package/src/types.ts +48 -1
- package/src/user-metrics.ts +9 -7
package/dist/types/types.d.ts
CHANGED
|
@@ -174,11 +174,57 @@ export interface UserMessageStats {
|
|
|
174
174
|
emphasis: number;
|
|
175
175
|
/** 1 when the prose leans on a script no lexicon covers; such rows are unscored, not calm. */
|
|
176
176
|
unsupportedScript: number;
|
|
177
|
-
/** Judge-facing prose (see `
|
|
177
|
+
/** Judge-facing prose (see `analysisProse`); empty when the message was all markup. */
|
|
178
178
|
prose: string;
|
|
179
179
|
/** Hex `Bun.hash` of `prose`, keying the shared verdict cache; empty when `prose` is. */
|
|
180
180
|
proseHash: string;
|
|
181
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Model-side analysis for one assistant turn: the text body stored for
|
|
184
|
+
* offline analysis plus the signals computed from it at ingest (see
|
|
185
|
+
* `model-metrics.ts`). Keyed like `user_messages` by (session_file, entry_id).
|
|
186
|
+
* Turns without prose (tool-call-only replies) emit no row.
|
|
187
|
+
*/
|
|
188
|
+
export interface AssistantTextStats {
|
|
189
|
+
/** Database ID */
|
|
190
|
+
id?: number;
|
|
191
|
+
/** Session file path */
|
|
192
|
+
sessionFile: string;
|
|
193
|
+
/** Assistant message entry ID within the session */
|
|
194
|
+
entryId: string;
|
|
195
|
+
/** Folder/project path (extracted from session filename) */
|
|
196
|
+
folder: string;
|
|
197
|
+
/** Unix timestamp in ms */
|
|
198
|
+
timestamp: number;
|
|
199
|
+
/** Model that produced the turn */
|
|
200
|
+
model: string;
|
|
201
|
+
/** Provider that served the turn */
|
|
202
|
+
provider: string;
|
|
203
|
+
/** Which agent produced the turn (main agent, task subagent, advisor) */
|
|
204
|
+
agentType: AgentType;
|
|
205
|
+
/** Analyzed-prose characters (code and markup stripped, capped) */
|
|
206
|
+
chars: number;
|
|
207
|
+
/** Analyzed-prose words */
|
|
208
|
+
words: number;
|
|
209
|
+
/** Apology / self-blame markers */
|
|
210
|
+
selfBlame: number;
|
|
211
|
+
/** Repeated-restatement markers plus near-duplicate previous turn */
|
|
212
|
+
restatement: number;
|
|
213
|
+
/** Hedged fallback markers */
|
|
214
|
+
hedge: number;
|
|
215
|
+
/** 1 when the turn is long-winded */
|
|
216
|
+
verbose: number;
|
|
217
|
+
/** Failed tool results preceding the turn */
|
|
218
|
+
toolErrors: number;
|
|
219
|
+
/** 1 when the preceding failures form a spiral */
|
|
220
|
+
toolSpiral: number;
|
|
221
|
+
/** Per-turn aggregate (see `AssistantMessageMetrics`) */
|
|
222
|
+
collapse: number;
|
|
223
|
+
/** 1 when the prose leans on a script no lexicon covers */
|
|
224
|
+
unsupportedScript: number;
|
|
225
|
+
/** Stored analysis prose (see `analysisProse`); never empty for an emitted row. */
|
|
226
|
+
prose: string;
|
|
227
|
+
}
|
|
182
228
|
/**
|
|
183
229
|
* Pair emitted by the parser when it sees an assistant message whose
|
|
184
230
|
* `parentId` points to a user message that wasn't parsed in the same pass
|
|
@@ -91,14 +91,34 @@ export interface UserMessageMetrics {
|
|
|
91
91
|
*/
|
|
92
92
|
unsupportedScript: number;
|
|
93
93
|
}
|
|
94
|
+
export declare const WORD_RE: RegExp;
|
|
95
|
+
/** 1 when the prose leans on a script the lexicons do not cover. */
|
|
96
|
+
export declare function detectUnsupportedScript(prose: string): number;
|
|
97
|
+
/**
|
|
98
|
+
* 1 when the message says essentially the same thing as the one before it -
|
|
99
|
+
* the one frustration signal that needs no lexicon and therefore works for
|
|
100
|
+
* any language and survives every transcription error.
|
|
101
|
+
*/
|
|
102
|
+
export declare function detectRepeatedMessage(prose: string, prevProse: string): number;
|
|
103
|
+
/** Count regex hits without materializing the match array. */
|
|
104
|
+
export declare function countMatches(text: string, re: RegExp): number;
|
|
105
|
+
/**
|
|
106
|
+
* Strip structured content so that pasted code, harness wrappers, file
|
|
107
|
+
* mentions and quoted blocks don't dilute or fake behavior signals.
|
|
108
|
+
* Each strip is replaced with a newline so subsequent line counting
|
|
109
|
+
* reflects what was removed instead of merging neighbors.
|
|
110
|
+
*/
|
|
111
|
+
export declare function stripStructuredContent(text: string): string;
|
|
94
112
|
/** Cap on the judge-facing prose stored per user message; longer prose is truncated. */
|
|
95
113
|
export declare const PROSE_MAX_CHARS = 4000;
|
|
96
114
|
/**
|
|
97
|
-
* The prose a
|
|
115
|
+
* The prose a message contributes to behavioral analysis: the same
|
|
98
116
|
* structured-content strip the regex signals score on, trimmed and capped at
|
|
99
117
|
* {@link PROSE_MAX_CHARS}. Empty when the message was nothing but markup.
|
|
118
|
+
* Shared by the user-message judge and the model-side analyzer so both store
|
|
119
|
+
* exactly the same body.
|
|
100
120
|
*/
|
|
101
|
-
export declare function
|
|
121
|
+
export declare function analysisProse(text: string): string;
|
|
102
122
|
/**
|
|
103
123
|
* Compute behavioral metrics for a user message.
|
|
104
124
|
*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@linxiraos/pi-stats",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.4.1",
|
|
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.
|
|
43
|
-
"@linxiraos/pi-catalog": "1.
|
|
44
|
-
"@linxiraos/pi-utils": "1.
|
|
42
|
+
"@linxiraos/pi-ai": "1.4.1",
|
|
43
|
+
"@linxiraos/pi-catalog": "1.4.1",
|
|
44
|
+
"@linxiraos/pi-utils": "1.4.1",
|
|
45
45
|
"lucide-react": "^1.24.0",
|
|
46
46
|
"react": "19.2.7",
|
|
47
47
|
"react-dom": "19.2.7"
|
package/src/aggregator.ts
CHANGED
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;
|
|
@@ -101,6 +101,46 @@ export const frustration = {
|
|
|
101
101
|
"frustration.cancelled": "Cancelled",
|
|
102
102
|
"frustration.failed": "Failed",
|
|
103
103
|
"frustration.done": "Done",
|
|
104
|
+
"frustration.model-side-title": "AI breakdown",
|
|
105
|
+
"frustration.model-side-description": "How the model's own replies read, per model version, in {window}.",
|
|
106
|
+
"frustration.model-turns": "Assistant turns",
|
|
107
|
+
"frustration.model-turns-title":
|
|
108
|
+
"Assistant replies with text in range; tool-call-only replies and thinking blocks are not analyzed",
|
|
109
|
+
"frustration.model-breakdown": "Breakdown rate",
|
|
110
|
+
"frustration.model-breakdown-title": "Share of analyzed turns with at least one breakdown signal",
|
|
111
|
+
"frustration.model-mean": "Signals / turn",
|
|
112
|
+
"frustration.model-mean-title": "Average per-turn aggregate: text markers plus capped failed tool results",
|
|
113
|
+
"frustration.model-self-blame": "Self-blame",
|
|
114
|
+
"frustration.model-self-blame-title": "Turns containing an apology or admission",
|
|
115
|
+
"frustration.model-restatement": "Restatement",
|
|
116
|
+
"frustration.model-restatement-title":
|
|
117
|
+
"Turns repeating earlier content, by explicit marker or near-duplicate reply",
|
|
118
|
+
"frustration.model-hedge": "Hedged",
|
|
119
|
+
"frustration.model-hedge-title": "Turns with hedged fallbacks: uncertainty or suggested alternatives",
|
|
120
|
+
"frustration.model-spiral": "Tool spirals",
|
|
121
|
+
"frustration.model-spiral-title": "Turns following three or more failed tool results",
|
|
122
|
+
"frustration.model-rambling": "Rambling",
|
|
123
|
+
"frustration.model-rambling-title": "Turns past the verbosity threshold",
|
|
124
|
+
"frustration.model-unscored-line": "Turns in a language without a lexicon: {count} — unscored, not calm.",
|
|
125
|
+
"frustration.model-by-model": "AI breakdown by model version",
|
|
126
|
+
"frustration.model-table-title": "Model breakdown detail",
|
|
127
|
+
"frustration.model-by-model-description":
|
|
128
|
+
"Share of each model's analyzed turns carrying a breakdown signal. Hue = model family.",
|
|
129
|
+
"frustration.model-over-time": "AI breakdown over time",
|
|
130
|
+
"frustration.model-over-time-description":
|
|
131
|
+
"Breakdown rate per bucket: turns with a signal over analyzed turns. Gaps are buckets with no turns.",
|
|
132
|
+
"frustration.model-sessions": "Sessions by breakdown",
|
|
133
|
+
"frustration.model-sessions-description": "Sessions with the most analyzed assistant turns.",
|
|
134
|
+
"frustration.model-table-description": "Rates are shares of analyzed assistant turns.",
|
|
135
|
+
"frustration.column-session": "Session",
|
|
136
|
+
"frustration.column-turns": "Turns",
|
|
137
|
+
"frustration.column-mean": "Signals/turn",
|
|
138
|
+
"frustration.column-tool-errors": "Tool errors",
|
|
139
|
+
"frustration.model-no-turns": "No assistant turns with text in this range",
|
|
140
|
+
"frustration.model-try-longer-range": "Try a longer range.",
|
|
141
|
+
"frustration.model-legend-breakdown": "Breakdown rate",
|
|
142
|
+
"frustration.turns-n": "{count} turns",
|
|
143
|
+
"frustration.model-hint-mean": "{value} signals per turn on average",
|
|
104
144
|
},
|
|
105
145
|
"zh-CN": {
|
|
106
146
|
"frustration.estimate-note":
|
|
@@ -194,5 +234,42 @@ export const frustration = {
|
|
|
194
234
|
"frustration.cancelled": "已取消",
|
|
195
235
|
"frustration.failed": "失败",
|
|
196
236
|
"frustration.done": "完成",
|
|
237
|
+
"frustration.model-side-title": "AI 崩溃度",
|
|
238
|
+
"frustration.model-side-description": "各模型版本自身回复的读感,统计窗口:{window}。",
|
|
239
|
+
"frustration.model-turns": "助手回合",
|
|
240
|
+
"frustration.model-turns-title": "时间范围内带文本的助手回复;仅含工具调用的回复与思考块不参与分析",
|
|
241
|
+
"frustration.model-breakdown": "崩溃率",
|
|
242
|
+
"frustration.model-breakdown-title": "至少带一个崩溃信号的已分析回合占比",
|
|
243
|
+
"frustration.model-mean": "信号 / 回合",
|
|
244
|
+
"frustration.model-mean-title": "每回合聚合均值:文本标记加上封顶的工具失败数",
|
|
245
|
+
"frustration.model-self-blame": "自责",
|
|
246
|
+
"frustration.model-self-blame-title": "包含道歉或认错的回合",
|
|
247
|
+
"frustration.model-restatement": "重复陈述",
|
|
248
|
+
"frustration.model-restatement-title": "以显式标记或与上一条回复高度相似的方式重复先前内容的回合",
|
|
249
|
+
"frustration.model-hedge": "含糊兜底",
|
|
250
|
+
"frustration.model-hedge-title": "带有含糊兜底的回合:不确定表述或建议替代方案",
|
|
251
|
+
"frustration.model-spiral": "工具失败螺旋",
|
|
252
|
+
"frustration.model-spiral-title": "紧随三个及以上工具失败之后的回合",
|
|
253
|
+
"frustration.model-rambling": "冗长",
|
|
254
|
+
"frustration.model-rambling-title": "超过冗长阈值的回合",
|
|
255
|
+
"frustration.model-unscored-line": "使用无词典语言的回合:{count} — 未评分,而非平静。",
|
|
256
|
+
"frustration.model-by-model": "各模型版本的 AI 崩溃度",
|
|
257
|
+
"frustration.model-table-title": "模型崩溃明细",
|
|
258
|
+
"frustration.model-by-model-description": "每个模型已分析回合中带崩溃信号的占比。色相 = 模型家族。",
|
|
259
|
+
"frustration.model-over-time": "AI 崩溃度随时间变化",
|
|
260
|
+
"frustration.model-over-time-description":
|
|
261
|
+
"每个时间桶的崩溃率:带信号的回合除以已分析回合。空白表示该桶没有回合。",
|
|
262
|
+
"frustration.model-sessions": "按崩溃度查看会话",
|
|
263
|
+
"frustration.model-sessions-description": "已分析助手回合最多的会话。",
|
|
264
|
+
"frustration.model-table-description": "比例为已分析助手回合的占比。",
|
|
265
|
+
"frustration.column-session": "会话",
|
|
266
|
+
"frustration.column-turns": "回合数",
|
|
267
|
+
"frustration.column-mean": "信号/回合",
|
|
268
|
+
"frustration.column-tool-errors": "工具错误",
|
|
269
|
+
"frustration.model-no-turns": "该时间范围内没有带文本的助手回合",
|
|
270
|
+
"frustration.model-try-longer-range": "试试更长的时间范围。",
|
|
271
|
+
"frustration.model-legend-breakdown": "崩溃率",
|
|
272
|
+
"frustration.turns-n": "{count} 个回合",
|
|
273
|
+
"frustration.model-hint-mean": "平均每回合 {value} 个信号",
|
|
197
274
|
},
|
|
198
275
|
} 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
|
+
}
|