️ feat: Accessible Model Selection Icons and Announcements (#11454)

* feat: more accessible model selection ui and announcements

* chore: formatting
This commit is contained in:
Dustin Healy 2026-01-21 10:53:10 -08:00 committed by GitHub
parent e2ec3f18c9
commit 9d612715a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 72 additions and 88 deletions

View file

@ -1,4 +1,5 @@
import { useMemo } from 'react';
import { VisuallyHidden } from '@ariakit/react';
import { Spinner, TooltipAnchor } from '@librechat/client';
import { CheckCircle2, MousePointerClick, SettingsIcon } from 'lucide-react';
import { EModelEndpoint, isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
@ -126,6 +127,8 @@ export function EndpointItem({ endpoint, endpointIndex }: EndpointItemProps) {
</div>
);
const isEndpointSelected = selectedEndpoint === endpoint.value;
if (endpoint.hasModels) {
const filteredModels = searchValue
? filterModels(
@ -153,9 +156,17 @@ export function EndpointItem({ endpoint, endpointIndex }: EndpointItemProps) {
label={
<div className="group flex w-full min-w-0 items-center justify-between gap-1.5 py-1 text-sm">
{renderIconLabel()}
{isUserProvided && (
<SettingsButton endpoint={endpoint} handleOpenKeyDialog={handleOpenKeyDialog} />
)}
<div className="flex shrink-0 items-center gap-1">
{isUserProvided && (
<SettingsButton endpoint={endpoint} handleOpenKeyDialog={handleOpenKeyDialog} />
)}
{isEndpointSelected && (
<>
<CheckCircle2 className="size-4 shrink-0 text-text-primary" aria-hidden="true" />
<VisuallyHidden>{localize('com_a11y_selected')}</VisuallyHidden>
</>
)}
</div>
</div>
}
>
@ -200,6 +211,7 @@ export function EndpointItem({ endpoint, endpointIndex }: EndpointItemProps) {
id={`endpoint-${endpoint.value}-menu`}
key={`endpoint-${endpoint.value}-item`}
onClick={() => handleSelectEndpoint(endpoint)}
aria-selected={isEndpointSelected || undefined}
className="group flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-sm"
>
{renderIconLabel()}
@ -218,8 +230,11 @@ export function EndpointItem({ endpoint, endpointIndex }: EndpointItemProps) {
}
/>
)}
{selectedEndpoint === endpoint.value && !isAssistantsNotLoaded && (
<CheckCircle2 className="size-4 shrink-0 text-text-primary" aria-hidden="true" />
{isEndpointSelected && !isAssistantsNotLoaded && (
<>
<CheckCircle2 className="size-4 shrink-0 text-text-primary" aria-hidden="true" />
<VisuallyHidden>{localize('com_a11y_selected')}</VisuallyHidden>
</>
)}
</div>
</MenuItem>

View file

@ -1,5 +1,6 @@
import React, { useRef, useState, useEffect } from 'react';
import { EarthIcon, Pin, PinOff } from 'lucide-react';
import { VisuallyHidden } from '@ariakit/react';
import { CheckCircle2, EarthIcon, Pin, PinOff } from 'lucide-react';
import { isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import { useModelSelectorContext } from '../ModelSelectorContext';
import { CustomMenuItem as MenuItem } from '../CustomMenu';
@ -110,6 +111,7 @@ export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointMod
<MenuItem
ref={itemRef}
onClick={() => handleSelectModel(endpoint, modelId ?? '')}
aria-selected={isSelected || undefined}
className="group flex w-full cursor-pointer items-center justify-between rounded-lg px-2 text-sm"
>
<div className="flex w-full min-w-0 items-center gap-2 px-1 py-1">
@ -133,23 +135,10 @@ export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointMod
)}
</button>
{isSelected && (
<div className="flex-shrink-0 self-center">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="block"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM16.0755 7.93219C16.5272 8.25003 16.6356 8.87383 16.3178 9.32549L11.5678 16.0755C11.3931 16.3237 11.1152 16.4792 10.8123 16.4981C10.5093 16.517 10.2142 16.3973 10.0101 16.1727L7.51006 13.4227C7.13855 13.014 7.16867 12.3816 7.57733 12.0101C7.98598 11.6386 8.61843 11.6687 8.98994 12.0773L10.6504 13.9039L14.6822 8.17451C15 7.72284 15.6238 7.61436 16.0755 7.93219Z"
fill="currentColor"
/>
</svg>
</div>
<>
<CheckCircle2 className="size-4 shrink-0 text-text-primary" aria-hidden="true" />
<VisuallyHidden>{localize('com_a11y_selected')}</VisuallyHidden>
</>
)}
</MenuItem>
);

View file

@ -1,7 +1,10 @@
import React from 'react';
import { CheckCircle2 } from 'lucide-react';
import { VisuallyHidden } from '@ariakit/react';
import type { TModelSpec } from 'librechat-data-provider';
import { CustomMenuItem as MenuItem } from '../CustomMenu';
import { useModelSelectorContext } from '../ModelSelectorContext';
import { useLocalize } from '~/hooks';
import SpecIcon from './SpecIcon';
import { cn } from '~/utils';
@ -11,12 +14,14 @@ interface ModelSpecItemProps {
}
export function ModelSpecItem({ spec, isSelected }: ModelSpecItemProps) {
const localize = useLocalize();
const { handleSelectSpec, endpointsConfig } = useModelSelectorContext();
const { showIconInMenu = true } = spec;
return (
<MenuItem
key={spec.name}
onClick={() => handleSelectSpec(spec)}
aria-selected={isSelected || undefined}
className={cn(
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 text-sm',
)}
@ -40,23 +45,13 @@ export function ModelSpecItem({ spec, isSelected }: ModelSpecItemProps) {
</div>
</div>
{isSelected && (
<div className="flex-shrink-0 self-center">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="block"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM16.0755 7.93219C16.5272 8.25003 16.6356 8.87383 16.3178 9.32549L11.5678 16.0755C11.3931 16.3237 11.1152 16.4792 10.8123 16.4981C10.5093 16.517 10.2142 16.3973 10.0101 16.1727L7.51006 13.4227C7.13855 13.014 7.16867 12.3816 7.57733 12.0101C7.98598 11.6386 8.61843 11.6687 8.98994 12.0773L10.6504 13.9039L14.6822 8.17451C15 7.72284 15.6238 7.61436 16.0755 7.93219Z"
fill="currentColor"
/>
</svg>
</div>
<>
<CheckCircle2
className="size-4 shrink-0 self-center text-text-primary"
aria-hidden="true"
/>
<VisuallyHidden>{localize('com_a11y_selected')}</VisuallyHidden>
</>
)}
</MenuItem>
);

View file

@ -1,5 +1,6 @@
import React, { Fragment } from 'react';
import { EarthIcon } from 'lucide-react';
import { VisuallyHidden } from '@ariakit/react';
import { CheckCircle2, EarthIcon } from 'lucide-react';
import { isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type { TModelSpec } from 'librechat-data-provider';
import type { Endpoint } from '~/common';
@ -60,6 +61,7 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP
<MenuItem
key={spec.name}
onClick={() => handleSelectSpec(spec)}
aria-selected={selectedSpec === spec.name || undefined}
className={cn(
'flex w-full cursor-pointer justify-between rounded-lg px-2 text-sm',
spec.description ? 'items-start' : 'items-center',
@ -84,23 +86,16 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP
</div>
</div>
{selectedSpec === spec.name && (
<div className={cn('flex-shrink-0', spec.description ? 'pt-1' : '')}>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="block"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM16.0755 7.93219C16.5272 8.25003 16.6356 8.87383 16.3178 9.32549L11.5678 16.0755C11.3931 16.3237 11.1152 16.4792 10.8123 16.4981C10.5093 16.517 10.2142 16.3973 10.0101 16.1727L7.51006 13.4227C7.13855 13.014 7.16867 12.3816 7.57733 12.0101C7.98598 11.6386 8.61843 11.6687 8.98994 12.0773L10.6504 13.9039L14.6822 8.17451C15 7.72284 15.6238 7.61436 16.0755 7.93219Z"
fill="currentColor"
/>
</svg>
</div>
<>
<CheckCircle2
className={cn(
'size-4 shrink-0 text-text-primary',
spec.description ? 'mt-1' : '',
)}
aria-hidden="true"
/>
<VisuallyHidden>{localize('com_a11y_selected')}</VisuallyHidden>
</>
)}
</MenuItem>
);
@ -164,10 +159,13 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP
modelName = endpoint.assistantNames[modelId];
}
const isModelSelected =
selectedEndpoint === endpoint.value && selectedModel === modelId;
return (
<MenuItem
key={`${endpoint.value}-${modelId}-search-${i}`}
onClick={() => handleSelectModel(endpoint, modelId)}
aria-selected={isModelSelected || undefined}
className="flex w-full cursor-pointer items-center justify-start rounded-lg px-3 py-2 pl-6 text-sm"
>
<div className="flex items-center gap-2">
@ -185,22 +183,14 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP
{isGlobal && (
<EarthIcon className="ml-auto size-4 text-green-400" aria-hidden="true" />
)}
{selectedEndpoint === endpoint.value && selectedModel === modelId && (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="block"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM16.0755 7.93219C16.5272 8.25003 16.6356 8.87383 16.3178 9.32549L11.5678 16.0755C11.3931 16.3237 11.1152 16.4792 10.8123 16.4981C10.5093 16.517 10.2142 16.3973 10.0101 16.1727L7.51006 13.4227C7.13855 13.014 7.16867 12.3816 7.57733 12.0101C7.98598 11.6386 8.61843 11.6687 8.98994 12.0773L10.6504 13.9039L14.6822 8.17451C15 7.72284 15.6238 7.61436 16.0755 7.93219Z"
fill="currentColor"
{isModelSelected && (
<>
<CheckCircle2
className="size-4 shrink-0 text-text-primary"
aria-hidden="true"
/>
</svg>
<VisuallyHidden>{localize('com_a11y_selected')}</VisuallyHidden>
</>
)}
</MenuItem>
);
@ -209,10 +199,12 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP
);
} else {
// Endpoints with no models
const isEndpointSelected = selectedEndpoint === endpoint.value;
return (
<MenuItem
key={`endpoint-${endpoint.value}-search-item`}
onClick={() => handleSelectEndpoint(endpoint)}
aria-selected={isEndpointSelected || undefined}
className="flex w-full cursor-pointer items-center justify-between rounded-xl px-3 py-2 text-sm"
>
<div className="flex items-center gap-2">
@ -226,22 +218,14 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP
)}
<span>{endpoint.label}</span>
</div>
{selectedEndpoint === endpoint.value && (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="block"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM16.0755 7.93219C16.5272 8.25003 16.6356 8.87383 16.3178 9.32549L11.5678 16.0755C11.3931 16.3237 11.1152 16.4792 10.8123 16.4981C10.5093 16.517 10.2142 16.3973 10.0101 16.1727L7.51006 13.4227C7.13855 13.014 7.16867 12.3816 7.57733 12.0101C7.98598 11.6386 8.61843 11.6687 8.98994 12.0773L10.6504 13.9039L14.6822 8.17451C15 7.72284 15.6238 7.61436 16.0755 7.93219Z"
fill="currentColor"
{isEndpointSelected && (
<>
<CheckCircle2
className="size-4 shrink-0 text-text-primary"
aria-hidden="true"
/>
</svg>
<VisuallyHidden>{localize('com_a11y_selected')}</VisuallyHidden>
</>
)}
</MenuItem>
);

View file

@ -3,6 +3,7 @@
"chat_direction_right_to_left": "Right to Left",
"com_a11y_ai_composing": "The AI is still composing.",
"com_a11y_end": "The AI has finished their reply.",
"com_a11y_selected": "selected",
"com_a11y_start": "The AI has started their reply.",
"com_agents_agent_card_label": "{{name}} agent. {{description}}",
"com_agents_all": "All Agents",