mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 17:30:16 +01:00
feat: improve token usage progress visualization with hover card and detailed breakdown
This commit is contained in:
parent
0420ed2e68
commit
963ad6e29c
2 changed files with 161 additions and 21 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { TooltipAnchor } from '@librechat/client';
|
import { HoverCard, HoverCardTrigger, HoverCardContent, HoverCardPortal } from '@librechat/client';
|
||||||
import { useLocalize, useTokenUsage } from '~/hooks';
|
import { useLocalize, useTokenUsage } from '~/hooks';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
|
|
@ -13,6 +13,147 @@ function formatTokens(n: number): string {
|
||||||
return n.toString();
|
return n.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ProgressBarProps {
|
||||||
|
value: number;
|
||||||
|
max: number;
|
||||||
|
colorClass: string;
|
||||||
|
showPercentage?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProgressBar({ value, max, colorClass, showPercentage = false }: ProgressBarProps) {
|
||||||
|
const percentage = max > 0 ? Math.min((value / max) * 100, 100) : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-2 flex-1 overflow-hidden rounded-full bg-surface-tertiary">
|
||||||
|
<div
|
||||||
|
className={cn('h-full rounded-full transition-all duration-300', colorClass)}
|
||||||
|
style={{ width: `${percentage}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{showPercentage && (
|
||||||
|
<span className="min-w-[3rem] text-right text-xs text-text-secondary">
|
||||||
|
{Math.round(percentage)}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TokenRowProps {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
total: number;
|
||||||
|
colorClass: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TokenRow({ label, value, total, colorClass }: TokenRowProps) {
|
||||||
|
const percentage = total > 0 ? Math.round((value / total) * 100) : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-text-secondary">{label}</span>
|
||||||
|
<span className="font-medium text-text-primary">
|
||||||
|
{formatTokens(value)}
|
||||||
|
<span className="ml-1 text-xs text-text-tertiary">({percentage}%)</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ProgressBar value={value} max={total} colorClass={colorClass} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TokenUsageContent() {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { inputTokens, outputTokens, maxContext } = useTokenUsage();
|
||||||
|
|
||||||
|
const totalUsed = inputTokens + outputTokens;
|
||||||
|
const hasMaxContext = maxContext !== null && maxContext > 0;
|
||||||
|
const percentage = hasMaxContext ? Math.min((totalUsed / maxContext) * 100, 100) : 0;
|
||||||
|
|
||||||
|
const getMainProgressColor = () => {
|
||||||
|
if (!hasMaxContext) {
|
||||||
|
return 'bg-text-secondary';
|
||||||
|
}
|
||||||
|
if (percentage > 90) {
|
||||||
|
return 'bg-red-500';
|
||||||
|
}
|
||||||
|
if (percentage > 75) {
|
||||||
|
return 'bg-yellow-500';
|
||||||
|
}
|
||||||
|
return 'bg-green-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full space-y-3">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium text-text-primary">
|
||||||
|
{localize('com_ui_token_usage_context')}
|
||||||
|
</span>
|
||||||
|
{hasMaxContext && (
|
||||||
|
<span
|
||||||
|
className={cn('text-xs font-medium', {
|
||||||
|
'text-red-500': percentage > 90,
|
||||||
|
'text-yellow-500': percentage > 75 && percentage <= 90,
|
||||||
|
'text-green-500': percentage <= 75,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{localize('com_ui_token_usage_percent', { 0: Math.round(percentage).toString() })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Progress Bar */}
|
||||||
|
{hasMaxContext && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<ProgressBar value={totalUsed} max={maxContext} colorClass={getMainProgressColor()} />
|
||||||
|
<div className="flex justify-between text-xs text-text-tertiary">
|
||||||
|
<span>{formatTokens(totalUsed)}</span>
|
||||||
|
<span>{formatTokens(maxContext)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="border-t border-border-light" />
|
||||||
|
|
||||||
|
{/* Input/Output Breakdown */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<TokenRow
|
||||||
|
label={localize('com_ui_token_usage_input')}
|
||||||
|
value={inputTokens}
|
||||||
|
total={totalUsed}
|
||||||
|
colorClass="bg-blue-500"
|
||||||
|
/>
|
||||||
|
<TokenRow
|
||||||
|
label={localize('com_ui_token_usage_output')}
|
||||||
|
value={outputTokens}
|
||||||
|
total={totalUsed}
|
||||||
|
colorClass="bg-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Total Section */}
|
||||||
|
<div className="border-t border-border-light pt-2">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-text-secondary">{localize('com_ui_token_usage_total')}</span>
|
||||||
|
<span className="font-medium text-text-primary">{formatTokens(totalUsed)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Max Context (when available) */}
|
||||||
|
{hasMaxContext && (
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-text-secondary">{localize('com_ui_token_usage_max_context')}</span>
|
||||||
|
<span className="font-medium text-text-primary">{formatTokens(maxContext)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const TokenUsageIndicator = memo(function TokenUsageIndicator() {
|
const TokenUsageIndicator = memo(function TokenUsageIndicator() {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { inputTokens, outputTokens, maxContext } = useTokenUsage();
|
const { inputTokens, outputTokens, maxContext } = useTokenUsage();
|
||||||
|
|
@ -28,17 +169,6 @@ const TokenUsageIndicator = memo(function TokenUsageIndicator() {
|
||||||
const circumference = 2 * Math.PI * radius;
|
const circumference = 2 * Math.PI * radius;
|
||||||
const offset = circumference - (percentage / 100) * circumference;
|
const offset = circumference - (percentage / 100) * circumference;
|
||||||
|
|
||||||
const tooltipText = hasMaxContext
|
|
||||||
? localize('com_ui_token_usage_with_max', {
|
|
||||||
0: formatTokens(inputTokens),
|
|
||||||
1: formatTokens(outputTokens),
|
|
||||||
2: formatTokens(maxContext),
|
|
||||||
})
|
|
||||||
: localize('com_ui_token_usage_no_max', {
|
|
||||||
0: formatTokens(inputTokens),
|
|
||||||
1: formatTokens(outputTokens),
|
|
||||||
});
|
|
||||||
|
|
||||||
const ariaLabel = hasMaxContext
|
const ariaLabel = hasMaxContext
|
||||||
? localize('com_ui_token_usage_aria_full', {
|
? localize('com_ui_token_usage_aria_full', {
|
||||||
0: formatTokens(inputTokens),
|
0: formatTokens(inputTokens),
|
||||||
|
|
@ -66,12 +196,11 @@ const TokenUsageIndicator = memo(function TokenUsageIndicator() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipAnchor
|
<HoverCard openDelay={200} closeDelay={100}>
|
||||||
description={tooltipText}
|
<HoverCardTrigger asChild>
|
||||||
render={
|
<button
|
||||||
<div
|
type="button"
|
||||||
className="flex size-9 items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover"
|
className="flex size-9 items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
|
||||||
role="img"
|
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -103,9 +232,14 @@ const TokenUsageIndicator = memo(function TokenUsageIndicator() {
|
||||||
className={cn('transition-all duration-300', getProgressColor())}
|
className={cn('transition-all duration-300', getProgressColor())}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</button>
|
||||||
}
|
</HoverCardTrigger>
|
||||||
/>
|
<HoverCardPortal>
|
||||||
|
<HoverCardContent side="top" align="end" className="p-3">
|
||||||
|
<TokenUsageContent />
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCardPortal>
|
||||||
|
</HoverCard>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1324,8 +1324,14 @@
|
||||||
"com_ui_token_url": "Token URL",
|
"com_ui_token_url": "Token URL",
|
||||||
"com_ui_token_usage_aria_full": "Token usage: {{0}} input, {{1}} output, {{2}} max context, {{3}}% used",
|
"com_ui_token_usage_aria_full": "Token usage: {{0}} input, {{1}} output, {{2}} max context, {{3}}% used",
|
||||||
"com_ui_token_usage_aria_no_max": "Token usage: {{0}} input, {{1}} output",
|
"com_ui_token_usage_aria_no_max": "Token usage: {{0}} input, {{1}} output",
|
||||||
|
"com_ui_token_usage_context": "Context Usage",
|
||||||
"com_ui_token_usage_indicator": "Token usage indicator",
|
"com_ui_token_usage_indicator": "Token usage indicator",
|
||||||
|
"com_ui_token_usage_input": "Input",
|
||||||
|
"com_ui_token_usage_max_context": "Max Context",
|
||||||
"com_ui_token_usage_no_max": "Input: {{0}} | Output: {{1}} | Max: N/A",
|
"com_ui_token_usage_no_max": "Input: {{0}} | Output: {{1}} | Max: N/A",
|
||||||
|
"com_ui_token_usage_output": "Output",
|
||||||
|
"com_ui_token_usage_percent": "{{0}}% used",
|
||||||
|
"com_ui_token_usage_total": "Total",
|
||||||
"com_ui_token_usage_with_max": "Input: {{0}} | Output: {{1}} | Max: {{2}}",
|
"com_ui_token_usage_with_max": "Input: {{0}} | Output: {{1}} | Max: {{2}}",
|
||||||
"com_ui_tokens": "tokens",
|
"com_ui_tokens": "tokens",
|
||||||
"com_ui_tool_collection_prefix": "A collection of tools from",
|
"com_ui_tool_collection_prefix": "A collection of tools from",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue