@jeffreycao/copilot-api 2.7.39 → 2.7.40
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-BVjOyO_F.js +1 -0
- package/dist/{auth-DBOz2FcU.js → auth-ByQjInU9.js} +2 -2
- package/dist/{auth-DBOz2FcU.js.map → auth-ByQjInU9.js.map} +1 -1
- package/dist/{copilot-runtime-0CujVONa.js → copilot-runtime-D6YjAamL.js} +2 -2
- package/dist/{copilot-runtime-0CujVONa.js.map → copilot-runtime-D6YjAamL.js.map} +1 -1
- package/dist/main.js +2 -2
- package/dist/{server-Jvc5DYJV.js → server-DhHL2SxL.js} +329 -7
- package/dist/{server-Jvc5DYJV.js.map → server-DhHL2SxL.js.map} +1 -1
- package/dist/{start-BQBvYKD-.js → start-CxJBWBbt.js} +5 -5
- package/dist/{start-BQBvYKD-.js.map → start-CxJBWBbt.js.map} +1 -1
- package/dist/{token-_1CBLbqH.js → token-CNWvfwYi.js} +7 -4
- package/dist/{token-_1CBLbqH.js.map → token-CNWvfwYi.js.map} +1 -1
- package/package.json +1 -1
- package/pages/index.html +304 -325
- package/dist/auth-IiEbOqcu.js +0 -1
package/pages/index.html
CHANGED
|
@@ -114,6 +114,7 @@
|
|
|
114
114
|
|
|
115
115
|
.page-tabs {
|
|
116
116
|
display: inline-flex;
|
|
117
|
+
max-width: 100%;
|
|
117
118
|
gap: 4px;
|
|
118
119
|
margin-bottom: 14px;
|
|
119
120
|
padding: 4px;
|
|
@@ -390,6 +391,10 @@
|
|
|
390
391
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
391
392
|
}
|
|
392
393
|
|
|
394
|
+
.subscription-grid {
|
|
395
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
|
|
396
|
+
}
|
|
397
|
+
|
|
393
398
|
.metric-grid-tight .metric-label {
|
|
394
399
|
white-space: nowrap;
|
|
395
400
|
}
|
|
@@ -531,6 +536,8 @@
|
|
|
531
536
|
}
|
|
532
537
|
|
|
533
538
|
@media (max-width: 639px) {
|
|
539
|
+
.page-tabs { display: flex; width: 100%; gap: 2px; }
|
|
540
|
+
.page-tab { flex: 1 1 auto; min-width: 0; justify-content: center; gap: 5px; padding: 8px; font-size: 12px; white-space: nowrap; }
|
|
534
541
|
.main-shell {
|
|
535
542
|
padding: 12px 14px 18px;
|
|
536
543
|
}
|
|
@@ -556,6 +563,82 @@
|
|
|
556
563
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
557
564
|
}
|
|
558
565
|
}
|
|
566
|
+
|
|
567
|
+
/* Subscription allowance workspace. Provider SVGs are bundled from models.dev. */
|
|
568
|
+
.subscriptions-page {
|
|
569
|
+
--quota-text: var(--color-ink, var(--color-fg-lightest));
|
|
570
|
+
--quota-muted: var(--color-ink-soft, var(--color-gray));
|
|
571
|
+
--quota-faint: var(--color-ink-faint, var(--color-fg-darker));
|
|
572
|
+
--quota-line: var(--color-line, var(--color-bg-light-2));
|
|
573
|
+
--quota-soft: var(--color-sunken, var(--color-bg-light-1));
|
|
574
|
+
--quota-surface: var(--color-surface, var(--color-bg));
|
|
575
|
+
padding: 0;
|
|
576
|
+
color: var(--quota-text);
|
|
577
|
+
}
|
|
578
|
+
.subscriptions-page-heading { display: flex; justify-content: space-between; align-items: center; gap: 20px; margin-bottom: 26px; }
|
|
579
|
+
.subscriptions-eyebrow { margin: 0 0 7px; font-size: 10px; font-weight: 650; letter-spacing: .16em; color: var(--quota-faint); }
|
|
580
|
+
.subscriptions-page-heading h2 { margin: 0; font-size: 25px; font-weight: 650; letter-spacing: -.04em; line-height: 1.25; }
|
|
581
|
+
.subscriptions-description { margin: 9px 0 0; font-size: 13px; line-height: 1.6; color: var(--quota-muted); }
|
|
582
|
+
.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); }
|
|
583
|
+
.subscriptions-count > span { width: 5px; height: 5px; border-radius: 50%; background: #22a06b; }
|
|
584
|
+
.subscriptions-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px; align-items: start; }
|
|
585
|
+
.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%); }
|
|
586
|
+
.quota-provider-card[data-provider="codex"] { --quota-brand: #179876; }
|
|
587
|
+
.quota-provider-card[data-provider="xai"] { --quota-brand: #386fce; }
|
|
588
|
+
.dark .quota-provider-card { --quota-brand: #b095ef; }
|
|
589
|
+
.dark .quota-provider-card[data-provider="codex"] { --quota-brand: #50bd9b; }
|
|
590
|
+
.dark .quota-provider-card[data-provider="xai"] { --quota-brand: #7aa8f1; }
|
|
591
|
+
.quota-card-header { display: flex; align-items: center; gap: 11px; }
|
|
592
|
+
.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); }
|
|
593
|
+
.quota-provider-logo img { width: 32px; height: 32px; object-fit: contain; }
|
|
594
|
+
.quota-card-header h3 { margin: 0; font-size: 16px; font-weight: 650; letter-spacing: -.025em; line-height: 1.3; }
|
|
595
|
+
.quota-provider-product { margin: 3px 0 0; font-size: 11px; color: var(--quota-faint); }
|
|
596
|
+
.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; }
|
|
597
|
+
.quota-refresh:hover { background: var(--quota-soft); color: var(--quota-text); }
|
|
598
|
+
.quota-refresh:disabled { opacity: .45; cursor: default; }
|
|
599
|
+
.quota-card-meta { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 17px 0 0; }
|
|
600
|
+
.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; }
|
|
601
|
+
.quota-status { display: inline-flex; align-items: center; gap: 5px; flex-shrink: 0; font-size: 10px; color: var(--quota-faint); }
|
|
602
|
+
.quota-status > span { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
|
|
603
|
+
.quota-status[data-state="ready"] { color: var(--quota-muted); }
|
|
604
|
+
.quota-status[data-state="warning"] { color: #d58a25; }
|
|
605
|
+
.quota-summary { margin: 18px 0 22px; padding: 18px; border-radius: 13px; background: color-mix(in srgb, var(--quota-brand) 4%, var(--quota-soft)); }
|
|
606
|
+
.quota-summary-label { margin: 0 0 9px; font-size: 11px; font-weight: 500; color: var(--quota-muted); }
|
|
607
|
+
.quota-summary-value { font-size: 40px; line-height: 1.05; font-weight: 600; letter-spacing: -.055em; font-variant-numeric: tabular-nums; }
|
|
608
|
+
.quota-summary-context { margin: 9px 0 0; font-size: 11px; color: var(--quota-faint); }
|
|
609
|
+
.quota-details { font-size: 12px; }
|
|
610
|
+
.quota-window + .quota-window { margin-top: 18px; }
|
|
611
|
+
.quota-window-heading { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 7px; font-size: 12px; }
|
|
612
|
+
.quota-window-heading > :first-child { color: var(--quota-muted); }
|
|
613
|
+
.quota-window-heading > :last-child { font-weight: 550; color: var(--quota-text); }
|
|
614
|
+
.quota-progress { height: 6px; overflow: hidden; border-radius: 99px; background: var(--quota-soft); }
|
|
615
|
+
.quota-progress-fill { height: 100%; border-radius: inherit; background: var(--quota-brand); transition: width .25s ease; }
|
|
616
|
+
.quota-window[data-warning="true"] .quota-progress-fill { background: #d58a25; }
|
|
617
|
+
.quota-window-note { margin: 6px 0 0; font-size: 11px; color: var(--quota-faint); line-height: 1.5; }
|
|
618
|
+
.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); }
|
|
619
|
+
.quota-balance strong { color: var(--quota-text); font-weight: 600; }
|
|
620
|
+
.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; }
|
|
621
|
+
.quota-card-footer { margin-top: 22px; padding-top: 12px; border-top: 1px solid var(--quota-line); color: var(--quota-faint); font-size: 10px; }
|
|
622
|
+
.quota-card-message { display: flex; align-items: center; min-height: 180px; margin: 0; font-size: 12px; line-height: 1.8; color: var(--quota-muted); }
|
|
623
|
+
.quota-card-message[role="alert"] { color: #b67822; }
|
|
624
|
+
.quota-skeleton { display: flex; flex-direction: column; justify-content: center; gap: 16px; min-height: 230px; animation: quota-pulse 1.5s ease-in-out infinite; }
|
|
625
|
+
.quota-skeleton > div { height: 9px; border-radius: 6px; background: var(--quota-soft); }
|
|
626
|
+
.quota-skeleton > div:first-child { width: 55%; height: 42px; margin-bottom: 22px; }
|
|
627
|
+
.quota-skeleton > div:last-child { width: 75%; }
|
|
628
|
+
@keyframes quota-pulse { 50% { opacity: .45; } }
|
|
629
|
+
.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; }
|
|
630
|
+
.subscriptions-empty h3 { margin: 0; font-size: 17px; font-weight: 600; color: var(--quota-text); }
|
|
631
|
+
.subscriptions-empty p { max-width: 360px; margin: 0; line-height: 1.7; }
|
|
632
|
+
.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; }
|
|
633
|
+
@media (max-width: 899px) {
|
|
634
|
+
.subscriptions-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
635
|
+
}
|
|
636
|
+
@media (max-width: 639px) {
|
|
637
|
+
.subscriptions-page-heading { align-items: flex-start; gap: 12px; margin-bottom: 20px; flex-direction: column; }
|
|
638
|
+
.subscriptions-page-heading h2 { font-size: 23px; }
|
|
639
|
+
.subscriptions-grid { grid-template-columns: minmax(0, 1fr); }
|
|
640
|
+
.quota-provider-card { padding: 20px 18px 14px; }
|
|
641
|
+
}
|
|
559
642
|
</style>
|
|
560
643
|
</head>
|
|
561
644
|
<body class="antialiased">
|
|
@@ -587,6 +670,10 @@
|
|
|
587
670
|
<i data-lucide="bar-chart-3" class="h-4 w-4" aria-hidden="true"></i>
|
|
588
671
|
Token Usage
|
|
589
672
|
</button>
|
|
673
|
+
<button id="subscriptions-tab" type="button" class="page-tab" role="tab" data-tab="subscriptions" aria-selected="false" aria-controls="content-area" tabindex="-1">
|
|
674
|
+
<i data-lucide="gauge" class="h-4 w-4" aria-hidden="true"></i>
|
|
675
|
+
Subscriptions
|
|
676
|
+
</button>
|
|
590
677
|
<button
|
|
591
678
|
id="models-tab"
|
|
592
679
|
type="button"
|
|
@@ -668,6 +755,7 @@
|
|
|
668
755
|
const pageTabs = document.getElementById("page-tabs");
|
|
669
756
|
const tabButtons = [
|
|
670
757
|
document.getElementById("usage-tab"),
|
|
758
|
+
document.getElementById("subscriptions-tab"),
|
|
671
759
|
document.getElementById("models-tab"),
|
|
672
760
|
];
|
|
673
761
|
const periodField = document.getElementById("token-usage-period-field");
|
|
@@ -682,6 +770,29 @@
|
|
|
682
770
|
fetchButton.style.backgroundColor = "var(--color-blue)";
|
|
683
771
|
});
|
|
684
772
|
|
|
773
|
+
// models.dev provider SVGs are bundled to work without external requests.
|
|
774
|
+
const SUBSCRIPTION_PROVIDERS = {
|
|
775
|
+
"github-copilot": {
|
|
776
|
+
"name": "GitHub Copilot",
|
|
777
|
+
"product": "GitHub",
|
|
778
|
+
"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"
|
|
779
|
+
},
|
|
780
|
+
"codex": {
|
|
781
|
+
"name": "Codex",
|
|
782
|
+
"product": "OpenAI",
|
|
783
|
+
"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"
|
|
784
|
+
},
|
|
785
|
+
"xai": {
|
|
786
|
+
"name": "xAI",
|
|
787
|
+
"product": "Grok Build",
|
|
788
|
+
"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"
|
|
789
|
+
},
|
|
790
|
+
"opencode-go": {
|
|
791
|
+
"name": "OpenCode Go",
|
|
792
|
+
"product": "OpenCode",
|
|
793
|
+
"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"
|
|
794
|
+
}
|
|
795
|
+
};
|
|
685
796
|
const DEFAULT_ENDPOINT = "http://localhost:4141/usage";
|
|
686
797
|
const DEFAULT_TOKEN_USAGE_PERIOD = "today";
|
|
687
798
|
const DEFAULT_TOKEN_USAGE_EVENTS_PAGE_SIZE = 20;
|
|
@@ -722,8 +833,15 @@
|
|
|
722
833
|
isLoading: false,
|
|
723
834
|
isTokenUsageLoading: false,
|
|
724
835
|
isEventsLoading: false,
|
|
725
|
-
|
|
726
|
-
|
|
836
|
+
subscriptionUsage: {},
|
|
837
|
+
subscriptionUsageErrors: {},
|
|
838
|
+
isSubscriptionsLoading: false,
|
|
839
|
+
subscriptionRequestId: 0,
|
|
840
|
+
subscriptionRequests: {},
|
|
841
|
+
subscriptionLoadingProviders: {},
|
|
842
|
+
subscriptionsLoaded: false,
|
|
843
|
+
subscriptionsConnectionKey: null,
|
|
844
|
+
subscriptionPageError: null,
|
|
727
845
|
tokenUsageDailySummary: null,
|
|
728
846
|
tokenUsageSummary: null,
|
|
729
847
|
tokenUsageSessionsPage: null,
|
|
@@ -1049,6 +1167,8 @@
|
|
|
1049
1167
|
const currentUrl = new URL(window.location.href);
|
|
1050
1168
|
const endpoint = endpointUrlInput.value.trim();
|
|
1051
1169
|
const period = getSelectedPeriod();
|
|
1170
|
+
if (state.activeTab === "usage") currentUrl.searchParams.delete("tab");
|
|
1171
|
+
else currentUrl.searchParams.set("tab", state.activeTab);
|
|
1052
1172
|
|
|
1053
1173
|
if (endpoint) {
|
|
1054
1174
|
currentUrl.searchParams.set("endpoint", endpoint);
|
|
@@ -1067,17 +1187,17 @@
|
|
|
1067
1187
|
const isLoading =
|
|
1068
1188
|
state.activeTab === "models"
|
|
1069
1189
|
? state.isModelsLoading
|
|
1070
|
-
: state.isLoading;
|
|
1190
|
+
: state.activeTab === "subscriptions" ? state.isSubscriptionsLoading : state.isLoading;
|
|
1071
1191
|
fetchButton.disabled = isLoading;
|
|
1072
1192
|
fetchButtonLabel.textContent = isLoading
|
|
1073
1193
|
? "Refreshing..."
|
|
1074
|
-
: "Refresh";
|
|
1194
|
+
: state.activeTab === "subscriptions" ? "Refresh all" : "Refresh";
|
|
1075
1195
|
tokenUsagePeriodSelect.disabled =
|
|
1076
1196
|
state.isLoading || state.isTokenUsageLoading;
|
|
1077
|
-
periodField.hidden = state.activeTab
|
|
1197
|
+
periodField.hidden = state.activeTab !== "usage";
|
|
1078
1198
|
endpointForm.classList.toggle(
|
|
1079
1199
|
"models-view",
|
|
1080
|
-
state.activeTab
|
|
1200
|
+
state.activeTab !== "usage"
|
|
1081
1201
|
);
|
|
1082
1202
|
contentArea.setAttribute("aria-labelledby", `${state.activeTab}-tab`);
|
|
1083
1203
|
contentArea.setAttribute("aria-busy", String(isLoading));
|
|
@@ -1239,41 +1359,22 @@
|
|
|
1239
1359
|
return;
|
|
1240
1360
|
}
|
|
1241
1361
|
|
|
1362
|
+
if (state.activeTab === "subscriptions") {
|
|
1363
|
+
contentArea.innerHTML = renderSubscriptionUsageSection();
|
|
1364
|
+
createIcons();
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1242
1368
|
if (state.isLoading) {
|
|
1243
1369
|
contentArea.innerHTML = renderSpinner();
|
|
1244
1370
|
createIcons();
|
|
1245
1371
|
return;
|
|
1246
1372
|
}
|
|
1247
1373
|
|
|
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
|
-
}
|
|
1374
|
+
const hasContent = Boolean(state.isTokenUsageLoading || state.isEventsLoading
|
|
1375
|
+
|| state.tokenUsageSummary || state.tokenUsageDailySummary || state.tokenUsageEventsPage
|
|
1376
|
+
|| state.tokenUsageSummaryError || state.tokenUsageDailyError || state.tokenUsageEventsError);
|
|
1377
|
+
contentArea.innerHTML = hasContent ? renderTokenUsageSection() : renderWelcomeMessage();
|
|
1277
1378
|
|
|
1278
1379
|
// Replace placeholder icons with actual Lucide icons
|
|
1279
1380
|
createIcons();
|
|
@@ -1287,8 +1388,6 @@
|
|
|
1287
1388
|
"modelBreakdown",
|
|
1288
1389
|
"sessionBreakdown",
|
|
1289
1390
|
"requestEvents",
|
|
1290
|
-
"copilotQuotas",
|
|
1291
|
-
"usageApiResponse",
|
|
1292
1391
|
]);
|
|
1293
1392
|
|
|
1294
1393
|
function readCollapsedSections() {
|
|
@@ -1488,7 +1587,7 @@
|
|
|
1488
1587
|
`;
|
|
1489
1588
|
}
|
|
1490
1589
|
|
|
1491
|
-
function
|
|
1590
|
+
function getConnectionKey() {
|
|
1492
1591
|
return JSON.stringify([
|
|
1493
1592
|
endpointUrlInput.value.trim(),
|
|
1494
1593
|
apiKeyInput.value.trim(),
|
|
@@ -1496,7 +1595,7 @@
|
|
|
1496
1595
|
}
|
|
1497
1596
|
|
|
1498
1597
|
function syncModelsConnection() {
|
|
1499
|
-
const key =
|
|
1598
|
+
const key = getConnectionKey();
|
|
1500
1599
|
if (state.modelsConnectionKey !== key) {
|
|
1501
1600
|
state.modelsConnectionKey = key;
|
|
1502
1601
|
state.modelsRequestId += 1;
|
|
@@ -1506,9 +1605,24 @@
|
|
|
1506
1605
|
}
|
|
1507
1606
|
}
|
|
1508
1607
|
|
|
1608
|
+
function syncSubscriptionsConnection() {
|
|
1609
|
+
const key = getConnectionKey();
|
|
1610
|
+
if (state.subscriptionsConnectionKey !== key) {
|
|
1611
|
+
state.subscriptionsConnectionKey = key;
|
|
1612
|
+
state.subscriptionRequestId += 1;
|
|
1613
|
+
state.subscriptionUsage = {};
|
|
1614
|
+
state.subscriptionUsageErrors = {};
|
|
1615
|
+
state.subscriptionLoadingProviders = {};
|
|
1616
|
+
state.subscriptionsLoaded = false;
|
|
1617
|
+
state.isSubscriptionsLoading = false;
|
|
1618
|
+
state.subscriptionPageError = null;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1509
1622
|
function handleModelsConnectionChange() {
|
|
1510
1623
|
syncModelsConnection();
|
|
1511
|
-
|
|
1624
|
+
syncSubscriptionsConnection();
|
|
1625
|
+
if (state.activeTab === "models" || state.activeTab === "subscriptions") {
|
|
1512
1626
|
render();
|
|
1513
1627
|
}
|
|
1514
1628
|
}
|
|
@@ -1520,7 +1634,7 @@
|
|
|
1520
1634
|
const requestId = ++state.modelsRequestId;
|
|
1521
1635
|
const isCurrentRequest = () =>
|
|
1522
1636
|
requestId === state.modelsRequestId
|
|
1523
|
-
&& connectionKey ===
|
|
1637
|
+
&& connectionKey === getConnectionKey();
|
|
1524
1638
|
state.models = null;
|
|
1525
1639
|
state.modelsError = null;
|
|
1526
1640
|
state.isModelsLoading = true;
|
|
@@ -1550,16 +1664,21 @@
|
|
|
1550
1664
|
}
|
|
1551
1665
|
|
|
1552
1666
|
function activateTab(tab) {
|
|
1553
|
-
if (
|
|
1667
|
+
if (!["usage", "subscriptions", "models"].includes(tab)) {
|
|
1554
1668
|
return;
|
|
1555
1669
|
}
|
|
1556
1670
|
syncEndpointCredential();
|
|
1557
1671
|
syncModelsConnection();
|
|
1672
|
+
syncSubscriptionsConnection();
|
|
1558
1673
|
state.activeTab = tab;
|
|
1674
|
+
syncUrlState();
|
|
1559
1675
|
render();
|
|
1560
1676
|
if (tab === "models" && !state.models && !state.isModelsLoading) {
|
|
1561
1677
|
void fetchModels();
|
|
1562
1678
|
}
|
|
1679
|
+
if (tab === "subscriptions" && !state.subscriptionsLoaded && !state.isSubscriptionsLoading) {
|
|
1680
|
+
void refreshSubscriptionQuotas();
|
|
1681
|
+
}
|
|
1563
1682
|
}
|
|
1564
1683
|
|
|
1565
1684
|
function handleTabsKeydown(event) {
|
|
@@ -1568,10 +1687,10 @@
|
|
|
1568
1687
|
return;
|
|
1569
1688
|
}
|
|
1570
1689
|
event.preventDefault();
|
|
1571
|
-
const index =
|
|
1690
|
+
const index = tabButtons.findIndex((button) => button.dataset.tab === state.activeTab);
|
|
1572
1691
|
const nextIndex = event.key === "Home" ? 0
|
|
1573
|
-
: event.key === "End" ? 1
|
|
1574
|
-
: 1 -
|
|
1692
|
+
: event.key === "End" ? tabButtons.length - 1
|
|
1693
|
+
: (index + (event.key === "ArrowRight" ? 1 : -1) + tabButtons.length) % tabButtons.length;
|
|
1575
1694
|
activateTab(tabButtons[nextIndex].dataset.tab);
|
|
1576
1695
|
tabButtons[nextIndex].focus();
|
|
1577
1696
|
}
|
|
@@ -1600,197 +1719,85 @@
|
|
|
1600
1719
|
feedback.classList.add("py-3");
|
|
1601
1720
|
}
|
|
1602
1721
|
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1722
|
+
// Subscription quotas use the same normalized server response as desktop.
|
|
1723
|
+
function subscriptionQuotaLabel(window) {
|
|
1724
|
+
let label = "Included allowance";
|
|
1725
|
+
if (window.id.startsWith("copilot:")) return window.name || "Premium";
|
|
1726
|
+
if (window.kind === "spend") return "Monthly credit limit";
|
|
1727
|
+
if (window.kind === "on-demand") return "Pay as you go · Monthly";
|
|
1728
|
+
const seconds = window.windowSeconds;
|
|
1729
|
+
if (window.period === "weekly" || seconds === 604800) label = "Weekly limit";
|
|
1730
|
+
else if (window.period === "monthly") label = "Monthly limit";
|
|
1731
|
+
else if (Number.isFinite(seconds) && seconds >= 86400) label = `${seconds / 86400}-day limit`;
|
|
1732
|
+
else if (Number.isFinite(seconds) && seconds >= 3600) label = `${seconds / 3600}-hour limit`;
|
|
1733
|
+
else if (Number.isFinite(seconds) && seconds > 0) label = `${seconds / 60}-minute limit`;
|
|
1734
|
+
return window.name ? `${window.name} · ${label}` : label;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function formatSubscriptionAmount(value, unit) {
|
|
1738
|
+
if (!Number.isFinite(value)) return "—";
|
|
1739
|
+
return unit === "USD"
|
|
1740
|
+
? value.toLocaleString(undefined, { style: "currency", currency: "USD" })
|
|
1741
|
+
: value.toLocaleString(undefined, { maximumFractionDigits: 2 });
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
function renderSubscriptionQuotaWindow(window) {
|
|
1745
|
+
const percent = window.usedPercent;
|
|
1746
|
+
const hasPercent = Number.isFinite(percent) && !window.unlimited;
|
|
1747
|
+
const width = hasPercent ? Math.min(100, Math.max(0, percent)) : 0;
|
|
1748
|
+
const label = escapeHtml(subscriptionQuotaLabel(window));
|
|
1749
|
+
const reset = Number.isFinite(window.resetAt) ? new Date(window.resetAt * 1000) : null;
|
|
1750
|
+
const resetDate = window.resetDate || (reset && Number.isFinite(reset.getTime()) ? reset.toLocaleString() : null);
|
|
1617
1751
|
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>
|
|
1752
|
+
<div class="quota-window" data-warning="${Boolean(window.limitReached || percent >= 90)}">
|
|
1753
|
+
<div class="quota-window-heading"><span>${label}</span><span>${window.unlimited ? "Unlimited" : hasPercent ? `${Math.round(percent * 10) / 10}% used` : "—"}</span></div>
|
|
1754
|
+
${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>` : ""}
|
|
1755
|
+
${!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>` : ""}
|
|
1756
|
+
${resetDate ? `<p class="quota-window-note">Resets ${escapeHtml(resetDate)}</p>` : ""}
|
|
1712
1757
|
</div>
|
|
1713
1758
|
`;
|
|
1714
1759
|
}
|
|
1715
1760
|
|
|
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
|
-
`;
|
|
1761
|
+
function renderSubscriptionSummary(usage) {
|
|
1762
|
+
const main = usage.windows.find((window) => window.kind === "usage") || usage.windows[0];
|
|
1763
|
+
const credits = usage.credits;
|
|
1764
|
+
const showCredits = credits && (credits.hasCredits || credits.unlimited || !main);
|
|
1765
|
+
const value = showCredits ? credits.unlimited ? "∞"
|
|
1766
|
+
: formatSubscriptionAmount(credits.balance, credits.unit)
|
|
1767
|
+
: main?.unlimited ? "∞"
|
|
1768
|
+
: Number.isFinite(main?.usedPercent) ? `${formatSubscriptionAmount(Math.max(0, Math.min(100, 100 - main.usedPercent)))}%` : "—";
|
|
1769
|
+
const label = showCredits ? credits.unit === "USD" ? "Prepaid balance" : "Credits" : "Remaining allowance";
|
|
1770
|
+
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>`;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function renderSubscriptionUsageSection() {
|
|
1774
|
+
const ids = Object.keys(SUBSCRIPTION_PROVIDERS).filter((provider) => state.subscriptionUsage?.[provider] || state.subscriptionUsageErrors?.[provider] || state.subscriptionLoadingProviders?.[provider]);
|
|
1775
|
+
const cards = ids.map((provider) => {
|
|
1776
|
+
const meta = SUBSCRIPTION_PROVIDERS[provider];
|
|
1777
|
+
const usage = state.subscriptionUsage?.[provider];
|
|
1778
|
+
const failed = state.subscriptionUsageErrors?.[provider];
|
|
1779
|
+
const loading = state.subscriptionLoadingProviders?.[provider];
|
|
1780
|
+
const credits = usage?.credits;
|
|
1781
|
+
let balance = "—";
|
|
1782
|
+
if (credits?.unlimited) balance = "Unlimited";
|
|
1783
|
+
else if (Number.isFinite(credits?.balance)) balance = formatSubscriptionAmount(credits.balance, credits.unit);
|
|
1784
|
+
else if (credits?.hasCredits) balance = "Available";
|
|
1785
|
+
let limitMessage = "Usage limit reached";
|
|
1786
|
+
if (usage?.limitReason === "workspace_owner_credits_depleted" || usage?.limitReason === "workspace_member_credits_depleted") limitMessage = "Workspace credits are depleted";
|
|
1787
|
+
if (usage?.limitReason === "workspace_owner_usage_limit_reached" || usage?.limitReason === "workspace_member_usage_limit_reached") limitMessage = "Workspace spending limit reached";
|
|
1788
|
+
const stateName = loading ? "loading" : failed || usage?.limitReached ? "warning" : "ready";
|
|
1789
|
+
const status = loading ? "Loading…" : failed ? "Could not load" : usage?.limitReached ? "Limit reached" : "Available";
|
|
1790
|
+
return `
|
|
1791
|
+
<section class="quota-provider-card" data-provider="${provider}" aria-label="${meta.name} subscription quota">
|
|
1792
|
+
<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>
|
|
1793
|
+
<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>
|
|
1794
|
+
${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>`}
|
|
1795
|
+
<div class="quota-card-footer">Subscription allowance · Refresh anytime</div>
|
|
1796
|
+
</section>
|
|
1797
|
+
`;
|
|
1798
|
+
}).join("");
|
|
1799
|
+
const empty = state.subscriptionPageError || (state.subscriptionsLoaded ? "No subscriptions available. Enable a subscription provider and refresh." : "Refresh to load your subscription allowances.");
|
|
1800
|
+
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
1801
|
}
|
|
1795
1802
|
|
|
1796
1803
|
function renderTokenUsageSection() {
|
|
@@ -2685,111 +2692,72 @@
|
|
|
2685
2692
|
<div class="panel text-center py-16 px-4">
|
|
2686
2693
|
<i data-lucide="info" class="mx-auto h-10 w-10" style="color: var(--color-blue);"></i>
|
|
2687
2694
|
<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
|
|
2695
|
+
<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
2696
|
</div>
|
|
2690
2697
|
`;
|
|
2691
2698
|
}
|
|
2692
2699
|
|
|
2693
2700
|
// --- Data Fetching ---
|
|
2694
2701
|
|
|
2702
|
+
async function refreshSubscriptionQuotas(provider) {
|
|
2703
|
+
syncSubscriptionsConnection();
|
|
2704
|
+
try {
|
|
2705
|
+
if (!endpointUrlInput.value.trim()) throw new Error("Endpoint URL cannot be empty.");
|
|
2706
|
+
await fetchSubscriptionQuotas(resolveUsageUrl(endpointUrlInput.value.trim()), provider);
|
|
2707
|
+
} catch (error) {
|
|
2708
|
+
state.subscriptionPageError = getErrorMessage(error);
|
|
2709
|
+
render("subscriptions");
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
async function fetchSubscriptionQuotas(usageUrl, selectedProvider) {
|
|
2714
|
+
if (selectedProvider && !Object.hasOwn(SUBSCRIPTION_PROVIDERS, selectedProvider)) return;
|
|
2715
|
+
if (!selectedProvider) {
|
|
2716
|
+
state.subscriptionRequestId += 1;
|
|
2717
|
+
state.subscriptionUsage = {};
|
|
2718
|
+
state.subscriptionUsageErrors = {};
|
|
2719
|
+
state.subscriptionLoadingProviders = {};
|
|
2720
|
+
}
|
|
2721
|
+
const requestId = state.subscriptionRequestId;
|
|
2722
|
+
const connectionKey = getConnectionKey();
|
|
2723
|
+
state.subscriptionPageError = null;
|
|
2724
|
+
const providers = selectedProvider ? [selectedProvider] : Object.keys(SUBSCRIPTION_PROVIDERS);
|
|
2725
|
+
await Promise.all(providers.map(async (provider) => {
|
|
2726
|
+
const revision = (state.subscriptionRequests[provider] || 0) + 1;
|
|
2727
|
+
state.subscriptionRequests[provider] = revision;
|
|
2728
|
+
delete state.subscriptionUsage[provider];
|
|
2729
|
+
delete state.subscriptionUsageErrors[provider];
|
|
2730
|
+
state.subscriptionLoadingProviders[provider] = true;
|
|
2731
|
+
state.isSubscriptionsLoading = true;
|
|
2732
|
+
render("subscriptions");
|
|
2733
|
+
const isCurrent = () => requestId === state.subscriptionRequestId
|
|
2734
|
+
&& connectionKey === getConnectionKey() && revision === state.subscriptionRequests[provider];
|
|
2735
|
+
try {
|
|
2736
|
+
const usage = await fetchJson(deriveEndpointUrl(usageUrl, `usage/${provider}`));
|
|
2737
|
+
if (!isCurrent()) return;
|
|
2738
|
+
if (usage?.provider !== provider || !Array.isArray(usage.windows)) throw new Error("Invalid subscription quota response");
|
|
2739
|
+
state.subscriptionUsage[provider] = usage;
|
|
2740
|
+
} catch (error) {
|
|
2741
|
+
if (!isCurrent()) return;
|
|
2742
|
+
if (error?.status !== 404) state.subscriptionUsageErrors[provider] = true;
|
|
2743
|
+
} finally {
|
|
2744
|
+
if (isCurrent()) {
|
|
2745
|
+
delete state.subscriptionLoadingProviders[provider];
|
|
2746
|
+
state.isSubscriptionsLoading = Object.keys(state.subscriptionLoadingProviders).length > 0;
|
|
2747
|
+
state.subscriptionsLoaded = true;
|
|
2748
|
+
render("subscriptions");
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}));
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2695
2754
|
/**
|
|
2696
2755
|
* Fetches usage data and token usage data from the specified API endpoint.
|
|
2697
2756
|
*/
|
|
2698
2757
|
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
2758
|
state.isLoading = true;
|
|
2718
|
-
state.error = null;
|
|
2719
|
-
state.tokenUsageDailyError = null;
|
|
2720
|
-
state.tokenUsageSummaryError = null;
|
|
2721
|
-
state.tokenUsageEventsError = null;
|
|
2722
|
-
render();
|
|
2723
|
-
|
|
2724
2759
|
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);
|
|
2760
|
+
await fetchTokenUsageSummaryAndEvents(page);
|
|
2793
2761
|
} finally {
|
|
2794
2762
|
state.isLoading = false;
|
|
2795
2763
|
render("usage");
|
|
@@ -2982,6 +2950,8 @@
|
|
|
2982
2950
|
syncUrlState();
|
|
2983
2951
|
if (state.activeTab === "models") {
|
|
2984
2952
|
void fetchModels();
|
|
2953
|
+
} else if (state.activeTab === "subscriptions") {
|
|
2954
|
+
void refreshSubscriptionQuotas();
|
|
2985
2955
|
} else {
|
|
2986
2956
|
void fetchData();
|
|
2987
2957
|
}
|
|
@@ -2996,6 +2966,11 @@
|
|
|
2996
2966
|
if (!(event.target instanceof Element)) {
|
|
2997
2967
|
return;
|
|
2998
2968
|
}
|
|
2969
|
+
const quotaRefresh = event.target.closest("[data-quota-refresh]");
|
|
2970
|
+
if (quotaRefresh) {
|
|
2971
|
+
void refreshSubscriptionQuotas(quotaRefresh.dataset.quotaRefresh);
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2999
2974
|
const copyButton = event.target.closest("[data-model-copy]");
|
|
3000
2975
|
if (copyButton) {
|
|
3001
2976
|
void copyModelId(copyButton.dataset.modelCopy);
|
|
@@ -3129,6 +3104,8 @@
|
|
|
3129
3104
|
const endpointFromUrl = urlParams.get("endpoint");
|
|
3130
3105
|
const initialEndpoint = endpointFromUrl || DEFAULT_ENDPOINT;
|
|
3131
3106
|
|
|
3107
|
+
const initialTab = urlParams.get("tab");
|
|
3108
|
+
if (["usage", "subscriptions", "models"].includes(initialTab)) state.activeTab = initialTab;
|
|
3132
3109
|
state.tokenUsagePeriod = periodFromUrl;
|
|
3133
3110
|
tokenUsagePeriodSelect.value = periodFromUrl;
|
|
3134
3111
|
endpointUrlInput.value = initialEndpoint;
|
|
@@ -3137,7 +3114,9 @@
|
|
|
3137
3114
|
state.collapsedSections = readCollapsedSections();
|
|
3138
3115
|
|
|
3139
3116
|
if (endpointFromUrl) {
|
|
3140
|
-
void
|
|
3117
|
+
if (state.activeTab === "subscriptions") void refreshSubscriptionQuotas();
|
|
3118
|
+
else if (state.activeTab === "models") void fetchModels();
|
|
3119
|
+
else void fetchData();
|
|
3141
3120
|
} else {
|
|
3142
3121
|
render();
|
|
3143
3122
|
}
|