copilot-api / pages /index.html
imseldrith's picture
Initial upload from Colab
9e27976 verified
<!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);
}
</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="flex flex-col sm:flex-row items-center gap-3"
>
<label
for="endpoint-url"
class="font-semibold whitespace-nowrap text-sm"
style="color: var(--color-fg-lightest)"
>API Endpoint URL</label
>
<input
type="text"
id="endpoint-url"
class="w-full px-3 py-1.5 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"
/>
<button
id="fetch-button"
type="submit"
class="w-full sm:w-auto font-bold py-1.5 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>
</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 contentArea = document.getElementById("content-area");
const fetchButton = document.getElementById("fetch-button");
// 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";
// --- State Management ---
const state = {
isLoading: false,
error: null,
data: null,
};
// --- Rendering Logic ---
/**
* Safely calls lucide.createIcons() if the library is available.
*/
function createIcons() {
if (typeof lucide !== "undefined") {
lucide.createIcons();
}
}
/**
* Renders the entire UI based on the current state.
*/
function render() {
if (state.isLoading) {
contentArea.innerHTML = renderSpinner();
return;
}
if (state.error) {
contentArea.innerHTML = renderError(state.error);
} else if (state.data) {
contentArea.innerHTML = `
${renderUsageQuotas(state.data.quota_snapshots)}
${renderDetailedData(state.data)}
`;
} else {
contentArea.innerHTML = renderWelcomeMessage();
}
// 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);">${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);">${JSON.stringify(obj)}</span>`;
}
return (
'<div class="pl-4">' +
Object.entries(obj)
.map(([key, value]) => {
const formattedKey = 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)"}">${value}</span>`;
} else {
displayValue = `<span style="color: var(--color-blue-accent);">${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> Detailed Information
</h2>
<div class="border p-4 relative font-mono text-xs" style="background-color: var(--color-bg-darkest); border-color: var(--color-bg-light-2);">
${formattedDetails}
</div>
</section>
`;
}
/**
* 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) {
const container = document.createElement("div");
container.className = "p-3 border";
container.style.backgroundColor = "rgba(204, 36, 29, 0.2)";
container.style.borderColor = "var(--color-red)";
container.style.color = "var(--color-red-accent)";
container.setAttribute("role", "alert");
container.innerHTML = `
<div class="flex items-center">
<i data-lucide="alert-triangle" class="h-5 w-5 mr-3"></i>
<div>
<p class="font-bold text-sm">An Error Occurred</p>
<p class="text-xs">${message}</p>
</div>
</div>
`;
// Must create icons *after* innerHTML is set
setTimeout(
() =>
lucide.createIcons({
nodes: [container.querySelector("[data-lucide]")],
}),
0
);
return container.outerHTML;
}
/**
* 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 an API endpoint URL and click "Fetch" to see usage data.</p>
</div>
`;
}
// --- Data Fetching ---
/**
* Fetches data from the specified API endpoint.
*/
async function fetchData() {
const url = endpointUrlInput.value.trim();
if (!url) {
state.error = "Endpoint URL cannot be empty.";
state.isLoading = false;
render();
return;
}
state.isLoading = true;
state.error = null;
render();
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`Request failed with status ${response.status}: ${response.statusText}`
);
}
const jsonData = await response.json();
state.data = jsonData;
} catch (error) {
console.error("Fetch error:", error);
state.data = null;
state.error = error.message;
} finally {
state.isLoading = 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();
const url = endpointUrlInput.value.trim();
// Update URL query parameter, catching potential security errors in sandboxed environments
try {
const currentUrl = new URL(window.location);
currentUrl.searchParams.set("endpoint", url);
window.history.pushState({}, "", currentUrl);
} catch (e) {
console.warn("Could not update URL: ", e.message);
}
fetchData();
}
/**
* Initializes the application.
*/
function init() {
endpointForm.addEventListener("submit", handleFormSubmit);
// Get endpoint from URL param on load
const urlParams = new URLSearchParams(window.location.search);
const endpointFromUrl = urlParams.get("endpoint");
if (endpointFromUrl) {
endpointUrlInput.value = endpointFromUrl;
fetchData();
} else {
endpointUrlInput.value = DEFAULT_ENDPOINT;
render(); // Render initial welcome message
}
}
// 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>