@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
|
@@ -6,17 +6,21 @@ import {
|
|
|
6
6
|
getFrustrationEstimate,
|
|
7
7
|
startFrustrationRun,
|
|
8
8
|
} from "../api";
|
|
9
|
-
import { Chart, type ChartSeries, Legend, useHiddenSeries } from "../charts";
|
|
9
|
+
import { BarList, Chart, type ChartSeries, Legend, TimeChart, useHiddenSeries } from "../charts";
|
|
10
10
|
import { t } from "../i18n";
|
|
11
11
|
import { SERIES_COLORS } from "../data/colors";
|
|
12
12
|
import { formatCost, formatInteger, formatPercent } from "../data/formatters";
|
|
13
13
|
import { useQuery } from "../data/query";
|
|
14
|
-
import { rangeMeta } from "../data/range";
|
|
14
|
+
import { bucketAxis, rangeMeta } from "../data/range";
|
|
15
15
|
import type {
|
|
16
16
|
FrustrationCounts,
|
|
17
17
|
FrustrationEstimate,
|
|
18
18
|
FrustrationJobStatus,
|
|
19
19
|
FrustrationModelStats,
|
|
20
|
+
ModelSideCounts,
|
|
21
|
+
ModelSideModelStats,
|
|
22
|
+
ModelSideSessionStats,
|
|
23
|
+
ModelSideTimeSeriesPoint,
|
|
20
24
|
TimeRange,
|
|
21
25
|
} from "../types";
|
|
22
26
|
import {
|
|
@@ -110,6 +114,47 @@ export function FrustrationRoute({ active, range }: FrustrationRouteProps) {
|
|
|
110
114
|
>
|
|
111
115
|
{data => <FrustrationModelsSection models={data.byModel} stale={stats.stale} />}
|
|
112
116
|
</QueryView>
|
|
117
|
+
|
|
118
|
+
{/* Model side: the same page, second section. Its signals are
|
|
119
|
+
language-neutral lexicons computed at ingest, so it needs no
|
|
120
|
+
judge run and appears immediately. */}
|
|
121
|
+
<QueryView query={stats} skeleton={<ChartSkeleton height={96} />}>
|
|
122
|
+
{data => (
|
|
123
|
+
<div className="frustration-model-summary" data-stale={stats.stale}>
|
|
124
|
+
<ModelSideSummary overall={data.modelSide.overall} window={t(meta.windowKey)} />
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</QueryView>
|
|
128
|
+
|
|
129
|
+
<QueryView
|
|
130
|
+
query={stats}
|
|
131
|
+
skeleton={
|
|
132
|
+
<Card index={4} title={t("frustration.model-by-model")}>
|
|
133
|
+
<ChartSkeleton height={300} />
|
|
134
|
+
<TableSkeleton rows={5} />
|
|
135
|
+
</Card>
|
|
136
|
+
}
|
|
137
|
+
>
|
|
138
|
+
{data => (
|
|
139
|
+
<ModelSideModels
|
|
140
|
+
models={data.modelSide.byModel}
|
|
141
|
+
series={data.modelSide.series}
|
|
142
|
+
range={range}
|
|
143
|
+
stale={stats.stale}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
146
|
+
</QueryView>
|
|
147
|
+
|
|
148
|
+
<QueryView
|
|
149
|
+
query={stats}
|
|
150
|
+
skeleton={
|
|
151
|
+
<Card index={7} title={t("frustration.model-sessions")}>
|
|
152
|
+
<TableSkeleton rows={5} />
|
|
153
|
+
</Card>
|
|
154
|
+
}
|
|
155
|
+
>
|
|
156
|
+
{data => <ModelSideSessions sessions={data.modelSide.bySession} stale={stats.stale} />}
|
|
157
|
+
</QueryView>
|
|
113
158
|
</div>
|
|
114
159
|
);
|
|
115
160
|
}
|
|
@@ -497,7 +542,7 @@ function JudgeResultLine({ job }: { job: FrustrationJobStatus }) {
|
|
|
497
542
|
// ---------------------------------------------------------------------------
|
|
498
543
|
|
|
499
544
|
/** Chart/filter identity of a model family: `class/family`. */
|
|
500
|
-
function familyKey(row:
|
|
545
|
+
function familyKey(row: { modelClass: string; family: string | null }): string {
|
|
501
546
|
return `${row.modelClass}/${row.family ?? "unclassified"}`;
|
|
502
547
|
}
|
|
503
548
|
|
|
@@ -520,7 +565,7 @@ function hashString(value: string): number {
|
|
|
520
565
|
* and probes forward past taken slots, so families present together stay
|
|
521
566
|
* distinct (until the palette runs out) and keep their hue across ranges.
|
|
522
567
|
*/
|
|
523
|
-
function assignFamilyColors(models: readonly
|
|
568
|
+
function assignFamilyColors(models: readonly { modelClass: string; family: string | null }[]): Map<string, string> {
|
|
524
569
|
const keys = [...new Set(models.map(familyKey))].sort();
|
|
525
570
|
const taken = new Set<number>();
|
|
526
571
|
const colors = new Map<string, string>();
|
|
@@ -937,3 +982,393 @@ function FrustrationTable({
|
|
|
937
982
|
</Card>
|
|
938
983
|
);
|
|
939
984
|
}
|
|
985
|
+
|
|
986
|
+
// ---------------------------------------------------------------------------
|
|
987
|
+
// Model side (AI breakdown)
|
|
988
|
+
// ---------------------------------------------------------------------------
|
|
989
|
+
|
|
990
|
+
function ModelSideSummary({ overall, window }: { overall: ModelSideCounts; window: string }) {
|
|
991
|
+
const mean = overall.messages > 0 ? (overall.collapse / overall.messages).toFixed(2) : "–";
|
|
992
|
+
return (
|
|
993
|
+
<>
|
|
994
|
+
<div className="card-titles">
|
|
995
|
+
<h2 className="page-title">{t("frustration.model-side-title")}</h2>
|
|
996
|
+
<p className="page-description">{t("frustration.model-side-description", { window })}</p>
|
|
997
|
+
</div>
|
|
998
|
+
<div className="frustration-model-notes">
|
|
999
|
+
<StatGrid min={170}>
|
|
1000
|
+
<Stat
|
|
1001
|
+
label={t("frustration.model-turns")}
|
|
1002
|
+
title={t("frustration.model-turns-title")}
|
|
1003
|
+
value={formatInteger(overall.messages)}
|
|
1004
|
+
/>
|
|
1005
|
+
<Stat
|
|
1006
|
+
label={t("frustration.model-breakdown")}
|
|
1007
|
+
title={t("frustration.model-breakdown-title")}
|
|
1008
|
+
value={rate(overall.breakdown, overall.messages)}
|
|
1009
|
+
hint={t("frustration.turns-n", { count: formatInteger(overall.breakdown) })}
|
|
1010
|
+
/>
|
|
1011
|
+
<Stat
|
|
1012
|
+
label={t("frustration.model-mean")}
|
|
1013
|
+
title={t("frustration.model-mean-title")}
|
|
1014
|
+
value={mean}
|
|
1015
|
+
hint={t("frustration.model-hint-mean", { value: mean })}
|
|
1016
|
+
/>
|
|
1017
|
+
<Stat
|
|
1018
|
+
label={t("frustration.model-self-blame")}
|
|
1019
|
+
title={t("frustration.model-self-blame-title")}
|
|
1020
|
+
value={rate(overall.blamed, overall.messages)}
|
|
1021
|
+
/>
|
|
1022
|
+
<Stat
|
|
1023
|
+
label={t("frustration.model-hedge")}
|
|
1024
|
+
title={t("frustration.model-hedge-title")}
|
|
1025
|
+
value={rate(overall.hedged, overall.messages)}
|
|
1026
|
+
/>
|
|
1027
|
+
<Stat
|
|
1028
|
+
label={t("frustration.model-spiral")}
|
|
1029
|
+
title={t("frustration.model-spiral-title")}
|
|
1030
|
+
value={rate(overall.spiraled, overall.messages)}
|
|
1031
|
+
/>
|
|
1032
|
+
</StatGrid>
|
|
1033
|
+
{overall.unscored > 0 && (
|
|
1034
|
+
<p className="micro dim">
|
|
1035
|
+
{t("frustration.model-unscored-line", { count: formatInteger(overall.unscored) })}
|
|
1036
|
+
</p>
|
|
1037
|
+
)}
|
|
1038
|
+
</div>
|
|
1039
|
+
</>
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/** Share of turns carrying a signal, as the 0..100 value a chart axis wants. */
|
|
1044
|
+
function signalShare(part: number, whole: number): number {
|
|
1045
|
+
return whole > 0 ? (part / whole) * 100 : 0;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function ModelSideModels({
|
|
1049
|
+
models,
|
|
1050
|
+
series,
|
|
1051
|
+
range,
|
|
1052
|
+
stale,
|
|
1053
|
+
}: {
|
|
1054
|
+
models: ModelSideModelStats[];
|
|
1055
|
+
series: ModelSideTimeSeriesPoint[];
|
|
1056
|
+
range: TimeRange;
|
|
1057
|
+
stale: boolean;
|
|
1058
|
+
}) {
|
|
1059
|
+
const meta = rangeMeta(range);
|
|
1060
|
+
const familyColors = useMemo(() => assignFamilyColors(models), [models]);
|
|
1061
|
+
const buckets = useMemo(
|
|
1062
|
+
() =>
|
|
1063
|
+
bucketAxis(
|
|
1064
|
+
range,
|
|
1065
|
+
series.map(point => point.timestamp),
|
|
1066
|
+
meta.bucketMs,
|
|
1067
|
+
),
|
|
1068
|
+
[series, range, meta.bucketMs],
|
|
1069
|
+
);
|
|
1070
|
+
const byBucket = useMemo(() => new Map(series.map(point => [point.timestamp, point])), [series]);
|
|
1071
|
+
const chartSeries = useMemo(
|
|
1072
|
+
(): ChartSeries[] => [
|
|
1073
|
+
{
|
|
1074
|
+
key: "breakdown",
|
|
1075
|
+
label: t("frustration.model-legend-breakdown"),
|
|
1076
|
+
color: "var(--chart-primary)",
|
|
1077
|
+
values: buckets.map(bucket => {
|
|
1078
|
+
const point = byBucket.get(bucket);
|
|
1079
|
+
// A bucket without turns is a gap, not a calm 0%.
|
|
1080
|
+
return point && point.messages > 0 ? signalShare(point.breakdown, point.messages) : null;
|
|
1081
|
+
}),
|
|
1082
|
+
},
|
|
1083
|
+
],
|
|
1084
|
+
[buckets, byBucket],
|
|
1085
|
+
);
|
|
1086
|
+
const ranked = useMemo(
|
|
1087
|
+
() =>
|
|
1088
|
+
[...models]
|
|
1089
|
+
.sort(
|
|
1090
|
+
(a, b) =>
|
|
1091
|
+
signalShare(b.breakdown, b.messages) - signalShare(a.breakdown, a.messages) ||
|
|
1092
|
+
b.messages - a.messages,
|
|
1093
|
+
)
|
|
1094
|
+
.slice(0, 12),
|
|
1095
|
+
[models],
|
|
1096
|
+
);
|
|
1097
|
+
|
|
1098
|
+
if (models.length === 0) {
|
|
1099
|
+
return (
|
|
1100
|
+
<Card index={4} title={t("frustration.model-by-model")} stale={stale}>
|
|
1101
|
+
<EmptyState title={t("frustration.model-no-turns")} hint={t("frustration.model-try-longer-range")} />
|
|
1102
|
+
</Card>
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
return (
|
|
1107
|
+
<>
|
|
1108
|
+
<Card
|
|
1109
|
+
index={4}
|
|
1110
|
+
title={t("frustration.model-by-model")}
|
|
1111
|
+
description={t("frustration.model-by-model-description")}
|
|
1112
|
+
stale={stale}
|
|
1113
|
+
>
|
|
1114
|
+
<BarList
|
|
1115
|
+
max={100}
|
|
1116
|
+
items={ranked.map(row => ({
|
|
1117
|
+
key: row.key,
|
|
1118
|
+
label: row.label,
|
|
1119
|
+
value: signalShare(row.breakdown, row.messages),
|
|
1120
|
+
display: rate(row.breakdown, row.messages),
|
|
1121
|
+
color: familyColors.get(familyKey(row)) ?? SERIES_COLORS[0],
|
|
1122
|
+
}))}
|
|
1123
|
+
/>
|
|
1124
|
+
</Card>
|
|
1125
|
+
|
|
1126
|
+
<Card
|
|
1127
|
+
index={5}
|
|
1128
|
+
title={t("frustration.model-over-time")}
|
|
1129
|
+
description={t("frustration.model-over-time-description")}
|
|
1130
|
+
actions={
|
|
1131
|
+
<Legend
|
|
1132
|
+
items={chartSeries.map(entry => ({
|
|
1133
|
+
key: entry.key,
|
|
1134
|
+
label: entry.label,
|
|
1135
|
+
color: entry.color,
|
|
1136
|
+
}))}
|
|
1137
|
+
/>
|
|
1138
|
+
}
|
|
1139
|
+
stale={stale}
|
|
1140
|
+
>
|
|
1141
|
+
<TimeChart
|
|
1142
|
+
buckets={buckets}
|
|
1143
|
+
bucketMs={meta.bucketMs}
|
|
1144
|
+
series={chartSeries}
|
|
1145
|
+
height={260}
|
|
1146
|
+
yMax={100}
|
|
1147
|
+
format={value => `${value}%`}
|
|
1148
|
+
formatTooltip={value => `${value.toFixed(1)}%`}
|
|
1149
|
+
emptyLabel={t("frustration.model-no-turns")}
|
|
1150
|
+
/>
|
|
1151
|
+
</Card>
|
|
1152
|
+
|
|
1153
|
+
<ModelSideTable rows={models} familyColors={familyColors} stale={stale} />
|
|
1154
|
+
</>
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function ModelSideTable({
|
|
1159
|
+
rows,
|
|
1160
|
+
familyColors,
|
|
1161
|
+
stale,
|
|
1162
|
+
}: {
|
|
1163
|
+
rows: ModelSideModelStats[];
|
|
1164
|
+
familyColors: Map<string, string>;
|
|
1165
|
+
stale: boolean;
|
|
1166
|
+
}) {
|
|
1167
|
+
const columns = useMemo(
|
|
1168
|
+
(): Column<ModelSideModelStats>[] => [
|
|
1169
|
+
{
|
|
1170
|
+
key: "model",
|
|
1171
|
+
header: t("common.model"),
|
|
1172
|
+
sort: row => row.label,
|
|
1173
|
+
render: row => (
|
|
1174
|
+
<span className="row" title={row.key}>
|
|
1175
|
+
<span
|
|
1176
|
+
className="swatch"
|
|
1177
|
+
style={{ background: familyColors.get(familyKey(row)) ?? SERIES_COLORS[0] }}
|
|
1178
|
+
/>
|
|
1179
|
+
<span className="cell-primary">{row.label}</span>
|
|
1180
|
+
</span>
|
|
1181
|
+
),
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
key: "ids",
|
|
1185
|
+
header: t("frustration.column-model-ids"),
|
|
1186
|
+
wrap: true,
|
|
1187
|
+
render: row => <span className="mono micro dim">{row.models.join(", ")}</span>,
|
|
1188
|
+
},
|
|
1189
|
+
{
|
|
1190
|
+
key: "turns",
|
|
1191
|
+
header: t("frustration.column-turns"),
|
|
1192
|
+
align: "right",
|
|
1193
|
+
sort: row => row.messages,
|
|
1194
|
+
render: row => <span className="num">{formatInteger(row.messages)}</span>,
|
|
1195
|
+
},
|
|
1196
|
+
{
|
|
1197
|
+
key: "selfBlame",
|
|
1198
|
+
header: t("frustration.model-self-blame"),
|
|
1199
|
+
align: "right",
|
|
1200
|
+
sort: row => signalShare(row.blamed, row.messages),
|
|
1201
|
+
render: row => (
|
|
1202
|
+
<span className="num" title={t("frustration.turns-n", { count: formatInteger(row.blamed) })}>
|
|
1203
|
+
{rate(row.blamed, row.messages)}
|
|
1204
|
+
</span>
|
|
1205
|
+
),
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
key: "restatement",
|
|
1209
|
+
header: t("frustration.model-restatement"),
|
|
1210
|
+
title: t("frustration.model-restatement-title"),
|
|
1211
|
+
align: "right",
|
|
1212
|
+
sort: row => signalShare(row.restated, row.messages),
|
|
1213
|
+
render: row => (
|
|
1214
|
+
<span className="num" title={t("frustration.turns-n", { count: formatInteger(row.restated) })}>
|
|
1215
|
+
{rate(row.restated, row.messages)}
|
|
1216
|
+
</span>
|
|
1217
|
+
),
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
key: "hedge",
|
|
1221
|
+
header: t("frustration.model-hedge"),
|
|
1222
|
+
align: "right",
|
|
1223
|
+
sort: row => signalShare(row.hedged, row.messages),
|
|
1224
|
+
render: row => (
|
|
1225
|
+
<span className="num" title={t("frustration.turns-n", { count: formatInteger(row.hedged) })}>
|
|
1226
|
+
{rate(row.hedged, row.messages)}
|
|
1227
|
+
</span>
|
|
1228
|
+
),
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
key: "rambling",
|
|
1232
|
+
header: t("frustration.model-rambling"),
|
|
1233
|
+
title: t("frustration.model-rambling-title"),
|
|
1234
|
+
align: "right",
|
|
1235
|
+
sort: row => signalShare(row.verbose, row.messages),
|
|
1236
|
+
render: row => (
|
|
1237
|
+
<span className="num" title={t("frustration.turns-n", { count: formatInteger(row.verbose) })}>
|
|
1238
|
+
{rate(row.verbose, row.messages)}
|
|
1239
|
+
</span>
|
|
1240
|
+
),
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
key: "spiral",
|
|
1244
|
+
header: t("frustration.model-spiral"),
|
|
1245
|
+
align: "right",
|
|
1246
|
+
sort: row => signalShare(row.spiraled, row.messages),
|
|
1247
|
+
render: row => (
|
|
1248
|
+
<span className="num" title={t("frustration.turns-n", { count: formatInteger(row.spiraled) })}>
|
|
1249
|
+
{rate(row.spiraled, row.messages)}
|
|
1250
|
+
</span>
|
|
1251
|
+
),
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
key: "breakdown",
|
|
1255
|
+
header: t("frustration.model-breakdown"),
|
|
1256
|
+
align: "right",
|
|
1257
|
+
sort: row => signalShare(row.breakdown, row.messages),
|
|
1258
|
+
render: row => (
|
|
1259
|
+
<span className="num" title={t("frustration.turns-n", { count: formatInteger(row.breakdown) })}>
|
|
1260
|
+
{rate(row.breakdown, row.messages)}
|
|
1261
|
+
</span>
|
|
1262
|
+
),
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
key: "mean",
|
|
1266
|
+
header: t("frustration.column-mean"),
|
|
1267
|
+
align: "right",
|
|
1268
|
+
sort: row => (row.messages > 0 ? row.collapse / row.messages : 0),
|
|
1269
|
+
render: row => (
|
|
1270
|
+
<span className="num">{row.messages > 0 ? (row.collapse / row.messages).toFixed(2) : "–"}</span>
|
|
1271
|
+
),
|
|
1272
|
+
},
|
|
1273
|
+
],
|
|
1274
|
+
[familyColors],
|
|
1275
|
+
);
|
|
1276
|
+
|
|
1277
|
+
return (
|
|
1278
|
+
<Card
|
|
1279
|
+
index={6}
|
|
1280
|
+
title={t("frustration.model-table-title")}
|
|
1281
|
+
description={t("frustration.model-table-description")}
|
|
1282
|
+
flush
|
|
1283
|
+
stale={stale}
|
|
1284
|
+
>
|
|
1285
|
+
<Table
|
|
1286
|
+
rows={rows}
|
|
1287
|
+
rowKey={row => row.key}
|
|
1288
|
+
columns={columns}
|
|
1289
|
+
dense
|
|
1290
|
+
empty={<EmptyState title={t("frustration.model-no-turns")} />}
|
|
1291
|
+
/>
|
|
1292
|
+
</Card>
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function ModelSideSessions({ sessions, stale }: { sessions: ModelSideSessionStats[]; stale: boolean }) {
|
|
1297
|
+
const columns = useMemo(
|
|
1298
|
+
(): Column<ModelSideSessionStats>[] => [
|
|
1299
|
+
{
|
|
1300
|
+
key: "session",
|
|
1301
|
+
header: t("frustration.column-session"),
|
|
1302
|
+
render: row => (
|
|
1303
|
+
<span className="stack" title={row.sessionFile}>
|
|
1304
|
+
<span className="cell-primary mono micro">
|
|
1305
|
+
{row.sessionFile.split(/[\\/]/).pop() ?? row.sessionFile}
|
|
1306
|
+
</span>
|
|
1307
|
+
<span className="micro dim truncate">{row.folder}</span>
|
|
1308
|
+
</span>
|
|
1309
|
+
),
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
key: "models",
|
|
1313
|
+
header: t("common.model"),
|
|
1314
|
+
wrap: true,
|
|
1315
|
+
render: row => <span className="mono micro dim">{row.models.join(", ")}</span>,
|
|
1316
|
+
},
|
|
1317
|
+
{
|
|
1318
|
+
key: "turns",
|
|
1319
|
+
header: t("frustration.column-turns"),
|
|
1320
|
+
align: "right",
|
|
1321
|
+
sort: row => row.messages,
|
|
1322
|
+
render: row => <span className="num">{formatInteger(row.messages)}</span>,
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
key: "breakdown",
|
|
1326
|
+
header: t("frustration.model-breakdown"),
|
|
1327
|
+
align: "right",
|
|
1328
|
+
sort: row => signalShare(row.breakdown, row.messages),
|
|
1329
|
+
render: row => (
|
|
1330
|
+
<span className="num" title={t("frustration.turns-n", { count: formatInteger(row.breakdown) })}>
|
|
1331
|
+
{rate(row.breakdown, row.messages)}
|
|
1332
|
+
</span>
|
|
1333
|
+
),
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
key: "mean",
|
|
1337
|
+
header: t("frustration.column-mean"),
|
|
1338
|
+
align: "right",
|
|
1339
|
+
sort: row => (row.messages > 0 ? row.collapse / row.messages : 0),
|
|
1340
|
+
render: row => (
|
|
1341
|
+
<span className="num">{row.messages > 0 ? (row.collapse / row.messages).toFixed(2) : "–"}</span>
|
|
1342
|
+
),
|
|
1343
|
+
},
|
|
1344
|
+
{
|
|
1345
|
+
key: "toolErrors",
|
|
1346
|
+
header: t("frustration.column-tool-errors"),
|
|
1347
|
+
align: "right",
|
|
1348
|
+
sort: row => row.toolErrors,
|
|
1349
|
+
render: row => (
|
|
1350
|
+
<span className={row.toolErrors > 0 ? "num tone-bad" : "num"}>{formatInteger(row.toolErrors)}</span>
|
|
1351
|
+
),
|
|
1352
|
+
},
|
|
1353
|
+
],
|
|
1354
|
+
[],
|
|
1355
|
+
);
|
|
1356
|
+
|
|
1357
|
+
return (
|
|
1358
|
+
<Card
|
|
1359
|
+
index={7}
|
|
1360
|
+
title={t("frustration.model-sessions")}
|
|
1361
|
+
description={t("frustration.model-sessions-description")}
|
|
1362
|
+
flush
|
|
1363
|
+
stale={stale}
|
|
1364
|
+
>
|
|
1365
|
+
<Table
|
|
1366
|
+
rows={sessions}
|
|
1367
|
+
rowKey={row => row.sessionFile}
|
|
1368
|
+
columns={columns}
|
|
1369
|
+
dense
|
|
1370
|
+
empty={<EmptyState title={t("frustration.model-no-turns")} />}
|
|
1371
|
+
/>
|
|
1372
|
+
</Card>
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
@@ -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>,
|
|
@@ -121,3 +121,16 @@
|
|
|
121
121
|
justify-content: flex-end;
|
|
122
122
|
gap: 6px;
|
|
123
123
|
}
|
|
124
|
+
|
|
125
|
+
/* Model-side summary: section header above the stat tiles. */
|
|
126
|
+
.frustration-model-summary {
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-direction: column;
|
|
129
|
+
gap: 20px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.frustration-model-notes {
|
|
133
|
+
display: flex;
|
|
134
|
+
flex-direction: column;
|
|
135
|
+
gap: 8px;
|
|
136
|
+
}
|
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;
|