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(() =>
|
await waitFor(() =>
|
||||||
expect(document.body).toHaveTextContent("$819 / $1,200 USD"),
|
expect(document.body).toHaveTextContent("$819 / $1,200 USD"),
|
||||||
);
|
);
|
||||||
expect(document.body).toHaveTextContent("(AI spend/month)");
|
expect(document.body).toHaveTextContent("(Estimated AI spend/month)");
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole("progressbar", { name: "AI spend usage" }),
|
screen.getByRole("progressbar", { name: "AI spend usage" }),
|
||||||
).toHaveAttribute("aria-valuenow", "68");
|
).toHaveAttribute("aria-valuenow", "68");
|
||||||
@@ -97,7 +97,7 @@ export const AISpendWarning: Story = {
|
|||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(document.body).toHaveTextContent("$1,080 / $1,200 USD"),
|
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(
|
expect(
|
||||||
screen.getByRole("progressbar", { name: "AI spend usage" }),
|
screen.getByRole("progressbar", { name: "AI spend usage" }),
|
||||||
).toHaveAttribute("aria-valuenow", "90");
|
).toHaveAttribute("aria-valuenow", "90");
|
||||||
@@ -144,7 +144,7 @@ export const AISpendExceeded: Story = {
|
|||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(document.body).toHaveTextContent("$1,500 / $1,200 USD"),
|
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(
|
expect(
|
||||||
screen.getByRole("progressbar", { name: "AI spend usage" }),
|
screen.getByRole("progressbar", { name: "AI spend usage" }),
|
||||||
).toHaveAttribute("aria-valuenow", "100");
|
).toHaveAttribute("aria-valuenow", "100");
|
||||||
@@ -165,7 +165,7 @@ export const AISpendUnlimited: Story = {
|
|||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(document.body).toHaveTextContent("$819 / Unlimited USD"),
|
expect(document.body).toHaveTextContent("$819 / Unlimited USD"),
|
||||||
);
|
);
|
||||||
expect(document.body).toHaveTextContent("(AI spend/month)");
|
expect(document.body).toHaveTextContent("(Estimated AI spend/month)");
|
||||||
expect(
|
expect(
|
||||||
screen.queryByRole("progressbar", { name: "AI spend usage" }),
|
screen.queryByRole("progressbar", { name: "AI spend usage" }),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
@@ -272,7 +272,7 @@ export const AISpendHiddenOnInvalidData: Story = {
|
|||||||
play: async ({ canvasElement, step }) => {
|
play: async ({ canvasElement, step }) => {
|
||||||
await step("hides AI spend on invalid data", async () => {
|
await step("hides AI spend on invalid data", async () => {
|
||||||
await openDropdown(canvasElement);
|
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 }) => {
|
play: async ({ canvasElement, step }) => {
|
||||||
await step("hides AI spend on a negative limit", async () => {
|
await step("hides AI spend on a negative limit", async () => {
|
||||||
await openDropdown(canvasElement);
|
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">
|
<div className="mt-1 text-xs text-content-secondary">
|
||||||
(AI spend/month)
|
(Estimated AI spend/month)
|
||||||
</div>
|
</div>
|
||||||
</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 { isEveryoneGroup } from "#/modules/groups";
|
||||||
import { cn } from "#/utils/cn";
|
import { cn } from "#/utils/cn";
|
||||||
import { formatBudgetUSD } from "#/utils/currency";
|
import { formatBudgetUSD } from "#/utils/currency";
|
||||||
|
import { SpendEstimateDocsLink } from "./AICostControl";
|
||||||
import {
|
import {
|
||||||
effectiveBudgetGroup,
|
effectiveBudgetGroup,
|
||||||
GroupMemberBudgetCells,
|
GroupMemberBudgetCells,
|
||||||
@@ -112,7 +113,7 @@ const GroupMembersPage: FC = () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const aiBudgetNote = [
|
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.
|
// Spend resets at period_end, rendered in the viewer's local time.
|
||||||
aiSpend &&
|
aiSpend &&
|
||||||
`Resets ${dayjs(aiSpend.period_end).format("MMM D, YYYY h:mm A")}.`,
|
`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."
|
message="AI spend couldn't be loaded, so budgets aren't shown."
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<StatusIconTooltip message={aiBudgetNote} />
|
<StatusIconTooltip
|
||||||
|
message={
|
||||||
|
<>
|
||||||
|
{aiBudgetNote} <SpendEstimateDocsLink />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ export const WithMemberAIBudget: Story = {
|
|||||||
);
|
);
|
||||||
await expect(
|
await expect(
|
||||||
await body.findByText(
|
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();
|
).toBeInTheDocument();
|
||||||
await userEvent.click(
|
await userEvent.click(
|
||||||
@@ -434,7 +434,9 @@ export const AIBudgetActionDisabledForOtherGroup: Story = {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await expect(
|
await expect(
|
||||||
await body.findByText(/^Monthly AI spend for this user\. Resets .*\.$/),
|
await body.findByText(
|
||||||
|
/^Estimated monthly AI spend for this user\. Resets .*\.$/,
|
||||||
|
),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
await userEvent.keyboard("{Escape}");
|
await userEvent.keyboard("{Escape}");
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
} from "#/components/TableLoader/TableLoader";
|
} from "#/components/TableLoader/TableLoader";
|
||||||
import { useClickableTableRow } from "#/hooks/useClickableTableRow";
|
import { useClickableTableRow } from "#/hooks/useClickableTableRow";
|
||||||
import { docs } from "#/utils/docs";
|
import { docs } from "#/utils/docs";
|
||||||
|
import { SpendEstimateDocsLink } from "./AICostControl";
|
||||||
import { StatusIconTooltip } from "./StatusIconTooltip";
|
import { StatusIconTooltip } from "./StatusIconTooltip";
|
||||||
|
|
||||||
const EM_DASH = "\u2014";
|
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."
|
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>
|
</div>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
|||||||
Reference in New Issue
Block a user