feat(site): add usage indicator to agents sidebar (#23307)

This commit is contained in:
Kyle Carberry
2026-03-19 17:40:58 -04:00
committed by GitHub
parent 999bbf9685
commit 4da273ba3c
6 changed files with 227 additions and 4 deletions
+8
View File
@@ -3307,6 +3307,14 @@ class ApiMethods {
return response.data;
};
getChatUsageLimitStatus =
async (): Promise<TypesGen.ChatUsageLimitStatus> => {
const response = await this.axios.get<TypesGen.ChatUsageLimitStatus>(
"/api/experimental/chats/usage-limits/status",
);
return response.data;
};
updateChatUsageLimitConfig = async (
req: TypesGen.ChatUsageLimitConfig,
): Promise<TypesGen.ChatUsageLimitConfig> => {
+11
View File
@@ -560,6 +560,17 @@ export const prInsights = (params?: {
staleTime: 60_000,
});
export const chatUsageLimitStatusKey = [
...chatsKey,
"usageLimitStatus",
] as const;
export const chatUsageLimitStatus = () => ({
queryKey: chatUsageLimitStatusKey,
queryFn: () => API.getChatUsageLimitStatus(),
refetchInterval: 60_000,
});
const chatUsageLimitConfigKey = [...chatsKey, "usageLimitConfig"] as const;
export const chatUsageLimitConfig = () => ({
+5 -3
View File
@@ -72,6 +72,7 @@ import { cn } from "utils/cn";
import { shortRelativeTime } from "utils/time";
import { getNormalizedModelRef } from "./modelOptions";
import { getTimeGroup, TIME_GROUPS } from "./timeGroups";
import { UsageIndicator } from "./UsageIndicator";
type SidebarView =
| { panel: "chats" }
@@ -898,8 +899,9 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
</div>
</ScrollArea>
<div className="hidden border-0 border-t border-solid md:block">
<div className="flex items-center">
<div className="flex items-stretch">
<DropdownMenu>
{" "}
<DropdownMenuTrigger asChild>
<button
type="button"
@@ -932,11 +934,11 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
/>
</DropdownMenuContent>
</DropdownMenu>
<UsageIndicator />
</div>
</div>
</div>
{/* ── Panel 2: Sub-navigation (Settings) ── */}
{/* ── Panel 2: Sub-navigation (Settings) ── */}{" "}
<div
className={cn(
"absolute inset-0 flex flex-col transition-transform duration-200 ease-in-out",
@@ -25,7 +25,7 @@ interface ChatCostSummaryViewProps {
emptyMessage: string;
}
const getUsageLimitPeriodLabel = (
export const getUsageLimitPeriodLabel = (
period: TypesGen.ChatUsageLimitPeriod | undefined,
): string => {
if (!period) {
@@ -0,0 +1,92 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { chatUsageLimitStatusKey } from "api/queries/chats";
import type { FC } from "react";
import { useQueryClient } from "react-query";
import { UsageIndicator } from "./UsageIndicator";
const withUsageLimitStatus =
(status: {
is_limited: boolean;
period?: "day" | "week" | "month";
spend_limit_micros?: number;
current_spend: number;
period_start?: string;
period_end?: string;
}) =>
(Story: FC) => {
const queryClient = useQueryClient();
queryClient.setQueryData(chatUsageLimitStatusKey, status);
return <Story />;
};
const periodStart = new Date().toISOString();
const periodEnd = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
const meta: Meta<typeof UsageIndicator> = {
title: "pages/AgentsPage/UsageIndicator",
component: UsageIndicator,
};
export default meta;
type Story = StoryObj<typeof UsageIndicator>;
export const LowUsage: Story = {
decorators: [
withUsageLimitStatus({
is_limited: true,
period: "month",
spend_limit_micros: 50_000_000,
current_spend: 12_500_000,
period_start: periodStart,
period_end: periodEnd,
}),
],
};
export const MediumUsage: Story = {
decorators: [
withUsageLimitStatus({
is_limited: true,
period: "week",
spend_limit_micros: 20_000_000,
current_spend: 16_000_000,
period_start: periodStart,
period_end: periodEnd,
}),
],
};
export const HighUsage: Story = {
decorators: [
withUsageLimitStatus({
is_limited: true,
period: "day",
spend_limit_micros: 10_000_000,
current_spend: 9_500_000,
period_start: periodStart,
period_end: periodEnd,
}),
],
};
export const LimitExceeded: Story = {
decorators: [
withUsageLimitStatus({
is_limited: true,
period: "month",
spend_limit_micros: 30_000_000,
current_spend: 32_000_000,
period_start: periodStart,
period_end: periodEnd,
}),
],
};
export const NotLimited: Story = {
decorators: [
withUsageLimitStatus({
is_limited: false,
current_spend: 0,
}),
],
};
@@ -0,0 +1,110 @@
import { chatUsageLimitStatus } from "api/queries/chats";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "components/DropdownMenu/DropdownMenu";
import dayjs from "dayjs";
import type { FC } from "react";
import { useQuery } from "react-query";
import { Link } from "react-router";
import { formatCostMicros } from "utils/currency";
import { getUsageLimitPeriodLabel } from "./ChatCostSummaryView";
export const UsageIndicator: FC = () => {
const { data, isLoading, isError } = useQuery(chatUsageLimitStatus());
if (isLoading || isError || !data?.is_limited) {
return null;
}
const spendLimit = data.spend_limit_micros ?? 0;
const currentSpend = data.current_spend;
const percent =
spendLimit > 0 ? Math.min((currentSpend / spendLimit) * 100, 100) : 0;
const roundedPercent = Math.round(percent);
const exceeded = spendLimit > 0 && currentSpend >= spendLimit;
const periodLabel = getUsageLimitPeriodLabel(data.period);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="ml-auto flex self-stretch flex-col justify-center items-start gap-1 border-none bg-transparent px-3 cursor-pointer select-none transition-colors text-content-secondary hover:bg-surface-tertiary/50 outline-none text-[13px]"
>
<span className="shrink-0 whitespace-nowrap">
{periodLabel} Usage
</span>
<div
role="progressbar"
aria-label={`${periodLabel} spend usage`}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={roundedPercent}
className="h-1.5 w-full overflow-hidden rounded-full bg-surface-tertiary shrink-0"
>
<div
className="h-full rounded-full bg-content-secondary transition-all duration-300 ease-out"
style={{ width: `${percent}%` }}
/>
</div>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-auto w-[240px]">
{/* Header */}
<div className="flex items-center justify-between px-2 py-1.5">
<span className="text-sm font-medium text-content-primary">
{periodLabel} Usage
</span>
<span className="text-xs text-content-secondary">
{roundedPercent}%
</span>
</div>
{/* Progress bar */}
<div className="px-2 pb-2">
<div
role="progressbar"
aria-label={`${periodLabel} spend usage`}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={roundedPercent}
className="h-1.5 overflow-hidden rounded-full bg-surface-tertiary"
>
<div
className="h-full rounded-full bg-content-secondary transition-all duration-300 ease-out"
style={{ width: `${percent}%` }}
/>
</div>
</div>
{/* Spend detail */}
<div className="px-2 pb-1.5 text-xs text-content-secondary">
{formatCostMicros(currentSpend)} of {formatCostMicros(spendLimit)}{" "}
used
{exceeded && (
<span className="ml-1 text-content-destructive">
— limit exceeded
</span>
)}
</div>
{data.period_end && (
<div className="px-2 pb-2 text-xs text-content-secondary">
Resets {dayjs(data.period_end).format("MMM D, YYYY")}
</div>
)}
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link to="/agents/analytics">View details</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};