diff --git a/src/components/settings/ai/__tests__/ai-usage-card.test.tsx b/src/components/settings/ai/__tests__/ai-usage-card.test.tsx index 99cf881fd..bddf99e48 100644 --- a/src/components/settings/ai/__tests__/ai-usage-card.test.tsx +++ b/src/components/settings/ai/__tests__/ai-usage-card.test.tsx @@ -100,6 +100,39 @@ describe("AiUsageCard", () => { expect(html).not.toContain('data-slot="ai-usage-use-default"'); }); + it("follows the server's estimates, not a day threshold of its own", () => { + // Two measured days, but the server already published estimates: the + // card shows the measured line because the server said it may. + const ready = render({ ...CAPPED, measuredDays: 2 }); + expect(ready).toContain( + "Measured: 100,000 tokens a day over the last 2 days", + ); + // Seven days, but the server withheld every estimate: no measured line. + const withheld = render({ + ...CAPPED, + options: CAPPED.options.map((o) => ({ + ...o, + estimatedTokensPerDay: null, + })), + }); + expect(withheld).toContain( + "An estimate per level appears after three days", + ); + }); + + it("stacks the estimate under the level on a narrow screen", () => { + const html = render(CAPPED); + expect(html).toMatch( + /class="[^"]*flex-col[^"]*sm:flex-row[^"]*"[^>]*>\s*/, + ); + expect(html).toMatch( + /data-slot="ai-usage-estimate" class="[^"]*sm:shrink-0/, + ); + expect(html).not.toMatch( + /data-slot="ai-usage-estimate" class="(?:(?!sm:)[^"])*\bshrink-0/, + ); + }); + it("offers the way back to the default when a choice is stored", () => { const html = render({ ...CAPPED, diff --git a/src/components/settings/ai/ai-usage-card.tsx b/src/components/settings/ai/ai-usage-card.tsx index a7817ab78..9075b48bf 100644 --- a/src/components/settings/ai/ai-usage-card.tsx +++ b/src/components/settings/ai/ai-usage-card.tsx @@ -70,6 +70,12 @@ export function AiUsageCard() { const editable = provider.canConfigure && !save.isPending; const tokens = (value: number) => new Intl.NumberFormat(locale).format(value); const levelName = (level: AiUsageLevel) => t(LEVEL_KEYS[level].label); + // Whether the history suffices for an estimate is the server's call: it + // publishes the per-level estimates only once it does. The card reads that + // decision instead of repeating its day threshold. + const estimatesReady = usage.options.some( + (option) => option.estimatedTokensPerDay !== null, + ); const allowedLevels = usage.options .filter((option) => option.allowed) .map((option) => option.level); @@ -144,31 +150,35 @@ export function AiUsageCard() { ) : null} - - - {levelName(option.level)} - - - {t(LEVEL_KEYS[option.level].hint)} - - {!option.allowed ? ( - - {t("settings.ai.usage.limited")} + {/* Stacked under 640 px so the estimate never squeezes the + level's name and hint into a sliver; side by side above. */} + + + + {levelName(option.level)} - ) : null} - - - {option.estimatedTokensPerDay === null - ? null - : t("settings.ai.usage.estimate", { - tokens: tokens(option.estimatedTokensPerDay), - })} + + {t(LEVEL_KEYS[option.level].hint)} + + {!option.allowed ? ( + + {t("settings.ai.usage.limited")} + + ) : null} + + + {option.estimatedTokensPerDay === null + ? null + : t("settings.ai.usage.estimate", { + tokens: tokens(option.estimatedTokensPerDay), + })} + ); @@ -179,7 +189,7 @@ export function AiUsageCard() { data-slot="ai-usage-measured" className="text-muted-foreground text-xs" > - {usage.measuredTokensPerDay === null || usage.measuredDays < 3 + {usage.measuredTokensPerDay === null || !estimatesReady ? t("settings.ai.usage.noEstimate") : t("settings.ai.usage.measured", { tokens: tokens(usage.measuredTokensPerDay),