copilot-api / pages /index.html
imspsycho's picture
Initial upload from Google Colab
98c9143 verified
Raw
History Blame Contribute Delete
68.9 kB
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Copilot API Usage Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<script src="https://unpkg.com/lucide-react@0.378.0/dist/umd/lucide.min.js"></script>
<style>
/* Gruvbox-themed color palette */
:root {
/* Main Color Palette */
--color-red: #cc241d;
--color-green: #98971a;
--color-yellow: #d79921;
--color-blue: #458588;
--color-purple: #b16286;
--color-aqua: #689d6a;
--color-orange: #d65d0e;
--color-gray: #a89984;
/* Accent/Lighter/Darker Shades of Main Colors */
--color-red-accent: #fb4934;
--color-green-accent: #b8bb26;
--color-yellow-accent: #fabd2f;
--color-blue-accent: #83a598;
--color-purple-accent: #d3869b;
--color-aqua-accent: #8ec07c;
--color-orange-accent: #fe8019;
--color-gray-accent: #928374;
/* Background Colors */
--color-bg-darkest: #1d2021; /* bg0_h */
--color-bg: #282828; /* bg and bg0 */
--color-bg-light-1: #3c3836; /* bg1 */
--color-bg-light-2: #504945; /* bg2 */
--color-bg-light-3: #665c54; /* bg3 */
--color-bg-light-4: #7c6f64; /* bg4 */
--color-bg-soft: #32302f; /* bg0_s */
/* Foreground Colors */
--color-fg-darker: #a89984; /* fg4 - duplicate of gray */
--color-fg-dark: #bdae93; /* fg3 */
--color-fg-medium: #d5c4a1; /* fg2 */
--color-fg-light: #ebdbb2; /* fg and fg1 */
--color-fg-lightest: #fbf1c7; /* fg0 */
}
/* Custom styles using the new palette */
body {
font-family: "Inter", sans-serif;
background-color: var(--color-bg-darkest);
color: var(--color-fg-light);
}
/* Custom progress bar styles */
.progress-bar-bg {
background-color: var(--color-bg-light-1);
}
.progress-bar-fg {
transition: width 0.5s ease-in-out;
}
/* Custom scrollbar for the raw data view */
.code-block::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.code-block::-webkit-scrollbar-track {
background: var(--color-bg);
}
.code-block::-webkit-scrollbar-thumb {
background: var(--color-bg-light-3);
}
.code-block::-webkit-scrollbar-thumb:hover {
background: var(--color-bg-light-4);
}
/* Style for focus rings to use variables */
.input-focus:focus {
--tw-ring-color: var(--color-blue);
border-color: var(--color-blue);
}
button:disabled,
select:disabled {
cursor: not-allowed;
opacity: 0.55;
}
</style>
</head>
<body class="antialiased">
<div id="app" class="min-h-screen p-4 sm:p-6">
<div class="max-w-7xl mx-auto">
<!-- Header Section -->
<header class="mb-6">
<h1
class="text-2xl font-bold flex items-center gap-2"
style="color: var(--color-fg-lightest)"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-gauge-circle h-7 w-7"
style="color: var(--color-aqua-accent)"
>
<path d="M15.6 3.3a10 10 0 1 0 5.1 5.1" />
<path
d="M12 12a1 1 0 0 0-1-1v4a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-3z"
/>
<path d="M12 6.8a10 10 0 0 0 -3.2 7.2" />
</svg>
<span>Copilot API Usage Dashboard</span>
</h1>
<p class="mt-1 text-sm" style="color: var(--color-gray)">
Should be the same as the one in VSCode
</p>
</header>
<!-- Form Section -->
<div
class="mb-6 p-4 border"
style="
background-color: var(--color-bg-soft);
border-color: var(--color-bg-light-2);
"
>
<form
id="endpoint-form"
class="grid grid-cols-1 xl:grid-cols-[minmax(0,2fr)_minmax(0,1fr)_180px_auto] gap-3 items-end"
>
<div class="w-full">
<label
for="endpoint-url"
class="mb-1 block font-semibold whitespace-nowrap text-sm"
style="color: var(--color-fg-lightest)"
>Usage Endpoint URL</label
>
<input
type="text"
id="endpoint-url"
class="w-full px-3 py-2 border focus:ring-1 transition input-focus text-sm"
style="
background-color: var(--color-bg-darkest);
border-color: var(--color-bg-light-3);
color: var(--color-fg-medium);
"
placeholder="http://localhost:4141/usage"
/>
</div>
<div class="w-full">
<label
for="x-api-key"
class="mb-1 block font-semibold whitespace-nowrap text-sm"
style="color: var(--color-fg-lightest)"
>x-api-key</label
>
<input
type="password"
id="x-api-key"
class="w-full px-3 py-2 border focus:ring-1 transition input-focus text-sm"
style="
background-color: var(--color-bg-darkest);
border-color: var(--color-bg-light-3);
color: var(--color-fg-medium);
"
placeholder="Optional when auth is enabled"
autocomplete="off"
spellcheck="false"
/>
</div>
<div class="w-full">
<label
for="token-usage-period"
class="mb-1 block font-semibold whitespace-nowrap text-sm"
style="color: var(--color-fg-lightest)"
>Token Usage Period</label
>
<select
id="token-usage-period"
class="w-full px-3 py-2 border focus:ring-1 transition input-focus text-sm"
style="
background-color: var(--color-bg-darkest);
border-color: var(--color-bg-light-3);
color: var(--color-fg-medium);
"
>
<option value="day">Day</option>
<option value="week">Last 7 days</option>
<option value="month">Last 30 days</option>
</select>
</div>
<button
id="fetch-button"
type="submit"
class="w-full xl:w-auto font-bold py-2 px-5 transition-colors flex items-center justify-center gap-2 text-sm"
style="
background-color: var(--color-blue);
color: var(--color-bg-darkest);
"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-refresh-cw h-4 w-4"
>
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
<path d="M3 21v-5h5" />
</svg>
<span>Fetch</span>
</button>
</form>
<p class="mt-3 text-xs" style="color: var(--color-gray-accent)">
The page derives <code>/token-usage</code> and
<code>/token-usage/events</code> from the usage endpoint and sends
<code>x-api-key</code> to all requests when provided.
</p>
</div>
<!-- Content Area for dynamic data -->
<main id="content-area"></main>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const endpointForm = document.getElementById("endpoint-form");
const endpointUrlInput = document.getElementById("endpoint-url");
const apiKeyInput = document.getElementById("x-api-key");
const tokenUsagePeriodSelect = document.getElementById(
"token-usage-period"
);
const contentArea = document.getElementById("content-area");
const fetchButton = document.getElementById("fetch-button");
const fetchButtonLabel = fetchButton.querySelector("span");
// Apply hover effect for fetch button via JS
fetchButton.addEventListener("mouseenter", () => {
fetchButton.style.backgroundColor = "var(--color-blue-accent)";
});
fetchButton.addEventListener("mouseleave", () => {
fetchButton.style.backgroundColor = "var(--color-blue)";
});
const DEFAULT_ENDPOINT = "http://localhost:4141/usage";
const DEFAULT_TOKEN_USAGE_PERIOD = "day";
const DEFAULT_TOKEN_USAGE_EVENTS_PAGE_SIZE = 20;
const ALL_METRICS_VALUE = "__all__";
const ALL_MODELS_VALUE = "__all__";
const API_KEY_STORAGE_KEY = "copilot-api.usage-viewer.x-api-key";
const VALID_PERIODS = new Set(["day", "week", "month"]);
const EMPTY_TOKEN_USAGE_TOTALS = {
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
input_tokens: 0,
output_tokens: 0,
request_count: 0,
total_tokens: 0,
};
// --- State Management ---
const state = {
isLoading: false,
isTokenUsageLoading: false,
isEventsLoading: false,
error: null,
data: null,
tokenUsageDailySummary: null,
tokenUsageSummary: null,
tokenUsageEventsPage: null,
tokenUsageDailyError: null,
tokenUsageSummaryError: null,
tokenUsageEventsError: null,
tokenUsagePeriod: DEFAULT_TOKEN_USAGE_PERIOD,
tokenUsageTrendDayIndex: null,
tokenUsageTrendMetric: ALL_METRICS_VALUE,
tokenUsageTrendModel: ALL_MODELS_VALUE,
};
// --- Rendering Logic ---
/**
* Safely calls lucide.createIcons() if the library is available.
*/
function createIcons() {
if (typeof lucide !== "undefined") {
lucide.createIcons();
}
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function getErrorMessage(error) {
if (error instanceof Error) {
return error.message;
}
return typeof error === "string" ? error : "Unknown error.";
}
function isMissingTokenUsageEndpointError(error) {
return Boolean(
error
&& typeof error === "object"
&& "status" in error
&& error.status === 404
);
}
function normalizePeriod(value) {
return VALID_PERIODS.has(value)
? value
: DEFAULT_TOKEN_USAGE_PERIOD;
}
function getSelectedPeriod() {
const normalized = normalizePeriod(tokenUsagePeriodSelect.value);
tokenUsagePeriodSelect.value = normalized;
state.tokenUsagePeriod = normalized;
return normalized;
}
function loadStoredApiKey() {
try {
return localStorage.getItem(API_KEY_STORAGE_KEY) || "";
} catch (error) {
console.warn("Could not read stored API key:", getErrorMessage(error));
return "";
}
}
function storeApiKey(value) {
try {
const trimmed = value.trim();
if (trimmed) {
localStorage.setItem(API_KEY_STORAGE_KEY, trimmed);
} else {
localStorage.removeItem(API_KEY_STORAGE_KEY);
}
} catch (error) {
console.warn("Could not store API key:", getErrorMessage(error));
}
}
function buildRequestHeaders() {
const headers = {};
const apiKey = apiKeyInput.value.trim();
if (apiKey) {
headers["x-api-key"] = apiKey;
}
return headers;
}
function resolveUsageUrl(endpoint) {
return new URL(endpoint, window.location.href).toString();
}
function deriveEndpointUrl(usageEndpoint, targetPath) {
const url = new URL(usageEndpoint, window.location.href);
const normalizedTargetPath = targetPath.replace(/^\/+/, "");
const normalizedPath = url.pathname.replace(/\/+$/, "");
const nextPath = normalizedPath.endsWith("/usage")
? normalizedPath.replace(/\/usage$/, `/${normalizedTargetPath}`)
: `${normalizedPath || ""}/${normalizedTargetPath}`;
url.pathname = nextPath.startsWith("/") ? nextPath : `/${nextPath}`;
url.search = "";
url.hash = "";
return url.toString();
}
function buildTokenUsageSummaryUrl(usageEndpoint, period) {
const url = new URL(deriveEndpointUrl(usageEndpoint, "token-usage"));
url.searchParams.set("period", period);
return url.toString();
}
function buildTokenUsageDailyUrl(usageEndpoint, period) {
const url = new URL(
deriveEndpointUrl(usageEndpoint, "token-usage/daily")
);
url.searchParams.set("period", period);
return url.toString();
}
function buildTokenUsageEventsUrl(usageEndpoint, period, page) {
const url = new URL(
deriveEndpointUrl(usageEndpoint, "token-usage/events")
);
url.searchParams.set("period", period);
url.searchParams.set("page", String(page));
url.searchParams.set(
"page_size",
String(DEFAULT_TOKEN_USAGE_EVENTS_PAGE_SIZE)
);
return url.toString();
}
async function fetchJson(url) {
const response = await fetch(url, {
headers: buildRequestHeaders(),
});
if (!response.ok) {
let message = response.statusText || "Request failed";
try {
const contentType = response.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
const payload = await response.json();
const apiMessage = payload?.error?.message || payload?.message;
if (typeof apiMessage === "string" && apiMessage.trim()) {
message = apiMessage.trim();
}
} else {
const text = await response.text();
if (text.trim()) {
message = text.trim();
}
}
} catch (error) {
console.warn(
"Could not parse error response:",
getErrorMessage(error)
);
}
const error = new Error(
`Request failed with status ${response.status}: ${message}`
);
error.status = response.status;
throw error;
}
return response.json();
}
function syncUrlState() {
try {
const currentUrl = new URL(window.location.href);
const endpoint = endpointUrlInput.value.trim();
const period = getSelectedPeriod();
if (endpoint) {
currentUrl.searchParams.set("endpoint", endpoint);
} else {
currentUrl.searchParams.delete("endpoint");
}
currentUrl.searchParams.set("period", period);
window.history.pushState({}, "", currentUrl);
} catch (error) {
console.warn("Could not update URL:", getErrorMessage(error));
}
}
function updateControls() {
fetchButton.disabled = state.isLoading;
fetchButtonLabel.textContent = state.isLoading ? "Fetching..." : "Fetch";
tokenUsagePeriodSelect.disabled =
state.isLoading || state.isTokenUsageLoading;
}
function formatNumber(value) {
return Number.isFinite(value) ? Number(value).toLocaleString() : "0";
}
function formatDateTime(value) {
if (!Number.isFinite(value)) {
return "N/A";
}
return new Date(value).toLocaleString();
}
function formatCellText(value) {
if (typeof value !== "string") {
return "—";
}
const trimmed = value.trim();
return trimmed || "—";
}
function renderTokenUsageRangeText(period, range) {
const labels = {
day: "Day window",
week: "Last 7 days",
month: "Last 30 days",
};
if (!range) {
return labels[period] || labels.day;
}
return `${labels[period] || labels.day}: ${formatDateTime(
range.start_ms
)} - ${formatDateTime(range.end_ms)}`;
}
/**
* Renders the entire UI based on the current state.
*/
function render() {
updateControls();
if (state.isLoading) {
contentArea.innerHTML = renderSpinner();
createIcons();
return;
}
const hasContent = Boolean(
state.error
|| state.data
|| state.isTokenUsageLoading
|| state.isEventsLoading
|| state.tokenUsageDailySummary
|| state.tokenUsageSummary
|| state.tokenUsageEventsPage
|| state.tokenUsageDailyError
|| state.tokenUsageSummaryError
|| state.tokenUsageEventsError
);
if (!hasContent) {
contentArea.innerHTML = renderWelcomeMessage();
} else if (state.error && !state.data) {
contentArea.innerHTML = `
${renderError(state.error, "Usage request failed")}
${renderTokenUsageSection()}
`;
} else if (state.data) {
contentArea.innerHTML = `
${renderUsageQuotas(state.data.quota_snapshots)}
${renderTokenUsageSection()}
${renderDetailedData(state.data)}
`;
} else {
contentArea.innerHTML = renderTokenUsageSection();
}
// Replace placeholder icons with actual Lucide icons
createIcons();
}
/**
* Renders the "Usage Quotas" section with progress bars.
* @param {object} snapshots - The quota_snapshots object from the API response.
* @returns {string} HTML string for the usage quotas section.
*/
function renderUsageQuotas(snapshots) {
if (!snapshots) return "";
const quotaCards = Object.entries(snapshots)
.map(([key, value]) => {
return renderQuotaCard(key, value);
})
.join("");
return `
<section id="usage-quotas" class="mb-6">
<h2 class="text-xl font-bold mb-3 flex items-center gap-2" style="color: var(--color-fg-lightest);">
<i data-lucide="bar-chart-big"></i> Usage Quotas
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
${quotaCards}
</div>
</section>
`;
}
/**
* Renders a single quota card.
* @param {string} title - The name of the quota (e.g., 'chat').
* @param {object} details - The details object for the quota.
* @returns {string} HTML string for a single card.
*/
function renderQuotaCard(title, details) {
const { entitlement, remaining, percent_remaining, unlimited } =
details;
const percentUsed = unlimited ? 0 : 100 - percent_remaining;
const used = unlimited
? "N/A"
: (entitlement - remaining).toLocaleString();
let progressBarColor = "var(--color-green)";
if (percentUsed > 75) progressBarColor = "var(--color-yellow)";
if (percentUsed > 90) progressBarColor = "var(--color-red)";
if (unlimited) progressBarColor = "var(--color-blue)";
return `
<div class="p-4 border" style="background-color: var(--color-bg); border-color: var(--color-bg-light-2);">
<div class="flex justify-between items-center mb-2">
<h3 class="text-md font-semibold capitalize" style="color: var(--color-fg-lightest);">${escapeHtml(title.replace(/_/g, " "))}</h3>
${
unlimited
? `<span class="px-2 py-0.5 text-xs font-medium" style="color: var(--color-blue-accent); background-color: var(--color-bg-light-1);">Unlimited</span>`
: `<span class="text-sm font-mono" style="color: var(--color-fg-medium);">${percentUsed.toFixed(1)}% Used</span>`
}
</div>
<div class="mb-3">
<div class="w-full progress-bar-bg h-2">
<div class="progress-bar-fg h-2" style="width: ${unlimited ? 100 : percentUsed}%; background-color: ${progressBarColor};"></div>
</div>
</div>
<div class="flex justify-between text-xs font-mono" style="color: var(--color-fg-dark);">
<span>${used} / ${unlimited ? "∞" : entitlement.toLocaleString()}</span>
<span>${unlimited ? "∞" : remaining.toLocaleString()} remaining</span>
</div>
</div>
`;
}
/**
* Recursively builds a formatted HTML list from a JSON object.
* @param {object} obj - The object to format.
* @returns {string} HTML string for the formatted list.
*/
function formatObject(obj) {
if (obj === null || typeof obj !== "object") {
return `<span style="color: var(--color-green-accent);">${escapeHtml(JSON.stringify(obj))}</span>`;
}
return (
'<div class="pl-4">' +
Object.entries(obj)
.map(([key, value]) => {
const formattedKey = escapeHtml(key.replace(/_/g, " "));
let displayValue;
if (Array.isArray(value)) {
displayValue =
value.length > 0
? `<span style='color: var(--color-gray-accent)'>[...${value.length} items]</span>`
: `<span style='color: var(--color-gray-accent)'>[]</span>`;
} else if (typeof value === "object" && value !== null) {
displayValue = formatObject(value);
} else if (typeof value === "boolean") {
displayValue = `<span class="font-semibold" style="color: ${value ? "var(--color-green-accent)" : "var(--color-red-accent)"}">${escapeHtml(String(value))}</span>`;
} else {
displayValue = `<span style="color: var(--color-blue-accent);">${escapeHtml(JSON.stringify(value))}</span>`;
}
return `<div class="mt-1">
<span class="capitalize font-semibold" style="color: var(--color-fg-medium);">${formattedKey}:</span>
${typeof value === "object" && value !== null && !Array.isArray(value) ? displayValue : ` ${displayValue}`}
</div>`;
})
.join("") +
"</div>"
);
}
/**
* Renders the section with the full, formatted API response.
* @param {object} data - The full API response data.
* @returns {string} HTML string for the full data section.
*/
function renderDetailedData(data) {
const formattedDetails = formatObject(data);
return `
<section id="detailed-data">
<h2 class="text-xl font-bold mb-3 flex items-center gap-2" style="color: var(--color-fg-lightest);">
<i data-lucide="file-text"></i> Usage API Response
</h2>
<div class="border p-4 relative font-mono text-xs code-block overflow-auto" style="background-color: var(--color-bg-darkest); border-color: var(--color-bg-light-2);">
${formattedDetails}
</div>
</section>
`;
}
function renderTokenUsageSection() {
const hasTokenUsageContent = Boolean(
state.isTokenUsageLoading
|| state.isEventsLoading
|| state.tokenUsageDailySummary
|| state.tokenUsageSummary
|| state.tokenUsageEventsPage
|| state.tokenUsageDailyError
|| state.tokenUsageSummaryError
|| state.tokenUsageEventsError
);
if (!hasTokenUsageContent) {
return "";
}
const summary = state.tokenUsageSummary;
const dailySummary = state.tokenUsageDailySummary;
const eventsPage = state.tokenUsageEventsPage;
const totals = summary?.totals || EMPTY_TOKEN_USAGE_TOTALS;
const activeRange =
summary?.range || dailySummary?.range || eventsPage?.range || null;
const totalPages = eventsPage ? Math.max(eventsPage.total_pages, 1) : 1;
const eventsMeta = eventsPage
? `Page ${eventsPage.page} / ${totalPages} · ${formatNumber(
eventsPage.total
)} events`
: "No detail page loaded yet.";
return `
<section id="token-usage" class="mb-6">
<div class="mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-xl font-bold flex items-center gap-2" style="color: var(--color-fg-lightest);">
<i data-lucide="database"></i> Token Usage
</h2>
<p class="mt-1 text-xs" style="color: var(--color-gray);">${escapeHtml(
renderTokenUsageRangeText(state.tokenUsagePeriod, activeRange)
)}</p>
</div>
<div class="flex flex-wrap items-center gap-2 text-xs" style="color: var(--color-gray-accent);">
${
state.isTokenUsageLoading
? "<span>Refreshing summary...</span>"
: ""
}
${
state.isEventsLoading
? "<span>Refreshing details...</span>"
: ""
}
</div>
</div>
${
state.tokenUsageSummaryError
? renderError(
state.tokenUsageSummaryError,
"Token usage summary failed"
)
: ""
}
<div class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-3 ${
state.isTokenUsageLoading ? "opacity-60" : ""
}">
${renderTokenUsageMetric(
"Total",
totals.total_tokens,
"var(--color-fg-lightest)"
)}
${renderTokenUsageMetric(
"Input",
totals.input_tokens,
"var(--color-blue-accent)"
)}
${renderTokenUsageMetric(
"Output",
totals.output_tokens,
"var(--color-green-accent)"
)}
${renderTokenUsageMetric(
"Cache Read",
totals.cache_read_input_tokens,
"var(--color-aqua-accent)"
)}
${renderTokenUsageMetric(
"Cache Write",
totals.cache_creation_input_tokens,
"var(--color-yellow-accent)"
)}
${renderTokenUsageMetric(
"Requests",
totals.request_count,
"var(--color-purple-accent)"
)}
</div>
${renderTokenUsageDailyTrend(
dailySummary,
state.tokenUsageDailyError
)}
<div class="mt-4 border" style="background-color: var(--color-bg); border-color: var(--color-bg-light-2);">
<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);">
<div>
<p class="text-sm font-semibold" style="color: var(--color-fg-lightest);">By Model</p>
<p class="mt-1 text-xs" style="color: var(--color-gray-accent);">${summary ? `${formatNumber(summary.byModel.length)} models` : "No summary rows loaded."}</p>
</div>
</div>
${renderTokenUsageModelBreakdown(summary)}
</div>
<div class="mt-4 border" style="background-color: var(--color-bg); border-color: var(--color-bg-light-2);">
<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);">
<div>
<p class="text-sm font-semibold" style="color: var(--color-fg-lightest);">Event Details</p>
<p class="mt-1 text-xs" style="color: var(--color-gray-accent);">${escapeHtml(
eventsMeta
)}</p>
</div>
<div class="flex items-center gap-2">
${renderPaginationButton(
"previous",
"Previous",
!eventsPage || state.isEventsLoading || eventsPage.page <= 1
)}
${renderPaginationButton(
"next",
"Next",
!eventsPage
|| state.isEventsLoading
|| eventsPage.page >= totalPages
)}
</div>
</div>
${
state.tokenUsageEventsError
? `<div class="p-4">${renderError(
state.tokenUsageEventsError,
"Token usage details failed"
)}</div>`
: ""
}
${renderTokenUsageEventsTable(eventsPage)}
</div>
</section>
`;
}
function getTokenUsageTrendMetrics() {
return [
{
color: "var(--color-fg-lightest)",
key: "total_tokens",
label: "Total",
},
{
color: "var(--color-blue-accent)",
key: "input_tokens",
label: "Input",
},
{
color: "var(--color-green-accent)",
key: "output_tokens",
label: "Output",
},
{
color: "var(--color-aqua-accent)",
key: "cache_read_input_tokens",
label: "Cache Read",
},
{
color: "var(--color-yellow-accent)",
key: "cache_creation_input_tokens",
label: "Cache Write",
},
];
}
function getDailyTrendTotals(day, selectedModel) {
if (selectedModel === ALL_MODELS_VALUE) {
return day.totals || EMPTY_TOKEN_USAGE_TOTALS;
}
return (
day.byModel?.find((model) => model.model === selectedModel)
|| EMPTY_TOKEN_USAGE_TOTALS
);
}
function formatChartDate(value) {
const parts = String(value).split("-");
return parts.length === 3 ? `${parts[1]}/${parts[2]}` : value;
}
function renderTokenUsageDailyTrend(dailySummary, error) {
if (state.tokenUsagePeriod === "day") {
return "";
}
const models = dailySummary?.byModel || [];
const selectedModel = models.some(
(model) => model.model === state.tokenUsageTrendModel
)
? state.tokenUsageTrendModel
: ALL_MODELS_VALUE;
const options = [
`<option value="${ALL_MODELS_VALUE}">All models</option>`,
...models.map(
(model) =>
`<option value="${escapeHtml(model.model)}" ${
selectedModel === model.model ? "selected" : ""
}>${escapeHtml(model.model)}</option>`
),
].join("");
return `
<div class="mt-4 border" style="background-color: var(--color-bg); border-color: var(--color-bg-light-2);">
<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);">
<p class="text-sm font-semibold" style="color: var(--color-fg-lightest);">Daily Trend</p>
<select
data-token-usage-trend-model
class="px-3 py-1.5 border input-focus text-xs"
style="background-color: var(--color-bg); border-color: var(--color-bg-light-2); color: var(--color-fg-light);"
>${options}</select>
</div>
${
error
? `<div class="p-4">${renderError(
error,
"Token usage daily trend failed"
)}</div>`
: ""
}
${renderTokenUsageTrendChart(dailySummary, selectedModel)}
</div>
`;
}
function renderTokenUsageTrendChart(dailySummary, selectedModel) {
const days = dailySummary?.days || [];
const metrics = getTokenUsageTrendMetrics();
const visibleMetrics =
state.tokenUsageTrendMetric === ALL_METRICS_VALUE
? metrics
: metrics.filter(
(metric) => metric.key === state.tokenUsageTrendMetric
);
const hasUsage = days.some(
(day) => getDailyTrendTotals(day, selectedModel).request_count > 0
);
const latestUsageIndex = days.reduce((latest, day, index) => {
return getDailyTrendTotals(day, selectedModel).request_count > 0
? index
: latest;
}, -1);
const activeIndex =
Number.isInteger(state.tokenUsageTrendDayIndex)
&& state.tokenUsageTrendDayIndex >= 0
&& state.tokenUsageTrendDayIndex < days.length
? state.tokenUsageTrendDayIndex
: latestUsageIndex >= 0
? latestUsageIndex
: Math.max(0, days.length - 1);
const activeDay = days[activeIndex];
const activeTotals = activeDay
? getDailyTrendTotals(activeDay, selectedModel)
: EMPTY_TOKEN_USAGE_TOTALS;
if (state.isTokenUsageLoading && !dailySummary) {
return `<div class="p-4 min-h-[14rem] text-sm animate-pulse" style="color: var(--color-gray);">Loading...</div>`;
}
if (!dailySummary || days.length === 0 || !hasUsage) {
return renderEmptyState(
"No token usage recorded for the selected period."
);
}
const width = 720;
const height = 220;
const padding = { bottom: 34, left: 58, right: 16, top: 16 };
const plotWidth = width - padding.left - padding.right;
const plotHeight = height - padding.top - padding.bottom;
const allValues = days.flatMap((day) => {
const totals = getDailyTrendTotals(day, selectedModel);
return visibleMetrics.map((metric) => totals[metric.key] || 0);
});
const maxValue = Math.max(1, ...allValues);
const xForIndex = (index) =>
days.length <= 1
? padding.left + plotWidth / 2
: padding.left + (plotWidth * index) / (days.length - 1);
const hitWidth =
days.length <= 1 ? plotWidth : plotWidth / (days.length - 1);
const yForValue = (value) =>
padding.top
+ plotHeight
- (Math.max(0, value) / maxValue) * plotHeight;
const labelEvery = Math.max(1, Math.ceil(days.length / 6));
const yTicks = [0, 0.25, 0.5, 0.75, 1].map((ratio) =>
Math.round(maxValue * ratio)
);
const grid = yTicks
.map((tick) => {
const y = yForValue(tick);
return `
<line x1="${padding.left}" x2="${
width - padding.right
}" y1="${y}" y2="${y}" stroke="var(--color-bg-light-2)" stroke-width="1" />
<text x="${padding.left - 8}" y="${
y + 4
}" text-anchor="end" font-size="11" fill="var(--color-gray-accent)">${formatNumber(
tick
)}</text>
`;
})
.join("");
const xLabels = days
.map((day, index) => {
if (index % labelEvery !== 0 && index !== days.length - 1) {
return "";
}
return `<text x="${xForIndex(index)}" y="${
height - 10
}" text-anchor="middle" font-size="11" fill="var(--color-gray-accent)">${escapeHtml(
formatChartDate(day.date)
)}</text>`;
})
.join("");
const lines = visibleMetrics
.map((metric) => {
const points = days
.map((day, index) => {
const totals = getDailyTrendTotals(day, selectedModel);
return `${xForIndex(index)},${yForValue(
totals[metric.key] || 0
)}`;
})
.join(" ");
const circles = days
.map((day, index) => {
const totals = getDailyTrendTotals(day, selectedModel);
const value = totals[metric.key] || 0;
return `<circle cx="${xForIndex(index)}" cy="${yForValue(
value
)}" r="2.5" fill="${
metric.color
}" data-trend-day-index="${index}">
<title>${escapeHtml(
`${formatChartDate(day.date)} ${metric.label}: ${formatNumber(
value
)}`
)}</title>
</circle>`;
})
.join("");
return `
<polyline fill="none" points="${points}" stroke="${metric.color}" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.25">
<title>${escapeHtml(metric.label)}</title>
</polyline>
${circles}
`;
})
.join("");
const activeMarker = activeDay
? `<line x1="${xForIndex(activeIndex)}" x2="${xForIndex(
activeIndex
)}" y1="${padding.top}" y2="${
padding.top + plotHeight
}" stroke="var(--color-bg-light-4)" stroke-dasharray="4 4" stroke-width="1" />`
: "";
const hitRects = days
.map((day, index) => {
const x = xForIndex(index);
const hitStart = Math.max(padding.left, x - hitWidth / 2);
const hitEnd = Math.min(
width - padding.right,
x + hitWidth / 2
);
const totals = getDailyTrendTotals(day, selectedModel);
const title = [
formatChartDate(day.date),
...visibleMetrics.map(
(metric) =>
`${metric.label}: ${formatNumber(
totals[metric.key] || 0
)}`
),
].join("\n");
return `<rect x="${hitStart}" y="${padding.top}" width="${
hitEnd - hitStart
}" height="${plotHeight}" fill="transparent" style="cursor: pointer;" data-trend-day-index="${index}">
<title>${escapeHtml(title)}</title>
</rect>`;
})
.join("");
const legend = [
`<button type="button" data-trend-metric="${ALL_METRICS_VALUE}" class="inline-flex items-center border px-2 py-1 text-xs ${
state.tokenUsageTrendMetric === ALL_METRICS_VALUE
? "font-semibold"
: ""
}" style="background-color: ${
state.tokenUsageTrendMetric === ALL_METRICS_VALUE
? "var(--color-bg-light-1)"
: "var(--color-bg)"
}; border-color: var(--color-bg-light-2); color: var(--color-fg-light);">All</button>`,
...metrics.map(
(metric) => `
<button type="button" data-trend-metric="${
metric.key
}" class="inline-flex items-center gap-1.5 border px-2 py-1 text-xs ${
state.tokenUsageTrendMetric === metric.key
? "font-semibold"
: ""
}" style="background-color: ${
state.tokenUsageTrendMetric === metric.key
? "var(--color-bg-light-1)"
: "var(--color-bg)"
}; border-color: var(--color-bg-light-2); color: var(--color-fg-light);">
<span class="h-2 w-2 rounded-full" style="background-color: ${
metric.color
};"></span>
${escapeHtml(metric.label)}
</button>
`
),
].join("");
const activeValues = activeDay
? `
<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);">
<span class="font-semibold" style="color: var(--color-fg-lightest);">${escapeHtml(
formatChartDate(activeDay.date)
)}</span>
${visibleMetrics
.map(
(metric) =>
`<span>${escapeHtml(metric.label)}: ${formatNumber(
activeTotals[metric.key] || 0
)}</span>`
)
.join("")}
</div>
`
: "";
return `
<div class="p-4 ${state.isTokenUsageLoading ? "opacity-60" : ""}">
<svg class="w-full h-56" viewBox="0 0 ${width} ${height}" role="img">
${grid}
${xLabels}
${activeMarker}
${lines}
${hitRects}
</svg>
<div class="mt-2 flex flex-wrap gap-3">
${legend}
</div>
${activeValues}
</div>
`;
}
function renderTokenUsageMetric(label, value, accentColor) {
return `
<div class="p-4 border" style="background-color: var(--color-bg); border-color: var(--color-bg-light-2);">
<div class="text-lg font-bold" style="color: ${accentColor};">${formatNumber(
value
)}</div>
<div class="mt-1 text-xs uppercase tracking-wide" style="color: var(--color-gray-accent);">${escapeHtml(
label
)}</div>
</div>
`;
}
function renderTokenUsageModelBreakdown(summary) {
if (!summary || summary.byModel.length === 0) {
return renderEmptyState(
"No token usage recorded for the selected period."
);
}
const rows = summary.byModel
.map((model) => {
return `
<tr class="border-b last:border-b-0" style="border-color: var(--color-bg-light-1);">
<td class="px-4 py-2 max-w-[280px] truncate" style="color: var(--color-fg-lightest);" title="${escapeHtml(
model.model
)}">${escapeHtml(model.model)}</td>
<td class="px-4 py-2 text-right" style="color: var(--color-fg-dark);">${formatNumber(
model.request_count
)}</td>
<td class="px-4 py-2 text-right" style="color: var(--color-fg-dark);">${formatNumber(
model.input_tokens
)}</td>
<td class="px-4 py-2 text-right" style="color: var(--color-fg-dark);">${formatNumber(
model.output_tokens
)}</td>
<td class="px-4 py-2 text-right" style="color: var(--color-fg-dark);">${formatNumber(
model.cache_read_input_tokens
)}</td>
<td class="px-4 py-2 text-right" style="color: var(--color-fg-dark);">${formatNumber(
model.cache_creation_input_tokens
)}</td>
<td class="px-4 py-2 text-right font-semibold" style="color: var(--color-yellow-accent);">${formatNumber(
model.total_tokens
)}</td>
</tr>
`;
})
.join("");
return `
<div class="overflow-auto ${state.isTokenUsageLoading ? "opacity-60" : ""}">
<table class="w-full min-w-[760px] text-left text-xs sm:text-sm">
<thead style="background-color: var(--color-bg-light-1); color: var(--color-fg-medium);">
<tr>
<th class="px-4 py-2 font-semibold">Model</th>
<th class="px-4 py-2 text-right font-semibold">Requests</th>
<th class="px-4 py-2 text-right font-semibold">Input</th>
<th class="px-4 py-2 text-right font-semibold">Output</th>
<th class="px-4 py-2 text-right font-semibold">Cache Read</th>
<th class="px-4 py-2 text-right font-semibold">Cache Write</th>
<th class="px-4 py-2 text-right font-semibold">Total</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
</div>
`;
}
function renderTokenUsageEventsTable(eventsPage) {
if (!eventsPage || eventsPage.items.length === 0) {
return renderEmptyState(
"No token usage detail rows for the selected period."
);
}
const rows = eventsPage.items
.map((event) => {
const userId = formatCellText(event.user_id);
const sessionId = formatCellText(event.session_id);
const traceId = formatCellText(event.trace_id);
return `
<tr class="border-b last:border-b-0" style="border-color: var(--color-bg-light-1);">
<td class="px-4 py-2 whitespace-nowrap" style="color: var(--color-fg-dark);" title="${escapeHtml(
event.created_at_utc
)}">${escapeHtml(formatDateTime(event.created_at_ms))}</td>
<td class="px-4 py-2 max-w-[160px] truncate" style="color: var(--color-fg-lightest);" title="${escapeHtml(
userId
)}">${escapeHtml(userId)}</td>
<td class="px-4 py-2 whitespace-nowrap" style="color: var(--color-fg-dark);">${escapeHtml(
event.endpoint.replace(/_/g, " ")
)}</td>
<td class="px-4 py-2 max-w-[220px] truncate" style="color: var(--color-fg-lightest);" title="${escapeHtml(
event.model
)}">${escapeHtml(event.model)}</td>
<td class="px-4 py-2 max-w-[180px] truncate font-mono" style="color: var(--color-fg-dark);" title="${escapeHtml(
sessionId
)}">${escapeHtml(sessionId)}</td>
<td class="px-4 py-2 max-w-[200px] truncate font-mono" style="color: var(--color-fg-dark);" title="${escapeHtml(
traceId
)}">${escapeHtml(traceId)}</td>
<td class="px-4 py-2 text-right" style="color: var(--color-fg-dark);">${formatNumber(
event.input_tokens
)}</td>
<td class="px-4 py-2 text-right" style="color: var(--color-fg-dark);">${formatNumber(
event.output_tokens
)}</td>
<td class="px-4 py-2 text-right" style="color: var(--color-fg-dark);">${formatNumber(
event.cache_read_input_tokens
)}</td>
<td class="px-4 py-2 text-right" style="color: var(--color-fg-dark);">${formatNumber(
event.cache_creation_input_tokens
)}</td>
<td class="px-4 py-2 text-right font-semibold" style="color: var(--color-yellow-accent);">${formatNumber(
event.total_tokens
)}</td>
</tr>
`;
})
.join("");
return `
<div class="overflow-auto ${state.isEventsLoading ? "opacity-60" : ""}">
<table class="w-full min-w-[1180px] text-left text-xs sm:text-sm">
<thead style="background-color: var(--color-bg-light-1); color: var(--color-fg-medium);">
<tr>
<th class="px-4 py-2 font-semibold">Time</th>
<th class="px-4 py-2 font-semibold">User</th>
<th class="px-4 py-2 font-semibold">Endpoint</th>
<th class="px-4 py-2 font-semibold">Model</th>
<th class="px-4 py-2 font-semibold">Session</th>
<th class="px-4 py-2 font-semibold">Trace</th>
<th class="px-4 py-2 text-right font-semibold">Input</th>
<th class="px-4 py-2 text-right font-semibold">Output</th>
<th class="px-4 py-2 text-right font-semibold">Cache Read</th>
<th class="px-4 py-2 text-right font-semibold">Cache Write</th>
<th class="px-4 py-2 text-right font-semibold">Total</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
</div>
`;
}
function renderPaginationButton(action, label, disabled) {
return `
<button
type="button"
data-page-action="${action}"
class="px-3 py-1.5 border text-xs font-medium"
style="background-color: var(--color-bg-darkest); border-color: var(--color-bg-light-2); color: var(--color-fg-light);"
${disabled ? "disabled" : ""}
>
${label}
</button>
`;
}
function renderEmptyState(message) {
return `
<div class="px-4 py-6 text-sm" style="color: var(--color-gray);">
${escapeHtml(message)}
</div>
`;
}
/**
* Renders a loading spinner.
* @returns {string} HTML string for the spinner.
*/
function renderSpinner() {
return `
<div class="flex justify-center items-center py-20">
<div class="animate-spin h-12 w-12 rounded-full border-4 border-transparent border-t-4" style="border-top-color: var(--color-blue);"></div>
</div>`;
}
/**
* Renders an error message box.
* @param {string} message - The error message to display.
* @returns {string} HTML string for the error message.
*/
function renderError(message, title = "An Error Occurred") {
return `
<div
class="p-3 border"
style="background-color: rgba(204, 36, 29, 0.2); border-color: var(--color-red); color: var(--color-red-accent);"
role="alert"
>
<div class="flex items-start">
<i data-lucide="alert-triangle" class="h-5 w-5 mr-3 mt-0.5"></i>
<div>
<p class="font-bold text-sm">${escapeHtml(title)}</p>
<p class="text-xs">${escapeHtml(message)}</p>
</div>
</div>
</div>
`;
}
/**
* Renders a welcome message when the page first loads.
* @returns {string} HTML string for the welcome message.
*/
function renderWelcomeMessage() {
return `
<div class="text-center py-16 px-4 border" style="background-color: var(--color-bg-soft); border-color: var(--color-bg-light-2);">
<i data-lucide="info" class="mx-auto h-10 w-10" style="color: var(--color-gray-accent);"></i>
<h3 class="mt-2 text-lg font-semibold" style="color: var(--color-fg-lightest);">Welcome!</h3>
<p class="mt-1 text-sm" style="color: var(--color-gray);">Enter a usage endpoint, optionally provide an x-api-key, and click "Fetch" to load usage and token usage data.</p>
</div>
`;
}
// --- Data Fetching ---
/**
* Fetches usage data and token usage data from the specified API endpoint.
*/
async function fetchData(page = 1) {
const endpoint = endpointUrlInput.value.trim();
if (!endpoint) {
state.data = null;
state.tokenUsageDailySummary = null;
state.tokenUsageSummary = null;
state.tokenUsageEventsPage = null;
state.error = "Endpoint URL cannot be empty.";
state.tokenUsageDailyError =
"Token usage requires a valid endpoint URL.";
state.tokenUsageSummaryError =
"Token usage requires a valid endpoint URL.";
state.tokenUsageEventsError = null;
render();
return;
}
const period = getSelectedPeriod();
state.isLoading = true;
state.error = null;
state.tokenUsageDailyError = null;
state.tokenUsageSummaryError = null;
state.tokenUsageEventsError = null;
render();
try {
const usageUrl = resolveUsageUrl(endpoint);
const [usageResult, summaryResult, dailyResult, eventsResult] =
await Promise.allSettled([
fetchJson(usageUrl),
fetchJson(buildTokenUsageSummaryUrl(usageUrl, period)),
fetchJson(buildTokenUsageDailyUrl(usageUrl, period)),
fetchJson(buildTokenUsageEventsUrl(usageUrl, period, page)),
]);
if (usageResult.status === "fulfilled") {
state.data = usageResult.value;
} else {
state.data = null;
state.error = getErrorMessage(usageResult.reason);
}
if (summaryResult.status === "fulfilled") {
state.tokenUsageSummary = summaryResult.value;
} else if (
isMissingTokenUsageEndpointError(summaryResult.reason)
) {
state.tokenUsageSummary = null;
state.tokenUsageSummaryError = null;
} else {
state.tokenUsageSummary = null;
state.tokenUsageSummaryError = getErrorMessage(
summaryResult.reason
);
}
if (dailyResult.status === "fulfilled") {
state.tokenUsageDailySummary = dailyResult.value;
} else if (
isMissingTokenUsageEndpointError(dailyResult.reason)
) {
state.tokenUsageDailySummary = null;
state.tokenUsageDailyError = null;
} else {
state.tokenUsageDailySummary = null;
state.tokenUsageDailyError = getErrorMessage(
dailyResult.reason
);
}
if (eventsResult.status === "fulfilled") {
state.tokenUsageEventsPage = eventsResult.value;
} else if (
isMissingTokenUsageEndpointError(eventsResult.reason)
) {
state.tokenUsageEventsPage = null;
state.tokenUsageEventsError = null;
} else {
state.tokenUsageEventsPage = null;
state.tokenUsageEventsError = getErrorMessage(
eventsResult.reason
);
}
} catch (error) {
console.error("Fetch error:", error);
state.data = null;
state.tokenUsageDailySummary = null;
state.tokenUsageSummary = null;
state.tokenUsageEventsPage = null;
state.error = getErrorMessage(error);
state.tokenUsageDailyError = getErrorMessage(error);
state.tokenUsageSummaryError = getErrorMessage(error);
state.tokenUsageEventsError = getErrorMessage(error);
} finally {
state.isLoading = false;
render();
}
}
async function fetchTokenUsageSummaryAndEvents(page = 1) {
const endpoint = endpointUrlInput.value.trim();
if (!endpoint) {
state.tokenUsageDailySummary = null;
state.tokenUsageSummary = null;
state.tokenUsageEventsPage = null;
state.tokenUsageDailyError =
"Token usage requires a valid endpoint URL.";
state.tokenUsageSummaryError =
"Token usage requires a valid endpoint URL.";
state.tokenUsageEventsError = null;
render();
return;
}
const period = getSelectedPeriod();
state.isTokenUsageLoading = true;
state.tokenUsageDailySummary = null;
state.tokenUsageSummary = null;
state.tokenUsageEventsPage = null;
state.tokenUsageDailyError = null;
state.tokenUsageSummaryError = null;
state.tokenUsageEventsError = null;
render();
try {
const usageUrl = resolveUsageUrl(endpoint);
const [summaryResult, dailyResult, eventsResult] =
await Promise.allSettled([
fetchJson(buildTokenUsageSummaryUrl(usageUrl, period)),
fetchJson(buildTokenUsageDailyUrl(usageUrl, period)),
fetchJson(buildTokenUsageEventsUrl(usageUrl, period, page)),
]);
if (summaryResult.status === "fulfilled") {
state.tokenUsageSummary = summaryResult.value;
} else if (
isMissingTokenUsageEndpointError(summaryResult.reason)
) {
state.tokenUsageSummary = null;
state.tokenUsageSummaryError = null;
} else {
state.tokenUsageSummary = null;
state.tokenUsageSummaryError = getErrorMessage(
summaryResult.reason
);
}
if (dailyResult.status === "fulfilled") {
state.tokenUsageDailySummary = dailyResult.value;
} else if (
isMissingTokenUsageEndpointError(dailyResult.reason)
) {
state.tokenUsageDailySummary = null;
state.tokenUsageDailyError = null;
} else {
state.tokenUsageDailySummary = null;
state.tokenUsageDailyError = getErrorMessage(
dailyResult.reason
);
}
if (eventsResult.status === "fulfilled") {
state.tokenUsageEventsPage = eventsResult.value;
} else if (
isMissingTokenUsageEndpointError(eventsResult.reason)
) {
state.tokenUsageEventsPage = null;
state.tokenUsageEventsError = null;
} else {
state.tokenUsageEventsPage = null;
state.tokenUsageEventsError = getErrorMessage(
eventsResult.reason
);
}
} catch (error) {
console.error("Token usage fetch error:", error);
state.tokenUsageDailySummary = null;
state.tokenUsageSummary = null;
state.tokenUsageEventsPage = null;
state.tokenUsageDailyError = getErrorMessage(error);
state.tokenUsageSummaryError = getErrorMessage(error);
state.tokenUsageEventsError = getErrorMessage(error);
} finally {
state.isTokenUsageLoading = false;
render();
}
}
async function fetchTokenUsageEventsPage(page) {
const endpoint = endpointUrlInput.value.trim();
if (!endpoint) {
state.tokenUsageEventsError =
"Token usage requires a valid endpoint URL.";
render();
return;
}
state.isEventsLoading = true;
state.tokenUsageEventsError = null;
render();
try {
const usageUrl = resolveUsageUrl(endpoint);
state.tokenUsageEventsPage = await fetchJson(
buildTokenUsageEventsUrl(usageUrl, getSelectedPeriod(), page)
);
} catch (error) {
if (isMissingTokenUsageEndpointError(error)) {
state.tokenUsageEventsPage = null;
state.tokenUsageEventsError = null;
} else {
console.error("Token usage events fetch error:", error);
state.tokenUsageEventsError = getErrorMessage(error);
}
} finally {
state.isEventsLoading = false;
render();
}
}
// --- Event Handlers & Initialization ---
/**
* Handles the form submission to trigger a data fetch.
* @param {Event} event - The form submission event.
*/
function handleFormSubmit(event) {
event.preventDefault();
storeApiKey(apiKeyInput.value);
syncUrlState();
void fetchData();
}
function handlePeriodChange() {
syncUrlState();
void fetchTokenUsageSummaryAndEvents(1);
}
function handleContentAreaClick(event) {
if (!(event.target instanceof Element)) {
return;
}
const trendMetric = event.target.closest("[data-trend-metric]");
if (trendMetric) {
state.tokenUsageTrendMetric =
trendMetric.getAttribute("data-trend-metric")
|| ALL_METRICS_VALUE;
render();
return;
}
const trendPoint = event.target.closest("[data-trend-day-index]");
if (trendPoint) {
const index = Number.parseInt(
trendPoint.getAttribute("data-trend-day-index") || "",
10
);
if (Number.isFinite(index) && index >= 0) {
state.tokenUsageTrendDayIndex = index;
render();
}
return;
}
const actionButton = event.target.closest("[data-page-action]");
if (!actionButton || !state.tokenUsageEventsPage || state.isEventsLoading) {
return;
}
const action = actionButton.getAttribute("data-page-action");
if (
action === "previous"
&& state.tokenUsageEventsPage.page > 1
) {
void fetchTokenUsageEventsPage(state.tokenUsageEventsPage.page - 1);
}
if (
action === "next"
&& state.tokenUsageEventsPage.page
< state.tokenUsageEventsPage.total_pages
) {
void fetchTokenUsageEventsPage(state.tokenUsageEventsPage.page + 1);
}
}
function handleContentAreaChange(event) {
if (!(event.target instanceof HTMLSelectElement)) {
return;
}
if (!event.target.matches("[data-token-usage-trend-model]")) {
return;
}
state.tokenUsageTrendModel =
event.target.value || ALL_MODELS_VALUE;
render();
}
/**
* Initializes the application.
*/
function init() {
endpointForm.addEventListener("submit", handleFormSubmit);
tokenUsagePeriodSelect.addEventListener("change", handlePeriodChange);
apiKeyInput.addEventListener("input", () => {
storeApiKey(apiKeyInput.value);
});
contentArea.addEventListener("click", handleContentAreaClick);
contentArea.addEventListener("change", handleContentAreaChange);
apiKeyInput.value = loadStoredApiKey();
const urlParams = new URLSearchParams(window.location.search);
const periodFromUrl = normalizePeriod(urlParams.get("period"));
const endpointFromUrl = urlParams.get("endpoint");
state.tokenUsagePeriod = periodFromUrl;
tokenUsagePeriodSelect.value = periodFromUrl;
if (endpointFromUrl) {
endpointUrlInput.value = endpointFromUrl;
void fetchData();
} else {
endpointUrlInput.value = DEFAULT_ENDPOINT;
render();
}
}
// Start the app
init();
});
</script>
<footer
class="text-center py-4 text-xs"
style="color: var(--color-gray-accent)"
>
<p>
Vibe coded by
<a
href="https://gemini.google.com"
target="_blank"
rel="noopener noreferrer"
class="underline transition-colors"
style="color: var(--color-fg-dark)"
onmouseover="this.style.color='var(--color-fg-light)'"
onmouseout="this.style.color='var(--color-fg-dark)'"
>
Gemini
</a>
- Based on
<a
href="https://github.com/uheej0625/copilot-usage-viewer"
target="_blank"
rel="noopener noreferrer"
class="underline transition-colors"
style="color: var(--color-fg-dark)"
onmouseover="this.style.color='var(--color-fg-light)'"
onmouseout="this.style.color='var(--color-fg-dark)'"
>
copilot-usage-viewer</a
>
</p>
</footer>
</body>
</html>