@jeffreycao/copilot-api 2.7.39 → 2.7.41
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/auth-CelraKXD.js +1 -0
- package/dist/{auth-DBOz2FcU.js → auth-jRuZhT_b.js} +2 -2
- package/dist/{auth-DBOz2FcU.js.map → auth-jRuZhT_b.js.map} +1 -1
- package/dist/{copilot-runtime-0CujVONa.js → copilot-runtime-CjQPxPeV.js} +2 -2
- package/dist/{copilot-runtime-0CujVONa.js.map → copilot-runtime-CjQPxPeV.js.map} +1 -1
- package/dist/main.js +2 -2
- package/dist/{server-Jvc5DYJV.js → server-h60svXN0.js} +333 -10
- package/dist/server-h60svXN0.js.map +1 -0
- package/dist/{start-BQBvYKD-.js → start-C6fQ39n5.js} +5 -5
- package/dist/{start-BQBvYKD-.js.map → start-C6fQ39n5.js.map} +1 -1
- package/dist/{token-_1CBLbqH.js → token-CZmr-geT.js} +13 -8
- package/dist/token-CZmr-geT.js.map +1 -0
- package/package.json +1 -1
- package/pages/index.html +308 -326
- package/dist/auth-IiEbOqcu.js +0 -1
- package/dist/server-Jvc5DYJV.js.map +0 -1
- package/dist/token-_1CBLbqH.js.map +0 -1
package/pages/index.html
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
<style>
|
|
20
20
|
:root {
|
|
21
|
+
--page-zoom: 0.9;
|
|
22
|
+
zoom: var(--page-zoom);
|
|
23
|
+
|
|
21
24
|
--color-red: #dc2626;
|
|
22
25
|
--color-green: #16a34a;
|
|
23
26
|
--color-yellow: #d97706;
|
|
@@ -62,7 +65,7 @@
|
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
.app-shell {
|
|
65
|
-
min-height: 100vh;
|
|
68
|
+
min-height: calc(100vh / var(--page-zoom));
|
|
66
69
|
background: linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%);
|
|
67
70
|
}
|
|
68
71
|
|
|
@@ -114,6 +117,7 @@
|
|
|
114
117
|
|
|
115
118
|
.page-tabs {
|
|
116
119
|
display: inline-flex;
|
|
120
|
+
max-width: 100%;
|
|
117
121
|
gap: 4px;
|
|
118
122
|
margin-bottom: 14px;
|
|
119
123
|
padding: 4px;
|
|
@@ -390,6 +394,10 @@
|
|
|
390
394
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
391
395
|
}
|
|
392
396
|
|
|
397
|
+
.subscription-grid {
|
|
398
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
|
|
399
|
+
}
|
|
400
|
+
|
|
393
401
|
.metric-grid-tight .metric-label {
|
|
394
402
|
white-space: nowrap;
|
|
395
403
|
}
|
|
@@ -531,6 +539,8 @@
|
|
|
531
539
|
}
|
|
532
540
|
|
|
533
541
|
@media (max-width: 639px) {
|
|
542
|
+
.page-tabs { display: flex; width: 100%; gap: 2px; }
|
|
543
|
+
.page-tab { flex: 1 1 auto; min-width: 0; justify-content: center; gap: 5px; padding: 8px; font-size: 12px; white-space: nowrap; }
|
|
534
544
|
.main-shell {
|
|
535
545
|
padding: 12px 14px 18px;
|
|
536
546
|
}
|
|
@@ -556,6 +566,82 @@
|
|
|
556
566
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
557
567
|
}
|
|
558
568
|
}
|
|
569
|
+
|
|
570
|
+
/* Subscription allowance workspace. Provider SVGs are bundled from models.dev. */
|
|
571
|
+
.subscriptions-page {
|
|
572
|
+
--quota-text: var(--color-ink, var(--color-fg-lightest));
|
|
573
|
+
--quota-muted: var(--color-ink-soft, var(--color-gray));
|
|
574
|
+
--quota-faint: var(--color-ink-faint, var(--color-fg-darker));
|
|
575
|
+
--quota-line: var(--color-line, var(--color-bg-light-2));
|
|
576
|
+
--quota-soft: var(--color-sunken, var(--color-bg-light-1));
|
|
577
|
+
--quota-surface: var(--color-surface, var(--color-bg));
|
|
578
|
+
padding: 0;
|
|
579
|
+
color: var(--quota-text);
|
|
580
|
+
}
|
|
581
|
+
.subscriptions-page-heading { display: flex; justify-content: space-between; align-items: center; gap: 20px; margin-bottom: 26px; }
|
|
582
|
+
.subscriptions-eyebrow { margin: 0 0 7px; font-size: 10px; font-weight: 650; letter-spacing: .16em; color: var(--quota-faint); }
|
|
583
|
+
.subscriptions-page-heading h2 { margin: 0; font-size: 25px; font-weight: 650; letter-spacing: -.04em; line-height: 1.25; }
|
|
584
|
+
.subscriptions-description { margin: 9px 0 0; font-size: 13px; line-height: 1.6; color: var(--quota-muted); }
|
|
585
|
+
.subscriptions-count { display: inline-flex; align-items: center; gap: 7px; flex-shrink: 0; padding: 7px 11px; border: 1px solid var(--quota-line); border-radius: 99px; font-size: 11px; color: var(--quota-muted); background: var(--quota-surface); }
|
|
586
|
+
.subscriptions-count > span { width: 5px; height: 5px; border-radius: 50%; background: #22a06b; }
|
|
587
|
+
.subscriptions-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px; align-items: start; }
|
|
588
|
+
.quota-provider-card { --quota-brand: #7c5ac7; position: relative; min-width: 0; padding: 22px 20px 14px; overflow: hidden; border: 1px solid var(--quota-line); border-radius: 18px; background: var(--quota-surface); box-shadow: 0 3px 12px rgb(0 0 0 / 2%); }
|
|
589
|
+
.quota-provider-card[data-provider="codex"] { --quota-brand: #179876; }
|
|
590
|
+
.quota-provider-card[data-provider="xai"] { --quota-brand: #386fce; }
|
|
591
|
+
.dark .quota-provider-card { --quota-brand: #b095ef; }
|
|
592
|
+
.dark .quota-provider-card[data-provider="codex"] { --quota-brand: #50bd9b; }
|
|
593
|
+
.dark .quota-provider-card[data-provider="xai"] { --quota-brand: #7aa8f1; }
|
|
594
|
+
.quota-card-header { display: flex; align-items: center; gap: 11px; }
|
|
595
|
+
.quota-provider-logo { display: grid; place-items: center; width: 44px; height: 44px; flex-shrink: 0; border: 1px solid var(--quota-line); border-radius: 13px; background: var(--quota-soft); }
|
|
596
|
+
.quota-provider-logo img { width: 32px; height: 32px; object-fit: contain; }
|
|
597
|
+
.quota-card-header h3 { margin: 0; font-size: 16px; font-weight: 650; letter-spacing: -.025em; line-height: 1.3; }
|
|
598
|
+
.quota-provider-product { margin: 3px 0 0; font-size: 11px; color: var(--quota-faint); }
|
|
599
|
+
.quota-refresh { display: grid; place-items: center; flex-shrink: 0; width: 30px; height: 30px; padding: 0; margin-left: auto; border: 1px solid var(--quota-line); border-radius: 9px; color: var(--quota-muted); background: var(--quota-surface); cursor: pointer; }
|
|
600
|
+
.quota-refresh:hover { background: var(--quota-soft); color: var(--quota-text); }
|
|
601
|
+
.quota-refresh:disabled { opacity: .45; cursor: default; }
|
|
602
|
+
.quota-card-meta { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 17px 0 0; }
|
|
603
|
+
.quota-plan { min-width: 0; max-width: 65%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 4px 8px; border-radius: 6px; background: var(--quota-soft); color: var(--quota-muted); font-size: 11px; font-weight: 500; }
|
|
604
|
+
.quota-status { display: inline-flex; align-items: center; gap: 5px; flex-shrink: 0; font-size: 10px; color: var(--quota-faint); }
|
|
605
|
+
.quota-status > span { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
|
|
606
|
+
.quota-status[data-state="ready"] { color: var(--quota-muted); }
|
|
607
|
+
.quota-status[data-state="warning"] { color: #d58a25; }
|
|
608
|
+
.quota-summary { margin: 18px 0 22px; padding: 18px; border-radius: 13px; background: color-mix(in srgb, var(--quota-brand) 4%, var(--quota-soft)); }
|
|
609
|
+
.quota-summary-label { margin: 0 0 9px; font-size: 11px; font-weight: 500; color: var(--quota-muted); }
|
|
610
|
+
.quota-summary-value { font-size: 40px; line-height: 1.05; font-weight: 600; letter-spacing: -.055em; font-variant-numeric: tabular-nums; }
|
|
611
|
+
.quota-summary-context { margin: 9px 0 0; font-size: 11px; color: var(--quota-faint); }
|
|
612
|
+
.quota-details { font-size: 12px; }
|
|
613
|
+
.quota-window + .quota-window { margin-top: 18px; }
|
|
614
|
+
.quota-window-heading { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 7px; font-size: 12px; }
|
|
615
|
+
.quota-window-heading > :first-child { color: var(--quota-muted); }
|
|
616
|
+
.quota-window-heading > :last-child { font-weight: 550; color: var(--quota-text); }
|
|
617
|
+
.quota-progress { height: 6px; overflow: hidden; border-radius: 99px; background: var(--quota-soft); }
|
|
618
|
+
.quota-progress-fill { height: 100%; border-radius: inherit; background: var(--quota-brand); transition: width .25s ease; }
|
|
619
|
+
.quota-window[data-warning="true"] .quota-progress-fill { background: #d58a25; }
|
|
620
|
+
.quota-window-note { margin: 6px 0 0; font-size: 11px; color: var(--quota-faint); line-height: 1.5; }
|
|
621
|
+
.quota-balance { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--quota-line); font-size: 12px; color: var(--quota-muted); }
|
|
622
|
+
.quota-balance strong { color: var(--quota-text); font-weight: 600; }
|
|
623
|
+
.quota-notice { margin: 0 0 15px; padding: 10px 12px; border-radius: 9px; background: rgb(213 138 37 / 8%); color: #b67822; font-size: 12px; line-height: 1.6; }
|
|
624
|
+
.quota-card-footer { margin-top: 22px; padding-top: 12px; border-top: 1px solid var(--quota-line); color: var(--quota-faint); font-size: 10px; }
|
|
625
|
+
.quota-card-message { display: flex; align-items: center; min-height: 180px; margin: 0; font-size: 12px; line-height: 1.8; color: var(--quota-muted); }
|
|
626
|
+
.quota-card-message[role="alert"] { color: #b67822; }
|
|
627
|
+
.quota-skeleton { display: flex; flex-direction: column; justify-content: center; gap: 16px; min-height: 230px; animation: quota-pulse 1.5s ease-in-out infinite; }
|
|
628
|
+
.quota-skeleton > div { height: 9px; border-radius: 6px; background: var(--quota-soft); }
|
|
629
|
+
.quota-skeleton > div:first-child { width: 55%; height: 42px; margin-bottom: 22px; }
|
|
630
|
+
.quota-skeleton > div:last-child { width: 75%; }
|
|
631
|
+
@keyframes quota-pulse { 50% { opacity: .45; } }
|
|
632
|
+
.subscriptions-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; min-height: 260px; padding: 30px; text-align: center; border: 1px dashed var(--quota-line); border-radius: 18px; color: var(--quota-muted); font-size: 13px; }
|
|
633
|
+
.subscriptions-empty h3 { margin: 0; font-size: 17px; font-weight: 600; color: var(--quota-text); }
|
|
634
|
+
.subscriptions-empty p { max-width: 360px; margin: 0; line-height: 1.7; }
|
|
635
|
+
.subscriptions-action { margin-top: 5px; border: 0; border-radius: 9px; padding: 8px 14px; background: var(--quota-text); color: var(--quota-surface); cursor: pointer; font-size: 12px; }
|
|
636
|
+
@media (max-width: 899px) {
|
|
637
|
+
.subscriptions-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
638
|
+
}
|
|
639
|
+
@media (max-width: 639px) {
|
|
640
|
+
.subscriptions-page-heading { align-items: flex-start; gap: 12px; margin-bottom: 20px; flex-direction: column; }
|
|
641
|
+
.subscriptions-page-heading h2 { font-size: 23px; }
|
|
642
|
+
.subscriptions-grid { grid-template-columns: minmax(0, 1fr); }
|
|
643
|
+
.quota-provider-card { padding: 20px 18px 14px; }
|
|
644
|
+
}
|
|
559
645
|
</style>
|
|
560
646
|
</head>
|
|
561
647
|
<body class="antialiased">
|
|
@@ -587,6 +673,10 @@
|
|
|
587
673
|
<i data-lucide="bar-chart-3" class="h-4 w-4" aria-hidden="true"></i>
|
|
588
674
|
Token Usage
|
|
589
675
|
</button>
|
|
676
|
+
<button id="subscriptions-tab" type="button" class="page-tab" role="tab" data-tab="subscriptions" aria-selected="false" aria-controls="content-area" tabindex="-1">
|
|
677
|
+
<i data-lucide="gauge" class="h-4 w-4" aria-hidden="true"></i>
|
|
678
|
+
Subscriptions
|
|
679
|
+
</button>
|
|
590
680
|
<button
|
|
591
681
|
id="models-tab"
|
|
592
682
|
type="button"
|
|
@@ -668,6 +758,7 @@
|
|
|
668
758
|
const pageTabs = document.getElementById("page-tabs");
|
|
669
759
|
const tabButtons = [
|
|
670
760
|
document.getElementById("usage-tab"),
|
|
761
|
+
document.getElementById("subscriptions-tab"),
|
|
671
762
|
document.getElementById("models-tab"),
|
|
672
763
|
];
|
|
673
764
|
const periodField = document.getElementById("token-usage-period-field");
|
|
@@ -682,6 +773,29 @@
|
|
|
682
773
|
fetchButton.style.backgroundColor = "var(--color-blue)";
|
|
683
774
|
});
|
|
684
775
|
|
|
776
|
+
// models.dev provider SVGs are bundled to work without external requests.
|
|
777
|
+
const SUBSCRIPTION_PROVIDERS = {
|
|
778
|
+
"github-copilot": {
|
|
779
|
+
"name": "GitHub Copilot",
|
|
780
|
+
"product": "GitHub",
|
|
781
|
+
"logo": "data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2040%2040%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M35.3993%2026.4544C34.2871%2028.3855%2027.8314%2032.9425%2020%2032.9425C12.1686%2032.9425%205.71288%2028.3855%204.60075%2026.4544C4.5427%2026.3532%204.50826%2026.2401%204.5%2026.1237V22.418C4.51199%2022.3209%204.53496%2022.2256%204.56846%2022.1338C5.04896%2020.9261%206.30833%2019.1733%207.93325%2018.7031C8.14896%2018.149%208.468%2017.3404%208.76508%2016.7437C8.71727%2016.2776%208.69485%2015.8094%208.69792%2015.3409C8.69792%2013.6217%209.06217%2012.113%2010.1601%2010.9906C10.6729%2010.4662%2011.3097%2010.0644%2012.064%209.76091C13.871%208.29357%2016.4453%207.05745%2019.9716%207.05745C23.4978%207.05745%2026.129%208.29357%2027.936%209.76091C28.6903%2010.0644%2029.3271%2010.4662%2029.8399%2010.9906C30.9378%2012.113%2031.3021%2013.6217%2031.3021%2015.3409C31.3021%2015.8162%2031.284%2016.2877%2031.2349%2016.7437C31.532%2017.3404%2031.851%2018.149%2032.0667%2018.7031C33.6917%2019.1733%2034.951%2020.9261%2035.4315%2022.1338C35.4677%2022.2248%2035.4908%2022.3205%2035.5%2022.418V26.1237C35.4917%2026.2401%2035.4573%2026.3532%2035.3993%2026.4544ZM20.2222%2018.7147H19.7778C19.6422%2018.945%2019.4889%2019.1644%2019.3193%2019.3709C18.3247%2020.5941%2016.8419%2021.2981%2014.7881%2021.2981C12.56%2021.2981%2010.9273%2020.8344%209.90304%2019.6719C9.86495%2019.6283%209.82833%2019.5835%209.79325%2019.5375L9.66667%2019.6719V28.1775C11.5202%2029.1837%2015.4972%2030.992%2020%2030.992C24.5028%2030.992%2028.4798%2029.1837%2030.3333%2028.1775V19.6719L30.2067%2019.5375C30.2067%2019.5375%2030.1641%2019.5957%2030.097%2019.6719C29.0727%2020.8344%2027.44%2021.2981%2025.2119%2021.2981C23.1581%2021.2981%2021.6753%2020.5941%2020.6807%2019.3709C20.5111%2019.1644%2020.3578%2018.945%2020.2222%2018.7147ZM23.2292%2022.9127C23.9383%2022.9127%2024.5208%2023.4952%2024.5208%2024.2043V26.7877C24.5208%2027.4968%2023.9383%2028.0793%2023.2292%2028.0793C22.52%2028.0793%2021.9375%2027.4968%2021.9375%2026.7877V24.2043C21.9375%2023.4952%2022.52%2022.9127%2023.2292%2022.9127ZM16.7708%2022.9127C17.48%2022.9127%2018.0625%2023.4952%2018.0625%2024.2043V26.7877C18.0625%2027.4968%2017.48%2028.0793%2016.7708%2028.0793C16.0617%2028.0793%2015.4792%2027.4968%2015.4792%2026.7877V24.2043C15.4792%2023.4952%2016.0617%2022.9127%2016.7708%2022.9127ZM21.0501%2014.9237C21.2258%2016.289%2021.5707%2017.3947%2022.1842%2018.149C22.7551%2018.8517%2023.649%2019.3606%2025.2119%2019.3606C27.2437%2019.3606%2028.1724%2018.9253%2028.6438%2018.3905C29.1398%2017.8287%2029.3646%2016.9051%2029.3646%2015.3409C29.3646%2013.8684%2029.0507%2012.9552%2028.454%2012.3455C27.8378%2011.7152%2026.7502%2011.2321%2024.8063%2011.0216C22.8856%2010.8136%2021.975%2011.1998%2021.5345%2011.7049C21.187%2012.1014%2020.97%2012.7485%2020.9688%2013.7431V13.7702C20.9688%2014.1121%2020.9959%2014.4966%2021.0501%2014.9237ZM18.9499%2014.9237C19.0041%2014.4966%2019.0312%2014.1117%2019.0312%2013.7689V13.7431C19.03%2012.7485%2018.813%2012.1014%2018.4655%2011.7049C18.025%2011.1998%2017.1144%2010.8136%2015.1937%2011.0216C13.2498%2011.2321%2012.1622%2011.7152%2011.546%2012.3455C10.9493%2012.9552%2010.6354%2013.8684%2010.6354%2015.3409C10.6354%2016.9051%2010.8615%2017.8287%2011.3562%2018.3905C11.8276%2018.9253%2012.7563%2019.3606%2014.7881%2019.3606C16.351%2019.3606%2017.2449%2018.8517%2017.8158%2018.149C18.4293%2017.3947%2018.7742%2016.289%2018.9499%2014.9237Z%22%20fill%3D%22currentColor%22%2F%3E%0A%3C%2Fsvg%3E%0A"
|
|
782
|
+
},
|
|
783
|
+
"codex": {
|
|
784
|
+
"name": "Codex",
|
|
785
|
+
"product": "OpenAI",
|
|
786
|
+
"logo": "data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2040%2040%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M32.8377%2017.282C33.2127%2016.25%2033.3072%2015.218%2033.2127%2014.1875C33.1197%2013.1571%2032.7447%2012.1251%2032.2752%2011.1876C31.4322%209.78209%2030.2127%208.6571%2028.8072%208.0001C27.3072%207.34461%2025.7127%207.15711%2024.1197%207.53211C23.3698%206.78212%2022.5253%206.12512%2021.5878%205.65713C20.6503%205.18913%2019.5253%205.00013%2018.4948%205.00013C16.8851%204.99074%2015.3125%205.48246%2013.9948%206.40712C12.6824%207.34311%2011.7449%208.6571%2011.2754%2010.1571C10.1504%2010.4376%209.21289%2010.9071%208.27539%2011.4696C7.4324%2012.1251%206.77541%2012.9696%206.21291%2013.8126C5.36992%2015.2195%205.08792%2016.8125%205.27542%2018.407C5.46399%2019.9968%206.11605%2021.496%207.1504%2022.718C6.79608%2023.7086%206.66795%2024.7659%206.77541%2025.8124C6.86991%2026.8444%207.2449%2027.8749%207.7129%2028.8124C8.55739%2030.2194%209.77538%2031.3444%2011.1824%2031.9999C12.6824%2032.6569%2014.2753%2032.8444%2015.8698%2032.4694C16.6198%2033.2194%2017.4628%2033.8749%2018.4003%2034.3444C19.3378%2034.8139%2020.4628%2034.9999%2021.4948%2034.9999C23.1043%2035.0097%2024.6769%2034.5185%2025.9947%2033.5944C27.3072%2032.6569%2028.2447%2031.3444%2028.7127%2029.8444C29.7719%2029.6432%2030.7682%2029.1934%2031.6197%2028.5319C32.4627%2027.8749%2033.2127%2027.1249%2033.6822%2026.1874C34.5251%2024.7819%2034.8071%2023.1875%2034.6196%2021.5945C34.4322%2020%2033.8697%2018.5015%2032.8377%2017.282ZM21.5878%2033.0304C20.0878%2033.0304%2018.9628%2032.5609%2017.9323%2031.7179C17.9323%2031.7179%2018.0253%2031.6234%2018.1198%2031.6234L24.1197%2028.1554C24.2862%2028.0803%2024.4196%2027.9469%2024.4947%2027.7804C24.5698%2027.636%2024.6021%2027.4731%2024.5877%2027.3109V18.875L27.1197%2020.375V27.3124C27.1455%2028.0547%2027.0215%2028.7945%2026.755%2029.4878C26.4885%2030.181%2026.085%2030.8134%2025.5687%2031.3473C25.0523%2031.8811%2024.4337%2032.3054%2023.7497%2032.5949C23.0658%2032.8843%2022.3305%2033.0314%2021.5878%2033.0304ZM9.49488%2027.8749C8.83789%2026.7499%208.55739%2025.4374%208.83789%2024.125C8.83789%2024.125%208.93239%2024.2195%209.02539%2024.2195L15.0253%2027.6874C15.1693%2027.7638%2015.3325%2027.7966%2015.4948%2027.7819C15.6823%2027.7819%2015.8698%2027.7819%2015.9628%2027.6874L23.2753%2023.4695V26.3749L17.1823%2029.9374C16.5506%2030.3042%2015.8527%2030.5427%2015.1287%2030.6393C14.4046%2030.7358%2013.6686%2030.6884%2012.9629%2030.4999C11.4629%2030.1249%2010.2449%2029.1874%209.49488%2027.8749ZM7.9004%2014.8445C8.56239%2013.7234%209.58826%2012.8627%2010.8074%2012.4056V19.532C10.8074%2019.718%2010.8074%2019.907%2010.9004%2020C10.9755%2020.1665%2011.1089%2020.2998%2011.2754%2020.375L18.5878%2024.5944L16.0573%2026.0944L10.0574%2022.625C9.41842%2022.2639%208.85742%2021.7797%208.40684%2021.2004C7.95627%2020.6211%207.62506%2019.9582%207.4324%2019.25C7.05741%2017.8445%207.1504%2016.157%207.9004%2014.8445ZM28.6197%2019.625L21.3073%2015.407L23.8377%2013.9071L29.8377%2017.375C30.7752%2017.9375%2031.5252%2018.6875%2031.9947%2019.625C32.4642%2020.5625%2032.7447%2021.5945%2032.6502%2022.7195C32.5603%2023.7755%2032.1699%2024.7837%2031.5252%2025.6249C30.8697%2026.4694%2030.0252%2027.1249%2028.9947%2027.4999V20.375C28.9947%2020.1875%2028.9947%2020%2028.9002%2019.907C28.9002%2019.907%2028.8072%2019.718%2028.6197%2019.625ZM31.1502%2015.875C31.1502%2015.875%2031.0572%2015.782%2030.9627%2015.782L24.9627%2012.3126C24.7752%2012.2196%2024.6822%2012.2196%2024.4947%2012.2196C24.3072%2012.2196%2024.1197%2012.2196%2024.0252%2012.3126L16.7128%2016.532V13.6251L22.8073%2010.0626C23.7448%209.50009%2024.7752%209.31259%2025.9002%209.31259C26.9322%209.31259%2027.9627%209.68759%2028.9002%2010.3446C29.7447%2011.0001%2030.4947%2011.8446%2030.8697%2012.7821C31.2447%2013.7196%2031.3377%2014.8445%2031.1502%2015.875ZM15.4003%2021.125L12.8699%2019.625V12.5946C12.8699%2011.5626%2013.1503%2010.4376%2013.7128%209.59459C14.2753%208.6571%2015.1198%208.0001%2016.0573%207.53211C17.0127%207.05249%2018.0956%206.88812%2019.1503%207.06261C20.1823%207.15711%2021.2128%207.62511%2022.0573%208.2821C22.0573%208.2821%2021.9628%208.3751%2021.8698%208.3751L15.8698%2011.8446C15.7033%2011.9197%2015.57%2012.0531%2015.4948%2012.2196C15.4003%2012.4071%2015.4003%2012.5001%2015.4003%2012.6876V21.125ZM16.7128%2018.125L19.9948%2016.25L23.2753%2018.125V21.875L19.9948%2023.75L16.7128%2021.875V18.125Z%22%20fill%3D%22currentColor%22%2F%3E%0A%3C%2Fsvg%3E%0A"
|
|
787
|
+
},
|
|
788
|
+
"xai": {
|
|
789
|
+
"name": "xAI",
|
|
790
|
+
"product": "Grok Build",
|
|
791
|
+
"logo": "data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2040%2040%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M12.4579%2015.6036L26.1529%2035H20.0656L6.37059%2015.6036H12.4579ZM12.4524%2026.3764L15.4974%2030.6909L12.4551%2035H6.36377L12.4524%2026.3764ZM33.6365%207.15727V35H28.647V14.2236L33.6365%207.15727ZM33.6365%205L20.0656%2024.2205L17.0206%2019.9073L27.5451%205H33.6365Z%22%20fill%3D%22currentColor%22%2F%3E%0A%3C%2Fsvg%3E%0A"
|
|
792
|
+
},
|
|
793
|
+
"opencode-go": {
|
|
794
|
+
"name": "OpenCode Go",
|
|
795
|
+
"product": "OpenCode",
|
|
796
|
+
"logo": "data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M15.6%2017.4H8.4V10.2H15.6V17.4Z%22%20fill%3D%22currentColor%22%20fill-opacity%3D%220.4%22%2F%3E%0A%3Cpath%20d%3D%22M15.6%206.6H8.4V17.4H15.6V6.6ZM19.2%2021H4.8V3H19.2V21Z%22%20fill%3D%22currentColor%22%2F%3E%0A%3C%2Fsvg%3E%0A"
|
|
797
|
+
}
|
|
798
|
+
};
|
|
685
799
|
const DEFAULT_ENDPOINT = "http://localhost:4141/usage";
|
|
686
800
|
const DEFAULT_TOKEN_USAGE_PERIOD = "today";
|
|
687
801
|
const DEFAULT_TOKEN_USAGE_EVENTS_PAGE_SIZE = 20;
|
|
@@ -722,8 +836,15 @@
|
|
|
722
836
|
isLoading: false,
|
|
723
837
|
isTokenUsageLoading: false,
|
|
724
838
|
isEventsLoading: false,
|
|
725
|
-
|
|
726
|
-
|
|
839
|
+
subscriptionUsage: {},
|
|
840
|
+
subscriptionUsageErrors: {},
|
|
841
|
+
isSubscriptionsLoading: false,
|
|
842
|
+
subscriptionRequestId: 0,
|
|
843
|
+
subscriptionRequests: {},
|
|
844
|
+
subscriptionLoadingProviders: {},
|
|
845
|
+
subscriptionsLoaded: false,
|
|
846
|
+
subscriptionsConnectionKey: null,
|
|
847
|
+
subscriptionPageError: null,
|
|
727
848
|
tokenUsageDailySummary: null,
|
|
728
849
|
tokenUsageSummary: null,
|
|
729
850
|
tokenUsageSessionsPage: null,
|
|
@@ -1049,6 +1170,8 @@
|
|
|
1049
1170
|
const currentUrl = new URL(window.location.href);
|
|
1050
1171
|
const endpoint = endpointUrlInput.value.trim();
|
|
1051
1172
|
const period = getSelectedPeriod();
|
|
1173
|
+
if (state.activeTab === "usage") currentUrl.searchParams.delete("tab");
|
|
1174
|
+
else currentUrl.searchParams.set("tab", state.activeTab);
|
|
1052
1175
|
|
|
1053
1176
|
if (endpoint) {
|
|
1054
1177
|
currentUrl.searchParams.set("endpoint", endpoint);
|
|
@@ -1067,17 +1190,17 @@
|
|
|
1067
1190
|
const isLoading =
|
|
1068
1191
|
state.activeTab === "models"
|
|
1069
1192
|
? state.isModelsLoading
|
|
1070
|
-
: state.isLoading;
|
|
1193
|
+
: state.activeTab === "subscriptions" ? state.isSubscriptionsLoading : state.isLoading;
|
|
1071
1194
|
fetchButton.disabled = isLoading;
|
|
1072
1195
|
fetchButtonLabel.textContent = isLoading
|
|
1073
1196
|
? "Refreshing..."
|
|
1074
|
-
: "Refresh";
|
|
1197
|
+
: state.activeTab === "subscriptions" ? "Refresh all" : "Refresh";
|
|
1075
1198
|
tokenUsagePeriodSelect.disabled =
|
|
1076
1199
|
state.isLoading || state.isTokenUsageLoading;
|
|
1077
|
-
periodField.hidden = state.activeTab
|
|
1200
|
+
periodField.hidden = state.activeTab !== "usage";
|
|
1078
1201
|
endpointForm.classList.toggle(
|
|
1079
1202
|
"models-view",
|
|
1080
|
-
state.activeTab
|
|
1203
|
+
state.activeTab !== "usage"
|
|
1081
1204
|
);
|
|
1082
1205
|
contentArea.setAttribute("aria-labelledby", `${state.activeTab}-tab`);
|
|
1083
1206
|
contentArea.setAttribute("aria-busy", String(isLoading));
|
|
@@ -1239,41 +1362,22 @@
|
|
|
1239
1362
|
return;
|
|
1240
1363
|
}
|
|
1241
1364
|
|
|
1365
|
+
if (state.activeTab === "subscriptions") {
|
|
1366
|
+
contentArea.innerHTML = renderSubscriptionUsageSection();
|
|
1367
|
+
createIcons();
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1242
1371
|
if (state.isLoading) {
|
|
1243
1372
|
contentArea.innerHTML = renderSpinner();
|
|
1244
1373
|
createIcons();
|
|
1245
1374
|
return;
|
|
1246
1375
|
}
|
|
1247
1376
|
|
|
1248
|
-
const hasContent = Boolean(
|
|
1249
|
-
state.
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|| state.isEventsLoading
|
|
1253
|
-
|| state.tokenUsageDailySummary
|
|
1254
|
-
|| state.tokenUsageSummary
|
|
1255
|
-
|| state.tokenUsageEventsPage
|
|
1256
|
-
|| state.tokenUsageDailyError
|
|
1257
|
-
|| state.tokenUsageSummaryError
|
|
1258
|
-
|| state.tokenUsageEventsError
|
|
1259
|
-
);
|
|
1260
|
-
|
|
1261
|
-
if (!hasContent) {
|
|
1262
|
-
contentArea.innerHTML = renderWelcomeMessage();
|
|
1263
|
-
} else if (state.error && !state.data) {
|
|
1264
|
-
contentArea.innerHTML = `
|
|
1265
|
-
${renderTokenUsageSection()}
|
|
1266
|
-
${renderUsageErrorSection()}
|
|
1267
|
-
`;
|
|
1268
|
-
} else if (state.data) {
|
|
1269
|
-
contentArea.innerHTML = `
|
|
1270
|
-
${renderTokenUsageSection()}
|
|
1271
|
-
${renderUsageQuotas(state.data.quota_snapshots)}
|
|
1272
|
-
${renderDetailedData(state.data)}
|
|
1273
|
-
`;
|
|
1274
|
-
} else {
|
|
1275
|
-
contentArea.innerHTML = renderTokenUsageSection();
|
|
1276
|
-
}
|
|
1377
|
+
const hasContent = Boolean(state.isTokenUsageLoading || state.isEventsLoading
|
|
1378
|
+
|| state.tokenUsageSummary || state.tokenUsageDailySummary || state.tokenUsageEventsPage
|
|
1379
|
+
|| state.tokenUsageSummaryError || state.tokenUsageDailyError || state.tokenUsageEventsError);
|
|
1380
|
+
contentArea.innerHTML = hasContent ? renderTokenUsageSection() : renderWelcomeMessage();
|
|
1277
1381
|
|
|
1278
1382
|
// Replace placeholder icons with actual Lucide icons
|
|
1279
1383
|
createIcons();
|
|
@@ -1287,8 +1391,6 @@
|
|
|
1287
1391
|
"modelBreakdown",
|
|
1288
1392
|
"sessionBreakdown",
|
|
1289
1393
|
"requestEvents",
|
|
1290
|
-
"copilotQuotas",
|
|
1291
|
-
"usageApiResponse",
|
|
1292
1394
|
]);
|
|
1293
1395
|
|
|
1294
1396
|
function readCollapsedSections() {
|
|
@@ -1488,7 +1590,7 @@
|
|
|
1488
1590
|
`;
|
|
1489
1591
|
}
|
|
1490
1592
|
|
|
1491
|
-
function
|
|
1593
|
+
function getConnectionKey() {
|
|
1492
1594
|
return JSON.stringify([
|
|
1493
1595
|
endpointUrlInput.value.trim(),
|
|
1494
1596
|
apiKeyInput.value.trim(),
|
|
@@ -1496,7 +1598,7 @@
|
|
|
1496
1598
|
}
|
|
1497
1599
|
|
|
1498
1600
|
function syncModelsConnection() {
|
|
1499
|
-
const key =
|
|
1601
|
+
const key = getConnectionKey();
|
|
1500
1602
|
if (state.modelsConnectionKey !== key) {
|
|
1501
1603
|
state.modelsConnectionKey = key;
|
|
1502
1604
|
state.modelsRequestId += 1;
|
|
@@ -1506,9 +1608,24 @@
|
|
|
1506
1608
|
}
|
|
1507
1609
|
}
|
|
1508
1610
|
|
|
1611
|
+
function syncSubscriptionsConnection() {
|
|
1612
|
+
const key = getConnectionKey();
|
|
1613
|
+
if (state.subscriptionsConnectionKey !== key) {
|
|
1614
|
+
state.subscriptionsConnectionKey = key;
|
|
1615
|
+
state.subscriptionRequestId += 1;
|
|
1616
|
+
state.subscriptionUsage = {};
|
|
1617
|
+
state.subscriptionUsageErrors = {};
|
|
1618
|
+
state.subscriptionLoadingProviders = {};
|
|
1619
|
+
state.subscriptionsLoaded = false;
|
|
1620
|
+
state.isSubscriptionsLoading = false;
|
|
1621
|
+
state.subscriptionPageError = null;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1509
1625
|
function handleModelsConnectionChange() {
|
|
1510
1626
|
syncModelsConnection();
|
|
1511
|
-
|
|
1627
|
+
syncSubscriptionsConnection();
|
|
1628
|
+
if (state.activeTab === "models" || state.activeTab === "subscriptions") {
|
|
1512
1629
|
render();
|
|
1513
1630
|
}
|
|
1514
1631
|
}
|
|
@@ -1520,7 +1637,7 @@
|
|
|
1520
1637
|
const requestId = ++state.modelsRequestId;
|
|
1521
1638
|
const isCurrentRequest = () =>
|
|
1522
1639
|
requestId === state.modelsRequestId
|
|
1523
|
-
&& connectionKey ===
|
|
1640
|
+
&& connectionKey === getConnectionKey();
|
|
1524
1641
|
state.models = null;
|
|
1525
1642
|
state.modelsError = null;
|
|
1526
1643
|
state.isModelsLoading = true;
|
|
@@ -1550,16 +1667,21 @@
|
|
|
1550
1667
|
}
|
|
1551
1668
|
|
|
1552
1669
|
function activateTab(tab) {
|
|
1553
|
-
if (
|
|
1670
|
+
if (!["usage", "subscriptions", "models"].includes(tab)) {
|
|
1554
1671
|
return;
|
|
1555
1672
|
}
|
|
1556
1673
|
syncEndpointCredential();
|
|
1557
1674
|
syncModelsConnection();
|
|
1675
|
+
syncSubscriptionsConnection();
|
|
1558
1676
|
state.activeTab = tab;
|
|
1677
|
+
syncUrlState();
|
|
1559
1678
|
render();
|
|
1560
1679
|
if (tab === "models" && !state.models && !state.isModelsLoading) {
|
|
1561
1680
|
void fetchModels();
|
|
1562
1681
|
}
|
|
1682
|
+
if (tab === "subscriptions" && !state.subscriptionsLoaded && !state.isSubscriptionsLoading) {
|
|
1683
|
+
void refreshSubscriptionQuotas();
|
|
1684
|
+
}
|
|
1563
1685
|
}
|
|
1564
1686
|
|
|
1565
1687
|
function handleTabsKeydown(event) {
|
|
@@ -1568,10 +1690,10 @@
|
|
|
1568
1690
|
return;
|
|
1569
1691
|
}
|
|
1570
1692
|
event.preventDefault();
|
|
1571
|
-
const index =
|
|
1693
|
+
const index = tabButtons.findIndex((button) => button.dataset.tab === state.activeTab);
|
|
1572
1694
|
const nextIndex = event.key === "Home" ? 0
|
|
1573
|
-
: event.key === "End" ? 1
|
|
1574
|
-
: 1 -
|
|
1695
|
+
: event.key === "End" ? tabButtons.length - 1
|
|
1696
|
+
: (index + (event.key === "ArrowRight" ? 1 : -1) + tabButtons.length) % tabButtons.length;
|
|
1575
1697
|
activateTab(tabButtons[nextIndex].dataset.tab);
|
|
1576
1698
|
tabButtons[nextIndex].focus();
|
|
1577
1699
|
}
|
|
@@ -1600,197 +1722,85 @@
|
|
|
1600
1722
|
feedback.classList.add("py-3");
|
|
1601
1723
|
}
|
|
1602
1724
|
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1725
|
+
// Subscription quotas use the same normalized server response as desktop.
|
|
1726
|
+
function subscriptionQuotaLabel(window) {
|
|
1727
|
+
let label = "Included allowance";
|
|
1728
|
+
if (window.id.startsWith("copilot:")) return window.name || "Premium";
|
|
1729
|
+
if (window.kind === "spend") return "Monthly credit limit";
|
|
1730
|
+
if (window.kind === "on-demand") return "Pay as you go · Monthly";
|
|
1731
|
+
const seconds = window.windowSeconds;
|
|
1732
|
+
if (window.period === "weekly" || seconds === 604800) label = "Weekly limit";
|
|
1733
|
+
else if (window.period === "monthly") label = "Monthly limit";
|
|
1734
|
+
else if (Number.isFinite(seconds) && seconds >= 86400) label = `${seconds / 86400}-day limit`;
|
|
1735
|
+
else if (Number.isFinite(seconds) && seconds >= 3600) label = `${seconds / 3600}-hour limit`;
|
|
1736
|
+
else if (Number.isFinite(seconds) && seconds > 0) label = `${seconds / 60}-minute limit`;
|
|
1737
|
+
return window.name ? `${window.name} · ${label}` : label;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function formatSubscriptionAmount(value, unit) {
|
|
1741
|
+
if (!Number.isFinite(value)) return "—";
|
|
1742
|
+
return unit === "USD"
|
|
1743
|
+
? value.toLocaleString(undefined, { style: "currency", currency: "USD" })
|
|
1744
|
+
: value.toLocaleString(undefined, { maximumFractionDigits: 2 });
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
function renderSubscriptionQuotaWindow(window) {
|
|
1748
|
+
const percent = window.usedPercent;
|
|
1749
|
+
const hasPercent = Number.isFinite(percent) && !window.unlimited;
|
|
1750
|
+
const width = hasPercent ? Math.min(100, Math.max(0, percent)) : 0;
|
|
1751
|
+
const label = escapeHtml(subscriptionQuotaLabel(window));
|
|
1752
|
+
const reset = Number.isFinite(window.resetAt) ? new Date(window.resetAt * 1000) : null;
|
|
1753
|
+
const resetDate = window.resetDate || (reset && Number.isFinite(reset.getTime()) ? reset.toLocaleString() : null);
|
|
1617
1754
|
return `
|
|
1618
|
-
<
|
|
1619
|
-
<
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
)}
|
|
1624
|
-
</h2>
|
|
1625
|
-
${renderCollapseBody("copilotQuotas", quotaCards, "metric-grid")}
|
|
1626
|
-
</section>
|
|
1627
|
-
`;
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
function renderUsageErrorSection() {
|
|
1631
|
-
if (!state.error || state.data) {
|
|
1632
|
-
return "";
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
return `
|
|
1636
|
-
<section id="usage-quotas" class="dashboard-section">
|
|
1637
|
-
<h2 class="section-heading">
|
|
1638
|
-
${renderCollapseToggle(
|
|
1639
|
-
"copilotQuotas",
|
|
1640
|
-
`<i data-lucide="gauge" class="h-5 w-5" style="color: var(--color-blue);"></i><span>Copilot Quotas</span>`
|
|
1641
|
-
)}
|
|
1642
|
-
</h2>
|
|
1643
|
-
${renderCollapseBody(
|
|
1644
|
-
"copilotQuotas",
|
|
1645
|
-
renderError(state.error, "Usage request failed")
|
|
1646
|
-
)}
|
|
1647
|
-
</section>
|
|
1648
|
-
`;
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
/**
|
|
1652
|
-
* Renders a single quota card.
|
|
1653
|
-
* @param {string} title - The name of the quota (e.g., 'chat').
|
|
1654
|
-
* @param {object} details - The details object for the quota.
|
|
1655
|
-
* @returns {string} HTML string for a single card.
|
|
1656
|
-
*/
|
|
1657
|
-
function getCopilotQuotaRemaining(details) {
|
|
1658
|
-
if (Number.isFinite(details.remaining)) return details.remaining;
|
|
1659
|
-
if (Number.isFinite(details.quota_remaining)) {
|
|
1660
|
-
return details.quota_remaining;
|
|
1661
|
-
}
|
|
1662
|
-
return null;
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
function getCopilotQuotaPercentRemaining(details) {
|
|
1666
|
-
if (details.unlimited) return 100;
|
|
1667
|
-
const percent = Number.isFinite(details.percent_remaining)
|
|
1668
|
-
? details.percent_remaining
|
|
1669
|
-
: Number.isFinite(details.entitlement) && details.entitlement > 0
|
|
1670
|
-
? ((getCopilotQuotaRemaining(details) ?? details.entitlement) /
|
|
1671
|
-
details.entitlement) * 100
|
|
1672
|
-
: 100;
|
|
1673
|
-
return Math.max(0, Math.min(100, percent));
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
function renderQuotaCard(title, details) {
|
|
1677
|
-
const { entitlement, unlimited } = details;
|
|
1678
|
-
const remaining = getCopilotQuotaRemaining(details) ?? 0;
|
|
1679
|
-
|
|
1680
|
-
const percentUsed = 100 - getCopilotQuotaPercentRemaining(details);
|
|
1681
|
-
const used = unlimited
|
|
1682
|
-
? "N/A"
|
|
1683
|
-
: (entitlement - remaining).toLocaleString();
|
|
1684
|
-
|
|
1685
|
-
let progressBarColor = "var(--color-green)";
|
|
1686
|
-
if (percentUsed > 75) progressBarColor = "var(--color-yellow)";
|
|
1687
|
-
if (percentUsed > 90) progressBarColor = "var(--color-red)";
|
|
1688
|
-
if (unlimited) progressBarColor = "var(--color-blue)";
|
|
1689
|
-
|
|
1690
|
-
return `
|
|
1691
|
-
<div class="metric-card">
|
|
1692
|
-
<div class="flex items-center justify-between gap-3">
|
|
1693
|
-
<div class="metric-label capitalize">
|
|
1694
|
-
<span class="metric-dot" style="background-color: ${progressBarColor};"></span>
|
|
1695
|
-
<span>${escapeHtml(title.replace(/_/g, " "))}</span>
|
|
1696
|
-
</div>
|
|
1697
|
-
${
|
|
1698
|
-
unlimited
|
|
1699
|
-
? `<span class="rounded-md px-2 py-1 text-xs font-semibold" style="background-color: rgba(37, 99, 235, 0.1); color: var(--color-blue);">Unlimited</span>`
|
|
1700
|
-
: `<span class="text-xs font-semibold" style="color: var(--color-gray);">${percentUsed.toFixed(1)}% used</span>`
|
|
1701
|
-
}
|
|
1702
|
-
</div>
|
|
1703
|
-
<div class="mt-5">
|
|
1704
|
-
<div class="w-full progress-bar-bg h-2">
|
|
1705
|
-
<div class="progress-bar-fg h-2" style="width: ${unlimited ? 100 : percentUsed}%; background-color: ${progressBarColor};"></div>
|
|
1706
|
-
</div>
|
|
1707
|
-
</div>
|
|
1708
|
-
<div class="mt-2 flex items-center justify-between gap-4 text-xs font-medium" style="color: var(--color-gray);">
|
|
1709
|
-
<span>${used} / ${unlimited ? "∞" : entitlement.toLocaleString()}</span>
|
|
1710
|
-
<span>${unlimited ? "∞" : remaining.toLocaleString()} remaining</span>
|
|
1711
|
-
</div>
|
|
1755
|
+
<div class="quota-window" data-warning="${Boolean(window.limitReached || percent >= 90)}">
|
|
1756
|
+
<div class="quota-window-heading"><span>${label}</span><span>${window.unlimited ? "Unlimited" : hasPercent ? `${Math.round(percent * 10) / 10}% used` : "—"}</span></div>
|
|
1757
|
+
${hasPercent ? `<div role="progressbar" aria-label="${label}" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${width}" class="quota-progress"><div class="quota-progress-fill" style="width: ${width}%;"></div></div>` : ""}
|
|
1758
|
+
${!window.unlimited && (window.used !== undefined || window.limit !== undefined) ? `<p class="quota-window-note">${formatSubscriptionAmount(window.used, window.unit)} / ${formatSubscriptionAmount(window.limit, window.unit)}${window.unit === "credits" ? " Credits" : window.unit === "requests" ? " requests" : ""}</p>` : ""}
|
|
1759
|
+
${resetDate ? `<p class="quota-window-note">Resets ${escapeHtml(resetDate)}</p>` : ""}
|
|
1712
1760
|
</div>
|
|
1713
1761
|
`;
|
|
1714
1762
|
}
|
|
1715
1763
|
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
const formattedKey = escapeHtml(key.replace(/_/g, " "));
|
|
1757
|
-
return `<div class="mt-1">
|
|
1758
|
-
<span class="capitalize font-semibold" style="color: var(--color-fg-lightest);">${formattedKey}:</span>
|
|
1759
|
-
${formatObject(value)}
|
|
1760
|
-
</div>`;
|
|
1761
|
-
})
|
|
1762
|
-
.join("") +
|
|
1763
|
-
"</div>"
|
|
1764
|
-
);
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
/**
|
|
1768
|
-
* Renders the section with the full, formatted API response.
|
|
1769
|
-
* @param {object} data - The full API response data.
|
|
1770
|
-
* @returns {string} HTML string for the full data section.
|
|
1771
|
-
*/
|
|
1772
|
-
function renderDetailedData(data) {
|
|
1773
|
-
if (data === null || typeof data === "undefined") {
|
|
1774
|
-
return "";
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
const formattedDetails = formatObject(data);
|
|
1778
|
-
return `
|
|
1779
|
-
<section id="detailed-data" class="dashboard-section">
|
|
1780
|
-
<h2 class="section-heading">
|
|
1781
|
-
${renderCollapseToggle(
|
|
1782
|
-
"usageApiResponse",
|
|
1783
|
-
`<i data-lucide="file-text" class="h-5 w-5" style="color: var(--color-blue);"></i><span>Copilot Usage API Response</span>`
|
|
1784
|
-
)}
|
|
1785
|
-
</h2>
|
|
1786
|
-
${renderCollapseBody(
|
|
1787
|
-
"usageApiResponse",
|
|
1788
|
-
`<div class="panel p-4 relative font-mono text-xs code-block overflow-auto" style="max-height: 420px;">
|
|
1789
|
-
${formattedDetails}
|
|
1790
|
-
</div>`
|
|
1791
|
-
)}
|
|
1792
|
-
</section>
|
|
1793
|
-
`;
|
|
1764
|
+
function renderSubscriptionSummary(usage) {
|
|
1765
|
+
const main = usage.windows.find((window) => window.kind === "usage") || usage.windows[0];
|
|
1766
|
+
const credits = usage.credits;
|
|
1767
|
+
const showCredits = credits && (credits.unlimited || (credits.hasCredits && credits.balance !== undefined) || !main);
|
|
1768
|
+
const value = showCredits ? credits.unlimited ? "∞"
|
|
1769
|
+
: formatSubscriptionAmount(credits.balance, credits.unit)
|
|
1770
|
+
: main?.unlimited ? "∞"
|
|
1771
|
+
: Number.isFinite(main?.usedPercent) ? `${formatSubscriptionAmount(Math.max(0, Math.min(100, 100 - main.usedPercent)))}%` : "—";
|
|
1772
|
+
const label = showCredits ? credits.unit === "USD" ? "Prepaid balance" : "Credits" : "Remaining allowance";
|
|
1773
|
+
return `<div class="quota-summary"><p class="quota-summary-label">${label}</p><div class="quota-summary-value">${value}</div><p class="quota-summary-context">${showCredits ? credits.unlimited ? "Unlimited" : "Available balance" : main ? escapeHtml(subscriptionQuotaLabel(main)) : "Included allowance"}</p></div>`;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
function renderSubscriptionUsageSection() {
|
|
1777
|
+
const ids = Object.keys(SUBSCRIPTION_PROVIDERS).filter((provider) => state.subscriptionUsage?.[provider] || state.subscriptionUsageErrors?.[provider] || state.subscriptionLoadingProviders?.[provider]);
|
|
1778
|
+
const cards = ids.map((provider) => {
|
|
1779
|
+
const meta = SUBSCRIPTION_PROVIDERS[provider];
|
|
1780
|
+
const usage = state.subscriptionUsage?.[provider];
|
|
1781
|
+
const failed = state.subscriptionUsageErrors?.[provider];
|
|
1782
|
+
const loading = state.subscriptionLoadingProviders?.[provider];
|
|
1783
|
+
const credits = usage?.credits;
|
|
1784
|
+
let balance = "—";
|
|
1785
|
+
if (credits?.unlimited) balance = "Unlimited";
|
|
1786
|
+
else if (Number.isFinite(credits?.balance)) balance = formatSubscriptionAmount(credits.balance, credits.unit);
|
|
1787
|
+
else if (credits?.hasCredits) balance = "Available";
|
|
1788
|
+
let limitMessage = "Usage limit reached";
|
|
1789
|
+
if (usage?.limitReason === "workspace_owner_credits_depleted" || usage?.limitReason === "workspace_member_credits_depleted") limitMessage = "Workspace credits are depleted";
|
|
1790
|
+
if (usage?.limitReason === "workspace_owner_usage_limit_reached" || usage?.limitReason === "workspace_member_usage_limit_reached") limitMessage = "Workspace spending limit reached";
|
|
1791
|
+
const stateName = loading ? "loading" : failed || usage?.limitReached ? "warning" : "ready";
|
|
1792
|
+
const status = loading ? "Loading…" : failed ? "Could not load" : usage?.limitReached ? "Limit reached" : "Available";
|
|
1793
|
+
return `
|
|
1794
|
+
<section class="quota-provider-card" data-provider="${provider}" aria-label="${meta.name} subscription quota">
|
|
1795
|
+
<div class="quota-card-header"><span class="quota-provider-logo"><img src="${meta.logo}" alt="" /></span><div style="min-width: 0; flex: 1;"><h3>${meta.name}</h3><p class="quota-provider-product">${meta.product}</p></div><button class="quota-refresh" type="button" data-quota-refresh="${provider}" aria-label="Refresh ${meta.name} quota" title="Refresh" ${loading ? "disabled" : ""}><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 7v5h-5M4 17v-5h5" /><path d="M6.1 7a7 7 0 0 1 11.6-2L20 8M4 16l2.3 3A7 7 0 0 0 18 17" /></svg></button></div>
|
|
1796
|
+
<div class="quota-card-meta"><span class="quota-plan">${escapeHtml(usage?.planType || meta.product)}</span><span class="quota-status" data-state="${stateName}"><span></span>${status}</span></div>
|
|
1797
|
+
${loading ? `<div class="quota-skeleton" aria-label="Loading"><div></div><div></div><div></div></div>` : failed ? `<p role="alert" class="quota-card-message">Could not load quota. Refresh to retry.</p>` : `${renderSubscriptionSummary(usage)}<div class="quota-details">${usage.limitReached ? `<p class="quota-notice">${limitMessage}</p>` : ""}${usage.windows.map(renderSubscriptionQuotaWindow).join("")}${credits && usage.windows.length ? `<div class="quota-balance"><span>${credits.unit === "USD" ? "Prepaid balance" : "Credits"}</span><strong>${balance}</strong></div>` : ""}${!usage.windows.length && !credits && !usage.limitReached ? `<p class="quota-window-note">No quota details reported.</p>` : ""}</div>`}
|
|
1798
|
+
<div class="quota-card-footer">Subscription allowance · Refresh anytime</div>
|
|
1799
|
+
</section>
|
|
1800
|
+
`;
|
|
1801
|
+
}).join("");
|
|
1802
|
+
const empty = state.subscriptionPageError || (state.subscriptionsLoaded ? "No subscriptions available. Enable a subscription provider and refresh." : "Refresh to load your subscription allowances.");
|
|
1803
|
+
return `<section id="subscription-quotas" class="subscriptions-page"><div class="subscriptions-page-heading"><div><p class="subscriptions-eyebrow">ACCOUNT USAGE</p><h2>Subscriptions</h2><p class="subscriptions-description">Your subscriptions, balances and reset times in one place.</p></div>${ids.length ? `<span class="subscriptions-count"><span></span>${ids.length} providers</span>` : ""}</div>${cards ? `<div class="subscriptions-grid">${cards}</div>` : `<div class="subscriptions-empty"><h3>Your subscriptions will appear here</h3><p>${escapeHtml(empty)}</p></div>`}</section>`;
|
|
1794
1804
|
}
|
|
1795
1805
|
|
|
1796
1806
|
function renderTokenUsageSection() {
|
|
@@ -2685,111 +2695,72 @@
|
|
|
2685
2695
|
<div class="panel text-center py-16 px-4">
|
|
2686
2696
|
<i data-lucide="info" class="mx-auto h-10 w-10" style="color: var(--color-blue);"></i>
|
|
2687
2697
|
<h3 class="mt-3 text-lg font-bold" style="color: var(--color-fg-lightest);">Usage data is ready to load</h3>
|
|
2688
|
-
<p class="mt-2 text-sm" style="color: var(--color-gray);">Refresh the configured endpoint to view
|
|
2698
|
+
<p class="mt-2 text-sm" style="color: var(--color-gray);">Refresh the configured endpoint to view token totals, trends, and request events. Open Subscriptions for account allowances.</p>
|
|
2689
2699
|
</div>
|
|
2690
2700
|
`;
|
|
2691
2701
|
}
|
|
2692
2702
|
|
|
2693
2703
|
// --- Data Fetching ---
|
|
2694
2704
|
|
|
2705
|
+
async function refreshSubscriptionQuotas(provider) {
|
|
2706
|
+
syncSubscriptionsConnection();
|
|
2707
|
+
try {
|
|
2708
|
+
if (!endpointUrlInput.value.trim()) throw new Error("Endpoint URL cannot be empty.");
|
|
2709
|
+
await fetchSubscriptionQuotas(resolveUsageUrl(endpointUrlInput.value.trim()), provider);
|
|
2710
|
+
} catch (error) {
|
|
2711
|
+
state.subscriptionPageError = getErrorMessage(error);
|
|
2712
|
+
render("subscriptions");
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
async function fetchSubscriptionQuotas(usageUrl, selectedProvider) {
|
|
2717
|
+
if (selectedProvider && !Object.hasOwn(SUBSCRIPTION_PROVIDERS, selectedProvider)) return;
|
|
2718
|
+
if (!selectedProvider) {
|
|
2719
|
+
state.subscriptionRequestId += 1;
|
|
2720
|
+
state.subscriptionUsage = {};
|
|
2721
|
+
state.subscriptionUsageErrors = {};
|
|
2722
|
+
state.subscriptionLoadingProviders = {};
|
|
2723
|
+
}
|
|
2724
|
+
const requestId = state.subscriptionRequestId;
|
|
2725
|
+
const connectionKey = getConnectionKey();
|
|
2726
|
+
state.subscriptionPageError = null;
|
|
2727
|
+
const providers = selectedProvider ? [selectedProvider] : Object.keys(SUBSCRIPTION_PROVIDERS);
|
|
2728
|
+
await Promise.all(providers.map(async (provider) => {
|
|
2729
|
+
const revision = (state.subscriptionRequests[provider] || 0) + 1;
|
|
2730
|
+
state.subscriptionRequests[provider] = revision;
|
|
2731
|
+
delete state.subscriptionUsage[provider];
|
|
2732
|
+
delete state.subscriptionUsageErrors[provider];
|
|
2733
|
+
state.subscriptionLoadingProviders[provider] = true;
|
|
2734
|
+
state.isSubscriptionsLoading = true;
|
|
2735
|
+
render("subscriptions");
|
|
2736
|
+
const isCurrent = () => requestId === state.subscriptionRequestId
|
|
2737
|
+
&& connectionKey === getConnectionKey() && revision === state.subscriptionRequests[provider];
|
|
2738
|
+
try {
|
|
2739
|
+
const usage = await fetchJson(deriveEndpointUrl(usageUrl, `usage/${provider}`));
|
|
2740
|
+
if (!isCurrent()) return;
|
|
2741
|
+
if (usage?.provider !== provider || !Array.isArray(usage.windows)) throw new Error("Invalid subscription quota response");
|
|
2742
|
+
state.subscriptionUsage[provider] = usage;
|
|
2743
|
+
} catch (error) {
|
|
2744
|
+
if (!isCurrent()) return;
|
|
2745
|
+
if (error?.status !== 404) state.subscriptionUsageErrors[provider] = true;
|
|
2746
|
+
} finally {
|
|
2747
|
+
if (isCurrent()) {
|
|
2748
|
+
delete state.subscriptionLoadingProviders[provider];
|
|
2749
|
+
state.isSubscriptionsLoading = Object.keys(state.subscriptionLoadingProviders).length > 0;
|
|
2750
|
+
state.subscriptionsLoaded = true;
|
|
2751
|
+
render("subscriptions");
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
}));
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2695
2757
|
/**
|
|
2696
2758
|
* Fetches usage data and token usage data from the specified API endpoint.
|
|
2697
2759
|
*/
|
|
2698
2760
|
async function fetchData(page = 1) {
|
|
2699
|
-
invalidateSessionsRequest();
|
|
2700
|
-
const endpoint = endpointUrlInput.value.trim();
|
|
2701
|
-
if (!endpoint) {
|
|
2702
|
-
state.data = null;
|
|
2703
|
-
state.tokenUsageDailySummary = null;
|
|
2704
|
-
state.tokenUsageSummary = null;
|
|
2705
|
-
state.tokenUsageEventsPage = null;
|
|
2706
|
-
state.error = "Endpoint URL cannot be empty.";
|
|
2707
|
-
state.tokenUsageDailyError =
|
|
2708
|
-
"Token usage requires a valid endpoint URL.";
|
|
2709
|
-
state.tokenUsageSummaryError =
|
|
2710
|
-
"Token usage requires a valid endpoint URL.";
|
|
2711
|
-
state.tokenUsageEventsError = null;
|
|
2712
|
-
render();
|
|
2713
|
-
return;
|
|
2714
|
-
}
|
|
2715
|
-
|
|
2716
|
-
const period = getSelectedPeriod();
|
|
2717
2761
|
state.isLoading = true;
|
|
2718
|
-
state.error = null;
|
|
2719
|
-
state.tokenUsageDailyError = null;
|
|
2720
|
-
state.tokenUsageSummaryError = null;
|
|
2721
|
-
state.tokenUsageEventsError = null;
|
|
2722
|
-
render();
|
|
2723
|
-
|
|
2724
2762
|
try {
|
|
2725
|
-
|
|
2726
|
-
const [usageResult, summaryResult, dailyResult, eventsResult] =
|
|
2727
|
-
await Promise.allSettled([
|
|
2728
|
-
fetchJson(usageUrl),
|
|
2729
|
-
fetchJson(buildTokenUsageSummaryUrl(usageUrl, period)),
|
|
2730
|
-
fetchJson(buildTokenUsageDailyUrl(usageUrl, period)),
|
|
2731
|
-
fetchJson(buildTokenUsageEventsUrl(usageUrl, period, page)),
|
|
2732
|
-
]);
|
|
2733
|
-
|
|
2734
|
-
if (usageResult.status === "fulfilled") {
|
|
2735
|
-
state.data = usageResult.value;
|
|
2736
|
-
} else {
|
|
2737
|
-
state.data = null;
|
|
2738
|
-
state.error = getErrorMessage(usageResult.reason);
|
|
2739
|
-
}
|
|
2740
|
-
|
|
2741
|
-
if (summaryResult.status === "fulfilled") {
|
|
2742
|
-
state.tokenUsageSummary = summaryResult.value;
|
|
2743
|
-
void fetchTokenUsageSessionsPage(1);
|
|
2744
|
-
} else if (
|
|
2745
|
-
isMissingTokenUsageEndpointError(summaryResult.reason)
|
|
2746
|
-
) {
|
|
2747
|
-
state.tokenUsageSummary = null;
|
|
2748
|
-
state.tokenUsageSummaryError = null;
|
|
2749
|
-
} else {
|
|
2750
|
-
state.tokenUsageSummary = null;
|
|
2751
|
-
state.tokenUsageSummaryError = getErrorMessage(
|
|
2752
|
-
summaryResult.reason
|
|
2753
|
-
);
|
|
2754
|
-
}
|
|
2755
|
-
|
|
2756
|
-
if (dailyResult.status === "fulfilled") {
|
|
2757
|
-
state.tokenUsageDailySummary = dailyResult.value;
|
|
2758
|
-
} else if (
|
|
2759
|
-
isMissingTokenUsageEndpointError(dailyResult.reason)
|
|
2760
|
-
) {
|
|
2761
|
-
state.tokenUsageDailySummary = null;
|
|
2762
|
-
state.tokenUsageDailyError = null;
|
|
2763
|
-
} else {
|
|
2764
|
-
state.tokenUsageDailySummary = null;
|
|
2765
|
-
state.tokenUsageDailyError = getErrorMessage(
|
|
2766
|
-
dailyResult.reason
|
|
2767
|
-
);
|
|
2768
|
-
}
|
|
2769
|
-
|
|
2770
|
-
if (eventsResult.status === "fulfilled") {
|
|
2771
|
-
state.tokenUsageEventsPage = eventsResult.value;
|
|
2772
|
-
} else if (
|
|
2773
|
-
isMissingTokenUsageEndpointError(eventsResult.reason)
|
|
2774
|
-
) {
|
|
2775
|
-
state.tokenUsageEventsPage = null;
|
|
2776
|
-
state.tokenUsageEventsError = null;
|
|
2777
|
-
} else {
|
|
2778
|
-
state.tokenUsageEventsPage = null;
|
|
2779
|
-
state.tokenUsageEventsError = getErrorMessage(
|
|
2780
|
-
eventsResult.reason
|
|
2781
|
-
);
|
|
2782
|
-
}
|
|
2783
|
-
} catch (error) {
|
|
2784
|
-
console.error("Fetch error:", error);
|
|
2785
|
-
state.data = null;
|
|
2786
|
-
state.tokenUsageDailySummary = null;
|
|
2787
|
-
state.tokenUsageSummary = null;
|
|
2788
|
-
state.tokenUsageEventsPage = null;
|
|
2789
|
-
state.error = getErrorMessage(error);
|
|
2790
|
-
state.tokenUsageDailyError = getErrorMessage(error);
|
|
2791
|
-
state.tokenUsageSummaryError = getErrorMessage(error);
|
|
2792
|
-
state.tokenUsageEventsError = getErrorMessage(error);
|
|
2763
|
+
await fetchTokenUsageSummaryAndEvents(page);
|
|
2793
2764
|
} finally {
|
|
2794
2765
|
state.isLoading = false;
|
|
2795
2766
|
render("usage");
|
|
@@ -2982,6 +2953,8 @@
|
|
|
2982
2953
|
syncUrlState();
|
|
2983
2954
|
if (state.activeTab === "models") {
|
|
2984
2955
|
void fetchModels();
|
|
2956
|
+
} else if (state.activeTab === "subscriptions") {
|
|
2957
|
+
void refreshSubscriptionQuotas();
|
|
2985
2958
|
} else {
|
|
2986
2959
|
void fetchData();
|
|
2987
2960
|
}
|
|
@@ -2996,6 +2969,11 @@
|
|
|
2996
2969
|
if (!(event.target instanceof Element)) {
|
|
2997
2970
|
return;
|
|
2998
2971
|
}
|
|
2972
|
+
const quotaRefresh = event.target.closest("[data-quota-refresh]");
|
|
2973
|
+
if (quotaRefresh) {
|
|
2974
|
+
void refreshSubscriptionQuotas(quotaRefresh.dataset.quotaRefresh);
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2999
2977
|
const copyButton = event.target.closest("[data-model-copy]");
|
|
3000
2978
|
if (copyButton) {
|
|
3001
2979
|
void copyModelId(copyButton.dataset.modelCopy);
|
|
@@ -3129,6 +3107,8 @@
|
|
|
3129
3107
|
const endpointFromUrl = urlParams.get("endpoint");
|
|
3130
3108
|
const initialEndpoint = endpointFromUrl || DEFAULT_ENDPOINT;
|
|
3131
3109
|
|
|
3110
|
+
const initialTab = urlParams.get("tab");
|
|
3111
|
+
if (["usage", "subscriptions", "models"].includes(initialTab)) state.activeTab = initialTab;
|
|
3132
3112
|
state.tokenUsagePeriod = periodFromUrl;
|
|
3133
3113
|
tokenUsagePeriodSelect.value = periodFromUrl;
|
|
3134
3114
|
endpointUrlInput.value = initialEndpoint;
|
|
@@ -3137,7 +3117,9 @@
|
|
|
3137
3117
|
state.collapsedSections = readCollapsedSections();
|
|
3138
3118
|
|
|
3139
3119
|
if (endpointFromUrl) {
|
|
3140
|
-
void
|
|
3120
|
+
if (state.activeTab === "subscriptions") void refreshSubscriptionQuotas();
|
|
3121
|
+
else if (state.activeTab === "models") void fetchModels();
|
|
3122
|
+
else void fetchData();
|
|
3141
3123
|
} else {
|
|
3142
3124
|
render();
|
|
3143
3125
|
}
|