| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- 'use client';
- import { Button } from '@/components/ui/button';
- import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
- import { Progress } from '@/components/ui/progress';
- import { cn } from '@/lib/utils';
- import type { LanguageModelUsage } from 'ai';
- import { type ComponentProps, createContext, useContext } from 'react';
- import { getUsage } from 'tokenlens';
- const PERCENT_MAX = 100;
- const ICON_RADIUS = 10;
- const ICON_VIEWBOX = 24;
- const ICON_CENTER = 12;
- const ICON_STROKE_WIDTH = 2;
- type ModelId = string;
- type ContextSchema = {
- usedTokens: number;
- maxTokens: number;
- usage?: LanguageModelUsage;
- modelId?: ModelId;
- };
- const ContextContext = createContext<ContextSchema | null>(null);
- const useContextValue = () => {
- const context = useContext(ContextContext);
- if (!context) {
- throw new Error('Context components must be used within Context');
- }
- return context;
- };
- export type ContextProps = ComponentProps<typeof HoverCard> & ContextSchema;
- export const Context = ({ usedTokens, maxTokens, usage, modelId, ...props }: ContextProps) => (
- <ContextContext.Provider
- value={{
- usedTokens,
- maxTokens,
- usage,
- modelId,
- }}
- >
- <HoverCard closeDelay={0} openDelay={0} {...props} />
- </ContextContext.Provider>
- );
- const ContextIcon = () => {
- const { usedTokens, maxTokens } = useContextValue();
- const circumference = 2 * Math.PI * ICON_RADIUS;
- const usedPercent = usedTokens / maxTokens;
- const dashOffset = circumference * (1 - usedPercent);
- return (
- <svg
- aria-label="Model context usage"
- height="20"
- role="img"
- style={{ color: 'currentcolor' }}
- viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`}
- width="20"
- >
- <circle
- cx={ICON_CENTER}
- cy={ICON_CENTER}
- fill="none"
- opacity="0.25"
- r={ICON_RADIUS}
- stroke="currentColor"
- strokeWidth={ICON_STROKE_WIDTH}
- />
- <circle
- cx={ICON_CENTER}
- cy={ICON_CENTER}
- fill="none"
- opacity="0.7"
- r={ICON_RADIUS}
- stroke="currentColor"
- strokeDasharray={`${circumference} ${circumference}`}
- strokeDashoffset={dashOffset}
- strokeLinecap="round"
- strokeWidth={ICON_STROKE_WIDTH}
- style={{ transformOrigin: 'center', transform: 'rotate(-90deg)' }}
- />
- </svg>
- );
- };
- export type ContextTriggerProps = ComponentProps<typeof Button>;
- export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => {
- const { usedTokens, maxTokens } = useContextValue();
- const usedPercent = usedTokens / maxTokens;
- const renderedPercent = new Intl.NumberFormat('en-US', {
- style: 'percent',
- maximumFractionDigits: 1,
- }).format(usedPercent);
- return (
- <HoverCardTrigger asChild>
- {children ?? (
- <Button type="button" variant="ghost" {...props}>
- <span className="font-medium text-muted-foreground">{renderedPercent}</span>
- <ContextIcon />
- </Button>
- )}
- </HoverCardTrigger>
- );
- };
- export type ContextContentProps = ComponentProps<typeof HoverCardContent>;
- export const ContextContent = ({ className, ...props }: ContextContentProps) => (
- <HoverCardContent className={cn('min-w-60 divide-y overflow-hidden p-0', className)} {...props} />
- );
- export type ContextContentHeaderProps = ComponentProps<'div'>;
- export const ContextContentHeader = ({
- children,
- className,
- ...props
- }: ContextContentHeaderProps) => {
- const { usedTokens, maxTokens } = useContextValue();
- const usedPercent = usedTokens / maxTokens;
- const displayPct = new Intl.NumberFormat('en-US', {
- style: 'percent',
- maximumFractionDigits: 1,
- }).format(usedPercent);
- const used = new Intl.NumberFormat('en-US', {
- notation: 'compact',
- }).format(usedTokens);
- const total = new Intl.NumberFormat('en-US', {
- notation: 'compact',
- }).format(maxTokens);
- return (
- <div className={cn('w-full space-y-2 p-3', className)} {...props}>
- {children ?? (
- <>
- <div className="flex items-center justify-between gap-3 text-xs">
- <p>{displayPct}</p>
- <p className="font-mono text-muted-foreground">
- {used} / {total}
- </p>
- </div>
- <div className="space-y-2">
- <Progress className="bg-muted" value={usedPercent * PERCENT_MAX} />
- </div>
- </>
- )}
- </div>
- );
- };
- export type ContextContentBodyProps = ComponentProps<'div'>;
- export const ContextContentBody = ({ children, className, ...props }: ContextContentBodyProps) => (
- <div className={cn('w-full p-3', className)} {...props}>
- {children}
- </div>
- );
- export type ContextContentFooterProps = ComponentProps<'div'>;
- export const ContextContentFooter = ({
- children,
- className,
- ...props
- }: ContextContentFooterProps) => {
- const { modelId, usage } = useContextValue();
- const costUSD = modelId
- ? getUsage({
- modelId,
- usage: {
- input: usage?.inputTokens ?? 0,
- output: usage?.outputTokens ?? 0,
- },
- }).costUSD?.totalUSD
- : undefined;
- const totalCost = new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
- }).format(costUSD ?? 0);
- return (
- <div
- className={cn(
- 'flex w-full items-center justify-between gap-3 bg-secondary p-3 text-xs',
- className,
- )}
- {...props}
- >
- {children ?? (
- <>
- <span className="text-muted-foreground">Total cost</span>
- <span>{totalCost}</span>
- </>
- )}
- </div>
- );
- };
- export type ContextInputUsageProps = ComponentProps<'div'>;
- export const ContextInputUsage = ({ className, children, ...props }: ContextInputUsageProps) => {
- const { usage, modelId } = useContextValue();
- const inputTokens = usage?.inputTokens ?? 0;
- if (children) {
- return children;
- }
- if (!inputTokens) {
- return null;
- }
- const inputCost = modelId
- ? getUsage({
- modelId,
- usage: { input: inputTokens, output: 0 },
- }).costUSD?.totalUSD
- : undefined;
- const inputCostText = new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
- }).format(inputCost ?? 0);
- return (
- <div className={cn('flex items-center justify-between text-xs', className)} {...props}>
- <span className="text-muted-foreground">Input</span>
- <TokensWithCost costText={inputCostText} tokens={inputTokens} />
- </div>
- );
- };
- export type ContextOutputUsageProps = ComponentProps<'div'>;
- export const ContextOutputUsage = ({ className, children, ...props }: ContextOutputUsageProps) => {
- const { usage, modelId } = useContextValue();
- const outputTokens = usage?.outputTokens ?? 0;
- if (children) {
- return children;
- }
- if (!outputTokens) {
- return null;
- }
- const outputCost = modelId
- ? getUsage({
- modelId,
- usage: { input: 0, output: outputTokens },
- }).costUSD?.totalUSD
- : undefined;
- const outputCostText = new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
- }).format(outputCost ?? 0);
- return (
- <div className={cn('flex items-center justify-between text-xs', className)} {...props}>
- <span className="text-muted-foreground">Output</span>
- <TokensWithCost costText={outputCostText} tokens={outputTokens} />
- </div>
- );
- };
- export type ContextReasoningUsageProps = ComponentProps<'div'>;
- export const ContextReasoningUsage = ({
- className,
- children,
- ...props
- }: ContextReasoningUsageProps) => {
- const { usage, modelId } = useContextValue();
- const reasoningTokens = usage?.reasoningTokens ?? 0;
- if (children) {
- return children;
- }
- if (!reasoningTokens) {
- return null;
- }
- const reasoningCost = modelId
- ? getUsage({
- modelId,
- usage: { reasoningTokens },
- }).costUSD?.totalUSD
- : undefined;
- const reasoningCostText = new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
- }).format(reasoningCost ?? 0);
- return (
- <div className={cn('flex items-center justify-between text-xs', className)} {...props}>
- <span className="text-muted-foreground">Reasoning</span>
- <TokensWithCost costText={reasoningCostText} tokens={reasoningTokens} />
- </div>
- );
- };
- export type ContextCacheUsageProps = ComponentProps<'div'>;
- export const ContextCacheUsage = ({ className, children, ...props }: ContextCacheUsageProps) => {
- const { usage, modelId } = useContextValue();
- const cacheTokens = usage?.cachedInputTokens ?? 0;
- if (children) {
- return children;
- }
- if (!cacheTokens) {
- return null;
- }
- const cacheCost = modelId
- ? getUsage({
- modelId,
- usage: { cacheReads: cacheTokens, input: 0, output: 0 },
- }).costUSD?.totalUSD
- : undefined;
- const cacheCostText = new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
- }).format(cacheCost ?? 0);
- return (
- <div className={cn('flex items-center justify-between text-xs', className)} {...props}>
- <span className="text-muted-foreground">Cache</span>
- <TokensWithCost costText={cacheCostText} tokens={cacheTokens} />
- </div>
- );
- };
- const TokensWithCost = ({ tokens, costText }: { tokens?: number; costText?: string }) => (
- <span>
- {tokens === undefined
- ? '—'
- : new Intl.NumberFormat('en-US', {
- notation: 'compact',
- }).format(tokens)}
- {costText ? <span className="ml-2 text-muted-foreground">• {costText}</span> : null}
- </span>
- );
|