Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions app/(chat)/api/usage/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth";
import { getChatById } from "@/lib/db/queries";

export async function GET(request: Request) {
try {
const session = await auth();

if (!session || !session.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { searchParams } = new URL(request.url);
const chatId = searchParams.get("chatId");

if (!chatId) {
return NextResponse.json({ error: "Chat ID required" }, { status: 400 });
}

// Fetch the chat from database to get the latest usage context
const chat = await getChatById({ id: chatId });

if (!chat) {
return NextResponse.json({ error: "Chat not found" }, { status: 404 });
}

// Verify the chat belongs to the user
if (chat.userId !== session.user.id) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}

return NextResponse.json({ usage: chat.lastContext });
} catch (error) {
console.error("Error fetching usage:", error);
return NextResponse.json(
{ error: "Failed to fetch usage data" },
{ status: 500 }
);
}
}
65 changes: 60 additions & 5 deletions components/elements/context.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
"use client";

import type { ComponentProps } from "react";
import { useState } from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Progress } from "@/components/ui/progress";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
import { RefreshCwIcon } from "@/components/icons";
import type { AppUsage } from "@/lib/usage";
import { cn } from "@/lib/utils";

export type ContextProps = ComponentProps<"button"> & {
/** Optional full usage payload to enable breakdown view */
usage?: AppUsage;
/** Optional chat ID for refreshing usage data */
chatId?: string;
/** Optional callback when usage is refreshed */
onUsageRefresh?: (usage: AppUsage | undefined) => void;
};

const _THOUSAND = 1000;
Expand Down Expand Up @@ -99,14 +106,41 @@ function InfoRow({
);
}

export const Context = ({ className, usage, ...props }: ContextProps) => {
export const Context = ({
className,
usage,
chatId,
onUsageRefresh,
...props
}: ContextProps) => {
const [isRefreshing, setIsRefreshing] = useState(false);
const used = usage?.totalTokens ?? 0;
const max =
usage?.context?.totalMax ??
usage?.context?.combinedMax ??
usage?.context?.inputMax;
const hasMax = typeof max === "number" && Number.isFinite(max) && max > 0;
const usedPercent = hasMax ? Math.min(100, (used / max) * 100) : 0;

const handleRefresh = async () => {
if (!chatId || !onUsageRefresh) return;

setIsRefreshing(true);
try {
const response = await fetch(`/api/usage?chatId=${chatId}`);
if (response.ok) {
const data = await response.json();
onUsageRefresh(data.usage);
} else {
console.error("Failed to refresh usage data");
}
} catch (error) {
console.error("Error refreshing usage:", error);
} finally {
setIsRefreshing(false);
}
};

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
Expand All @@ -127,11 +161,32 @@ export const Context = ({ className, usage, ...props }: ContextProps) => {
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-fit p-3" side="top">
<div className="min-w-[240px] space-y-2">
<div className="flex items-start justify-between text-sm">
<div className="flex items-start justify-between gap-2 text-sm">
<span>{usedPercent.toFixed(1)}%</span>
<span className="text-muted-foreground">
{hasMax ? `${used} / ${max} tokens` : `${used} tokens`}
</span>
<div className="flex items-center gap-2">
<span className="text-muted-foreground">
{hasMax ? `${used} / ${max} tokens` : `${used} tokens`}
</span>
{chatId && onUsageRefresh && (
<Button
className="size-6 p-1 hover:bg-accent"
disabled={isRefreshing}
onClick={(e) => {
e.stopPropagation();
handleRefresh();
}}
title="Refresh usage data"
variant="ghost"
>
<RefreshCwIcon
size={14}
style={{
animation: isRefreshing ? "spin 1s linear infinite" : undefined,
}}
/>
</Button>
)}
</div>
</div>
<div className="space-y-2">
<Progress className="h-2 bg-muted" value={usedPercent} />
Expand Down
23 changes: 23 additions & 0 deletions components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1211,3 +1211,26 @@ export const WarningIcon = ({ size = 16 }: { size?: number }) => {
</svg>
);
};

export const RefreshCwIcon = ({
size = 16,
...props
}: { size?: number } & React.SVGProps<SVGSVGElement>) => {
return (
<svg
height={size}
strokeLinejoin="round"
style={{ color: "currentcolor", ...props.style }}
viewBox="0 0 16 16"
width={size}
{...props}
>
<path
clipRule="evenodd"
d="M8 2.5C5.51472 2.5 3.5 4.51472 3.5 7H5.25L2.625 10.5L0 7H1.5C1.5 3.41015 4.41015 0.5 8 0.5C11.5899 0.5 14.5 3.41015 14.5 7H13C13 4.51472 10.9853 2.5 8 2.5ZM13.375 5.5L16 9H14.5C14.5 12.5899 11.5899 15.5 8 15.5C4.41015 15.5 1.5 12.5899 1.5 9H3C3 11.4853 5.01472 13.5 8 13.5C10.4853 13.5 12.5 11.4853 12.5 9H10.75L13.375 5.5Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
};
18 changes: 16 additions & 2 deletions components/multimodal-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,25 @@ function PureMultimodalInput({
return myProvider.languageModel(selectedModelId);
}, [selectedModelId]);

const [refreshedUsage, setRefreshedUsage] = useState<AppUsage | undefined>(
usage
);

useEffect(() => {
setRefreshedUsage(usage);
}, [usage]);

const handleUsageRefresh = useCallback((newUsage: AppUsage | undefined) => {
setRefreshedUsage(newUsage);
}, []);

const contextProps = useMemo(
() => ({
usage,
usage: refreshedUsage,
chatId,
onUsageRefresh: handleUsageRefresh,
}),
[usage]
[refreshedUsage, chatId, handleUsageRefresh]
);

const handleFileChange = useCallback(
Expand Down
1 change: 1 addition & 0 deletions tsconfig.tsbuildinfo

Large diffs are not rendered by default.