@jeffreycao/copilot-api 1.10.0 → 1.10.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/dist/main.js +1 -1
- package/dist/{server-BnKth1Jp.js → server-DpVPS3zt.js} +116 -30
- package/dist/server-DpVPS3zt.js.map +1 -0
- package/dist/{start-1KA2mHVS.js → start-DDII-0ML.js} +2 -2
- package/dist/{start-1KA2mHVS.js.map → start-DDII-0ML.js.map} +1 -1
- package/package.json +1 -1
- package/pages/index.html +432 -10
- package/dist/server-BnKth1Jp.js.map +0 -1
package/pages/index.html
CHANGED
|
@@ -205,8 +205,8 @@
|
|
|
205
205
|
"
|
|
206
206
|
>
|
|
207
207
|
<option value="day">Day</option>
|
|
208
|
-
<option value="week">
|
|
209
|
-
<option value="month">
|
|
208
|
+
<option value="week">Last 7 days</option>
|
|
209
|
+
<option value="month">Last 30 days</option>
|
|
210
210
|
</select>
|
|
211
211
|
</div>
|
|
212
212
|
<button
|
|
@@ -273,6 +273,8 @@
|
|
|
273
273
|
const DEFAULT_ENDPOINT = "http://localhost:4141/usage";
|
|
274
274
|
const DEFAULT_TOKEN_USAGE_PERIOD = "day";
|
|
275
275
|
const DEFAULT_TOKEN_USAGE_EVENTS_PAGE_SIZE = 20;
|
|
276
|
+
const ALL_METRICS_VALUE = "__all__";
|
|
277
|
+
const ALL_MODELS_VALUE = "__all__";
|
|
276
278
|
const API_KEY_STORAGE_KEY = "copilot-api.usage-viewer.x-api-key";
|
|
277
279
|
const VALID_PERIODS = new Set(["day", "week", "month"]);
|
|
278
280
|
const EMPTY_TOKEN_USAGE_TOTALS = {
|
|
@@ -291,11 +293,16 @@
|
|
|
291
293
|
isEventsLoading: false,
|
|
292
294
|
error: null,
|
|
293
295
|
data: null,
|
|
296
|
+
tokenUsageDailySummary: null,
|
|
294
297
|
tokenUsageSummary: null,
|
|
295
298
|
tokenUsageEventsPage: null,
|
|
299
|
+
tokenUsageDailyError: null,
|
|
296
300
|
tokenUsageSummaryError: null,
|
|
297
301
|
tokenUsageEventsError: null,
|
|
298
302
|
tokenUsagePeriod: DEFAULT_TOKEN_USAGE_PERIOD,
|
|
303
|
+
tokenUsageTrendDayIndex: null,
|
|
304
|
+
tokenUsageTrendMetric: ALL_METRICS_VALUE,
|
|
305
|
+
tokenUsageTrendModel: ALL_MODELS_VALUE,
|
|
299
306
|
};
|
|
300
307
|
|
|
301
308
|
// --- Rendering Logic ---
|
|
@@ -402,6 +409,14 @@
|
|
|
402
409
|
return url.toString();
|
|
403
410
|
}
|
|
404
411
|
|
|
412
|
+
function buildTokenUsageDailyUrl(usageEndpoint, period) {
|
|
413
|
+
const url = new URL(
|
|
414
|
+
deriveEndpointUrl(usageEndpoint, "token-usage/daily")
|
|
415
|
+
);
|
|
416
|
+
url.searchParams.set("period", period);
|
|
417
|
+
return url.toString();
|
|
418
|
+
}
|
|
419
|
+
|
|
405
420
|
function buildTokenUsageEventsUrl(usageEndpoint, period, page) {
|
|
406
421
|
const url = new URL(
|
|
407
422
|
deriveEndpointUrl(usageEndpoint, "token-usage/events")
|
|
@@ -503,8 +518,8 @@
|
|
|
503
518
|
function renderTokenUsageRangeText(period, range) {
|
|
504
519
|
const labels = {
|
|
505
520
|
day: "Day window",
|
|
506
|
-
week: "
|
|
507
|
-
month: "
|
|
521
|
+
week: "Last 7 days",
|
|
522
|
+
month: "Last 30 days",
|
|
508
523
|
};
|
|
509
524
|
|
|
510
525
|
if (!range) {
|
|
@@ -533,8 +548,10 @@
|
|
|
533
548
|
|| state.data
|
|
534
549
|
|| state.isTokenUsageLoading
|
|
535
550
|
|| state.isEventsLoading
|
|
551
|
+
|| state.tokenUsageDailySummary
|
|
536
552
|
|| state.tokenUsageSummary
|
|
537
553
|
|| state.tokenUsageEventsPage
|
|
554
|
+
|| state.tokenUsageDailyError
|
|
538
555
|
|| state.tokenUsageSummaryError
|
|
539
556
|
|| state.tokenUsageEventsError
|
|
540
557
|
);
|
|
@@ -692,8 +709,10 @@
|
|
|
692
709
|
const hasTokenUsageContent = Boolean(
|
|
693
710
|
state.isTokenUsageLoading
|
|
694
711
|
|| state.isEventsLoading
|
|
712
|
+
|| state.tokenUsageDailySummary
|
|
695
713
|
|| state.tokenUsageSummary
|
|
696
714
|
|| state.tokenUsageEventsPage
|
|
715
|
+
|| state.tokenUsageDailyError
|
|
697
716
|
|| state.tokenUsageSummaryError
|
|
698
717
|
|| state.tokenUsageEventsError
|
|
699
718
|
);
|
|
@@ -703,9 +722,11 @@
|
|
|
703
722
|
}
|
|
704
723
|
|
|
705
724
|
const summary = state.tokenUsageSummary;
|
|
725
|
+
const dailySummary = state.tokenUsageDailySummary;
|
|
706
726
|
const eventsPage = state.tokenUsageEventsPage;
|
|
707
727
|
const totals = summary?.totals || EMPTY_TOKEN_USAGE_TOTALS;
|
|
708
|
-
const activeRange =
|
|
728
|
+
const activeRange =
|
|
729
|
+
summary?.range || dailySummary?.range || eventsPage?.range || null;
|
|
709
730
|
const totalPages = eventsPage ? Math.max(eventsPage.total_pages, 1) : 1;
|
|
710
731
|
const eventsMeta = eventsPage
|
|
711
732
|
? `Page ${eventsPage.page} / ${totalPages} · ${formatNumber(
|
|
@@ -782,6 +803,11 @@
|
|
|
782
803
|
)}
|
|
783
804
|
</div>
|
|
784
805
|
|
|
806
|
+
${renderTokenUsageDailyTrend(
|
|
807
|
+
dailySummary,
|
|
808
|
+
state.tokenUsageDailyError
|
|
809
|
+
)}
|
|
810
|
+
|
|
785
811
|
<div class="mt-4 border" style="background-color: var(--color-bg); border-color: var(--color-bg-light-2);">
|
|
786
812
|
<div class="px-4 py-3 border-b flex items-center justify-between gap-3" style="background-color: var(--color-bg-soft); border-color: var(--color-bg-light-2);">
|
|
787
813
|
<div>
|
|
@@ -829,6 +855,321 @@
|
|
|
829
855
|
`;
|
|
830
856
|
}
|
|
831
857
|
|
|
858
|
+
function getTokenUsageTrendMetrics() {
|
|
859
|
+
return [
|
|
860
|
+
{
|
|
861
|
+
color: "var(--color-fg-lightest)",
|
|
862
|
+
key: "total_tokens",
|
|
863
|
+
label: "Total",
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
color: "var(--color-blue-accent)",
|
|
867
|
+
key: "input_tokens",
|
|
868
|
+
label: "Input",
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
color: "var(--color-green-accent)",
|
|
872
|
+
key: "output_tokens",
|
|
873
|
+
label: "Output",
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
color: "var(--color-aqua-accent)",
|
|
877
|
+
key: "cache_read_input_tokens",
|
|
878
|
+
label: "Cache Read",
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
color: "var(--color-yellow-accent)",
|
|
882
|
+
key: "cache_creation_input_tokens",
|
|
883
|
+
label: "Cache Write",
|
|
884
|
+
},
|
|
885
|
+
];
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function getDailyTrendTotals(day, selectedModel) {
|
|
889
|
+
if (selectedModel === ALL_MODELS_VALUE) {
|
|
890
|
+
return day.totals || EMPTY_TOKEN_USAGE_TOTALS;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return (
|
|
894
|
+
day.byModel?.find((model) => model.model === selectedModel)
|
|
895
|
+
|| EMPTY_TOKEN_USAGE_TOTALS
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function formatChartDate(value) {
|
|
900
|
+
const parts = String(value).split("-");
|
|
901
|
+
return parts.length === 3 ? `${parts[1]}/${parts[2]}` : value;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function renderTokenUsageDailyTrend(dailySummary, error) {
|
|
905
|
+
if (state.tokenUsagePeriod === "day") {
|
|
906
|
+
return "";
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const models = dailySummary?.byModel || [];
|
|
910
|
+
const selectedModel = models.some(
|
|
911
|
+
(model) => model.model === state.tokenUsageTrendModel
|
|
912
|
+
)
|
|
913
|
+
? state.tokenUsageTrendModel
|
|
914
|
+
: ALL_MODELS_VALUE;
|
|
915
|
+
const options = [
|
|
916
|
+
`<option value="${ALL_MODELS_VALUE}">All models</option>`,
|
|
917
|
+
...models.map(
|
|
918
|
+
(model) =>
|
|
919
|
+
`<option value="${escapeHtml(model.model)}" ${
|
|
920
|
+
selectedModel === model.model ? "selected" : ""
|
|
921
|
+
}>${escapeHtml(model.model)}</option>`
|
|
922
|
+
),
|
|
923
|
+
].join("");
|
|
924
|
+
|
|
925
|
+
return `
|
|
926
|
+
<div class="mt-4 border" style="background-color: var(--color-bg); border-color: var(--color-bg-light-2);">
|
|
927
|
+
<div class="px-4 py-3 border-b flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between" style="background-color: var(--color-bg-soft); border-color: var(--color-bg-light-2);">
|
|
928
|
+
<p class="text-sm font-semibold" style="color: var(--color-fg-lightest);">Daily Trend</p>
|
|
929
|
+
<select
|
|
930
|
+
data-token-usage-trend-model
|
|
931
|
+
class="px-3 py-1.5 border input-focus text-xs"
|
|
932
|
+
style="background-color: var(--color-bg); border-color: var(--color-bg-light-2); color: var(--color-fg-light);"
|
|
933
|
+
>${options}</select>
|
|
934
|
+
</div>
|
|
935
|
+
${
|
|
936
|
+
error
|
|
937
|
+
? `<div class="p-4">${renderError(
|
|
938
|
+
error,
|
|
939
|
+
"Token usage daily trend failed"
|
|
940
|
+
)}</div>`
|
|
941
|
+
: ""
|
|
942
|
+
}
|
|
943
|
+
${renderTokenUsageTrendChart(dailySummary, selectedModel)}
|
|
944
|
+
</div>
|
|
945
|
+
`;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function renderTokenUsageTrendChart(dailySummary, selectedModel) {
|
|
949
|
+
const days = dailySummary?.days || [];
|
|
950
|
+
const metrics = getTokenUsageTrendMetrics();
|
|
951
|
+
const visibleMetrics =
|
|
952
|
+
state.tokenUsageTrendMetric === ALL_METRICS_VALUE
|
|
953
|
+
? metrics
|
|
954
|
+
: metrics.filter(
|
|
955
|
+
(metric) => metric.key === state.tokenUsageTrendMetric
|
|
956
|
+
);
|
|
957
|
+
const hasUsage = days.some(
|
|
958
|
+
(day) => getDailyTrendTotals(day, selectedModel).request_count > 0
|
|
959
|
+
);
|
|
960
|
+
const latestUsageIndex = days.reduce((latest, day, index) => {
|
|
961
|
+
return getDailyTrendTotals(day, selectedModel).request_count > 0
|
|
962
|
+
? index
|
|
963
|
+
: latest;
|
|
964
|
+
}, -1);
|
|
965
|
+
const activeIndex =
|
|
966
|
+
Number.isInteger(state.tokenUsageTrendDayIndex)
|
|
967
|
+
&& state.tokenUsageTrendDayIndex >= 0
|
|
968
|
+
&& state.tokenUsageTrendDayIndex < days.length
|
|
969
|
+
? state.tokenUsageTrendDayIndex
|
|
970
|
+
: latestUsageIndex >= 0
|
|
971
|
+
? latestUsageIndex
|
|
972
|
+
: Math.max(0, days.length - 1);
|
|
973
|
+
const activeDay = days[activeIndex];
|
|
974
|
+
const activeTotals = activeDay
|
|
975
|
+
? getDailyTrendTotals(activeDay, selectedModel)
|
|
976
|
+
: EMPTY_TOKEN_USAGE_TOTALS;
|
|
977
|
+
|
|
978
|
+
if (state.isTokenUsageLoading && !dailySummary) {
|
|
979
|
+
return `<div class="p-4 min-h-[14rem] text-sm animate-pulse" style="color: var(--color-gray);">Loading...</div>`;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (!dailySummary || days.length === 0 || !hasUsage) {
|
|
983
|
+
return renderEmptyState(
|
|
984
|
+
"No token usage recorded for the selected period."
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const width = 720;
|
|
989
|
+
const height = 220;
|
|
990
|
+
const padding = { bottom: 34, left: 58, right: 16, top: 16 };
|
|
991
|
+
const plotWidth = width - padding.left - padding.right;
|
|
992
|
+
const plotHeight = height - padding.top - padding.bottom;
|
|
993
|
+
const allValues = days.flatMap((day) => {
|
|
994
|
+
const totals = getDailyTrendTotals(day, selectedModel);
|
|
995
|
+
return visibleMetrics.map((metric) => totals[metric.key] || 0);
|
|
996
|
+
});
|
|
997
|
+
const maxValue = Math.max(1, ...allValues);
|
|
998
|
+
const xForIndex = (index) =>
|
|
999
|
+
days.length <= 1
|
|
1000
|
+
? padding.left + plotWidth / 2
|
|
1001
|
+
: padding.left + (plotWidth * index) / (days.length - 1);
|
|
1002
|
+
const hitWidth =
|
|
1003
|
+
days.length <= 1 ? plotWidth : plotWidth / (days.length - 1);
|
|
1004
|
+
const yForValue = (value) =>
|
|
1005
|
+
padding.top
|
|
1006
|
+
+ plotHeight
|
|
1007
|
+
- (Math.max(0, value) / maxValue) * plotHeight;
|
|
1008
|
+
const labelEvery = Math.max(1, Math.ceil(days.length / 6));
|
|
1009
|
+
const yTicks = [0, 0.25, 0.5, 0.75, 1].map((ratio) =>
|
|
1010
|
+
Math.round(maxValue * ratio)
|
|
1011
|
+
);
|
|
1012
|
+
|
|
1013
|
+
const grid = yTicks
|
|
1014
|
+
.map((tick) => {
|
|
1015
|
+
const y = yForValue(tick);
|
|
1016
|
+
return `
|
|
1017
|
+
<line x1="${padding.left}" x2="${
|
|
1018
|
+
width - padding.right
|
|
1019
|
+
}" y1="${y}" y2="${y}" stroke="var(--color-bg-light-2)" stroke-width="1" />
|
|
1020
|
+
<text x="${padding.left - 8}" y="${
|
|
1021
|
+
y + 4
|
|
1022
|
+
}" text-anchor="end" font-size="11" fill="var(--color-gray-accent)">${formatNumber(
|
|
1023
|
+
tick
|
|
1024
|
+
)}</text>
|
|
1025
|
+
`;
|
|
1026
|
+
})
|
|
1027
|
+
.join("");
|
|
1028
|
+
const xLabels = days
|
|
1029
|
+
.map((day, index) => {
|
|
1030
|
+
if (index % labelEvery !== 0 && index !== days.length - 1) {
|
|
1031
|
+
return "";
|
|
1032
|
+
}
|
|
1033
|
+
return `<text x="${xForIndex(index)}" y="${
|
|
1034
|
+
height - 10
|
|
1035
|
+
}" text-anchor="middle" font-size="11" fill="var(--color-gray-accent)">${escapeHtml(
|
|
1036
|
+
formatChartDate(day.date)
|
|
1037
|
+
)}</text>`;
|
|
1038
|
+
})
|
|
1039
|
+
.join("");
|
|
1040
|
+
const lines = visibleMetrics
|
|
1041
|
+
.map((metric) => {
|
|
1042
|
+
const points = days
|
|
1043
|
+
.map((day, index) => {
|
|
1044
|
+
const totals = getDailyTrendTotals(day, selectedModel);
|
|
1045
|
+
return `${xForIndex(index)},${yForValue(
|
|
1046
|
+
totals[metric.key] || 0
|
|
1047
|
+
)}`;
|
|
1048
|
+
})
|
|
1049
|
+
.join(" ");
|
|
1050
|
+
const circles = days
|
|
1051
|
+
.map((day, index) => {
|
|
1052
|
+
const totals = getDailyTrendTotals(day, selectedModel);
|
|
1053
|
+
const value = totals[metric.key] || 0;
|
|
1054
|
+
return `<circle cx="${xForIndex(index)}" cy="${yForValue(
|
|
1055
|
+
value
|
|
1056
|
+
)}" r="2.5" fill="${
|
|
1057
|
+
metric.color
|
|
1058
|
+
}" data-trend-day-index="${index}">
|
|
1059
|
+
<title>${escapeHtml(
|
|
1060
|
+
`${formatChartDate(day.date)} ${metric.label}: ${formatNumber(
|
|
1061
|
+
value
|
|
1062
|
+
)}`
|
|
1063
|
+
)}</title>
|
|
1064
|
+
</circle>`;
|
|
1065
|
+
})
|
|
1066
|
+
.join("");
|
|
1067
|
+
return `
|
|
1068
|
+
<polyline fill="none" points="${points}" stroke="${metric.color}" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.25">
|
|
1069
|
+
<title>${escapeHtml(metric.label)}</title>
|
|
1070
|
+
</polyline>
|
|
1071
|
+
${circles}
|
|
1072
|
+
`;
|
|
1073
|
+
})
|
|
1074
|
+
.join("");
|
|
1075
|
+
const activeMarker = activeDay
|
|
1076
|
+
? `<line x1="${xForIndex(activeIndex)}" x2="${xForIndex(
|
|
1077
|
+
activeIndex
|
|
1078
|
+
)}" y1="${padding.top}" y2="${
|
|
1079
|
+
padding.top + plotHeight
|
|
1080
|
+
}" stroke="var(--color-bg-light-4)" stroke-dasharray="4 4" stroke-width="1" />`
|
|
1081
|
+
: "";
|
|
1082
|
+
const hitRects = days
|
|
1083
|
+
.map((day, index) => {
|
|
1084
|
+
const x = xForIndex(index);
|
|
1085
|
+
const hitStart = Math.max(padding.left, x - hitWidth / 2);
|
|
1086
|
+
const hitEnd = Math.min(
|
|
1087
|
+
width - padding.right,
|
|
1088
|
+
x + hitWidth / 2
|
|
1089
|
+
);
|
|
1090
|
+
const totals = getDailyTrendTotals(day, selectedModel);
|
|
1091
|
+
const title = [
|
|
1092
|
+
formatChartDate(day.date),
|
|
1093
|
+
...visibleMetrics.map(
|
|
1094
|
+
(metric) =>
|
|
1095
|
+
`${metric.label}: ${formatNumber(
|
|
1096
|
+
totals[metric.key] || 0
|
|
1097
|
+
)}`
|
|
1098
|
+
),
|
|
1099
|
+
].join("\n");
|
|
1100
|
+
return `<rect x="${hitStart}" y="${padding.top}" width="${
|
|
1101
|
+
hitEnd - hitStart
|
|
1102
|
+
}" height="${plotHeight}" fill="transparent" style="cursor: pointer;" data-trend-day-index="${index}">
|
|
1103
|
+
<title>${escapeHtml(title)}</title>
|
|
1104
|
+
</rect>`;
|
|
1105
|
+
})
|
|
1106
|
+
.join("");
|
|
1107
|
+
const legend = [
|
|
1108
|
+
`<button type="button" data-trend-metric="${ALL_METRICS_VALUE}" class="inline-flex items-center border px-2 py-1 text-xs ${
|
|
1109
|
+
state.tokenUsageTrendMetric === ALL_METRICS_VALUE
|
|
1110
|
+
? "font-semibold"
|
|
1111
|
+
: ""
|
|
1112
|
+
}" style="background-color: ${
|
|
1113
|
+
state.tokenUsageTrendMetric === ALL_METRICS_VALUE
|
|
1114
|
+
? "var(--color-bg-light-1)"
|
|
1115
|
+
: "var(--color-bg)"
|
|
1116
|
+
}; border-color: var(--color-bg-light-2); color: var(--color-fg-light);">All</button>`,
|
|
1117
|
+
...metrics.map(
|
|
1118
|
+
(metric) => `
|
|
1119
|
+
<button type="button" data-trend-metric="${
|
|
1120
|
+
metric.key
|
|
1121
|
+
}" class="inline-flex items-center gap-1.5 border px-2 py-1 text-xs ${
|
|
1122
|
+
state.tokenUsageTrendMetric === metric.key
|
|
1123
|
+
? "font-semibold"
|
|
1124
|
+
: ""
|
|
1125
|
+
}" style="background-color: ${
|
|
1126
|
+
state.tokenUsageTrendMetric === metric.key
|
|
1127
|
+
? "var(--color-bg-light-1)"
|
|
1128
|
+
: "var(--color-bg)"
|
|
1129
|
+
}; border-color: var(--color-bg-light-2); color: var(--color-fg-light);">
|
|
1130
|
+
<span class="h-2 w-2 rounded-full" style="background-color: ${
|
|
1131
|
+
metric.color
|
|
1132
|
+
};"></span>
|
|
1133
|
+
${escapeHtml(metric.label)}
|
|
1134
|
+
</button>
|
|
1135
|
+
`
|
|
1136
|
+
),
|
|
1137
|
+
].join("");
|
|
1138
|
+
const activeValues = activeDay
|
|
1139
|
+
? `
|
|
1140
|
+
<div class="mt-2 flex flex-wrap gap-3 px-3 py-2 text-xs" style="background-color: var(--color-bg-soft); color: var(--color-fg-dark);">
|
|
1141
|
+
<span class="font-semibold" style="color: var(--color-fg-lightest);">${escapeHtml(
|
|
1142
|
+
formatChartDate(activeDay.date)
|
|
1143
|
+
)}</span>
|
|
1144
|
+
${visibleMetrics
|
|
1145
|
+
.map(
|
|
1146
|
+
(metric) =>
|
|
1147
|
+
`<span>${escapeHtml(metric.label)}: ${formatNumber(
|
|
1148
|
+
activeTotals[metric.key] || 0
|
|
1149
|
+
)}</span>`
|
|
1150
|
+
)
|
|
1151
|
+
.join("")}
|
|
1152
|
+
</div>
|
|
1153
|
+
`
|
|
1154
|
+
: "";
|
|
1155
|
+
|
|
1156
|
+
return `
|
|
1157
|
+
<div class="p-4 ${state.isTokenUsageLoading ? "opacity-60" : ""}">
|
|
1158
|
+
<svg class="w-full h-56" viewBox="0 0 ${width} ${height}" role="img">
|
|
1159
|
+
${grid}
|
|
1160
|
+
${xLabels}
|
|
1161
|
+
${activeMarker}
|
|
1162
|
+
${lines}
|
|
1163
|
+
${hitRects}
|
|
1164
|
+
</svg>
|
|
1165
|
+
<div class="mt-2 flex flex-wrap gap-3">
|
|
1166
|
+
${legend}
|
|
1167
|
+
</div>
|
|
1168
|
+
${activeValues}
|
|
1169
|
+
</div>
|
|
1170
|
+
`;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
832
1173
|
function renderTokenUsageMetric(label, value, accentColor) {
|
|
833
1174
|
return `
|
|
834
1175
|
<div class="p-4 border" style="background-color: var(--color-bg); border-color: var(--color-bg-light-2);">
|
|
@@ -1059,9 +1400,12 @@
|
|
|
1059
1400
|
const endpoint = endpointUrlInput.value.trim();
|
|
1060
1401
|
if (!endpoint) {
|
|
1061
1402
|
state.data = null;
|
|
1403
|
+
state.tokenUsageDailySummary = null;
|
|
1062
1404
|
state.tokenUsageSummary = null;
|
|
1063
1405
|
state.tokenUsageEventsPage = null;
|
|
1064
1406
|
state.error = "Endpoint URL cannot be empty.";
|
|
1407
|
+
state.tokenUsageDailyError =
|
|
1408
|
+
"Token usage requires a valid endpoint URL.";
|
|
1065
1409
|
state.tokenUsageSummaryError =
|
|
1066
1410
|
"Token usage requires a valid endpoint URL.";
|
|
1067
1411
|
state.tokenUsageEventsError = null;
|
|
@@ -1072,16 +1416,18 @@
|
|
|
1072
1416
|
const period = getSelectedPeriod();
|
|
1073
1417
|
state.isLoading = true;
|
|
1074
1418
|
state.error = null;
|
|
1419
|
+
state.tokenUsageDailyError = null;
|
|
1075
1420
|
state.tokenUsageSummaryError = null;
|
|
1076
1421
|
state.tokenUsageEventsError = null;
|
|
1077
1422
|
render();
|
|
1078
1423
|
|
|
1079
1424
|
try {
|
|
1080
1425
|
const usageUrl = resolveUsageUrl(endpoint);
|
|
1081
|
-
const [usageResult, summaryResult, eventsResult] =
|
|
1426
|
+
const [usageResult, summaryResult, dailyResult, eventsResult] =
|
|
1082
1427
|
await Promise.allSettled([
|
|
1083
1428
|
fetchJson(usageUrl),
|
|
1084
1429
|
fetchJson(buildTokenUsageSummaryUrl(usageUrl, period)),
|
|
1430
|
+
fetchJson(buildTokenUsageDailyUrl(usageUrl, period)),
|
|
1085
1431
|
fetchJson(buildTokenUsageEventsUrl(usageUrl, period, page)),
|
|
1086
1432
|
]);
|
|
1087
1433
|
|
|
@@ -1106,6 +1452,20 @@
|
|
|
1106
1452
|
);
|
|
1107
1453
|
}
|
|
1108
1454
|
|
|
1455
|
+
if (dailyResult.status === "fulfilled") {
|
|
1456
|
+
state.tokenUsageDailySummary = dailyResult.value;
|
|
1457
|
+
} else if (
|
|
1458
|
+
isMissingTokenUsageEndpointError(dailyResult.reason)
|
|
1459
|
+
) {
|
|
1460
|
+
state.tokenUsageDailySummary = null;
|
|
1461
|
+
state.tokenUsageDailyError = null;
|
|
1462
|
+
} else {
|
|
1463
|
+
state.tokenUsageDailySummary = null;
|
|
1464
|
+
state.tokenUsageDailyError = getErrorMessage(
|
|
1465
|
+
dailyResult.reason
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1109
1469
|
if (eventsResult.status === "fulfilled") {
|
|
1110
1470
|
state.tokenUsageEventsPage = eventsResult.value;
|
|
1111
1471
|
} else if (
|
|
@@ -1122,9 +1482,11 @@
|
|
|
1122
1482
|
} catch (error) {
|
|
1123
1483
|
console.error("Fetch error:", error);
|
|
1124
1484
|
state.data = null;
|
|
1485
|
+
state.tokenUsageDailySummary = null;
|
|
1125
1486
|
state.tokenUsageSummary = null;
|
|
1126
1487
|
state.tokenUsageEventsPage = null;
|
|
1127
1488
|
state.error = getErrorMessage(error);
|
|
1489
|
+
state.tokenUsageDailyError = getErrorMessage(error);
|
|
1128
1490
|
state.tokenUsageSummaryError = getErrorMessage(error);
|
|
1129
1491
|
state.tokenUsageEventsError = getErrorMessage(error);
|
|
1130
1492
|
} finally {
|
|
@@ -1136,8 +1498,11 @@
|
|
|
1136
1498
|
async function fetchTokenUsageSummaryAndEvents(page = 1) {
|
|
1137
1499
|
const endpoint = endpointUrlInput.value.trim();
|
|
1138
1500
|
if (!endpoint) {
|
|
1501
|
+
state.tokenUsageDailySummary = null;
|
|
1139
1502
|
state.tokenUsageSummary = null;
|
|
1140
1503
|
state.tokenUsageEventsPage = null;
|
|
1504
|
+
state.tokenUsageDailyError =
|
|
1505
|
+
"Token usage requires a valid endpoint URL.";
|
|
1141
1506
|
state.tokenUsageSummaryError =
|
|
1142
1507
|
"Token usage requires a valid endpoint URL.";
|
|
1143
1508
|
state.tokenUsageEventsError = null;
|
|
@@ -1147,18 +1512,22 @@
|
|
|
1147
1512
|
|
|
1148
1513
|
const period = getSelectedPeriod();
|
|
1149
1514
|
state.isTokenUsageLoading = true;
|
|
1515
|
+
state.tokenUsageDailySummary = null;
|
|
1150
1516
|
state.tokenUsageSummary = null;
|
|
1151
1517
|
state.tokenUsageEventsPage = null;
|
|
1518
|
+
state.tokenUsageDailyError = null;
|
|
1152
1519
|
state.tokenUsageSummaryError = null;
|
|
1153
1520
|
state.tokenUsageEventsError = null;
|
|
1154
1521
|
render();
|
|
1155
1522
|
|
|
1156
1523
|
try {
|
|
1157
1524
|
const usageUrl = resolveUsageUrl(endpoint);
|
|
1158
|
-
const [summaryResult, eventsResult] =
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1525
|
+
const [summaryResult, dailyResult, eventsResult] =
|
|
1526
|
+
await Promise.allSettled([
|
|
1527
|
+
fetchJson(buildTokenUsageSummaryUrl(usageUrl, period)),
|
|
1528
|
+
fetchJson(buildTokenUsageDailyUrl(usageUrl, period)),
|
|
1529
|
+
fetchJson(buildTokenUsageEventsUrl(usageUrl, period, page)),
|
|
1530
|
+
]);
|
|
1162
1531
|
|
|
1163
1532
|
if (summaryResult.status === "fulfilled") {
|
|
1164
1533
|
state.tokenUsageSummary = summaryResult.value;
|
|
@@ -1174,6 +1543,20 @@
|
|
|
1174
1543
|
);
|
|
1175
1544
|
}
|
|
1176
1545
|
|
|
1546
|
+
if (dailyResult.status === "fulfilled") {
|
|
1547
|
+
state.tokenUsageDailySummary = dailyResult.value;
|
|
1548
|
+
} else if (
|
|
1549
|
+
isMissingTokenUsageEndpointError(dailyResult.reason)
|
|
1550
|
+
) {
|
|
1551
|
+
state.tokenUsageDailySummary = null;
|
|
1552
|
+
state.tokenUsageDailyError = null;
|
|
1553
|
+
} else {
|
|
1554
|
+
state.tokenUsageDailySummary = null;
|
|
1555
|
+
state.tokenUsageDailyError = getErrorMessage(
|
|
1556
|
+
dailyResult.reason
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1177
1560
|
if (eventsResult.status === "fulfilled") {
|
|
1178
1561
|
state.tokenUsageEventsPage = eventsResult.value;
|
|
1179
1562
|
} else if (
|
|
@@ -1189,8 +1572,10 @@
|
|
|
1189
1572
|
}
|
|
1190
1573
|
} catch (error) {
|
|
1191
1574
|
console.error("Token usage fetch error:", error);
|
|
1575
|
+
state.tokenUsageDailySummary = null;
|
|
1192
1576
|
state.tokenUsageSummary = null;
|
|
1193
1577
|
state.tokenUsageEventsPage = null;
|
|
1578
|
+
state.tokenUsageDailyError = getErrorMessage(error);
|
|
1194
1579
|
state.tokenUsageSummaryError = getErrorMessage(error);
|
|
1195
1580
|
state.tokenUsageEventsError = getErrorMessage(error);
|
|
1196
1581
|
} finally {
|
|
@@ -1254,6 +1639,28 @@
|
|
|
1254
1639
|
return;
|
|
1255
1640
|
}
|
|
1256
1641
|
|
|
1642
|
+
const trendMetric = event.target.closest("[data-trend-metric]");
|
|
1643
|
+
if (trendMetric) {
|
|
1644
|
+
state.tokenUsageTrendMetric =
|
|
1645
|
+
trendMetric.getAttribute("data-trend-metric")
|
|
1646
|
+
|| ALL_METRICS_VALUE;
|
|
1647
|
+
render();
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
const trendPoint = event.target.closest("[data-trend-day-index]");
|
|
1652
|
+
if (trendPoint) {
|
|
1653
|
+
const index = Number.parseInt(
|
|
1654
|
+
trendPoint.getAttribute("data-trend-day-index") || "",
|
|
1655
|
+
10
|
|
1656
|
+
);
|
|
1657
|
+
if (Number.isFinite(index) && index >= 0) {
|
|
1658
|
+
state.tokenUsageTrendDayIndex = index;
|
|
1659
|
+
render();
|
|
1660
|
+
}
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1257
1664
|
const actionButton = event.target.closest("[data-page-action]");
|
|
1258
1665
|
if (!actionButton || !state.tokenUsageEventsPage || state.isEventsLoading) {
|
|
1259
1666
|
return;
|
|
@@ -1276,6 +1683,20 @@
|
|
|
1276
1683
|
}
|
|
1277
1684
|
}
|
|
1278
1685
|
|
|
1686
|
+
function handleContentAreaChange(event) {
|
|
1687
|
+
if (!(event.target instanceof HTMLSelectElement)) {
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
if (!event.target.matches("[data-token-usage-trend-model]")) {
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
state.tokenUsageTrendModel =
|
|
1696
|
+
event.target.value || ALL_MODELS_VALUE;
|
|
1697
|
+
render();
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1279
1700
|
/**
|
|
1280
1701
|
* Initializes the application.
|
|
1281
1702
|
*/
|
|
@@ -1286,6 +1707,7 @@
|
|
|
1286
1707
|
storeApiKey(apiKeyInput.value);
|
|
1287
1708
|
});
|
|
1288
1709
|
contentArea.addEventListener("click", handleContentAreaClick);
|
|
1710
|
+
contentArea.addEventListener("change", handleContentAreaChange);
|
|
1289
1711
|
|
|
1290
1712
|
apiKeyInput.value = loadStoredApiKey();
|
|
1291
1713
|
|