mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
feat: enhance accessibility in TokenUsageIndicator
This commit is contained in:
parent
01ca9b1655
commit
71b94cdcaa
1 changed files with 46 additions and 29 deletions
|
|
@ -17,15 +17,23 @@ interface ProgressBarProps {
|
||||||
value: number;
|
value: number;
|
||||||
max: number;
|
max: number;
|
||||||
colorClass: string;
|
colorClass: string;
|
||||||
|
label: string;
|
||||||
showPercentage?: boolean;
|
showPercentage?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProgressBar({ value, max, colorClass, showPercentage = false }: ProgressBarProps) {
|
function ProgressBar({ value, max, colorClass, label, showPercentage = false }: ProgressBarProps) {
|
||||||
const percentage = max > 0 ? Math.min((value / max) * 100, 100) : 0;
|
const percentage = max > 0 ? Math.min((value / max) * 100, 100) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-2 flex-1 overflow-hidden rounded-full bg-surface-secondary">
|
<div
|
||||||
|
role="progressbar"
|
||||||
|
aria-valuenow={Math.round(percentage)}
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={100}
|
||||||
|
aria-label={label}
|
||||||
|
className="h-2 flex-1 overflow-hidden rounded-full bg-surface-secondary"
|
||||||
|
>
|
||||||
<div className="flex h-full rounded-full">
|
<div className="flex h-full rounded-full">
|
||||||
<div
|
<div
|
||||||
className={cn('rounded-full transition-all duration-300', colorClass)}
|
className={cn('rounded-full transition-all duration-300', colorClass)}
|
||||||
|
|
@ -35,7 +43,7 @@ function ProgressBar({ value, max, colorClass, showPercentage = false }: Progres
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showPercentage && (
|
{showPercentage && (
|
||||||
<span className="min-w-[3rem] text-right text-xs text-text-secondary">
|
<span className="min-w-[3rem] text-right text-xs text-text-secondary" aria-hidden="true">
|
||||||
{Math.round(percentage)}%
|
{Math.round(percentage)}%
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -48,9 +56,10 @@ interface TokenRowProps {
|
||||||
value: number;
|
value: number;
|
||||||
total: number;
|
total: number;
|
||||||
colorClass: string;
|
colorClass: string;
|
||||||
|
ariaLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TokenRow({ label, value, total, colorClass }: TokenRowProps) {
|
function TokenRow({ label, value, total, colorClass, ariaLabel }: TokenRowProps) {
|
||||||
const percentage = total > 0 ? Math.round((value / total) * 100) : 0;
|
const percentage = total > 0 ? Math.round((value / total) * 100) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -59,10 +68,12 @@ function TokenRow({ label, value, total, colorClass }: TokenRowProps) {
|
||||||
<span className="text-text-secondary">{label}</span>
|
<span className="text-text-secondary">{label}</span>
|
||||||
<span className="font-medium text-text-primary">
|
<span className="font-medium text-text-primary">
|
||||||
{formatTokens(value)}
|
{formatTokens(value)}
|
||||||
<span className="ml-1 text-xs text-text-secondary">({percentage}%)</span>
|
<span className="ml-1 text-xs text-text-secondary" aria-hidden="true">
|
||||||
|
({percentage}%)
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ProgressBar value={value} max={total} colorClass={colorClass} />
|
<ProgressBar value={value} max={total} colorClass={colorClass} label={ariaLabel} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -88,11 +99,18 @@ function TokenUsageContent() {
|
||||||
return 'bg-green-500';
|
return 'bg-green-500';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const inputPercentage = totalUsed > 0 ? Math.round((inputTokens / totalUsed) * 100) : 0;
|
||||||
|
const outputPercentage = totalUsed > 0 ? Math.round((outputTokens / totalUsed) * 100) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full space-y-3">
|
<div
|
||||||
|
className="w-full space-y-3"
|
||||||
|
role="region"
|
||||||
|
aria-label={localize('com_ui_token_usage_context')}
|
||||||
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm font-medium text-text-primary">
|
<span className="text-sm font-medium text-text-primary" id="token-usage-title">
|
||||||
{localize('com_ui_token_usage_context')}
|
{localize('com_ui_token_usage_context')}
|
||||||
</span>
|
</span>
|
||||||
{hasMaxContext && (
|
{hasMaxContext && (
|
||||||
|
|
@ -111,8 +129,13 @@ function TokenUsageContent() {
|
||||||
{/* Main Progress Bar */}
|
{/* Main Progress Bar */}
|
||||||
{hasMaxContext && (
|
{hasMaxContext && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<ProgressBar value={totalUsed} max={maxContext} colorClass={getMainProgressColor()} />
|
<ProgressBar
|
||||||
<div className="flex justify-between text-xs text-text-secondary">
|
value={totalUsed}
|
||||||
|
max={maxContext}
|
||||||
|
colorClass={getMainProgressColor()}
|
||||||
|
label={`${localize('com_ui_token_usage_context')}: ${formatTokens(totalUsed)} of ${formatTokens(maxContext)}, ${Math.round(percentage)}%`}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-xs text-text-secondary" aria-hidden="true">
|
||||||
<span>{formatTokens(totalUsed)}</span>
|
<span>{formatTokens(totalUsed)}</span>
|
||||||
<span>{formatTokens(maxContext)}</span>
|
<span>{formatTokens(maxContext)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -120,7 +143,7 @@ function TokenUsageContent() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="border-t border-border-light" />
|
<div className="border-t border-border-light" role="separator" />
|
||||||
|
|
||||||
{/* Input/Output Breakdown */}
|
{/* Input/Output Breakdown */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|
@ -129,30 +152,16 @@ function TokenUsageContent() {
|
||||||
value={inputTokens}
|
value={inputTokens}
|
||||||
total={totalUsed}
|
total={totalUsed}
|
||||||
colorClass="bg-blue-500"
|
colorClass="bg-blue-500"
|
||||||
|
ariaLabel={`${localize('com_ui_token_usage_input')}: ${formatTokens(inputTokens)}, ${inputPercentage}% of total`}
|
||||||
/>
|
/>
|
||||||
<TokenRow
|
<TokenRow
|
||||||
label={localize('com_ui_token_usage_output')}
|
label={localize('com_ui_token_usage_output')}
|
||||||
value={outputTokens}
|
value={outputTokens}
|
||||||
total={totalUsed}
|
total={totalUsed}
|
||||||
colorClass="bg-green-500"
|
colorClass="bg-green-500"
|
||||||
|
ariaLabel={`${localize('com_ui_token_usage_output')}: ${formatTokens(outputTokens)}, ${outputPercentage}% of total`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -203,8 +212,9 @@ const TokenUsageIndicator = memo(function TokenUsageIndicator() {
|
||||||
<HoverCardTrigger asChild>
|
<HoverCardTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
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"
|
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"
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
|
aria-haspopup="dialog"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
|
|
@ -212,6 +222,7 @@ const TokenUsageIndicator = memo(function TokenUsageIndicator() {
|
||||||
viewBox={`0 0 ${size} ${size}`}
|
viewBox={`0 0 ${size} ${size}`}
|
||||||
className="rotate-[-90deg]"
|
className="rotate-[-90deg]"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
>
|
>
|
||||||
{/* Background ring */}
|
{/* Background ring */}
|
||||||
<circle
|
<circle
|
||||||
|
|
@ -238,7 +249,13 @@ const TokenUsageIndicator = memo(function TokenUsageIndicator() {
|
||||||
</button>
|
</button>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardPortal>
|
<HoverCardPortal>
|
||||||
<HoverCardContent side="top" align="end" className="p-3">
|
<HoverCardContent
|
||||||
|
side="top"
|
||||||
|
align="end"
|
||||||
|
className="p-3"
|
||||||
|
role="dialog"
|
||||||
|
aria-label={localize('com_ui_token_usage_context')}
|
||||||
|
>
|
||||||
<TokenUsageContent />
|
<TokenUsageContent />
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
</HoverCardPortal>
|
</HoverCardPortal>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue