mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-13 14:23:58 +00:00
fix(webui): align token usage heatmap
This commit is contained in:
parent
631fdb4a46
commit
7510918610
@ -1666,7 +1666,7 @@ function OverviewSettings({
|
||||
return (
|
||||
<div className="space-y-7">
|
||||
<section>
|
||||
<TokenUsageHeatmap usage={settings.usage} />
|
||||
<TokenUsageHeatmap usage={settings.usage} timeZone={settings.agent.timezone} />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
||||
@ -24,15 +24,16 @@ type TokenUsageMonthLabel = {
|
||||
label: string;
|
||||
column: number;
|
||||
};
|
||||
type CalendarDayParts = {
|
||||
year: string;
|
||||
month: string;
|
||||
day: string;
|
||||
};
|
||||
|
||||
const TOKEN_HEATMAP_CELLS = 371;
|
||||
const TOKEN_HEATMAP_COLUMNS = Math.ceil(TOKEN_HEATMAP_CELLS / 7);
|
||||
const TOKEN_USAGE_SOURCE_ORDER = ["user", "api", "cron", "dream", "system"] as const;
|
||||
|
||||
function startOfUtcDay(date: Date): Date {
|
||||
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
||||
}
|
||||
|
||||
function addUtcDays(date: Date, days: number): Date {
|
||||
const next = new Date(date);
|
||||
next.setUTCDate(next.getUTCDate() + days);
|
||||
@ -43,12 +44,56 @@ function isoDay(date: Date): string {
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function utcDateFromIsoDay(day: string): Date {
|
||||
const [year, month, date] = day.split("-").map(Number);
|
||||
return new Date(Date.UTC(year, month - 1, date));
|
||||
}
|
||||
|
||||
function utcDayParts(date: Date): CalendarDayParts {
|
||||
return {
|
||||
year: String(date.getUTCFullYear()).padStart(4, "0"),
|
||||
month: String(date.getUTCMonth() + 1).padStart(2, "0"),
|
||||
day: String(date.getUTCDate()).padStart(2, "0"),
|
||||
};
|
||||
}
|
||||
|
||||
function dayPartsForTimeZone(date: Date, timeZone: string | undefined): CalendarDayParts {
|
||||
if (!timeZone) return utcDayParts(date);
|
||||
try {
|
||||
const parts = new Intl.DateTimeFormat("en", {
|
||||
calendar: "gregory",
|
||||
numberingSystem: "latn",
|
||||
timeZone,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}).formatToParts(date);
|
||||
const values = Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
||||
if (values.year && values.month && values.day) {
|
||||
return {
|
||||
year: values.year.padStart(4, "0"),
|
||||
month: values.month.padStart(2, "0"),
|
||||
day: values.day.padStart(2, "0"),
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fall through to UTC when the browser cannot resolve the configured timezone.
|
||||
}
|
||||
return utcDayParts(date);
|
||||
}
|
||||
|
||||
function todayIsoDay(timeZone: string | undefined): string {
|
||||
const parts = dayPartsForTimeZone(new Date(), timeZone);
|
||||
return `${parts.year}-${parts.month}-${parts.day}`;
|
||||
}
|
||||
|
||||
function buildTokenUsageCalendar(
|
||||
days: TokenUsageDay[] | undefined,
|
||||
monthFormatter: Intl.DateTimeFormat,
|
||||
timeZone: string | undefined,
|
||||
): { cells: TokenUsageCell[]; monthLabels: TokenUsageMonthLabel[] } {
|
||||
const byDate = new Map((days ?? []).map((day) => [day.date, day]));
|
||||
const today = startOfUtcDay(new Date());
|
||||
const today = utcDateFromIsoDay(todayIsoDay(timeZone));
|
||||
const end = addUtcDays(today, 6 - today.getUTCDay());
|
||||
const start = addUtcDays(end, -(TOKEN_HEATMAP_CELLS - 1));
|
||||
const seenMonths = new Set<string>();
|
||||
@ -131,7 +176,13 @@ function tokenUsageCellClass(level: number, future: boolean): string {
|
||||
return "bg-neutral-200/70 ring-1 ring-black/[0.025] dark:bg-white/[0.08] dark:ring-white/[0.035]";
|
||||
}
|
||||
|
||||
export function TokenUsageHeatmap({ usage }: { usage?: TokenUsagePayload }) {
|
||||
export function TokenUsageHeatmap({
|
||||
usage,
|
||||
timeZone,
|
||||
}: {
|
||||
usage?: TokenUsagePayload;
|
||||
timeZone?: string;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const tx = (key: string, fallback: string, values?: Record<string, unknown>) =>
|
||||
t(key, { defaultValue: fallback, ...(values ?? {}) });
|
||||
@ -140,8 +191,8 @@ export function TokenUsageHeatmap({ usage }: { usage?: TokenUsagePayload }) {
|
||||
[i18n.language],
|
||||
);
|
||||
const { cells, monthLabels } = useMemo(
|
||||
() => buildTokenUsageCalendar(usage?.days, monthFormatter),
|
||||
[monthFormatter, usage?.days],
|
||||
() => buildTokenUsageCalendar(usage?.days, monthFormatter, timeZone),
|
||||
[monthFormatter, timeZone, usage?.days],
|
||||
);
|
||||
const maxTokens = Math.max(0, ...cells.map((cell) => cell.total));
|
||||
|
||||
@ -154,14 +205,14 @@ export function TokenUsageHeatmap({ usage }: { usage?: TokenUsagePayload }) {
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="mb-2 grid h-4 gap-1.5 text-[10px] font-normal leading-4 text-muted-foreground/62"
|
||||
className="mb-2 grid min-h-4 gap-1.5 text-[10px] font-normal leading-4 text-muted-foreground/62"
|
||||
style={{ gridTemplateColumns: `repeat(${TOKEN_HEATMAP_COLUMNS}, minmax(0, 1fr))` }}
|
||||
aria-hidden
|
||||
>
|
||||
{monthLabels.map((month) => (
|
||||
<span
|
||||
key={`${month.label}-${month.column}`}
|
||||
className="truncate"
|
||||
className="overflow-visible whitespace-nowrap"
|
||||
style={{ gridColumnStart: month.column, gridColumnEnd: "span 4" }}
|
||||
>
|
||||
{month.label}
|
||||
|
||||
@ -119,6 +119,7 @@ const installedAnyGen = {
|
||||
function renderSettingsView(
|
||||
options: {
|
||||
initialSection?: "overview" | "apps" | "advanced" | "models";
|
||||
initialSettings?: SettingsPayload;
|
||||
onSettingsChange?: (payload: SettingsPayload) => void;
|
||||
onNativeEngineRestart?: () => Promise<string>;
|
||||
} = {},
|
||||
@ -128,6 +129,7 @@ function renderSettingsView(
|
||||
<SettingsView
|
||||
theme="light"
|
||||
initialSection={options.initialSection ?? "apps"}
|
||||
initialSettings={options.initialSettings}
|
||||
onToggleTheme={() => {}}
|
||||
onBackToChat={() => {}}
|
||||
onModelNameChange={() => {}}
|
||||
@ -140,6 +142,7 @@ function renderSettingsView(
|
||||
|
||||
describe("SettingsView Apps catalog", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
@ -270,6 +273,44 @@ describe("SettingsView Apps catalog", () => {
|
||||
expect(screen.queryByText("Peak tokens")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("aligns token activity days with the configured timezone", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-06-02T18:00:00Z"));
|
||||
const payload: SettingsPayload = {
|
||||
...settingsPayload(),
|
||||
agent: {
|
||||
...settingsPayload().agent,
|
||||
timezone: "Asia/Shanghai",
|
||||
},
|
||||
usage: {
|
||||
days: [
|
||||
{
|
||||
date: "2026-06-03",
|
||||
prompt_tokens: 1200,
|
||||
completion_tokens: 300,
|
||||
cached_tokens: 500,
|
||||
total_tokens: 1500,
|
||||
requests: 2,
|
||||
},
|
||||
],
|
||||
total_tokens: 1500,
|
||||
total_tokens_30d: 1500,
|
||||
total_tokens_365d: 1500,
|
||||
peak_day_tokens: 1500,
|
||||
current_streak_days: 1,
|
||||
longest_streak_days: 1,
|
||||
active_days_30d: 1,
|
||||
requests_30d: 2,
|
||||
updated_at: "2026-06-03T00:00:00Z",
|
||||
},
|
||||
};
|
||||
vi.stubGlobal("fetch", vi.fn(() => new Promise<Response>(() => {})));
|
||||
|
||||
renderSettingsView({ initialSection: "overview", initialSettings: payload });
|
||||
|
||||
expect(screen.getByLabelText("2026-06-03: 1.5K tokens, 2 requests")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows context window options in model settings", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user