chore: use 'AI spend' and 'approximate' in budget UI (#27977)

The "AI budget" column shows spend against the configured limit, not the
limit itself, so "AI spend" describes what it actually displays. Renamed
in both the groups table and the group members table.

Also replaces "estimated" with "approximate" in the surrounding spend
copy, which reads more plainly as a signal that the amount isn't exact.

The user dropdown now keeps the date range on a single line, so the
label wraps above it instead of the dates splitting across two lines.

Before:
<img width="250" height="89" alt="Screenshot 2026-08-10 at 12 55 55"
src="https://github.com/user-attachments/assets/428f7032-0ead-44ac-987d-563e20c97d12"
/>

After:
<img width="248" height="92" alt="Screenshot 2026-08-10 at 12 57 09"
src="https://github.com/user-attachments/assets/cf0704ba-d0ab-4132-b207-a15ab2ea4f37"
/>

Related to internal slack thread:
https://codercom.slack.com/archives/C096PFVBZKN/p1786051712464009

> [!NOTE]
> Initially generated by Claude Opus 5, modified and reviewed by
@ssncferreira
This commit is contained in:
Susana Ferreira
2026-08-10 16:08:06 +01:00
committed by GitHub
parent 84f4a8bb17
commit 27414788f7
8 changed files with 31 additions and 28 deletions
@@ -18,7 +18,7 @@ const mockAISpend: UserAISpendStatus = {
period_end: "2026-07-01T00:00:00Z",
};
const spendPeriodLabel = "Estimated AI spend: June 1 - July 1, 2026";
const spendPeriodLabel = "Approximate AI spend June 1 - July 1, 2026";
const aiCostControl: { features: FeatureName[] } = {
features: ["aibridge"],
@@ -71,10 +71,10 @@ export const WithAISpend: Story = {
},
play: async ({ canvasElement, step }) => {
await step("shows AI spend", async () => {
const menu = await openDropdown(canvasElement);
await openDropdown(canvasElement);
await waitFor(() => {
expect(document.body).toHaveTextContent("$819 / $1,200 USD");
expect(menu.getByText(spendPeriodLabel)).toBeVisible();
expect(document.body).toHaveTextContent(spendPeriodLabel);
});
expect(
screen.getByRole("progressbar", { name: "AI spend usage" }),
@@ -95,10 +95,10 @@ export const AISpendWarning: Story = {
},
play: async ({ canvasElement, step }) => {
await step("shows the warning marker near the limit", async () => {
const menu = await openDropdown(canvasElement);
await openDropdown(canvasElement);
await waitFor(() => {
expect(document.body).toHaveTextContent("$1,080 / $1,200 USD");
expect(menu.getByText(spendPeriodLabel)).toBeVisible();
expect(document.body).toHaveTextContent(spendPeriodLabel);
});
expect(
screen.getByRole("progressbar", { name: "AI spend usage" }),
@@ -142,10 +142,10 @@ export const AISpendExceeded: Story = {
},
play: async ({ canvasElement, step }) => {
await step("shows the exceeded marker at the limit", async () => {
const menu = await openDropdown(canvasElement);
await openDropdown(canvasElement);
await waitFor(() => {
expect(document.body).toHaveTextContent("$1,500 / $1,200 USD");
expect(menu.getByText(spendPeriodLabel)).toBeVisible();
expect(document.body).toHaveTextContent(spendPeriodLabel);
});
expect(
screen.getByRole("progressbar", { name: "AI spend usage" }),
@@ -163,10 +163,10 @@ export const AISpendUnlimited: Story = {
},
play: async ({ canvasElement, step }) => {
await step("shows unlimited spend without a bar", async () => {
const menu = await openDropdown(canvasElement);
await openDropdown(canvasElement);
await waitFor(() => {
expect(document.body).toHaveTextContent("$819 / Unlimited USD");
expect(menu.getByText(spendPeriodLabel)).toBeVisible();
expect(document.body).toHaveTextContent(spendPeriodLabel);
});
expect(
screen.queryByRole("progressbar", { name: "AI spend usage" }),
@@ -274,7 +274,7 @@ export const AISpendHiddenOnInvalidData: Story = {
play: async ({ canvasElement, step }) => {
await step("hides AI spend on invalid data", async () => {
await openDropdown(canvasElement);
expect(screen.queryByText(spendPeriodLabel)).not.toBeInTheDocument();
expect(document.body).not.toHaveTextContent(spendPeriodLabel);
});
},
};
@@ -295,7 +295,7 @@ export const AISpendHiddenOnNegativeLimit: Story = {
play: async ({ canvasElement, step }) => {
await step("hides AI spend on a negative limit", async () => {
await openDropdown(canvasElement);
expect(screen.queryByText(spendPeriodLabel)).not.toBeInTheDocument();
expect(document.body).not.toHaveTextContent(spendPeriodLabel);
});
},
};
@@ -39,7 +39,10 @@ export const UserDropdownAISpend: FC<UserDropdownAISpendProps> = ({
/>
)}
<div className="mt-1 text-xs text-content-secondary">
Estimated AI spend: {formatSpendPeriodLabel(periodStart, periodEnd)}
Approximate AI spend{" "}
<span className="whitespace-nowrap">
{formatSpendPeriodLabel(periodStart, periodEnd)}
</span>
</div>
</div>
);
@@ -45,7 +45,7 @@ const meta: Meta<typeof GroupMemberBudgetCells> = {
<Table aria-label="Member budget">
<TableHeader>
<TableRow>
<TableHead>AI budget</TableHead>
<TableHead>AI spend</TableHead>
<TableHead>Budget group</TableHead>
</TableRow>
</TableHeader>
@@ -17,7 +17,7 @@ const OTHER_ORG_MESSAGE =
"This user's AI budget is managed by a group in another organization and isn't visible here.";
/**
* The AI budget and Budget group cells for a group member. Spend is scoped to
* The AI spend and Budget group cells for a group member. Spend is scoped to
* the viewed group; the limit comes from the member's effective group.
*/
export const GroupMemberBudgetCells: FC<{
@@ -101,7 +101,7 @@ const GroupMembersPage: FC = () => {
}),
);
const aiBudgetNote = [
"Estimated monthly AI spend for this user.",
"Approximate monthly AI spend for this user.",
// Spend resets at period_end, rendered in the viewer's local time.
aiSpend &&
`Resets ${dayjs(aiSpend.period_end).format("MMM D, YYYY h:mm A")}.`,
@@ -141,7 +141,7 @@ const GroupMembersPage: FC = () => {
<>
<TableHead>
<div className="flex items-center gap-1">
AI budget
AI spend
{membersSpendQuery.isError ? (
<StatusIconTooltip
kind="warning"
@@ -346,7 +346,7 @@ export const WithMemberAIBudget: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(await canvas.findByText("AI budget")).toBeInTheDocument();
await expect(await canvas.findByText("AI spend")).toBeInTheDocument();
await expect(await canvas.findByText("Budget group")).toBeInTheDocument();
await expect(
await canvas.findByText("AI budget period: June 1 - July 1, 2026"),
@@ -358,13 +358,13 @@ export const WithMemberAIBudget: Story = {
const body = within(document.body);
await userEvent.click(
within(canvas.getByText("AI budget")).getByRole("button", {
within(canvas.getByText("AI spend")).getByRole("button", {
name: "More info",
}),
);
await expect(
await body.findByText(
/^Estimated monthly AI spend for this user\. Resets .*The group's default limit is \$7,000 per member\.$/,
/^Approximate monthly AI spend for this user\. Resets .*The group's default limit is \$7,000 per member\.$/,
),
).toBeInTheDocument();
await userEvent.click(
@@ -391,7 +391,7 @@ export const WithoutMemberAIBudgetColumn: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByRole("table", { name: "Group members" });
expect(canvas.queryByText("AI budget")).not.toBeInTheDocument();
expect(canvas.queryByText("AI spend")).not.toBeInTheDocument();
expect(canvas.queryByText("Budget group")).not.toBeInTheDocument();
expect(canvas.queryByText(/AI budget period/)).not.toBeInTheDocument();
},
@@ -428,13 +428,13 @@ export const AIBudgetActionDisabledForOtherGroup: Story = {
// Without a group default budget, the header note ends at the reset date.
await userEvent.click(
within(canvas.getByText("AI budget")).getByRole("button", {
within(canvas.getByText("AI spend")).getByRole("button", {
name: "More info",
}),
);
await expect(
await body.findByText(
/^Estimated monthly AI spend for this user\. Resets .*\.$/,
/^Approximate monthly AI spend for this user\. Resets .*\.$/,
),
).toBeInTheDocument();
await userEvent.keyboard("{Escape}");
@@ -171,7 +171,7 @@ export const WithAIBudgetsLoading: Story = {
},
};
// Spend still loading: every AI budget cell falls back to an em dash.
// Spend still loading: every AI spend cell falls back to an em dash.
export const WithAIBudgetsSpendLoading: Story = {
args: {
groups: [aiGroup("ai-loading", "Spend loading")],
@@ -223,7 +223,7 @@ export const WithAIBudgetsSpendUnavailable: Story = {
},
};
// AI Bridge hidden: no AI budget column.
// AI Bridge hidden: no AI spend column.
export const WithoutAIBudgetColumn: Story = {
args: {
groups: [aiGroup("ai-hidden", "No AI column")],
@@ -233,7 +233,7 @@ export const WithoutAIBudgetColumn: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.queryByText("AI budget")).not.toBeInTheDocument();
expect(canvas.queryByText("AI spend")).not.toBeInTheDocument();
},
};
+3 -3
View File
@@ -92,7 +92,7 @@ export const GroupsPageView: FC<GroupsPageViewProps> = ({
{showAIBudget && (
<TableHead className="w-2/5">
<div className="flex items-center gap-1">
AI budget
AI spend
{spendError ? (
<StatusIconTooltip
kind="warning"
@@ -102,8 +102,8 @@ export const GroupsPageView: FC<GroupsPageViewProps> = ({
<StatusIconTooltip
message={
<>
Estimated AI spend compared to the group's AI budget for
the active period. <SpendEstimateDocsLink />
Approximate AI spend compared to the group's AI budget
for the active period. <SpendEstimateDocsLink />
</>
}
/>