mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): label AI spend as estimated (#27584)
AI spend is estimated by multiplying token usage by a snapshot of published model prices that ships with each Coder release, so it can differ from provider invoices. Label spend figures as estimated and link to the [How spend is estimated](https://coder.com/docs/@main/ai-coder/ai-gateway/cost-controls#how-spend-is-estimated) docs section. "Estimated spend" matches the terminology already defined in the cost controls doc. ## Changes - Group members tooltip: "Monthly AI spend for this user." becomes "Estimated monthly AI spend for this user.", followed by an inline docs link. - Groups table tooltip: "Current AI spend compared to the group's AI budget..." becomes "Estimated AI spend compared to...", with the same link. - User dropdown: "(AI spend/month)" becomes "(Estimated AI spend/month)". - `StatusIconTooltip` now exports `SpendEstimateDocsLink`, shared by both tooltips. > [!NOTE] > Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira
This commit is contained in:
@@ -73,7 +73,7 @@ export const WithAISpend: Story = {
|
||||
await waitFor(() =>
|
||||
expect(document.body).toHaveTextContent("$819 / $1,200 USD"),
|
||||
);
|
||||
expect(document.body).toHaveTextContent("(AI spend/month)");
|
||||
expect(document.body).toHaveTextContent("(Estimated AI spend/month)");
|
||||
expect(
|
||||
screen.getByRole("progressbar", { name: "AI spend usage" }),
|
||||
).toHaveAttribute("aria-valuenow", "68");
|
||||
@@ -97,7 +97,7 @@ export const AISpendWarning: Story = {
|
||||
await waitFor(() =>
|
||||
expect(document.body).toHaveTextContent("$1,080 / $1,200 USD"),
|
||||
);
|
||||
expect(document.body).toHaveTextContent("(AI spend/month)");
|
||||
expect(document.body).toHaveTextContent("(Estimated AI spend/month)");
|
||||
expect(
|
||||
screen.getByRole("progressbar", { name: "AI spend usage" }),
|
||||
).toHaveAttribute("aria-valuenow", "90");
|
||||
@@ -144,7 +144,7 @@ export const AISpendExceeded: Story = {
|
||||
await waitFor(() =>
|
||||
expect(document.body).toHaveTextContent("$1,500 / $1,200 USD"),
|
||||
);
|
||||
expect(document.body).toHaveTextContent("(AI spend/month)");
|
||||
expect(document.body).toHaveTextContent("(Estimated AI spend/month)");
|
||||
expect(
|
||||
screen.getByRole("progressbar", { name: "AI spend usage" }),
|
||||
).toHaveAttribute("aria-valuenow", "100");
|
||||
@@ -165,7 +165,7 @@ export const AISpendUnlimited: Story = {
|
||||
await waitFor(() =>
|
||||
expect(document.body).toHaveTextContent("$819 / Unlimited USD"),
|
||||
);
|
||||
expect(document.body).toHaveTextContent("(AI spend/month)");
|
||||
expect(document.body).toHaveTextContent("(Estimated AI spend/month)");
|
||||
expect(
|
||||
screen.queryByRole("progressbar", { name: "AI spend usage" }),
|
||||
).not.toBeInTheDocument();
|
||||
@@ -272,7 +272,7 @@ export const AISpendHiddenOnInvalidData: Story = {
|
||||
play: async ({ canvasElement, step }) => {
|
||||
await step("hides AI spend on invalid data", async () => {
|
||||
await openDropdown(canvasElement);
|
||||
expect(screen.queryByText("(AI spend/month)")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/spend\/month/)).not.toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -293,7 +293,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("(AI spend/month)")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/spend\/month/)).not.toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ export const UserDropdownAISpend: FC<UserDropdownAISpendProps> = ({
|
||||
/>
|
||||
)}
|
||||
<div className="mt-1 text-xs text-content-secondary">
|
||||
(AI spend/month)
|
||||
(Estimated AI spend/month)
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { FC } from "react";
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import { docs } from "#/utils/docs";
|
||||
|
||||
export const SpendEstimateDocsLink: FC = () => (
|
||||
<Link
|
||||
href={docs("/ai-coder/ai-gateway/cost-controls#how-spend-is-estimated")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
How spend is estimated
|
||||
</Link>
|
||||
);
|
||||
@@ -52,6 +52,7 @@ import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
|
||||
import { isEveryoneGroup } from "#/modules/groups";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { formatBudgetUSD } from "#/utils/currency";
|
||||
import { SpendEstimateDocsLink } from "./AICostControl";
|
||||
import {
|
||||
effectiveBudgetGroup,
|
||||
GroupMemberBudgetCells,
|
||||
@@ -112,7 +113,7 @@ const GroupMembersPage: FC = () => {
|
||||
}),
|
||||
);
|
||||
const aiBudgetNote = [
|
||||
"Monthly AI spend for this user.",
|
||||
"Estimated 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")}.`,
|
||||
@@ -173,7 +174,13 @@ const GroupMembersPage: FC = () => {
|
||||
message="AI spend couldn't be loaded, so budgets aren't shown."
|
||||
/>
|
||||
) : (
|
||||
<StatusIconTooltip message={aiBudgetNote} />
|
||||
<StatusIconTooltip
|
||||
message={
|
||||
<>
|
||||
{aiBudgetNote} <SpendEstimateDocsLink />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TableHead>
|
||||
|
||||
@@ -365,7 +365,7 @@ export const WithMemberAIBudget: Story = {
|
||||
);
|
||||
await expect(
|
||||
await body.findByText(
|
||||
/^Monthly AI spend for this user\. Resets .*The group's default limit is \$7,000 per member\.$/,
|
||||
/^Estimated monthly AI spend for this user\. Resets .*The group's default limit is \$7,000 per member\.$/,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
await userEvent.click(
|
||||
@@ -434,7 +434,9 @@ export const AIBudgetActionDisabledForOtherGroup: Story = {
|
||||
}),
|
||||
);
|
||||
await expect(
|
||||
await body.findByText(/^Monthly AI spend for this user\. Resets .*\.$/),
|
||||
await body.findByText(
|
||||
/^Estimated monthly AI spend for this user\. Resets .*\.$/,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
await userEvent.keyboard("{Escape}");
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "#/components/TableLoader/TableLoader";
|
||||
import { useClickableTableRow } from "#/hooks/useClickableTableRow";
|
||||
import { docs } from "#/utils/docs";
|
||||
import { SpendEstimateDocsLink } from "./AICostControl";
|
||||
import { StatusIconTooltip } from "./StatusIconTooltip";
|
||||
|
||||
const EM_DASH = "\u2014";
|
||||
@@ -94,7 +95,14 @@ export const GroupsPageView: FC<GroupsPageViewProps> = ({
|
||||
message="AI spend couldn't be loaded, so budgets aren't shown."
|
||||
/>
|
||||
) : (
|
||||
<StatusIconTooltip message="Current AI spend compared to the group's AI budget for the active period." />
|
||||
<StatusIconTooltip
|
||||
message={
|
||||
<>
|
||||
Estimated AI spend compared to the group's AI budget for
|
||||
the active period. <SpendEstimateDocsLink />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TableHead>
|
||||
|
||||
Reference in New Issue
Block a user