mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-22 18:26:12 +01:00
✅️ feat: Accessible Model Selection Icons and Announcements (#11454)
* feat: more accessible model selection ui and announcements * chore: formatting
This commit is contained in:
parent
e2ec3f18c9
commit
9d612715a5
5 changed files with 72 additions and 88 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue