c18ca4693c
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
70 lines
2.1 KiB
TypeScript
70 lines
2.1 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { CircleGauge } from "lucide-react";
|
|
import {
|
|
formatCompactTokens,
|
|
type ContextUsageEvent,
|
|
} from "@/lib/contextUsage";
|
|
import { cn } from "@/lib/utils";
|
|
import { useTranslations } from "next-intl";
|
|
|
|
/**
|
|
* Live context-occupancy pill.
|
|
*
|
|
* `measuredTokens` is the provider-measured prompt size of the most recent
|
|
* model call (covers system prompt, memory and tools) and is preferred over
|
|
* the middleware's message-text estimate, which systematically undercounts
|
|
* whenever prompts carry large non-message sections. The denominator is the
|
|
* snapshot-frozen `input_limit` carried by the stream event, so the pill
|
|
* appears once the session's first model call completes.
|
|
*/
|
|
export const ContextUsageIndicator = React.memo<{
|
|
usage: ContextUsageEvent | null;
|
|
measuredTokens: number | null;
|
|
}>(function ContextUsageIndicator({ usage, measuredTokens }) {
|
|
const t = useTranslations("chat");
|
|
if (!usage) return null;
|
|
const estimated = usage.usedTokens + usage.reservedTokens;
|
|
const occupied = measuredTokens ?? estimated;
|
|
const limit = usage.inputLimit;
|
|
|
|
const percent = Math.max(0, Math.round((occupied / limit) * 100));
|
|
const nearlyFull = occupied >= limit * 0.85;
|
|
const label = t("contextUsage.label", {
|
|
percent,
|
|
used: formatCompactTokens(occupied),
|
|
limit: formatCompactTokens(limit),
|
|
});
|
|
const tooltipParams = {
|
|
used: occupied.toLocaleString(),
|
|
limit: limit.toLocaleString(),
|
|
soft: usage.softTokens.toLocaleString(),
|
|
};
|
|
const tooltip =
|
|
measuredTokens !== null
|
|
? t("contextUsage.tooltipMeasured", tooltipParams)
|
|
: t("contextUsage.tooltipEstimate", tooltipParams);
|
|
|
|
return (
|
|
<span
|
|
title={tooltip}
|
|
aria-label={tooltip}
|
|
className={cn(
|
|
"flex shrink-0 items-center gap-1.5 rounded px-1.5 py-0.5",
|
|
nearlyFull
|
|
? "text-amber-700 dark:text-amber-300"
|
|
: "text-muted-foreground"
|
|
)}
|
|
>
|
|
<CircleGauge
|
|
className="size-3.5 shrink-0"
|
|
aria-hidden="true"
|
|
/>
|
|
<span className="hidden sm:inline">{label}</span>
|
|
</span>
|
|
);
|
|
});
|
|
|
|
ContextUsageIndicator.displayName = "ContextUsageIndicator";
|