@oharness/server 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/account-usage.d.ts +29 -0
- package/dist/account-usage.js +56 -0
- package/dist/account-usage.js.map +1 -0
- package/dist/files.d.ts +40 -1
- package/dist/files.js +94 -3
- package/dist/files.js.map +1 -1
- package/dist/i18n.js +126 -2
- package/dist/i18n.js.map +1 -1
- package/dist/page/client/base.js +4 -0
- package/dist/page/client/base.js.map +1 -1
- package/dist/page/client/browser.js +3 -1
- package/dist/page/client/browser.js.map +1 -1
- package/dist/page/client/composer.js +20 -0
- package/dist/page/client/composer.js.map +1 -1
- package/dist/page/client/files.d.ts +7 -0
- package/dist/page/client/files.js +412 -0
- package/dist/page/client/files.js.map +1 -0
- package/dist/page/client/index.js +4 -0
- package/dist/page/client/index.js.map +1 -1
- package/dist/page/client/panels.js +1 -1
- package/dist/page/client/routines.js +19 -1
- package/dist/page/client/routines.js.map +1 -1
- package/dist/page/client/shell.js +12 -1
- package/dist/page/client/shell.js.map +1 -1
- package/dist/page/client/state.js +12 -4
- package/dist/page/client/state.js.map +1 -1
- package/dist/page/client/terminal.d.ts +7 -0
- package/dist/page/client/terminal.js +347 -0
- package/dist/page/client/terminal.js.map +1 -0
- package/dist/page/client/transcript.js +167 -0
- package/dist/page/client/transcript.js.map +1 -1
- package/dist/page/client/usage.js +400 -70
- package/dist/page/client/usage.js.map +1 -1
- package/dist/page/style.js +258 -3
- package/dist/page/style.js.map +1 -1
- package/dist/page.js +35 -1
- package/dist/page.js.map +1 -1
- package/dist/server.js +238 -31
- package/dist/server.js.map +1 -1
- package/dist/terminal.d.ts +34 -0
- package/dist/terminal.js +211 -0
- package/dist/terminal.js.map +1 -0
- package/package.json +9 -8
|
@@ -597,7 +597,11 @@ function dateRangePicker(options) {
|
|
|
597
597
|
* The models drawn are the ones with a share worth a band; the rest are
|
|
598
598
|
* summed into one so the columns still add up to the total above them.
|
|
599
599
|
*/
|
|
600
|
-
function usageChart(body, models) {
|
|
600
|
+
function usageChart(body, models, opts) {
|
|
601
|
+
// How a column is read out, and what its date is: dollars by the day for
|
|
602
|
+
// this machine's transcripts, credits by the day or hour for the account.
|
|
603
|
+
const money = (opts && opts.money) || ((value) => "$" + value.toFixed(2));
|
|
604
|
+
const when = (opts && opts.when) || ((at) => new Date(at).toLocaleDateString());
|
|
601
605
|
const series = body.series || {};
|
|
602
606
|
const buckets = series.buckets || [];
|
|
603
607
|
const card = el("div", "row-card chart-card");
|
|
@@ -683,9 +687,10 @@ function usageChart(body, models) {
|
|
|
683
687
|
// ends of the window. Rendered as text beside the plot rather than as axis
|
|
684
688
|
// ticks, which at this size would be four numbers fighting for two pixels.
|
|
685
689
|
const scale = el("div", "chart-scale");
|
|
686
|
-
const reading = el("span", null,
|
|
690
|
+
const reading = el("span", null, money(peak));
|
|
687
691
|
scale.appendChild(reading);
|
|
688
|
-
scale.appendChild(el("span", "muted",
|
|
692
|
+
scale.appendChild(el("span", "muted",
|
|
693
|
+
t(series.step === "week" ? "usage.perWeek" : series.step === "hour" ? "usage.perHour" : "usage.perDay")));
|
|
689
694
|
card.appendChild(scale);
|
|
690
695
|
|
|
691
696
|
/*
|
|
@@ -710,21 +715,24 @@ function usageChart(body, models) {
|
|
|
710
715
|
const top = lines
|
|
711
716
|
.map((line) => ({ model: line.model, cost: line.points[at] || 0 }))
|
|
712
717
|
.sort((a, b) => b.cost - a.cost)[0];
|
|
713
|
-
const on =
|
|
718
|
+
const on = when(bucket.at);
|
|
714
719
|
reading.textContent = bucket.costUsd
|
|
715
|
-
? on + " ·
|
|
720
|
+
? on + " · " + money(bucket.costUsd) + (top && top.cost ? " · " + top.model : "")
|
|
716
721
|
: on + " · " + t("usage.nothing");
|
|
717
722
|
});
|
|
718
723
|
svg.addEventListener("mouseleave", () => {
|
|
719
724
|
highlight.setAttribute("opacity", "0");
|
|
720
|
-
reading.textContent =
|
|
725
|
+
reading.textContent = money(peak);
|
|
721
726
|
});
|
|
722
727
|
|
|
723
728
|
const span = el("div", "chart-span muted small");
|
|
724
|
-
span.appendChild(el("span", null,
|
|
725
|
-
span.appendChild(el("span", null,
|
|
729
|
+
span.appendChild(el("span", null, when(buckets[0].at)));
|
|
730
|
+
span.appendChild(el("span", null, when(buckets[buckets.length - 1].at)));
|
|
726
731
|
card.appendChild(span);
|
|
727
732
|
|
|
733
|
+
// One band is its own caption: the model is already named in the list
|
|
734
|
+
// under the chart and in the readout above, and a legend of one is noise.
|
|
735
|
+
if (lines.length < 2) return card;
|
|
728
736
|
const legend = el("div", "chart-legend");
|
|
729
737
|
lines.forEach((line, index) => {
|
|
730
738
|
const entry = el("span", "legend-item");
|
|
@@ -736,18 +744,34 @@ function usageChart(body, models) {
|
|
|
736
744
|
return card;
|
|
737
745
|
}
|
|
738
746
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
747
|
+
/**
|
|
748
|
+
* Whose figures the page shows: the account's ledger, or this machine's
|
|
749
|
+
* transcripts. Null until someone picks, which means the account when there
|
|
750
|
+
* is one — that is where every charge is, and this machine's transcripts are
|
|
751
|
+
* only the chat that happened on it.
|
|
752
|
+
*/
|
|
753
|
+
let usageSource = null;
|
|
754
|
+
/** Whether the last look found an account, so this machine's view can offer the other. */
|
|
755
|
+
let usageSignedIn = false;
|
|
756
|
+
/** Which kind of spend the account view is narrowed to. */
|
|
757
|
+
let usageKind = "all";
|
|
758
|
+
const USAGE_KINDS = ["all", "text", "image", "video", "audio"];
|
|
749
759
|
|
|
750
|
-
|
|
760
|
+
/**
|
|
761
|
+
* The window being asked about, as a query on path.
|
|
762
|
+
*
|
|
763
|
+
* The days ride along only when they are the window being asked for; the
|
|
764
|
+
* server ignores them otherwise, and a preset request that carried a stale
|
|
765
|
+
* pair would be one keystroke from answering for the wrong fortnight.
|
|
766
|
+
*/
|
|
767
|
+
function usageQuery(path) {
|
|
768
|
+
return usageRange === "custom" && usageFrom && usageTo
|
|
769
|
+
? path + "?range=custom&from=" + encodeURIComponent(usageFrom) + "&to=" + encodeURIComponent(usageTo)
|
|
770
|
+
: path + "?range=" + encodeURIComponent(usageRange);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** The page's heading, its way back, and — for an account — whose figures these are. */
|
|
774
|
+
function usageHead(panel) {
|
|
751
775
|
const head = el("div", "row-head");
|
|
752
776
|
head.appendChild(el("h2", null, t("usage.title")));
|
|
753
777
|
// Back to where this was opened from, because it was opened from a row in
|
|
@@ -759,29 +783,45 @@ async function showUsage() {
|
|
|
759
783
|
setNav("nav-settings");
|
|
760
784
|
void showSettings();
|
|
761
785
|
});
|
|
762
|
-
head.appendChild(back);
|
|
763
|
-
panel.appendChild(head);
|
|
764
|
-
|
|
765
786
|
/*
|
|
766
|
-
*
|
|
787
|
+
* Whose figures these are, as a small switch in the heading rather than a
|
|
788
|
+
* third row of tabs.
|
|
767
789
|
*
|
|
768
|
-
*
|
|
769
|
-
* are
|
|
770
|
-
*
|
|
771
|
-
* filtering what is already here — the totals, the models and the sessions
|
|
772
|
-
* all answer for the window, and a page where the chart moved and the
|
|
773
|
-
* figures above it did not would be lying about one of them.
|
|
790
|
+
* The account is the answer almost every visit wants; this machine's
|
|
791
|
+
* transcripts are the second look, for chat on a key someone brought. A
|
|
792
|
+
* switch as loud as the window and the kind would ask to be read first.
|
|
774
793
|
*/
|
|
794
|
+
const actions = el("span", "usage-head-actions");
|
|
795
|
+
if (usageSignedIn) {
|
|
796
|
+
const chosen = usageSource || "account";
|
|
797
|
+
const box = el("div", "seg usage-source");
|
|
798
|
+
for (const id of ["account", "device"]) {
|
|
799
|
+
const button = el("button", "seg-item" + (id === chosen ? " on" : ""), t("usage.source." + id));
|
|
800
|
+
button.type = "button";
|
|
801
|
+
button.addEventListener("click", () => {
|
|
802
|
+
if (id === chosen) return;
|
|
803
|
+
usageSource = id;
|
|
804
|
+
void showUsage();
|
|
805
|
+
});
|
|
806
|
+
box.appendChild(button);
|
|
807
|
+
}
|
|
808
|
+
actions.appendChild(box);
|
|
809
|
+
}
|
|
810
|
+
actions.appendChild(back);
|
|
811
|
+
head.appendChild(actions);
|
|
812
|
+
panel.appendChild(head);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** The window presets and the calendar, reloading the page on a change. */
|
|
816
|
+
function usageWindowRow(body, presets) {
|
|
775
817
|
const picked = body.range || "30d";
|
|
776
818
|
// What the server says the window is, which is not always what was asked
|
|
777
819
|
// for: a picked range is clamped to today and to a year, and the calendar
|
|
778
820
|
// has to open on the days the figures beside it are actually counting.
|
|
779
821
|
const today = new Date().toISOString().slice(0, 10);
|
|
780
|
-
const
|
|
781
|
-
const shownTo = body.to || today;
|
|
782
|
-
const windowRow = el("div", "usage-window");
|
|
822
|
+
const row = el("div", "usage-window");
|
|
783
823
|
const ranges = el("div", "seg");
|
|
784
|
-
for (const id of
|
|
824
|
+
for (const id of presets) {
|
|
785
825
|
const button = el("button", "seg-item" + (id === picked ? " on" : ""), t("usage.range." + id));
|
|
786
826
|
button.type = "button";
|
|
787
827
|
button.addEventListener("click", () => {
|
|
@@ -791,20 +831,10 @@ async function showUsage() {
|
|
|
791
831
|
});
|
|
792
832
|
ranges.appendChild(button);
|
|
793
833
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
* Beside them rather than as a fifth item on the same control: the presets
|
|
799
|
-
* are one click each and this one opens a calendar, and a segmented control
|
|
800
|
-
* whose last segment behaves differently from the other four is a control
|
|
801
|
-
* that has to be learned. It shows the days it is showing once it is the
|
|
802
|
-
* chosen window, so the page never has a range on it that nothing on screen
|
|
803
|
-
* names.
|
|
804
|
-
*/
|
|
805
|
-
windowRow.appendChild(dateRangePicker({
|
|
806
|
-
from: shownFrom,
|
|
807
|
-
to: shownTo,
|
|
834
|
+
row.appendChild(ranges);
|
|
835
|
+
row.appendChild(dateRangePicker({
|
|
836
|
+
from: body.from || today,
|
|
837
|
+
to: body.to || today,
|
|
808
838
|
max: today,
|
|
809
839
|
maxDays: MAX_CUSTOM_DAYS,
|
|
810
840
|
active: picked === "custom",
|
|
@@ -815,7 +845,315 @@ async function showUsage() {
|
|
|
815
845
|
void showUsage();
|
|
816
846
|
},
|
|
817
847
|
}));
|
|
818
|
-
|
|
848
|
+
return row;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** Credits as a bill reads them: at most two decimals, and no trailing zeros. */
|
|
852
|
+
const usageCredits = (micro) => t("usage.credits", { n: creditsOf(micro) });
|
|
853
|
+
const usageCalls = (n) => (n === 1 ? t("usage.callOne") : t("usage.calls", { n: n.toLocaleString(LOCALE) }));
|
|
854
|
+
const usageSeconds = (n) => t("usage.seconds", { n: Math.round(n).toLocaleString(LOCALE) });
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* The account's spend: chat from every device, and every image, video and
|
|
858
|
+
* clip of audio, from the same ledger the site's usage page reads.
|
|
859
|
+
*
|
|
860
|
+
* Split by kind the way the site splits it, because a credit spent on a video
|
|
861
|
+
* and one spent on a chat are the same money bought very differently — and
|
|
862
|
+
* the question someone opening this after a week of generating is asking is
|
|
863
|
+
* "what did the pictures cost", which a list of forty models does not answer.
|
|
864
|
+
*/
|
|
865
|
+
function showAccountUsage(body) {
|
|
866
|
+
panelBody.replaceChildren();
|
|
867
|
+
const panel = el("div", "panel usage");
|
|
868
|
+
usageHead(panel);
|
|
869
|
+
|
|
870
|
+
if (body.unavailable) {
|
|
871
|
+
panel.appendChild(el("p", "muted",
|
|
872
|
+
t(body.unavailable === "outdated" ? "usage.accountOutdated" : "usage.accountUnreachable")));
|
|
873
|
+
if (body.error) panel.appendChild(el("p", "muted small", body.error));
|
|
874
|
+
panelBody.appendChild(panel);
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
panel.appendChild(usageWindowRow(body, ["24h", "7d", "30d", "90d"]));
|
|
879
|
+
const summary = body.summary || {};
|
|
880
|
+
const byKind = summary.byKind || {};
|
|
881
|
+
const kind = body.kind || "all";
|
|
882
|
+
|
|
883
|
+
/*
|
|
884
|
+
* The kinds, as the figures themselves.
|
|
885
|
+
*
|
|
886
|
+
* Each card is what that kind cost and how often it ran, and choosing one is
|
|
887
|
+
* the filter — so the split is read before anything is chosen, and there is
|
|
888
|
+
* no row of tabs repeating the numbers under it. What the chosen kind was
|
|
889
|
+
* counted in (tokens, pictures, seconds) is the line under the cards; the
|
|
890
|
+
* others are not known until they are chosen, because the ledger is read
|
|
891
|
+
* for one kind at a time.
|
|
892
|
+
*/
|
|
893
|
+
const cards = el("div", "usage-kinds");
|
|
894
|
+
const total = Object.values(byKind).reduce((sum, each) => sum + (each.spentMicro || 0), 0);
|
|
895
|
+
const allCalls = Object.values(byKind).reduce((sum, each) => sum + (each.calls || 0), 0);
|
|
896
|
+
for (const id of USAGE_KINDS) {
|
|
897
|
+
const each = id === "all" ? { spentMicro: total, calls: allCalls } : byKind[id] || {};
|
|
898
|
+
const card = el("button", "usage-kind" + (id === kind ? " on" : "") + (each.spentMicro || each.calls ? "" : " empty"));
|
|
899
|
+
card.type = "button";
|
|
900
|
+
card.setAttribute("aria-pressed", id === kind ? "true" : "false");
|
|
901
|
+
card.appendChild(el("span", "usage-kind-label", t("usage.kind." + id)));
|
|
902
|
+
card.appendChild(el("span", "usage-kind-value", creditsOf(each.spentMicro || 0)));
|
|
903
|
+
card.appendChild(el("span", "usage-kind-sub", usageCalls(each.calls || 0)));
|
|
904
|
+
card.addEventListener("click", () => {
|
|
905
|
+
if (id === kind) return;
|
|
906
|
+
usageKind = id;
|
|
907
|
+
void showUsage();
|
|
908
|
+
});
|
|
909
|
+
cards.appendChild(card);
|
|
910
|
+
}
|
|
911
|
+
panel.appendChild(cards);
|
|
912
|
+
|
|
913
|
+
const media = summary.media || {};
|
|
914
|
+
const tokens = (summary.inputTokens || 0) + (summary.outputTokens || 0);
|
|
915
|
+
const counted = [];
|
|
916
|
+
if (tokens) {
|
|
917
|
+
counted.push(t("usage.tokens", { n: formatTokens(tokens) }) + " (" + t("usage.inOut", {
|
|
918
|
+
in: formatTokens(summary.inputTokens || 0),
|
|
919
|
+
out: formatTokens(summary.outputTokens || 0),
|
|
920
|
+
}) + ")");
|
|
921
|
+
}
|
|
922
|
+
if (media.images) counted.push(t("usage.images", { n: media.images.toLocaleString(LOCALE) }));
|
|
923
|
+
if (media.videoSeconds) counted.push(t("usage.videoSeconds", { n: usageSeconds(media.videoSeconds) }));
|
|
924
|
+
if (media.audioSeconds) counted.push(t("usage.audioSeconds", { n: usageSeconds(media.audioSeconds) }));
|
|
925
|
+
if (media.characters) counted.push(t("usage.characters", { n: media.characters.toLocaleString(LOCALE) }));
|
|
926
|
+
if (counted.length) panel.appendChild(el("div", "muted small usage-counted", counted.join(" · ")));
|
|
927
|
+
|
|
928
|
+
const series = summary.series || [];
|
|
929
|
+
if (series.length) {
|
|
930
|
+
const hourly = (summary.days || []).some((day) => day.at.length > 10);
|
|
931
|
+
const chartBody = {
|
|
932
|
+
series: {
|
|
933
|
+
step: hourly ? "hour" : "day",
|
|
934
|
+
buckets: (summary.days || []).map((day) => ({
|
|
935
|
+
at: day.at,
|
|
936
|
+
costUsd: (day.spentMicro || 0) / 1000000,
|
|
937
|
+
models: Object.fromEntries(Object.entries(day.byKey || {}).map(([key, value]) => [key, value / 1000000])),
|
|
938
|
+
})),
|
|
939
|
+
},
|
|
940
|
+
};
|
|
941
|
+
panel.appendChild(usageChart(chartBody, series.map((entry) => ({ model: entry.key })), {
|
|
942
|
+
money: (value) => usageCredits(value * 1000000),
|
|
943
|
+
// The ledger's buckets are UTC days and hours, and are read as such: a
|
|
944
|
+
// day drawn in local time would put yesterday's evening under today.
|
|
945
|
+
when: (at) => at.length > 10
|
|
946
|
+
? new Date(at + ":00:00Z").toLocaleString(LOCALE, { month: "short", day: "numeric", hour: "numeric" })
|
|
947
|
+
: new Date(at + "T00:00:00Z").toLocaleDateString(LOCALE, { timeZone: "UTC" }),
|
|
948
|
+
}));
|
|
949
|
+
|
|
950
|
+
// By model, and under "all" by kind first: forty models in one list is a
|
|
951
|
+
// list nobody reads, and the kind is the first thing anyone looks for.
|
|
952
|
+
const groups = kind === "all" ? USAGE_KINDS.slice(1) : [kind];
|
|
953
|
+
for (const group of groups) {
|
|
954
|
+
const rows = series.filter((entry) => entry.kind === group);
|
|
955
|
+
if (!rows.length) continue;
|
|
956
|
+
const title = el("h3", "section-title usage-kind-title");
|
|
957
|
+
title.appendChild(el("span", null, kind === "all" ? t("usage.kind." + group) : t("usage.byModel")));
|
|
958
|
+
title.appendChild(el("span", "muted small",
|
|
959
|
+
usageCredits(rows.reduce((sum, entry) => sum + (entry.spentMicro || 0), 0))));
|
|
960
|
+
panel.appendChild(title);
|
|
961
|
+
for (const entry of rows) {
|
|
962
|
+
const row = el("div", "row-card");
|
|
963
|
+
const line = el("div", "row-head");
|
|
964
|
+
line.appendChild(el("span", "row-title", entry.key));
|
|
965
|
+
line.appendChild(el("span", "muted small usage-amount", usageCredits(entry.spentMicro || 0)));
|
|
966
|
+
row.appendChild(line);
|
|
967
|
+
row.appendChild(el("div", "muted small", usageCalls(entry.calls || 0)));
|
|
968
|
+
panel.appendChild(row);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
} else {
|
|
972
|
+
panel.appendChild(el("p", "muted", t(kind === "all" ? "usage.none" : "usage.kindNone")));
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// What the numbers cover, and where the full ledger lives.
|
|
976
|
+
panel.appendChild(el("p", "muted small", t(body.team ? "usage.teamScope" : "usage.accountScope")));
|
|
977
|
+
if (body.site) {
|
|
978
|
+
const open = el("button", "ghost", t("usage.openSite"));
|
|
979
|
+
open.type = "button";
|
|
980
|
+
open.addEventListener("click", () =>
|
|
981
|
+
window.open(body.site + "/" + LOCALE + "/dashboard/usage", "_blank", "noopener"));
|
|
982
|
+
panel.appendChild(open);
|
|
983
|
+
}
|
|
984
|
+
panelBody.appendChild(panel);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/** How many sessions the usage page lists before "View more". */
|
|
988
|
+
const USAGE_RECENT = 10;
|
|
989
|
+
/** Sessions per request on the sessions page. */
|
|
990
|
+
const USAGE_SESSIONS_PAGE = 50;
|
|
991
|
+
|
|
992
|
+
/** Prompt tokens sent, and everything moved, for a session row. */
|
|
993
|
+
const usageSent = (row) => (row.input || 0) + (row.cacheRead || 0) + (row.cacheWrite || 0);
|
|
994
|
+
const usageMoved = (row) => usageSent(row) + (row.output || 0);
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* One session: what it cost, when, on what, and how much of it the cache
|
|
998
|
+
* carried — and a way back into it, which is what someone scanning a list of
|
|
999
|
+
* what they spent usually wants next.
|
|
1000
|
+
*/
|
|
1001
|
+
function usageSessionRow(entry) {
|
|
1002
|
+
const row = el("div", "row-card usage-session");
|
|
1003
|
+
const line = el("div", "row-head");
|
|
1004
|
+
line.appendChild(el("span", "row-title", entry.title || t("session.untitled")));
|
|
1005
|
+
if (entry.current) line.appendChild(el("span", "tag", t("usage.thisSession")));
|
|
1006
|
+
line.appendChild(el("span", "muted small usage-amount", "$" + (entry.costUsd || 0).toFixed(4)));
|
|
1007
|
+
row.appendChild(line);
|
|
1008
|
+
const detail = [ago(entry.updatedAt), entry.model, t("usage.tokens", { n: formatTokens(usageMoved(entry)) })];
|
|
1009
|
+
// The reason one session cost what it did is usually here: two runs of the
|
|
1010
|
+
// same length differ by what the cache carried.
|
|
1011
|
+
if (entry.cacheRead) detail.push(t("usage.cached", { pct: Math.round((entry.cacheRead / usageSent(entry)) * 100) }));
|
|
1012
|
+
row.appendChild(el("div", "muted small", detail.join(" · ")));
|
|
1013
|
+
row.tabIndex = 0;
|
|
1014
|
+
row.setAttribute("role", "button");
|
|
1015
|
+
const open = () => {
|
|
1016
|
+
setNav(null);
|
|
1017
|
+
if (panes.has(entry.id)) void focusTab(entry.id);
|
|
1018
|
+
else void openSession(entry.id, entry.cwd);
|
|
1019
|
+
};
|
|
1020
|
+
row.addEventListener("click", open);
|
|
1021
|
+
row.addEventListener("keydown", (event) => { if (event.key === "Enter") open(); });
|
|
1022
|
+
return row;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/** What the sessions page is searching for, kept while the page is open. */
|
|
1026
|
+
let usageSessionsQuery = "";
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Every session that spent something on this machine, newest first.
|
|
1030
|
+
*
|
|
1031
|
+
* Read a page at a time and drawn as the list is scrolled, because a machine
|
|
1032
|
+
* used daily has thousands and the first screen is the one almost always
|
|
1033
|
+
* wanted. The search runs on the server, so it finds sessions that have not
|
|
1034
|
+
* been loaded yet rather than filtering the ones that have.
|
|
1035
|
+
*/
|
|
1036
|
+
async function showUsageSessions() {
|
|
1037
|
+
panelBody.replaceChildren();
|
|
1038
|
+
const panel = el("div", "panel usage");
|
|
1039
|
+
const head = el("div", "row-head");
|
|
1040
|
+
head.appendChild(el("h2", null, t("usage.allSessions")));
|
|
1041
|
+
const back = el("button", "ghost", t("usage.backToUsage"));
|
|
1042
|
+
back.type = "button";
|
|
1043
|
+
back.addEventListener("click", () => {
|
|
1044
|
+
setNav("usage");
|
|
1045
|
+
void showUsage();
|
|
1046
|
+
});
|
|
1047
|
+
head.appendChild(back);
|
|
1048
|
+
panel.appendChild(head);
|
|
1049
|
+
|
|
1050
|
+
const search = el("input", "art-search");
|
|
1051
|
+
search.type = "search";
|
|
1052
|
+
search.placeholder = t("usage.searchSessions");
|
|
1053
|
+
search.spellcheck = false;
|
|
1054
|
+
search.autocomplete = "off";
|
|
1055
|
+
search.value = usageSessionsQuery;
|
|
1056
|
+
panel.appendChild(search);
|
|
1057
|
+
|
|
1058
|
+
const count = el("div", "muted small usage-sessions-count");
|
|
1059
|
+
panel.appendChild(count);
|
|
1060
|
+
const list = el("div", "usage-sessions");
|
|
1061
|
+
panel.appendChild(list);
|
|
1062
|
+
const foot = el("div", "muted small usage-sessions-foot");
|
|
1063
|
+
panel.appendChild(foot);
|
|
1064
|
+
panelBody.appendChild(panel);
|
|
1065
|
+
|
|
1066
|
+
// Which search the rows on screen belong to. A reply to an older search
|
|
1067
|
+
// that lands after a newer one started is dropped, not appended.
|
|
1068
|
+
let generation = 0;
|
|
1069
|
+
let offset = 0;
|
|
1070
|
+
let more = true;
|
|
1071
|
+
let loading = false;
|
|
1072
|
+
|
|
1073
|
+
async function load() {
|
|
1074
|
+
if (loading || !more) return;
|
|
1075
|
+
loading = true;
|
|
1076
|
+
const asked = generation;
|
|
1077
|
+
foot.textContent = t("usage.loading");
|
|
1078
|
+
const body = await getJson("/usage/sessions?limit=" + USAGE_SESSIONS_PAGE + "&offset=" + offset +
|
|
1079
|
+
(usageSessionsQuery ? "&q=" + encodeURIComponent(usageSessionsQuery) : ""));
|
|
1080
|
+
loading = false;
|
|
1081
|
+
if (asked !== generation || shownPanel !== "usage-sessions") return;
|
|
1082
|
+
if (body.error) {
|
|
1083
|
+
foot.textContent = body.error;
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
for (const entry of body.sessions || []) list.appendChild(usageSessionRow(entry));
|
|
1087
|
+
offset += (body.sessions || []).length;
|
|
1088
|
+
more = Boolean(body.more);
|
|
1089
|
+
count.textContent = body.total === 1 ? t("usage.sessionsOne") : t("usage.sessionsCount", { n: body.total || 0 });
|
|
1090
|
+
foot.textContent = !body.total
|
|
1091
|
+
? t(usageSessionsQuery ? "usage.sessionsNoMatch" : "usage.none")
|
|
1092
|
+
: more ? "" : body.truncated ? t("usage.sessionsCapped") : t("usage.sessionsEnd");
|
|
1093
|
+
// A first page shorter than the window leaves nothing to scroll, so the
|
|
1094
|
+
// observer below would never fire; ask again until the list fills it.
|
|
1095
|
+
if (more && sentinel.getBoundingClientRect().top < panelView.clientHeight + 400) void load();
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function restart() {
|
|
1099
|
+
generation += 1;
|
|
1100
|
+
offset = 0;
|
|
1101
|
+
more = true;
|
|
1102
|
+
loading = false;
|
|
1103
|
+
list.replaceChildren();
|
|
1104
|
+
void load();
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Loads the next page as the bottom of the list comes near, not when it is
|
|
1108
|
+
// reached, so scrolling does not stop at a spinner.
|
|
1109
|
+
const sentinel = el("div", "usage-sessions-sentinel");
|
|
1110
|
+
panel.appendChild(sentinel);
|
|
1111
|
+
const observer = new IntersectionObserver((entries) => {
|
|
1112
|
+
if (shownPanel !== "usage-sessions") return void observer.disconnect();
|
|
1113
|
+
if (entries.some((entry) => entry.isIntersecting)) void load();
|
|
1114
|
+
}, { root: panelView, rootMargin: "0px 0px 400px 0px" });
|
|
1115
|
+
observer.observe(sentinel);
|
|
1116
|
+
|
|
1117
|
+
let typing = null;
|
|
1118
|
+
search.addEventListener("input", () => {
|
|
1119
|
+
clearTimeout(typing);
|
|
1120
|
+
typing = setTimeout(() => {
|
|
1121
|
+
usageSessionsQuery = search.value.trim();
|
|
1122
|
+
restart();
|
|
1123
|
+
}, 250);
|
|
1124
|
+
});
|
|
1125
|
+
restart();
|
|
1126
|
+
search.focus();
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
async function showUsage() {
|
|
1130
|
+
if (usageSource !== "device") {
|
|
1131
|
+
const account = await getJson(
|
|
1132
|
+
usageQuery("/usage/account") + (usageKind !== "all" ? "&kind=" + encodeURIComponent(usageKind) : ""),
|
|
1133
|
+
);
|
|
1134
|
+
if (shownPanel !== "usage") return;
|
|
1135
|
+
usageSignedIn = Boolean(account.signedIn);
|
|
1136
|
+
if (account.signedIn) return void showAccountUsage(account);
|
|
1137
|
+
}
|
|
1138
|
+
const body = await getJson(usageQuery("/usage"));
|
|
1139
|
+
if (shownPanel !== "usage") return;
|
|
1140
|
+
panelBody.replaceChildren();
|
|
1141
|
+
if (body.error) return void panelBody.appendChild(panelError(body.error));
|
|
1142
|
+
|
|
1143
|
+
const panel = el("div", "panel usage");
|
|
1144
|
+
usageHead(panel);
|
|
1145
|
+
|
|
1146
|
+
/*
|
|
1147
|
+
* How far back the page is asking about.
|
|
1148
|
+
*
|
|
1149
|
+
* A segmented control rather than a dropdown: there are four answers, they
|
|
1150
|
+
* are read left to right as increasing time, and a menu would hide three of
|
|
1151
|
+
* them behind the one that happens to be chosen. It reloads rather than
|
|
1152
|
+
* filtering what is already here — the totals, the models and the sessions
|
|
1153
|
+
* all answer for the window, and a page where the chart moved and the
|
|
1154
|
+
* figures above it did not would be lying about one of them.
|
|
1155
|
+
*/
|
|
1156
|
+
panel.appendChild(usageWindowRow(body, ["7d", "30d", "90d", "all"]));
|
|
819
1157
|
|
|
820
1158
|
// Prompt tokens only. The cache share is a share of what was sent, and
|
|
821
1159
|
// nothing that was generated could have come from a cache.
|
|
@@ -881,7 +1219,7 @@ async function showUsage() {
|
|
|
881
1219
|
const row = el("div", "row-card");
|
|
882
1220
|
const line = el("div", "row-head");
|
|
883
1221
|
line.appendChild(el("span", "row-title", entry.model));
|
|
884
|
-
line.appendChild(el("span", "muted small", "$" + (entry.costUsd || 0).toFixed(2)));
|
|
1222
|
+
line.appendChild(el("span", "muted small usage-amount", "$" + (entry.costUsd || 0).toFixed(2)));
|
|
885
1223
|
row.appendChild(line);
|
|
886
1224
|
const bits = [
|
|
887
1225
|
t("usage.tokens", { n: formatTokens(moved(entry)) }),
|
|
@@ -896,27 +1234,19 @@ async function showUsage() {
|
|
|
896
1234
|
|
|
897
1235
|
const sessions = body.sessions || [];
|
|
898
1236
|
if (sessions.length) {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
// The reason one session cost what it did is usually here: two runs of
|
|
913
|
-
// the same length differ by what the cache carried, and per-session is
|
|
914
|
-
// the only place that comparison can be made.
|
|
915
|
-
const hit = cachePct(entry);
|
|
916
|
-
if (hit !== null) detail.push(t("usage.cached", { pct: hit }));
|
|
917
|
-
row.appendChild(el("div", "muted small", detail.join(" · ")));
|
|
918
|
-
panel.appendChild(row);
|
|
919
|
-
}
|
|
1237
|
+
// The latest few, and a way to all of them: a page of rows here pushed
|
|
1238
|
+
// the scope note below off the screen and still was not every session.
|
|
1239
|
+
const title = el("div", "usage-section-head");
|
|
1240
|
+
title.appendChild(el("h3", "section-title", t("usage.bySession")));
|
|
1241
|
+
const more = el("button", "ghost", t("usage.moreSessions"));
|
|
1242
|
+
more.type = "button";
|
|
1243
|
+
more.addEventListener("click", () => {
|
|
1244
|
+
setNav("usage-sessions");
|
|
1245
|
+
void showUsageSessions();
|
|
1246
|
+
});
|
|
1247
|
+
title.appendChild(more);
|
|
1248
|
+
panel.appendChild(title);
|
|
1249
|
+
for (const entry of sessions.slice(0, USAGE_RECENT)) panel.appendChild(usageSessionRow(entry));
|
|
920
1250
|
}
|
|
921
1251
|
|
|
922
1252
|
if (!models.length) panel.appendChild(el("p", "muted", t("usage.none")));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../../../src/page/client/usage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../../../src/page/client/usage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA4vC3B,CAAC"}
|