@roastcodes/ttdash 6.1.9 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/CustomTooltip-CdIOw3Ep.js +1 -0
- package/dist/assets/{DrillDownModal-BnZ6q6tF.js → DrillDownModal-d6hcut-I.js} +1 -1
- package/dist/assets/index-CMtAn7c8.js +4 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/report/chart-labels.js +12 -0
- package/server/report/charts.js +4 -8
- package/server/report/index.js +73 -16
- package/server/report/utils.js +64 -21
- package/shared/model-colors.d.ts +17 -0
- package/shared/model-colors.js +241 -0
- package/src/locales/de/common.json +9 -1
- package/src/locales/en/common.json +9 -1
- package/dist/assets/CustomTooltip-BXro6tIF.js +0 -1
- package/dist/assets/index-BfNaLs3g.js +0 -4
package/dist/index.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link rel="icon" type="image/png" sizes="256x256" href="/favicon.png" />
|
|
9
9
|
<link rel="shortcut icon" href="/favicon.png" />
|
|
10
10
|
<link rel="apple-touch-icon" href="/favicon.png" />
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-CMtAn7c8.js"></script>
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-COnpUsM8.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-CiBqdKXh.js">
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/react-vendor-0R1rd57Z.js">
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<link rel="modulepreload" crossorigin href="/assets/icons-vendor-z59La6A4.js">
|
|
18
18
|
<link rel="modulepreload" crossorigin href="/assets/dialog-CA-ZSHjK.js">
|
|
19
19
|
<link rel="modulepreload" crossorigin href="/assets/button-B26tLVFw.js">
|
|
20
|
-
<link rel="modulepreload" crossorigin href="/assets/CustomTooltip-
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/assets/CustomTooltip-CdIOw3Ep.js">
|
|
21
21
|
<link rel="stylesheet" crossorigin href="/assets/index-BkGSNAne.css">
|
|
22
22
|
</head>
|
|
23
23
|
<body>
|
package/package.json
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const TOP_MODEL_CHART_LABEL_MAX_LENGTH = 34;
|
|
2
|
+
|
|
3
|
+
function truncateTopModelChartLabel(value) {
|
|
4
|
+
const stringValue = String(value || '');
|
|
5
|
+
if (stringValue.length <= TOP_MODEL_CHART_LABEL_MAX_LENGTH) return stringValue;
|
|
6
|
+
return `${stringValue.slice(0, Math.max(1, TOP_MODEL_CHART_LABEL_MAX_LENGTH - 1)).trimEnd()}…`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
TOP_MODEL_CHART_LABEL_MAX_LENGTH,
|
|
11
|
+
truncateTopModelChartLabel,
|
|
12
|
+
};
|
package/server/report/charts.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { truncateTopModelChartLabel } = require('./chart-labels');
|
|
2
|
+
|
|
1
3
|
function escapeXml(value) {
|
|
2
4
|
return String(value)
|
|
3
5
|
.replace(/&/g, '&')
|
|
@@ -19,12 +21,6 @@ ${body}
|
|
|
19
21
|
|
|
20
22
|
const DEFAULT_FONT_FAMILY = 'Liberation Sans, DejaVu Sans, Arial, sans-serif';
|
|
21
23
|
|
|
22
|
-
function truncateSvgLabel(value, maxLength = 28) {
|
|
23
|
-
const stringValue = String(value || '');
|
|
24
|
-
if (stringValue.length <= maxLength) return stringValue;
|
|
25
|
-
return `${stringValue.slice(0, Math.max(1, maxLength - 1)).trimEnd()}…`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
24
|
function lineChart(
|
|
29
25
|
data,
|
|
30
26
|
{
|
|
@@ -136,7 +132,7 @@ function horizontalBarChart(
|
|
|
136
132
|
top: 46,
|
|
137
133
|
right: 100,
|
|
138
134
|
bottom: 24,
|
|
139
|
-
left: clamp(180 + longestLabelLength * 3.4, 220,
|
|
135
|
+
left: clamp(180 + longestLabelLength * 3.4, 220, 360),
|
|
140
136
|
};
|
|
141
137
|
const plotWidth = width - margin.left - margin.right;
|
|
142
138
|
const barGap = 18;
|
|
@@ -158,7 +154,7 @@ function horizontalBarChart(
|
|
|
158
154
|
const value = getValue(entry);
|
|
159
155
|
const barWidth = clamp((value / maxValue) * plotWidth, 0, plotWidth);
|
|
160
156
|
return `
|
|
161
|
-
<text x="${margin.left - 18}" y="${y + barHeight / 2 + 4}" text-anchor="end" font-size="13" font-family="${fontFamily}" fill="#122033">${escapeXml(
|
|
157
|
+
<text x="${margin.left - 18}" y="${y + barHeight / 2 + 4}" text-anchor="end" font-size="13" font-family="${fontFamily}" fill="#122033">${escapeXml(truncateTopModelChartLabel(getLabel(entry)))}</text>
|
|
162
158
|
<rect x="${margin.left}" y="${y}" width="${plotWidth}" height="${barHeight}" rx="12" fill="#eef3f8"/>
|
|
163
159
|
<rect x="${margin.left}" y="${y}" width="${barWidth}" height="${barHeight}" rx="12" fill="${getColor(entry)}"/>
|
|
164
160
|
<text x="${margin.left + plotWidth + 12}" y="${y + barHeight / 2 + 4}" font-size="12" font-family="${fontFamily}" fill="#475569">${escapeXml(formatter(value))}</text>
|
package/server/report/index.js
CHANGED
|
@@ -3,7 +3,7 @@ const os = require('os');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { spawn } = require('child_process');
|
|
5
5
|
const { buildReportData, formatCompactAxis, formatDateAxis } = require('./utils');
|
|
6
|
-
const { translate } = require('./i18n');
|
|
6
|
+
const { getLocale, translate } = require('./i18n');
|
|
7
7
|
const { horizontalBarChart, lineChart, stackedBarChart } = require('./charts');
|
|
8
8
|
|
|
9
9
|
function ensureTypstInstalled() {
|
|
@@ -39,10 +39,41 @@ function compileTypst(workingDir, typPath, pdfPath) {
|
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function formatCostAxisValue(value, language = 'de') {
|
|
43
|
+
const numericValue = Number(value) || 0;
|
|
44
|
+
const absoluteValue = Math.abs(numericValue);
|
|
45
|
+
const locale = getLocale(language);
|
|
46
|
+
|
|
47
|
+
if (absoluteValue >= 100) {
|
|
48
|
+
return `$${Math.round(numericValue).toLocaleString(locale)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (absoluteValue >= 10) {
|
|
52
|
+
return `$${numericValue.toLocaleString(locale, {
|
|
53
|
+
minimumFractionDigits: 0,
|
|
54
|
+
maximumFractionDigits: 1,
|
|
55
|
+
})}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (absoluteValue >= 1) {
|
|
59
|
+
return `$${numericValue.toLocaleString(locale, {
|
|
60
|
+
minimumFractionDigits: 0,
|
|
61
|
+
maximumFractionDigits: 2,
|
|
62
|
+
})}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return `$${numericValue.toLocaleString(locale, {
|
|
66
|
+
minimumFractionDigits: 2,
|
|
67
|
+
maximumFractionDigits: 2,
|
|
68
|
+
})}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
42
71
|
function buildTemplate() {
|
|
43
72
|
return `
|
|
44
73
|
#let report = json("report.json")
|
|
45
74
|
|
|
75
|
+
#set document(title: report.meta.reportTitle)
|
|
76
|
+
|
|
46
77
|
#set page(
|
|
47
78
|
paper: "a4",
|
|
48
79
|
margin: (x: 14mm, y: 16mm),
|
|
@@ -56,10 +87,10 @@ function buildTemplate() {
|
|
|
56
87
|
#let muted = rgb("#5c6b7e")
|
|
57
88
|
#let panel = rgb("#ffffff")
|
|
58
89
|
#let line = rgb("#d9e2ec")
|
|
59
|
-
#let accent = rgb("#
|
|
90
|
+
#let accent = rgb("#175fc0")
|
|
60
91
|
#let accent-soft = rgb("#eaf2ff")
|
|
61
92
|
#let good = rgb("#16825d")
|
|
62
|
-
#let warn = rgb("#
|
|
93
|
+
#let warn = rgb("#9a5a00")
|
|
63
94
|
|
|
64
95
|
#let metric-card(label, value, note: none, tone: accent) = rect(
|
|
65
96
|
inset: 10pt,
|
|
@@ -89,6 +120,22 @@ function buildTemplate() {
|
|
|
89
120
|
],
|
|
90
121
|
)
|
|
91
122
|
|
|
123
|
+
#let chart-panel(file, alt, summary, note: none) = rect(
|
|
124
|
+
inset: 10pt,
|
|
125
|
+
radius: 14pt,
|
|
126
|
+
fill: panel,
|
|
127
|
+
stroke: (paint: line, thickness: 0.8pt),
|
|
128
|
+
[
|
|
129
|
+
#image(file, width: 100%, alt: alt)
|
|
130
|
+
#v(6pt)
|
|
131
|
+
#text(size: 8.7pt, fill: muted)[#summary]
|
|
132
|
+
#if note != none [
|
|
133
|
+
#v(4pt)
|
|
134
|
+
#text(size: 8.5pt, fill: muted)[#note]
|
|
135
|
+
]
|
|
136
|
+
],
|
|
137
|
+
)
|
|
138
|
+
|
|
92
139
|
#show heading.where(level: 1): it => block(above: 0pt, below: 10pt)[
|
|
93
140
|
#text(size: 24pt, fill: ink, weight: "bold")[#it.body]
|
|
94
141
|
]
|
|
@@ -150,21 +197,26 @@ function buildTemplate() {
|
|
|
150
197
|
#grid(
|
|
151
198
|
columns: (1fr, 1fr),
|
|
152
199
|
gutter: 10pt,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
200
|
+
chart-panel(
|
|
201
|
+
"cost-trend.svg",
|
|
202
|
+
report.chartDescriptions.costTrend.alt,
|
|
203
|
+
report.chartDescriptions.costTrend.summary,
|
|
204
|
+
),
|
|
205
|
+
chart-panel(
|
|
206
|
+
"top-models.svg",
|
|
207
|
+
report.chartDescriptions.topModels.alt,
|
|
208
|
+
report.chartDescriptions.topModels.summary,
|
|
209
|
+
note: report.chartDescriptions.topModels.fullNamesNote,
|
|
210
|
+
),
|
|
159
211
|
)
|
|
160
212
|
|
|
161
213
|
#v(10pt)
|
|
162
214
|
|
|
163
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
215
|
+
#chart-panel(
|
|
216
|
+
"token-trend.svg",
|
|
217
|
+
report.chartDescriptions.tokenTrend.alt,
|
|
218
|
+
report.chartDescriptions.tokenTrend.summary,
|
|
219
|
+
)
|
|
168
220
|
|
|
169
221
|
#v(12pt)
|
|
170
222
|
|
|
@@ -279,14 +331,14 @@ function createChartAssets(reportData) {
|
|
|
279
331
|
title: reportData.text.charts.costTrend,
|
|
280
332
|
valueKey: 'cost',
|
|
281
333
|
secondaryKey: reportData.meta.filterSummary.viewModeKey === 'daily' ? 'ma7' : null,
|
|
282
|
-
formatter: (value) =>
|
|
334
|
+
formatter: (value) => formatCostAxisValue(value, reportData.meta.language),
|
|
283
335
|
}),
|
|
284
336
|
'top-models.svg': horizontalBarChart(topModels, {
|
|
285
337
|
title: reportData.text.charts.topModels,
|
|
286
338
|
getValue: (entry) => entry.cost,
|
|
287
339
|
getLabel: (entry) => entry.name,
|
|
288
340
|
getColor: (entry) => entry.color,
|
|
289
|
-
formatter: (value) =>
|
|
341
|
+
formatter: (value) => formatCostAxisValue(value, reportData.meta.language),
|
|
290
342
|
}),
|
|
291
343
|
'token-trend.svg': stackedBarChart(tokenTrend, {
|
|
292
344
|
title: reportData.text.charts.tokenTrend,
|
|
@@ -363,4 +415,9 @@ async function generatePdfReport(allDailyData, options = {}) {
|
|
|
363
415
|
|
|
364
416
|
module.exports = {
|
|
365
417
|
generatePdfReport,
|
|
418
|
+
__test__: {
|
|
419
|
+
buildTemplate,
|
|
420
|
+
createChartAssets,
|
|
421
|
+
formatCostAxisValue,
|
|
422
|
+
},
|
|
366
423
|
};
|
package/server/report/utils.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { version: APP_VERSION } = require('../../package.json');
|
|
2
2
|
const { getLanguage, getLocale, translate } = require('./i18n');
|
|
3
|
+
const { truncateTopModelChartLabel } = require('./chart-labels');
|
|
3
4
|
const {
|
|
4
5
|
aggregateToDailyFormat,
|
|
5
6
|
computeMetrics,
|
|
@@ -12,24 +13,12 @@ const {
|
|
|
12
13
|
normalizeModelName,
|
|
13
14
|
sortByDate,
|
|
14
15
|
} = require('../../shared/dashboard-domain');
|
|
15
|
-
|
|
16
|
-
const MODEL_COLORS = {
|
|
17
|
-
'Opus 4.6': 'rgb(175, 92, 224)',
|
|
18
|
-
'Opus 4.5': 'rgb(200, 66, 111)',
|
|
19
|
-
'Sonnet 4.6': 'rgb(71, 134, 221)',
|
|
20
|
-
'Sonnet 4.5': 'rgb(66, 161, 130)',
|
|
21
|
-
'Haiku 4.5': 'rgb(231, 146, 34)',
|
|
22
|
-
'GPT-5.4': 'rgb(230, 98, 56)',
|
|
23
|
-
'GPT-5': 'rgb(230, 98, 56)',
|
|
24
|
-
'Gemini 3 Flash Preview': 'rgb(237, 188, 8)',
|
|
25
|
-
Gemini: 'rgb(237, 188, 8)',
|
|
26
|
-
OpenCode: 'rgb(51, 181, 193)',
|
|
27
|
-
};
|
|
16
|
+
const { getModelColorRgb } = require('../../shared/model-colors.js');
|
|
28
17
|
|
|
29
18
|
const WEEKDAYS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
|
30
19
|
|
|
31
20
|
function getModelColor(name) {
|
|
32
|
-
return
|
|
21
|
+
return getModelColorRgb(name, { theme: 'light' });
|
|
33
22
|
}
|
|
34
23
|
|
|
35
24
|
function toCostChartData(data) {
|
|
@@ -232,6 +221,16 @@ function formatPercent(value, language = 'de') {
|
|
|
232
221
|
})}%`;
|
|
233
222
|
}
|
|
234
223
|
|
|
224
|
+
function findPeakEntry(data, getValue) {
|
|
225
|
+
let best = null;
|
|
226
|
+
for (const entry of data) {
|
|
227
|
+
if (!best || getValue(entry) > getValue(best)) {
|
|
228
|
+
best = entry;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return best;
|
|
232
|
+
}
|
|
233
|
+
|
|
235
234
|
function formatCompactNumber(value, language = 'de') {
|
|
236
235
|
if (!Number.isFinite(value)) return '0';
|
|
237
236
|
|
|
@@ -292,12 +291,6 @@ function summarizeSelection(
|
|
|
292
291
|
return `${visible.join(', ')}${suffix}`;
|
|
293
292
|
}
|
|
294
293
|
|
|
295
|
-
function truncateLabel(value, maxLength = 28) {
|
|
296
|
-
const stringValue = String(value || '');
|
|
297
|
-
if (stringValue.length <= maxLength) return stringValue;
|
|
298
|
-
return `${stringValue.slice(0, Math.max(1, maxLength - 1)).trimEnd()}…`;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
294
|
function buildInsights(metrics, { filteredDaily, filtered, language }) {
|
|
302
295
|
const insights = [];
|
|
303
296
|
|
|
@@ -409,6 +402,9 @@ function buildReportData(allDailyData, options = {}) {
|
|
|
409
402
|
const topProviderValue = metrics.topProvider ? metrics.topProvider.name : notAvailable;
|
|
410
403
|
const insights = buildInsights(metrics, { filteredDaily, filtered, language });
|
|
411
404
|
const avgPeriodCost = filtered.length > 0 ? metrics.totalCost / filtered.length : 0;
|
|
405
|
+
const latestPeriod = filtered[filtered.length - 1] || null;
|
|
406
|
+
const peakCostPeriod = findPeakEntry(filtered, (entry) => entry.totalCost);
|
|
407
|
+
const peakTokenPeriod = findPeakEntry(filtered, (entry) => entry.totalTokens);
|
|
412
408
|
const recentRows = sortByDate(filtered)
|
|
413
409
|
.slice(-12)
|
|
414
410
|
.reverse()
|
|
@@ -466,6 +462,34 @@ function buildReportData(allDailyData, options = {}) {
|
|
|
466
462
|
},
|
|
467
463
|
];
|
|
468
464
|
|
|
465
|
+
const topChartModels = modelRows.slice(0, 8);
|
|
466
|
+
const truncatedTopModelNames = topChartModels
|
|
467
|
+
.filter((entry) => truncateTopModelChartLabel(entry.name) !== entry.name)
|
|
468
|
+
.map((entry) => entry.name);
|
|
469
|
+
const topModelSummary = metrics.topModel
|
|
470
|
+
? translate(language, 'report.charts.topModelsSummary', {
|
|
471
|
+
model: metrics.topModel.name,
|
|
472
|
+
cost: formatCurrency(metrics.topModel.cost, language),
|
|
473
|
+
share: formatPercent(metrics.topModelShare, language),
|
|
474
|
+
})
|
|
475
|
+
: translate(language, 'report.charts.noDataSummary');
|
|
476
|
+
const costTrendSummary =
|
|
477
|
+
latestPeriod && peakCostPeriod
|
|
478
|
+
? translate(language, 'report.charts.costTrendSummary', {
|
|
479
|
+
latest: formatCurrency(latestPeriod.totalCost, language),
|
|
480
|
+
peak: formatCurrency(peakCostPeriod.totalCost, language),
|
|
481
|
+
date: formatDate(peakCostPeriod.date, 'long', language),
|
|
482
|
+
})
|
|
483
|
+
: translate(language, 'report.charts.noDataSummary');
|
|
484
|
+
const tokenTrendSummary =
|
|
485
|
+
peakTokenPeriod && metrics.totalTokens > 0
|
|
486
|
+
? translate(language, 'report.charts.tokenTrendSummary', {
|
|
487
|
+
total: formatCompact(metrics.totalTokens, language),
|
|
488
|
+
peak: formatCompact(peakTokenPeriod.totalTokens, language),
|
|
489
|
+
date: formatDate(peakTokenPeriod.date, 'long', language),
|
|
490
|
+
})
|
|
491
|
+
: translate(language, 'report.charts.noDataSummary');
|
|
492
|
+
|
|
469
493
|
const interpretationSummary = translate(language, 'report.interpretation.summary', {
|
|
470
494
|
days: formatInteger(filteredDaily.length, language),
|
|
471
495
|
periods: formatInteger(filtered.length, language),
|
|
@@ -530,6 +554,26 @@ function buildReportData(allDailyData, options = {}) {
|
|
|
530
554
|
tokensLabel: formatCompact(entry.tokens, language),
|
|
531
555
|
})),
|
|
532
556
|
recentPeriods: recentRows,
|
|
557
|
+
chartDescriptions: {
|
|
558
|
+
costTrend: {
|
|
559
|
+
alt: translate(language, 'report.charts.costTrendAlt'),
|
|
560
|
+
summary: costTrendSummary,
|
|
561
|
+
},
|
|
562
|
+
topModels: {
|
|
563
|
+
alt: translate(language, 'report.charts.topModelsAlt'),
|
|
564
|
+
summary: topModelSummary,
|
|
565
|
+
fullNamesNote:
|
|
566
|
+
truncatedTopModelNames.length > 0
|
|
567
|
+
? translate(language, 'report.charts.topModelsFullNames', {
|
|
568
|
+
names: truncatedTopModelNames.join(', '),
|
|
569
|
+
})
|
|
570
|
+
: null,
|
|
571
|
+
},
|
|
572
|
+
tokenTrend: {
|
|
573
|
+
alt: translate(language, 'report.charts.tokenTrendAlt'),
|
|
574
|
+
summary: tokenTrendSummary,
|
|
575
|
+
},
|
|
576
|
+
},
|
|
533
577
|
labels: {
|
|
534
578
|
dateRangeText: dateRange
|
|
535
579
|
? `${formatDate(dateRange.start, 'long', language)} - ${formatDate(dateRange.end, 'long', language)}`
|
|
@@ -618,7 +662,6 @@ module.exports = {
|
|
|
618
662
|
formatDate,
|
|
619
663
|
formatDateAxis,
|
|
620
664
|
getModelColor,
|
|
621
|
-
truncateLabel,
|
|
622
665
|
__test__: {
|
|
623
666
|
getModelProvider,
|
|
624
667
|
normalizeModelName,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type ModelColorTheme = 'dark' | 'light'
|
|
2
|
+
|
|
3
|
+
export interface ModelColorSpec {
|
|
4
|
+
h: number
|
|
5
|
+
s: number
|
|
6
|
+
l: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ModelColorOptions {
|
|
10
|
+
theme?: ModelColorTheme
|
|
11
|
+
alpha?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function normalizeTheme(theme?: string): ModelColorTheme
|
|
15
|
+
export function getModelColorSpec(name: string, options?: ModelColorOptions): ModelColorSpec
|
|
16
|
+
export function getModelColor(name: string, options?: ModelColorOptions): string
|
|
17
|
+
export function getModelColorRgb(name: string, options?: ModelColorOptions): string
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
const MODEL_COLOR_RULES = [
|
|
2
|
+
{
|
|
3
|
+
pattern: /^OpenCode$/i,
|
|
4
|
+
light: { h: 192, s: 76, l: 40 },
|
|
5
|
+
dark: { h: 192, s: 82, l: 58 },
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
pattern: /^Codex(?: .+)?$/i,
|
|
9
|
+
light: { h: 194, s: 72, l: 38 },
|
|
10
|
+
dark: { h: 194, s: 78, l: 55 },
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
pattern: /^GPT-5\.4 Codex$/i,
|
|
14
|
+
light: { h: 194, s: 76, l: 42 },
|
|
15
|
+
dark: { h: 194, s: 82, l: 60 },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
pattern: /^GPT-5\.3 Codex$/i,
|
|
19
|
+
light: { h: 194, s: 70, l: 36 },
|
|
20
|
+
dark: { h: 194, s: 74, l: 52 },
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
pattern: /^GPT-5\.4$/i,
|
|
24
|
+
light: { h: 148, s: 68, l: 40 },
|
|
25
|
+
dark: { h: 148, s: 72, l: 57 },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
pattern: /^GPT-5$/i,
|
|
29
|
+
light: { h: 148, s: 60, l: 33 },
|
|
30
|
+
dark: { h: 148, s: 64, l: 47 },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
pattern: /^GPT-(?:4o|4\.1)(?: .+)?$/i,
|
|
34
|
+
light: { h: 160, s: 58, l: 34 },
|
|
35
|
+
dark: { h: 160, s: 62, l: 49 },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
pattern: /^o4 Mini$/i,
|
|
39
|
+
light: { h: 166, s: 64, l: 33 },
|
|
40
|
+
dark: { h: 166, s: 68, l: 48 },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
pattern: /^o1$/i,
|
|
44
|
+
light: { h: 166, s: 56, l: 30 },
|
|
45
|
+
dark: { h: 166, s: 60, l: 43 },
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
pattern: /^Gemini 3 Flash Preview(?: .+)?$/i,
|
|
49
|
+
light: { h: 48, s: 100, l: 42 },
|
|
50
|
+
dark: { h: 52, s: 98, l: 61 },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
pattern: /^Gemini 2\.5 Flash$/i,
|
|
54
|
+
light: { h: 44, s: 92, l: 39 },
|
|
55
|
+
dark: { h: 46, s: 94, l: 56 },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
pattern: /^Gemini 2\.5 Pro$/i,
|
|
59
|
+
light: { h: 38, s: 86, l: 34 },
|
|
60
|
+
dark: { h: 40, s: 88, l: 49 },
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
pattern: /^Gemini(?: .+)?$/i,
|
|
64
|
+
light: { h: 42, s: 88, l: 36 },
|
|
65
|
+
dark: { h: 44, s: 90, l: 52 },
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
pattern: /^(?:Claude\s+)?Opus 4\.6$/i,
|
|
69
|
+
light: { h: 274, s: 68, l: 44 },
|
|
70
|
+
dark: { h: 274, s: 74, l: 66 },
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
pattern: /^(?:Claude\s+)?Opus 4\.5$/i,
|
|
74
|
+
light: { h: 274, s: 58, l: 36 },
|
|
75
|
+
dark: { h: 274, s: 62, l: 56 },
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
pattern: /^(?:Claude\s+)?Opus(?: .+)?$/i,
|
|
79
|
+
light: { h: 274, s: 62, l: 40 },
|
|
80
|
+
dark: { h: 274, s: 68, l: 60 },
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
pattern: /^(?:Claude\s+)?Sonnet 4\.6$/i,
|
|
84
|
+
light: { h: 214, s: 72, l: 44 },
|
|
85
|
+
dark: { h: 214, s: 80, l: 63 },
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
pattern: /^(?:Claude\s+)?Sonnet 4\.5$/i,
|
|
89
|
+
light: { h: 214, s: 60, l: 36 },
|
|
90
|
+
dark: { h: 214, s: 66, l: 52 },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
pattern: /^(?:Claude\s+)?Sonnet 4$/i,
|
|
94
|
+
light: { h: 214, s: 56, l: 34 },
|
|
95
|
+
dark: { h: 214, s: 62, l: 48 },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
pattern: /^(?:Claude\s+)?Sonnet(?: .+)?$/i,
|
|
99
|
+
light: { h: 214, s: 64, l: 40 },
|
|
100
|
+
dark: { h: 214, s: 70, l: 56 },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
pattern: /^(?:Claude\s+)?Haiku 4\.5$/i,
|
|
104
|
+
light: { h: 28, s: 90, l: 43 },
|
|
105
|
+
dark: { h: 28, s: 92, l: 61 },
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
pattern: /^(?:Claude\s+)?Haiku(?: .+)?$/i,
|
|
109
|
+
light: { h: 28, s: 84, l: 38 },
|
|
110
|
+
dark: { h: 28, s: 88, l: 56 },
|
|
111
|
+
},
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
const FALLBACK_HUES = [148, 168, 190, 208, 226, 248, 272, 332, 18, 30, 44]
|
|
115
|
+
|
|
116
|
+
function normalizeTheme(theme) {
|
|
117
|
+
return theme === 'light' ? 'light' : 'dark'
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeAlpha(alpha) {
|
|
121
|
+
if (typeof alpha !== 'number' || !Number.isFinite(alpha)) return null
|
|
122
|
+
if (alpha <= 0) return 0
|
|
123
|
+
if (alpha >= 1) return 1
|
|
124
|
+
return Math.round(alpha * 1000) / 1000
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function hashName(name) {
|
|
128
|
+
let hash = 0
|
|
129
|
+
const value = String(name || '')
|
|
130
|
+
.trim()
|
|
131
|
+
.toLowerCase()
|
|
132
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
133
|
+
hash = ((hash << 5) - hash + value.charCodeAt(index)) | 0
|
|
134
|
+
}
|
|
135
|
+
return hash
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function mod(value, divisor) {
|
|
139
|
+
return ((value % divisor) + divisor) % divisor
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function findKnownColor(name) {
|
|
143
|
+
return MODEL_COLOR_RULES.find((rule) => rule.pattern.test(String(name || '').trim())) ?? null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function fallbackColor(name, theme) {
|
|
147
|
+
const hash = hashName(name)
|
|
148
|
+
const baseHue = FALLBACK_HUES[mod(hash, FALLBACK_HUES.length)]
|
|
149
|
+
const hueShift = ((Math.abs(hash >> 4) % 7) - 3) * 4
|
|
150
|
+
const hue = mod(baseHue + hueShift, 360)
|
|
151
|
+
|
|
152
|
+
if (theme === 'light') {
|
|
153
|
+
return {
|
|
154
|
+
h: hue,
|
|
155
|
+
s: 62 + (Math.abs(hash >> 8) % 10),
|
|
156
|
+
l: 34 + (Math.abs(hash >> 12) % 8),
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
h: hue,
|
|
162
|
+
s: 68 + (Math.abs(hash >> 8) % 10),
|
|
163
|
+
l: 52 + (Math.abs(hash >> 12) % 8),
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function getModelColorSpec(name, options = {}) {
|
|
168
|
+
const theme = normalizeTheme(options.theme)
|
|
169
|
+
const known = findKnownColor(name)
|
|
170
|
+
return known ? { ...known[theme] } : fallbackColor(name, theme)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function toHslString(spec) {
|
|
174
|
+
return `hsl(${spec.h}, ${spec.s}%, ${spec.l}%)`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function toHslaString(spec, alpha) {
|
|
178
|
+
return `hsla(${spec.h}, ${spec.s}%, ${spec.l}%, ${alpha})`
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function hslToRgb(spec) {
|
|
182
|
+
const hue = mod(spec.h, 360)
|
|
183
|
+
const saturation = Math.max(0, Math.min(100, spec.s)) / 100
|
|
184
|
+
const lightness = Math.max(0, Math.min(100, spec.l)) / 100
|
|
185
|
+
const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation
|
|
186
|
+
const huePrime = hue / 60
|
|
187
|
+
const x = chroma * (1 - Math.abs((huePrime % 2) - 1))
|
|
188
|
+
|
|
189
|
+
let red = 0
|
|
190
|
+
let green = 0
|
|
191
|
+
let blue = 0
|
|
192
|
+
|
|
193
|
+
if (huePrime >= 0 && huePrime < 1) {
|
|
194
|
+
red = chroma
|
|
195
|
+
green = x
|
|
196
|
+
} else if (huePrime < 2) {
|
|
197
|
+
red = x
|
|
198
|
+
green = chroma
|
|
199
|
+
} else if (huePrime < 3) {
|
|
200
|
+
green = chroma
|
|
201
|
+
blue = x
|
|
202
|
+
} else if (huePrime < 4) {
|
|
203
|
+
green = x
|
|
204
|
+
blue = chroma
|
|
205
|
+
} else if (huePrime < 5) {
|
|
206
|
+
red = x
|
|
207
|
+
blue = chroma
|
|
208
|
+
} else {
|
|
209
|
+
red = chroma
|
|
210
|
+
blue = x
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const match = lightness - chroma / 2
|
|
214
|
+
return {
|
|
215
|
+
r: Math.round((red + match) * 255),
|
|
216
|
+
g: Math.round((green + match) * 255),
|
|
217
|
+
b: Math.round((blue + match) * 255),
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getModelColor(name, options = {}) {
|
|
222
|
+
const spec = getModelColorSpec(name, options)
|
|
223
|
+
const alpha = normalizeAlpha(options.alpha)
|
|
224
|
+
return alpha === null ? toHslString(spec) : toHslaString(spec, alpha)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function getModelColorRgb(name, options = {}) {
|
|
228
|
+
const spec = getModelColorSpec(name, options)
|
|
229
|
+
const { r, g, b } = hslToRgb(spec)
|
|
230
|
+
const alpha = normalizeAlpha(options.alpha)
|
|
231
|
+
if (alpha === null) return `rgb(${r}, ${g}, ${b})`
|
|
232
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
MODEL_COLOR_RULES,
|
|
237
|
+
getModelColor,
|
|
238
|
+
getModelColorRgb,
|
|
239
|
+
getModelColorSpec,
|
|
240
|
+
normalizeTheme,
|
|
241
|
+
}
|
|
@@ -1101,7 +1101,15 @@
|
|
|
1101
1101
|
"charts": {
|
|
1102
1102
|
"costTrend": "Kostenverlauf",
|
|
1103
1103
|
"topModels": "Top-Modelle nach Kosten",
|
|
1104
|
-
"tokenTrend": "Token-Mix pro Zeitraum"
|
|
1104
|
+
"tokenTrend": "Token-Mix pro Zeitraum",
|
|
1105
|
+
"costTrendAlt": "Liniendiagramm der Report-Kosten pro Zeitraum.",
|
|
1106
|
+
"costTrendSummary": "Letzter Wert {{latest}}. Peak {{peak}} am {{date}}.",
|
|
1107
|
+
"topModelsAlt": "Horizontales Balkendiagramm der teuersten Modelle im Report.",
|
|
1108
|
+
"topModelsSummary": "{{model}} führt mit {{cost}} und {{share}} der Report-Kosten.",
|
|
1109
|
+
"topModelsFullNames": "Vollständige Diagramm-Labels: {{names}}.",
|
|
1110
|
+
"tokenTrendAlt": "Gestapeltes Balkendiagramm des Token-Mix pro Report-Zeitraum.",
|
|
1111
|
+
"tokenTrendSummary": "Gesamt {{total}}. Höchstes Token-Volumen {{peak}} am {{date}}.",
|
|
1112
|
+
"noDataSummary": "Nicht genug Daten für eine belastbare Diagramm-Zusammenfassung."
|
|
1105
1113
|
},
|
|
1106
1114
|
"interpretation": {
|
|
1107
1115
|
"summary": "Dieser Report umfasst {{days}} Rohdaten-Tage und {{periods}} aggregierte Zeiträume. Spitzenzeitraum: {{peak}}. Top-Modell: {{topModel}}. Führender Anbieter: {{topProvider}}.",
|
|
@@ -1101,7 +1101,15 @@
|
|
|
1101
1101
|
"charts": {
|
|
1102
1102
|
"costTrend": "Cost trend",
|
|
1103
1103
|
"topModels": "Top models by cost",
|
|
1104
|
-
"tokenTrend": "Token mix by period"
|
|
1104
|
+
"tokenTrend": "Token mix by period",
|
|
1105
|
+
"costTrendAlt": "Line chart showing report cost by period.",
|
|
1106
|
+
"costTrendSummary": "Latest {{latest}}. Peak {{peak}} on {{date}}.",
|
|
1107
|
+
"topModelsAlt": "Horizontal bar chart comparing the highest-cost models in the report.",
|
|
1108
|
+
"topModelsSummary": "{{model}} leads with {{cost}} and {{share}} of report cost.",
|
|
1109
|
+
"topModelsFullNames": "Full chart labels: {{names}}.",
|
|
1110
|
+
"tokenTrendAlt": "Stacked bar chart showing the token mix for each report period.",
|
|
1111
|
+
"tokenTrendSummary": "Total {{total}}. Peak token volume {{peak}} on {{date}}.",
|
|
1112
|
+
"noDataSummary": "Not enough data for a stable chart summary."
|
|
1105
1113
|
},
|
|
1106
1114
|
"interpretation": {
|
|
1107
1115
|
"summary": "This report covers {{days}} raw days and {{periods}} aggregated periods. Peak period: {{peak}}. Top model: {{topModel}}. Leading provider: {{topProvider}}.",
|