diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/utils/rate-limit.test.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/utils/rate-limit.test.ts new file mode 100644 index 000000000..60d82ec95 --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/utils/rate-limit.test.ts @@ -0,0 +1,78 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" + +import { computeRateLimitMessage } from "./rate-limit" + +vi.mock("~/i18n", () => ({ + getI18n: () => ({ + language: "en-GB", + resolvedLanguage: "en-GB", + t: (key: string, options?: Record) => { + switch (key) { + case "rate_limit.depleted": { + return "AI credits depleted" + } + case "rate_limit.minute": { + return "minute" + } + case "rate_limit.minutes": { + return "minutes" + } + case "rate_limit.resets_at": { + return `resets at ${options?.time as string}` + } + case "rate_limit.resets_in": { + return `resets in ${options?.value as string}` + } + case "rate_limit.credits_left": { + return `${options?.count as number} credits left` + } + case "rate_limit.upgrade_to_get_more": { + return "Start free trial to get more AI credits." + } + default: { + return key + } + } + }, + }), +})) + +describe("computeRateLimitMessage", () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it("shows only the time when credits reset later today", () => { + vi.setSystemTime(new Date(2026, 3, 10, 8, 0)) + + const message = computeRateLimitMessage(undefined, { + usage: { + total: 5_000_000, + used: 5_000_000, + remaining: 0, + resetAt: new Date(2026, 3, 10, 20, 45), + }, + }) + + expect(message).toBe("AI credits depleted · resets at 20:45") + }) + + it("includes the date when credits reset on a future day", () => { + vi.setSystemTime(new Date(2026, 3, 10, 8, 0)) + + const message = computeRateLimitMessage(undefined, { + usage: { + total: 5_000_000, + used: 5_000_000, + remaining: 0, + resetAt: new Date(2026, 3, 12, 20, 45), + }, + }) + + expect(message).toBe("AI credits depleted · resets at 12 Apr, 20:45") + }) +}) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/utils/rate-limit.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/utils/rate-limit.ts index fb36bddd6..330adb373 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/utils/rate-limit.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/utils/rate-limit.ts @@ -20,6 +20,7 @@ const formatResetTime = (windowResetTime: Date) => { const now = new Date() const diffMs = resetDate.getTime() - now.getTime() const diffMinutes = Math.ceil(diffMs / (1000 * 60)) + const locale = i18n.language || i18n.resolvedLanguage || undefined if (diffMinutes < 60 && diffMinutes > 0) { const unit = @@ -30,12 +31,31 @@ const formatResetTime = (windowResetTime: Date) => { return t("rate_limit.resets_in", { ns: "ai", value }) } - const timeFormatter = new Intl.DateTimeFormat("en-GB", { + const isSameCalendarDay = + resetDate.getFullYear() === now.getFullYear() && + resetDate.getMonth() === now.getMonth() && + resetDate.getDate() === now.getDate() + + const time = new Intl.DateTimeFormat(locale, { hour: "2-digit", minute: "2-digit", hour12: false, - }) - return t("rate_limit.resets_at", { ns: "ai", time: timeFormatter.format(resetDate) }) + }).format(resetDate) + + if (isSameCalendarDay) { + return t("rate_limit.resets_at", { ns: "ai", time }) + } + + const dateTime = new Intl.DateTimeFormat(locale, { + ...(resetDate.getFullYear() !== now.getFullYear() ? { year: "numeric" as const } : {}), + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + hour12: false, + }).format(resetDate) + + return t("rate_limit.resets_at", { ns: "ai", time: dateTime }) } export function computeIsRateLimited(