From a5189052ec06dfda343a198d6e5e5174fe0ef31a Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Tue, 7 Oct 2025 20:12:49 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BF=EF=B8=8F=20fix:=20Accessibility,=20UI?= =?UTF-8?q?=20consistency,=20dialog=20&=20avatar=20refactors=20(#9975)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔧 refactor: Improve accessibility and styling in ChatGroupItem and FilterPrompts components * 🔧 fix: Add button type and keyboard accessibility to dropdown menu trigger in ChatGroupItem * 🔧 fix(757): Enhance accessibility by updating aria-labels and adding localization for prompt groups * 🔧 fix(618): Update version to 0.3.1 and enhance accessibility in InfoHoverCard component * 🔧 fix(618): Update aria-label in InfoHoverCard to use dynamic text prop for improved accessibility * 🔧 fix: Enhance accessibility by updating aria-labels and roles in Conversations components * 🔧 fix(620): Enhance accessibility by adding tabIndex to Tabs.Content components in ArtifactTabs, Settings, and Speech components * refactor: remove RevokeKeysButton component and update related components for accessibility - Deleted RevokeKeysButton component. - Updated SharedLinks and General components to use Label for accessibility. - Enhanced Personalization component with aria-labelledby and aria-describedby attributes. - Refactored ConversationModeSwitch to use ToggleSwitch for better state management. - Improved AutoSendTextSelector with local state management and accessibility attributes. - Replaced Switch components with ToggleSwitch in various Speech and TTS components for consistency. - Added aria-labelledby attributes to Dropdown components for better accessibility. - Updated translation.json to include new localization keys and improved existing ones. - Enhanced Slider component to support aria attributes for better accessibility. * 🔧 fix: Enhance user feedback for API key operations with success and error messages * 🔧 fix: Update aria-labels in Avatar component for improved localization and accessibility * 🔧 fix: Refactor handleFile and handleDrop functions for improved readability and maintainability --- .../src/components/Artifacts/ArtifactTabs.tsx | 3 +- client/src/components/Audio/Voices.tsx | 10 +- .../Conversations/Conversations.tsx | 5 +- client/src/components/Conversations/Convo.tsx | 8 +- .../components/Conversations/ConvoLink.tsx | 3 +- .../ConvoOptions/ConvoOptions.tsx | 1 + .../Input/SetKeyDialog/SetKeyDialog.tsx | 221 ++++++++--- client/src/components/Nav/Settings.tsx | 18 +- .../Nav/SettingsTabs/Account/Avatar.tsx | 272 +++++++++---- .../SettingsTabs/Account/DeleteAccount.tsx | 5 +- .../Account/DisableTwoFactorToggle.tsx | 2 +- .../Account/DisplayUsernameMessages.tsx | 3 +- .../Nav/SettingsTabs/Chat/ChatDirection.tsx | 14 +- .../SettingsTabs/Chat/FontSizeSelector.tsx | 5 +- .../Nav/SettingsTabs/Chat/ForkSettings.tsx | 9 +- .../SettingsTabs/Commands/AtCommandSwitch.tsx | 26 -- .../Nav/SettingsTabs/Commands/Commands.tsx | 63 ++- .../Commands/PlusCommandSwitch.tsx | 26 -- .../Commands/SlashCommandSwitch.tsx | 25 -- .../Nav/SettingsTabs/Data/ClearChats.tsx | 6 +- .../components/Nav/SettingsTabs/Data/Data.tsx | 4 +- .../Nav/SettingsTabs/Data/DeleteCache.tsx | 4 +- .../SettingsTabs/Data/ImportConversations.tsx | 152 ++++---- .../Nav/SettingsTabs/Data/RevokeAllKeys.tsx | 15 - .../Nav/SettingsTabs/Data/RevokeKeys.tsx | 72 ++++ .../SettingsTabs/Data/RevokeKeysButton.tsx | 84 ---- .../Nav/SettingsTabs/Data/SharedLinks.tsx | 6 +- .../Nav/SettingsTabs/General/General.tsx | 10 +- .../Nav/SettingsTabs/Personalization.tsx | 10 +- .../Speech/ConversationModeSwitch.tsx | 29 +- .../Speech/STT/AutoSendTextSelector.tsx | 126 ++++-- .../Speech/STT/AutoTranscribeAudioSwitch.tsx | 34 +- .../Speech/STT/DecibelSelector.tsx | 4 +- .../Speech/STT/EngineSTTDropdown.tsx | 5 +- .../Speech/STT/LanguageSTTDropdown.tsx | 5 +- .../Speech/STT/SpeechToTextSwitch.tsx | 33 +- .../Nav/SettingsTabs/Speech/Speech.tsx | 6 +- .../Speech/TTS/AutomaticPlaybackSwitch.tsx | 30 +- .../Speech/TTS/CacheTTSSwitch.tsx | 34 +- .../Speech/TTS/CloudBrowserVoicesSwitch.tsx | 36 +- .../Speech/TTS/EngineTTSDropdown.tsx | 5 +- .../SettingsTabs/Speech/TTS/PlaybackRate.tsx | 4 +- .../Speech/TTS/TextToSpeechSwitch.tsx | 33 +- .../Nav/SettingsTabs/ToggleSwitch.tsx | 18 +- .../src/components/Nav/SettingsTabs/index.ts | 9 +- .../Prompts/Groups/ChatGroupItem.tsx | 26 +- .../Prompts/Groups/DashGroupItem.tsx | 2 +- .../Prompts/Groups/FilterPrompts.tsx | 2 +- client/src/components/Prompts/Groups/List.tsx | 2 +- .../src/components/Prompts/PreviewPrompt.tsx | 2 +- client/src/locales/en/translation.json | 31 +- packages/client/src/components/Dropdown.tsx | 3 + .../client/src/components/DropdownMenu.tsx | 360 ++++++++++-------- .../client/src/components/InfoHoverCard.tsx | 17 +- packages/client/src/components/Label.tsx | 2 +- packages/client/src/components/Slider.tsx | 75 ++-- 56 files changed, 1158 insertions(+), 857 deletions(-) delete mode 100644 client/src/components/Nav/SettingsTabs/Commands/AtCommandSwitch.tsx delete mode 100644 client/src/components/Nav/SettingsTabs/Commands/PlusCommandSwitch.tsx delete mode 100644 client/src/components/Nav/SettingsTabs/Commands/SlashCommandSwitch.tsx delete mode 100644 client/src/components/Nav/SettingsTabs/Data/RevokeAllKeys.tsx create mode 100644 client/src/components/Nav/SettingsTabs/Data/RevokeKeys.tsx delete mode 100644 client/src/components/Nav/SettingsTabs/Data/RevokeKeysButton.tsx diff --git a/client/src/components/Artifacts/ArtifactTabs.tsx b/client/src/components/Artifacts/ArtifactTabs.tsx index cd8c441ad7..a463aca792 100644 --- a/client/src/components/Artifacts/ArtifactTabs.tsx +++ b/client/src/components/Artifacts/ArtifactTabs.tsx @@ -44,6 +44,7 @@ export default function ArtifactTabs({ value="code" id="artifacts-code" className={cn('flex-grow overflow-auto')} + tabIndex={-1} > {isMermaid ? ( @@ -58,7 +59,7 @@ export default function ArtifactTabs({ /> )} - + -
{localize('com_nav_voice_select')}
+
{localize('com_nav_voice_select')}
); @@ -48,9 +51,11 @@ export function ExternalVoiceDropdown() { } }; + const labelId = 'external-voice-dropdown-label'; + return (
-
{localize('com_nav_voice_select')}
+
{localize('com_nav_voice_select')}
); diff --git a/client/src/components/Conversations/Conversations.tsx b/client/src/components/Conversations/Conversations.tsx index 3a04f558f9..b16c6458c7 100644 --- a/client/src/components/Conversations/Conversations.tsx +++ b/client/src/components/Conversations/Conversations.tsx @@ -28,6 +28,8 @@ const LoadingSpinner = memo(() => { ); }); +LoadingSpinner.displayName = 'LoadingSpinner'; + const DateLabel: FC<{ groupName: string }> = memo(({ groupName }) => { const localize = useLocalize(); return ( @@ -74,6 +76,7 @@ const Conversations: FC = ({ isLoading, isSearchLoading, }) => { + const localize = useLocalize(); const isSmallScreen = useMediaQuery('(max-width: 768px)'); const convoHeight = isSmallScreen ? 44 : 34; @@ -181,7 +184,7 @@ const Conversations: FC = ({ {isSearchLoading ? (
- Loading... + {localize('com_ui_loading')}
) : (
diff --git a/client/src/components/Conversations/Convo.tsx b/client/src/components/Conversations/Convo.tsx index 190cef2a4e..048c2f129d 100644 --- a/client/src/components/Conversations/Convo.tsx +++ b/client/src/components/Conversations/Convo.tsx @@ -135,8 +135,9 @@ export default function Conversation({ conversation, retainView, toggleNav }: Co 'group relative flex h-12 w-full items-center rounded-lg transition-colors duration-200 md:h-9', isActiveConvo ? 'bg-surface-active-alt' : 'hover:bg-surface-active-alt', )} - role="listitem" - tabIndex={0} + role="button" + tabIndex={renaming ? -1 : 0} + aria-label={`${title || localize('com_ui_untitled')} conversation`} onClick={(e) => { if (renaming) { return; @@ -149,7 +150,8 @@ export default function Conversation({ conversation, retainView, toggleNav }: Co if (renaming) { return; } - if (e.key === 'Enter') { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); handleNavigation(false); } }} diff --git a/client/src/components/Conversations/ConvoLink.tsx b/client/src/components/Conversations/ConvoLink.tsx index 1667cf0980..68c16594a5 100644 --- a/client/src/components/Conversations/ConvoLink.tsx +++ b/client/src/components/Conversations/ConvoLink.tsx @@ -40,8 +40,7 @@ const ConvoLink: React.FC = ({ e.stopPropagation(); onRename(); }} - role="button" - aria-label={isSmallScreen ? undefined : title || localize('com_ui_untitled')} + aria-label={title || localize('com_ui_untitled')} > {title || localize('com_ui_untitled')}
diff --git a/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx b/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx index 7affbd8e93..9cf1a109d3 100644 --- a/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx +++ b/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx @@ -201,6 +201,7 @@ function ConvoOptions({ void; +}) => { + const localize = useLocalize(); + const [open, setOpen] = useState(false); + const { showToast } = useToastContext(); + const revokeKeyMutation = useRevokeUserKeyMutation(endpoint); + const revokeKeysMutation = useRevokeAllUserKeysMutation(); + + const handleSuccess = () => { + showToast({ + message: localize('com_ui_revoke_key_success'), + status: NotificationSeverity.SUCCESS, + }); + + if (!setDialogOpen) { + return; + } + + setDialogOpen(false); + }; + + const handleError = () => { + showToast({ + message: localize('com_ui_revoke_key_error'), + status: NotificationSeverity.ERROR, + }); + }; + + const onClick = () => { + revokeKeyMutation.mutate( + {}, + { + onSuccess: handleSuccess, + onError: handleError, + }, + ); + }; + + const isLoading = revokeKeyMutation.isLoading || revokeKeysMutation.isLoading; + + return ( +
+ + + + + + + {localize('com_ui_revoke_key_endpoint', { 0: endpoint })} + +
+ +
+ + + + +
+
+
+ ); +}; + const SetKeyDialog = ({ open, onOpenChange, @@ -83,7 +188,7 @@ const SetKeyDialog = ({ const submit = () => { const selectedOption = expirationOptions.find((option) => option.label === expiresAtLabel); - let expiresAt; + let expiresAt: number | null; if (selectedOption?.value === 0) { expiresAt = null; @@ -92,8 +197,20 @@ const SetKeyDialog = ({ } const saveKey = (key: string) => { - saveUserKey(key, expiresAt); - onOpenChange(false); + try { + saveUserKey(key, expiresAt); + showToast({ + message: localize('com_ui_save_key_success'), + status: NotificationSeverity.SUCCESS, + }); + onOpenChange(false); + } catch (error) { + logger.error('Error saving user key:', error); + showToast({ + message: localize('com_ui_save_key_error'), + status: NotificationSeverity.ERROR, + }); + } }; if (formSet.has(endpoint) || formSet.has(endpointType ?? '')) { @@ -148,6 +265,14 @@ const SetKeyDialog = ({ return; } + if (!userKey.trim()) { + showToast({ + message: localize('com_ui_key_required'), + status: NotificationSeverity.ERROR, + }); + return; + } + saveKey(userKey); setUserKey(''); }; @@ -159,56 +284,54 @@ const SetKeyDialog = ({ return ( - - - {expiryTime === 'never' - ? localize('com_endpoint_config_key_never_expires') - : `${localize('com_endpoint_config_key_encryption')} ${new Date( - expiryTime ?? 0, - ).toLocaleString()}`} - - option.label)} - sizeClasses="w-[185px]" - portal={false} + + + + {`${localize('com_endpoint_config_key_for')} ${alternateName[endpoint] ?? endpoint}`} + + +
+ + {expiryTime === 'never' + ? localize('com_endpoint_config_key_never_expires') + : `${localize('com_endpoint_config_key_encryption')} ${new Date( + expiryTime ?? 0, + ).toLocaleString()}`} + + option.label)} + sizeClasses="w-[185px]" + portal={false} + /> +
+ + -
- - - - -
- } - selection={{ - selectHandler: submit, - selectClasses: 'btn btn-primary', - selectText: localize('com_ui_submit'), - }} - leftButtons={ +
+ +
+ - } - /> + + + ); }; diff --git a/client/src/components/Nav/Settings.tsx b/client/src/components/Nav/Settings.tsx index d1c05b7bbc..868c987070 100644 --- a/client/src/components/Nav/Settings.tsx +++ b/client/src/components/Nav/Settings.tsx @@ -182,7 +182,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) { - {localize('com_ui_close')} + {localize('com_ui_close_settings')}
@@ -220,35 +220,35 @@ export default function Settings({ open, onOpenChange }: TDialogProps) { ))}
- + - + - + - + {hasAnyPersonalizationFeature && ( - + )} - + {startupConfig?.balance?.enabled && ( - + )} - +
diff --git a/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx b/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx index 0ce86231a7..ed677f771a 100644 --- a/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx @@ -1,9 +1,11 @@ import React, { useState, useRef, useCallback } from 'react'; import { useSetRecoilState } from 'recoil'; +// @ts-ignore - no type definitions available import AvatarEditor from 'react-avatar-editor'; -import { FileImage, RotateCw, Upload } from 'lucide-react'; +import { FileImage, RotateCw, Upload, ZoomIn, ZoomOut, Move, X } from 'lucide-react'; import { fileConfig as defaultFileConfig, mergeFileConfig } from 'librechat-data-provider'; import { + Label, Slider, Button, Spinner, @@ -25,14 +27,20 @@ interface AvatarEditorRef { getImage: () => HTMLImageElement; } +interface Position { + x: number; + y: number; +} + function Avatar() { const setUser = useSetRecoilState(store.user); const [scale, setScale] = useState(1); const [rotation, setRotation] = useState(0); + const [position, setPosition] = useState({ x: 0.5, y: 0.5 }); + const [isDragging, setIsDragging] = useState(false); const editorRef = useRef(null); const fileInputRef = useRef(null); - const openButtonRef = useRef(null); const [image, setImage] = useState(null); const [isDialogOpen, setDialogOpen] = useState(false); @@ -48,7 +56,6 @@ function Avatar() { onSuccess: (data) => { showToast({ message: localize('com_ui_upload_success') }); setUser((prev) => ({ ...prev, avatar: data.url }) as TUser); - openButtonRef.current?.click(); }, onError: (error) => { console.error('Error:', error); @@ -61,29 +68,45 @@ function Avatar() { handleFile(file); }; - const handleFile = (file: File | undefined) => { - if (fileConfig.avatarSizeLimit != null && file && file.size <= fileConfig.avatarSizeLimit) { - setImage(file); - setScale(1); - setRotation(0); - } else { - const megabytes = - fileConfig.avatarSizeLimit != null ? formatBytes(fileConfig.avatarSizeLimit) : 2; - showToast({ - message: localize('com_ui_upload_invalid_var', { 0: megabytes + '' }), - status: 'error', - }); - } - }; + const handleFile = useCallback( + (file: File | undefined) => { + if (fileConfig.avatarSizeLimit != null && file && file.size <= fileConfig.avatarSizeLimit) { + setImage(file); + setScale(1); + setRotation(0); + setPosition({ x: 0.5, y: 0.5 }); + } else { + const megabytes = + fileConfig.avatarSizeLimit != null ? formatBytes(fileConfig.avatarSizeLimit) : 2; + showToast({ + message: localize('com_ui_upload_invalid_var', { 0: megabytes + '' }), + status: 'error', + }); + } + }, + [fileConfig.avatarSizeLimit, localize, showToast], + ); const handleScaleChange = (value: number[]) => { setScale(value[0]); }; + const handleZoomIn = () => { + setScale((prev) => Math.min(prev + 0.2, 5)); + }; + + const handleZoomOut = () => { + setScale((prev) => Math.max(prev - 0.2, 1)); + }; + const handleRotate = () => { setRotation((prev) => (prev + 90) % 360); }; + const handlePositionChange = (position: Position) => { + setPosition(position); + }; + const handleUpload = () => { if (editorRef.current) { const canvas = editorRef.current.getImageScaledToCanvas(); @@ -98,11 +121,14 @@ function Avatar() { } }; - const handleDrop = useCallback((e: React.DragEvent) => { - e.preventDefault(); - const file = e.dataTransfer.files[0]; - handleFile(file); - }, []); + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + const file = e.dataTransfer.files[0]; + handleFile(file); + }, + [handleFile], + ); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); @@ -116,8 +142,15 @@ function Avatar() { setImage(null); setScale(1); setRotation(0); + setPosition({ x: 0.5, y: 0.5 }); }, []); + const handleReset = () => { + setScale(1); + setRotation(0); + setPosition({ x: 0.5, y: 0.5 }); + }; + return ( { - openButtonRef.current?.focus(); - }, 0); } }} >
{localize('com_nav_profile_picture')} - +
- + {image != null ? localize('com_ui_preview') : localize('com_ui_upload_image')} -
+
{image != null ? ( <> -
+
setIsDragging(true)} + onMouseUp={() => setIsDragging(false)} + onMouseLeave={() => setIsDragging(false)} + > + {!isDragging && ( +
+
+ +
+
+ )}
-
-
- {localize('com_ui_zoom')} - + +
+ {/* Zoom Controls */} +
+
+ + {Math.round(scale * 100)}% +
+
+ + + +
- + +
+ + +
+ + {/* Helper Text */} +

+ {localize('com_ui_editor_instructions')} +

+
+ + {/* Action Buttons */} +
+ +
- ) : (
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + openFileDialog(); + } + }} + aria-label={localize('com_ui_upload_avatar_label')} > - -

+ +

{localize('com_ui_drag_drop')}

-
)} diff --git a/client/src/components/Nav/SettingsTabs/Account/DeleteAccount.tsx b/client/src/components/Nav/SettingsTabs/Account/DeleteAccount.tsx index bd2e36c45f..29d1608b46 100644 --- a/client/src/components/Nav/SettingsTabs/Account/DeleteAccount.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/DeleteAccount.tsx @@ -1,6 +1,7 @@ import { LockIcon, Trash } from 'lucide-react'; import React, { useState, useCallback } from 'react'; import { + Label, Input, Button, Spinner, @@ -45,11 +46,11 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea <>
- {localize('com_nav_delete_account')} +
); diff --git a/client/src/components/Nav/SettingsTabs/Chat/ChatDirection.tsx b/client/src/components/Nav/SettingsTabs/Chat/ChatDirection.tsx index 7676617d32..a639d0ca42 100644 --- a/client/src/components/Nav/SettingsTabs/Chat/ChatDirection.tsx +++ b/client/src/components/Nav/SettingsTabs/Chat/ChatDirection.tsx @@ -19,16 +19,16 @@ const ChatDirection = () => {
); diff --git a/client/src/components/Nav/SettingsTabs/Chat/FontSizeSelector.tsx b/client/src/components/Nav/SettingsTabs/Chat/FontSizeSelector.tsx index c638244bcf..82fa2e746b 100644 --- a/client/src/components/Nav/SettingsTabs/Chat/FontSizeSelector.tsx +++ b/client/src/components/Nav/SettingsTabs/Chat/FontSizeSelector.tsx @@ -20,9 +20,11 @@ export default function FontSizeSelector() { { value: 'text-xl', label: localize('com_nav_font_size_xl') }, ]; + const labelId = 'font-size-selector-label'; + return (
-
{localize('com_nav_font_size')}
+
{localize('com_nav_font_size')}
); diff --git a/client/src/components/Nav/SettingsTabs/Chat/ForkSettings.tsx b/client/src/components/Nav/SettingsTabs/Chat/ForkSettings.tsx index a81d4f4f50..e1145fc3ca 100644 --- a/client/src/components/Nav/SettingsTabs/Chat/ForkSettings.tsx +++ b/client/src/components/Nav/SettingsTabs/Chat/ForkSettings.tsx @@ -20,13 +20,14 @@ export const ForkSettings = () => { <>
-
{localize('com_ui_fork_default')}
+
{localize('com_ui_fork_default')}
@@ -34,7 +35,7 @@ export const ForkSettings = () => {
-
{localize('com_ui_fork_change_default')}
+
{localize('com_ui_fork_change_default')}
{ sizeClasses="w-[200px]" testId="fork-setting-dropdown" className="z-[50]" + aria-labelledby="fork-change-default-label" />
@@ -54,7 +56,7 @@ export const ForkSettings = () => {
-
{localize('com_ui_fork_split_target_setting')}
+
{localize('com_ui_fork_split_target_setting')}
{ onCheckedChange={setSplitAtTarget} className="ml-4" data-testid="splitAtTarget" + aria-labelledby="split-at-target-label" />
diff --git a/client/src/components/Nav/SettingsTabs/Commands/AtCommandSwitch.tsx b/client/src/components/Nav/SettingsTabs/Commands/AtCommandSwitch.tsx deleted file mode 100644 index ab30c44dc0..0000000000 --- a/client/src/components/Nav/SettingsTabs/Commands/AtCommandSwitch.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useRecoilState } from 'recoil'; -import { Switch } from '@librechat/client'; -import { useLocalize } from '~/hooks'; -import store from '~/store'; - -export default function AtCommandSwitch() { - const [atCommand, setAtCommand] = useRecoilState(store.atCommand); - const localize = useLocalize(); - - const handleCheckedChange = (value: boolean) => { - setAtCommand(value); - }; - - return ( -
-
{localize('com_nav_at_command_description')}
- -
- ); -} diff --git a/client/src/components/Nav/SettingsTabs/Commands/Commands.tsx b/client/src/components/Nav/SettingsTabs/Commands/Commands.tsx index c06733f21a..ff04c9087b 100644 --- a/client/src/components/Nav/SettingsTabs/Commands/Commands.tsx +++ b/client/src/components/Nav/SettingsTabs/Commands/Commands.tsx @@ -1,10 +1,33 @@ import { memo } from 'react'; import { InfoHoverCard, ESide } from '@librechat/client'; import { PermissionTypes, Permissions } from 'librechat-data-provider'; -import SlashCommandSwitch from './SlashCommandSwitch'; import { useLocalize, useHasAccess } from '~/hooks'; -import PlusCommandSwitch from './PlusCommandSwitch'; -import AtCommandSwitch from './AtCommandSwitch'; +import ToggleSwitch from '../ToggleSwitch'; +import store from '~/store'; + +const commandSwitchConfigs = [ + { + stateAtom: store.atCommand, + localizationKey: 'com_nav_at_command_description' as const, + switchId: 'atCommand', + key: 'atCommand', + permissionType: undefined, + }, + { + stateAtom: store.plusCommand, + localizationKey: 'com_nav_plus_command_description' as const, + switchId: 'plusCommand', + key: 'plusCommand', + permissionType: PermissionTypes.MULTI_CONVO, + }, + { + stateAtom: store.slashCommand, + localizationKey: 'com_nav_slash_command_description' as const, + switchId: 'slashCommand', + key: 'slashCommand', + permissionType: PermissionTypes.PROMPTS, + }, +] as const; function Commands() { const localize = useLocalize(); @@ -19,6 +42,19 @@ function Commands() { permission: Permissions.USE, }); + const getShowSwitch = (permissionType?: PermissionTypes) => { + if (!permissionType) { + return true; + } + if (permissionType === PermissionTypes.MULTI_CONVO) { + return hasAccessToMultiConvo === true; + } + if (permissionType === PermissionTypes.PROMPTS) { + return hasAccessToPrompts === true; + } + return true; + }; + return (
@@ -28,19 +64,16 @@ function Commands() {
-
- -
- {hasAccessToMultiConvo === true && ( -
- + {commandSwitchConfigs.map((config) => ( +
+
- )} - {hasAccessToPrompts === true && ( -
- -
- )} + ))}
); diff --git a/client/src/components/Nav/SettingsTabs/Commands/PlusCommandSwitch.tsx b/client/src/components/Nav/SettingsTabs/Commands/PlusCommandSwitch.tsx deleted file mode 100644 index 2125f94a19..0000000000 --- a/client/src/components/Nav/SettingsTabs/Commands/PlusCommandSwitch.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useRecoilState } from 'recoil'; -import { Switch } from '@librechat/client'; -import { useLocalize } from '~/hooks'; -import store from '~/store'; - -export default function PlusCommandSwitch() { - const [plusCommand, setPlusCommand] = useRecoilState(store.plusCommand); - const localize = useLocalize(); - - const handleCheckedChange = (value: boolean) => { - setPlusCommand(value); - }; - - return ( -
-
{localize('com_nav_plus_command_description')}
- -
- ); -} diff --git a/client/src/components/Nav/SettingsTabs/Commands/SlashCommandSwitch.tsx b/client/src/components/Nav/SettingsTabs/Commands/SlashCommandSwitch.tsx deleted file mode 100644 index 68b4636365..0000000000 --- a/client/src/components/Nav/SettingsTabs/Commands/SlashCommandSwitch.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useRecoilState } from 'recoil'; -import { Switch } from '@librechat/client'; -import { useLocalize } from '~/hooks'; -import store from '~/store'; - -export default function SlashCommandSwitch() { - const [slashCommand, setSlashCommand] = useRecoilState(store.slashCommand); - const localize = useLocalize(); - - const handleCheckedChange = (value: boolean) => { - setSlashCommand(value); - }; - - return ( -
-
{localize('com_nav_slash_command_description')}
- -
- ); -} diff --git a/client/src/components/Nav/SettingsTabs/Data/ClearChats.tsx b/client/src/components/Nav/SettingsTabs/Data/ClearChats.tsx index bd745dcee8..44535e0a54 100644 --- a/client/src/components/Nav/SettingsTabs/Data/ClearChats.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/ClearChats.tsx @@ -31,12 +31,12 @@ export const ClearChats = () => { return (
- +
- +
diff --git a/client/src/components/Nav/SettingsTabs/Data/DeleteCache.tsx b/client/src/components/Nav/SettingsTabs/Data/DeleteCache.tsx index 573a87e7a4..48c7f3a434 100644 --- a/client/src/components/Nav/SettingsTabs/Data/DeleteCache.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/DeleteCache.tsx @@ -38,14 +38,14 @@ export const DeleteCache = ({ disabled = false }: { disabled?: boolean }) => { return (
- + diff --git a/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx b/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx index 7837a052c0..f3c6f4e8cb 100644 --- a/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx @@ -1,96 +1,114 @@ -import { useState, useRef } from 'react'; +import { useState, useRef, useCallback } from 'react'; import { Import } from 'lucide-react'; -import { Spinner, useToastContext } from '@librechat/client'; -import type { TError } from 'librechat-data-provider'; +import { Spinner, useToastContext, Label, Button } from '@librechat/client'; import { useUploadConversationsMutation } from '~/data-provider'; +import { NotificationSeverity } from '~/common'; import { useLocalize } from '~/hooks'; -import { cn } from '~/utils'; +import { cn, logger } from '~/utils'; function ImportConversations() { const localize = useLocalize(); const fileInputRef = useRef(null); - const { showToast } = useToastContext(); - const [, setErrors] = useState([]); - const [allowImport, setAllowImport] = useState(true); - const setError = (error: string) => setErrors((prevErrors) => [...prevErrors, error]); + + const [isUploading, setIsUploading] = useState(false); + + const handleSuccess = useCallback(() => { + showToast({ + message: localize('com_ui_import_conversation_success'), + status: NotificationSeverity.SUCCESS, + }); + setIsUploading(false); + }, [localize, showToast]); + + const handleError = useCallback( + (error: unknown) => { + logger.error('Import error:', error); + setIsUploading(false); + + const isUnsupportedType = error?.toString().includes('Unsupported import type'); + + showToast({ + message: localize( + isUnsupportedType + ? 'com_ui_import_conversation_file_type_error' + : 'com_ui_import_conversation_error', + ), + status: NotificationSeverity.ERROR, + }); + }, + [localize, showToast], + ); const uploadFile = useUploadConversationsMutation({ - onSuccess: () => { - showToast({ message: localize('com_ui_import_conversation_success') }); - setAllowImport(true); - }, - onError: (error) => { - console.error('Error: ', error); - setAllowImport(true); - setError( - (error as TError).response?.data?.message ?? 'An error occurred while uploading the file.', - ); - if (error?.toString().includes('Unsupported import type') === true) { - showToast({ - message: localize('com_ui_import_conversation_file_type_error'), - status: 'error', - }); - } else { - showToast({ message: localize('com_ui_import_conversation_error'), status: 'error' }); - } - }, - onMutate: () => { - setAllowImport(false); - }, + onSuccess: handleSuccess, + onError: handleError, + onMutate: () => setIsUploading(true), }); - const startUpload = async (file: File) => { - const formData = new FormData(); - formData.append('file', file, encodeURIComponent(file.name || 'File')); + const handleFileUpload = useCallback( + async (file: File) => { + try { + const formData = new FormData(); + formData.append('file', file, encodeURIComponent(file.name || 'File')); + uploadFile.mutate(formData); + } catch (error) { + logger.error('File processing error:', error); + setIsUploading(false); + showToast({ + message: localize('com_ui_import_conversation_upload_error'), + status: NotificationSeverity.ERROR, + }); + } + }, + [uploadFile, showToast, localize], + ); - uploadFile.mutate(formData); - }; + const handleFileChange = useCallback( + (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + handleFileUpload(file); + } + event.target.value = ''; + }, + [handleFileUpload], + ); - const handleFiles = async (_file: File) => { - try { - await startUpload(_file); - } catch (error) { - console.log('file handling error', error); - setError('An error occurred while processing the file.'); - } - }; - - const handleFileChange = (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (file) { - handleFiles(file); - } - }; - - const handleImportClick = () => { + const handleImportClick = useCallback(() => { fileInputRef.current?.click(); - }; + }, []); - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - handleImportClick(); - } - }; + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + handleImportClick(); + } + }, + [handleImportClick], + ); + + const isImportDisabled = isUploading; return (
-
{localize('com_ui_import_conversation_info')}
- + { - const localize = useLocalize(); - - return ( -
- - -
- ); -}; diff --git a/client/src/components/Nav/SettingsTabs/Data/RevokeKeys.tsx b/client/src/components/Nav/SettingsTabs/Data/RevokeKeys.tsx new file mode 100644 index 0000000000..25147146ba --- /dev/null +++ b/client/src/components/Nav/SettingsTabs/Data/RevokeKeys.tsx @@ -0,0 +1,72 @@ +import React, { useState } from 'react'; +import { useRevokeAllUserKeysMutation } from 'librechat-data-provider/react-query'; +import { + OGDialogTemplate, + Button, + Label, + OGDialog, + OGDialogTrigger, + Spinner, +} from '@librechat/client'; +import { useLocalize } from '~/hooks'; + +export const RevokeKeys = ({ + disabled = false, + setDialogOpen, +}: { + disabled?: boolean; + setDialogOpen?: (open: boolean) => void; +}) => { + const localize = useLocalize(); + const [open, setOpen] = useState(false); + const revokeKeysMutation = useRevokeAllUserKeysMutation(); + + const handleSuccess = () => { + if (!setDialogOpen) { + return; + } + + setDialogOpen(false); + }; + + const onClick = () => { + revokeKeysMutation.mutate({}, { onSuccess: handleSuccess }); + }; + + const isLoading = revokeKeysMutation.isLoading; + + return ( +
+ + + + + + + + {localize('com_ui_revoke_keys_confirm')} + + } + selection={{ + selectHandler: onClick, + selectClasses: + 'bg-destructive text-white transition-all duration-200 hover:bg-destructive/80', + selectText: isLoading ? : localize('com_ui_revoke'), + }} + /> + +
+ ); +}; diff --git a/client/src/components/Nav/SettingsTabs/Data/RevokeKeysButton.tsx b/client/src/components/Nav/SettingsTabs/Data/RevokeKeysButton.tsx deleted file mode 100644 index 51cf386a5d..0000000000 --- a/client/src/components/Nav/SettingsTabs/Data/RevokeKeysButton.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React, { useState } from 'react'; -import { - useRevokeAllUserKeysMutation, - useRevokeUserKeyMutation, -} from 'librechat-data-provider/react-query'; -import { - OGDialogTemplate, - Button, - Label, - OGDialog, - OGDialogTrigger, - Spinner, -} from '@librechat/client'; -import { useLocalize } from '~/hooks'; - -export const RevokeKeysButton = ({ - endpoint = '', - all = false, - disabled = false, - setDialogOpen, -}: { - endpoint?: string; - all?: boolean; - disabled?: boolean; - setDialogOpen?: (open: boolean) => void; -}) => { - const localize = useLocalize(); - const [open, setOpen] = useState(false); - const revokeKeyMutation = useRevokeUserKeyMutation(endpoint); - const revokeKeysMutation = useRevokeAllUserKeysMutation(); - - const handleSuccess = () => { - if (!setDialogOpen) { - return; - } - - setDialogOpen(false); - }; - - const onClick = () => { - if (all) { - revokeKeysMutation.mutate({}); - } else { - revokeKeyMutation.mutate({}, { onSuccess: handleSuccess }); - } - }; - - const dialogTitle = all - ? localize('com_ui_revoke_keys') - : localize('com_ui_revoke_key_endpoint', { 0: endpoint }); - - const dialogMessage = all - ? localize('com_ui_revoke_keys_confirm') - : localize('com_ui_revoke_key_confirm'); - - const isLoading = revokeKeyMutation.isLoading || revokeKeysMutation.isLoading; - - return ( - - - - - {dialogMessage}} - selection={{ - selectHandler: onClick, - selectClasses: - 'bg-destructive text-white transition-all duration-200 hover:bg-destructive/80', - selectText: isLoading ? : localize('com_ui_revoke'), - }} - /> - - ); -}; diff --git a/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx b/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx index 5f24a5770c..ae25223a9b 100644 --- a/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx @@ -286,11 +286,13 @@ export default function SharedLinks() { return (
-
{localize('com_nav_shared_links')}
+ setIsOpen(true)}> - + -
{localize('com_nav_theme')}
+
{localize('com_nav_theme')}
); @@ -112,9 +115,11 @@ export const LangSelector = ({ { value: 'uk-UA', label: localize('com_nav_lang_ukrainian') }, ]; + const labelId = 'language-selector-label'; + return (
-
{localize('com_nav_language')}
+
{localize('com_nav_language')}
); diff --git a/client/src/components/Nav/SettingsTabs/Personalization.tsx b/client/src/components/Nav/SettingsTabs/Personalization.tsx index 50ce452783..f9e43dc6f5 100644 --- a/client/src/components/Nav/SettingsTabs/Personalization.tsx +++ b/client/src/components/Nav/SettingsTabs/Personalization.tsx @@ -65,10 +65,13 @@ export default function Personalization({
-
+
{localize('com_ui_reference_saved_memories')}
-
+
{localize('com_ui_reference_saved_memories_description')}
@@ -76,7 +79,8 @@ export default function Personalization({ checked={referenceSavedMemories} onCheckedChange={handleMemoryToggle} disabled={updateMemoryPreferencesMutation.isLoading} - aria-label={localize('com_ui_reference_saved_memories')} + aria-labelledby="reference-saved-memories-label" + aria-describedby="reference-saved-memories-description" />
diff --git a/client/src/components/Nav/SettingsTabs/Speech/ConversationModeSwitch.tsx b/client/src/components/Nav/SettingsTabs/Speech/ConversationModeSwitch.tsx index 5745943e57..3a3df8efab 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/ConversationModeSwitch.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/ConversationModeSwitch.tsx @@ -1,6 +1,5 @@ -import { Switch } from '@librechat/client'; import { useRecoilState, useRecoilValue } from 'recoil'; -import { useLocalize } from '~/hooks'; +import ToggleSwitch from '../ToggleSwitch'; import store from '~/store'; export default function ConversationModeSwitch({ @@ -8,8 +7,6 @@ export default function ConversationModeSwitch({ }: { onCheckedChange?: (value: boolean) => void; }) { - const localize = useLocalize(); - const [conversationMode, setConversationMode] = useRecoilState(store.conversationMode); const speechToText = useRecoilValue(store.speechToText); const textToSpeech = useRecoilValue(store.textToSpeech); const [, setAutoSendText] = useRecoilState(store.autoSendText); @@ -20,27 +17,19 @@ export default function ConversationModeSwitch({ setAutoTranscribeAudio(value); setAutoSendText(3); setDecibelValue(-45); - setConversationMode(value); if (onCheckedChange) { onCheckedChange(value); } }; return ( -
-
- {localize('com_nav_conversation_mode')} -
-
- -
-
+ ); } diff --git a/client/src/components/Nav/SettingsTabs/Speech/STT/AutoSendTextSelector.tsx b/client/src/components/Nav/SettingsTabs/Speech/STT/AutoSendTextSelector.tsx index 5c5e8e3da3..a033ed322c 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/STT/AutoSendTextSelector.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/STT/AutoSendTextSelector.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; -import { Slider, InputNumber } from '@librechat/client'; +import { Slider, InputNumber, Switch } from '@librechat/client'; import { cn, defaultTextProps, optionText } from '~/utils/'; import { useLocalize } from '~/hooks'; import store from '~/store'; @@ -11,40 +11,104 @@ export default function AutoSendTextSelector() { const speechToText = useRecoilValue(store.speechToText); const [autoSendText, setAutoSendText] = useRecoilState(store.autoSendText); + // Local state for enabled/disabled toggle + const [isEnabled, setIsEnabled] = useState(autoSendText !== -1); + const [delayValue, setDelayValue] = useState(autoSendText === -1 ? 3 : autoSendText); + + // Sync local state when autoSendText changes externally + useEffect(() => { + setIsEnabled(autoSendText !== -1); + if (autoSendText !== -1) { + setDelayValue(autoSendText); + } + }, [autoSendText]); + + const handleToggle = (enabled: boolean) => { + setIsEnabled(enabled); + if (enabled) { + setAutoSendText(delayValue); + } else { + setAutoSendText(-1); + } + }; + + const handleSliderChange = (value: number[]) => { + const newValue = value[0]; + setDelayValue(newValue); + if (isEnabled) { + setAutoSendText(newValue); + } + }; + + const handleInputChange = (value: number[] | null) => { + const newValue = value ? value[0] : 3; + setDelayValue(newValue); + if (isEnabled) { + setAutoSendText(newValue); + } + }; + + const labelId = 'auto-send-text-label'; + return ( -
+
-
{localize('com_nav_auto_send_text')}
-
- ({localize('com_nav_auto_send_text_disabled')}) -
-
- setAutoSendText(value[0])} - onDoubleClick={() => setAutoSendText(-1)} - min={-1} - max={60} - step={1} - className="ml-4 flex h-4 w-24" +
+
{localize('com_nav_auto_send_text')}
+
+ -
- setAutoSendText(value ? value[0] : 0)} - min={-1} - max={60} - className={cn( - defaultTextProps, - cn( - optionText, - 'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200', - ), - )} - />
+ {isEnabled && ( +
+
+
+ {localize('com_nav_setting_delay')} +
+
+
+ { + setDelayValue(3); + if (isEnabled) { + setAutoSendText(3); + } + }} + min={0} + max={60} + step={1} + className="ml-4 flex h-4 w-24" + disabled={!speechToText || !isEnabled} + aria-labelledby="auto-send-delay-label" + /> +
+ +
+
+ )}
); } diff --git a/client/src/components/Nav/SettingsTabs/Speech/STT/AutoTranscribeAudioSwitch.tsx b/client/src/components/Nav/SettingsTabs/Speech/STT/AutoTranscribeAudioSwitch.tsx index 1304f35c63..00a8c9f833 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/STT/AutoTranscribeAudioSwitch.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/STT/AutoTranscribeAudioSwitch.tsx @@ -1,6 +1,5 @@ -import { Switch } from '@librechat/client'; -import { useRecoilState, useRecoilValue } from 'recoil'; -import { useLocalize } from '~/hooks'; +import { useRecoilValue } from 'recoil'; +import ToggleSwitch from '../../ToggleSwitch'; import store from '~/store'; export default function AutoTranscribeAudioSwitch({ @@ -8,30 +7,15 @@ export default function AutoTranscribeAudioSwitch({ }: { onCheckedChange?: (value: boolean) => void; }) { - const localize = useLocalize(); - const [autoTranscribeAudio, setAutoTranscribeAudio] = useRecoilState( - store.autoTranscribeAudio, - ); const speechToText = useRecoilValue(store.speechToText); - const handleCheckedChange = (value: boolean) => { - setAutoTranscribeAudio(value); - if (onCheckedChange) { - onCheckedChange(value); - } - }; - return ( -
-
{localize('com_nav_auto_transcribe_audio')}
- -
+ ); } diff --git a/client/src/components/Nav/SettingsTabs/Speech/STT/DecibelSelector.tsx b/client/src/components/Nav/SettingsTabs/Speech/STT/DecibelSelector.tsx index f23de198c6..b311355108 100755 --- a/client/src/components/Nav/SettingsTabs/Speech/STT/DecibelSelector.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/STT/DecibelSelector.tsx @@ -13,7 +13,7 @@ export default function DecibelSelector() { return (
-
{localize('com_nav_db_sensitivity')}
+
{localize('com_nav_db_sensitivity')}
({localize('com_endpoint_default_with_num', { 0: '-45' })}) @@ -29,6 +29,7 @@ export default function DecibelSelector() { step={1} className="ml-4 flex h-4 w-24" disabled={!speechToText} + aria-labelledby="decibel-selector-label" />
setDecibelValue(value ? value[0] : 0)} min={-100} max={-30} + aria-labelledby="decibel-selector-label" className={cn( defaultTextProps, cn( diff --git a/client/src/components/Nav/SettingsTabs/Speech/STT/EngineSTTDropdown.tsx b/client/src/components/Nav/SettingsTabs/Speech/STT/EngineSTTDropdown.tsx index f6bc4b91e7..8fc3dd8352 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/STT/EngineSTTDropdown.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/STT/EngineSTTDropdown.tsx @@ -23,9 +23,11 @@ const EngineSTTDropdown: React.FC = ({ external }) => { setEngineSTT(value); }; + const labelId = 'engine-stt-dropdown-label'; + return (
-
{localize('com_nav_engine')}
+
{localize('com_nav_engine')}
= ({ external }) => { sizeClasses="w-[180px]" testId="EngineSTTDropdown" className="z-50" + aria-labelledby={labelId} />
); diff --git a/client/src/components/Nav/SettingsTabs/Speech/STT/LanguageSTTDropdown.tsx b/client/src/components/Nav/SettingsTabs/Speech/STT/LanguageSTTDropdown.tsx index c3bb37ceef..53da4e7989 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/STT/LanguageSTTDropdown.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/STT/LanguageSTTDropdown.tsx @@ -94,9 +94,11 @@ export default function LanguageSTTDropdown() { setLanguageSTT(value); }; + const labelId = 'language-stt-dropdown-label'; + return (
-
{localize('com_nav_language')}
+
{localize('com_nav_language')}
); diff --git a/client/src/components/Nav/SettingsTabs/Speech/STT/SpeechToTextSwitch.tsx b/client/src/components/Nav/SettingsTabs/Speech/STT/SpeechToTextSwitch.tsx index 99c81f60e5..e06f3392d0 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/STT/SpeechToTextSwitch.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/STT/SpeechToTextSwitch.tsx @@ -1,6 +1,4 @@ -import { useRecoilState } from 'recoil'; -import { Switch } from '@librechat/client'; -import { useLocalize } from '~/hooks'; +import ToggleSwitch from '../../ToggleSwitch'; import store from '~/store'; export default function SpeechToTextSwitch({ @@ -8,28 +6,13 @@ export default function SpeechToTextSwitch({ }: { onCheckedChange?: (value: boolean) => void; }) { - const localize = useLocalize(); - const [speechToText, setSpeechToText] = useRecoilState(store.speechToText); - - const handleCheckedChange = (value: boolean) => { - setSpeechToText(value); - if (onCheckedChange) { - onCheckedChange(value); - } - }; - return ( -
-
- {localize('com_nav_speech_to_text')} -
- -
+ ); } diff --git a/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx b/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx index acd87fa233..ee4c1eb09d 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx @@ -23,7 +23,7 @@ import { } from './STT'; import ConversationModeSwitch from './ConversationModeSwitch'; import { useLocalize } from '~/hooks'; -import { cn, logger } from '~/utils'; +import { cn } from '~/utils'; import store from '~/store'; function Speech() { @@ -186,7 +186,7 @@ function Speech() {
- +
@@ -198,7 +198,7 @@ function Speech() {
- +
diff --git a/client/src/components/Nav/SettingsTabs/Speech/TTS/AutomaticPlaybackSwitch.tsx b/client/src/components/Nav/SettingsTabs/Speech/TTS/AutomaticPlaybackSwitch.tsx index 67537a8d65..916d38f5af 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/TTS/AutomaticPlaybackSwitch.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/TTS/AutomaticPlaybackSwitch.tsx @@ -1,6 +1,4 @@ -import { useRecoilState } from 'recoil'; -import { Switch } from '@librechat/client'; -import { useLocalize } from '~/hooks'; +import ToggleSwitch from '../../ToggleSwitch'; import store from '~/store'; export default function AutomaticPlaybackSwitch({ @@ -8,26 +6,12 @@ export default function AutomaticPlaybackSwitch({ }: { onCheckedChange?: (value: boolean) => void; }) { - const localize = useLocalize(); - const [automaticPlayback, setAutomaticPlayback] = useRecoilState(store.automaticPlayback); - - const handleCheckedChange = (value: boolean) => { - setAutomaticPlayback(value); - if (onCheckedChange) { - onCheckedChange(value); - } - }; - return ( -
-
{localize('com_nav_automatic_playback')}
- -
+ ); } diff --git a/client/src/components/Nav/SettingsTabs/Speech/TTS/CacheTTSSwitch.tsx b/client/src/components/Nav/SettingsTabs/Speech/TTS/CacheTTSSwitch.tsx index b3268e1a4b..6ebc54e3da 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/TTS/CacheTTSSwitch.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/TTS/CacheTTSSwitch.tsx @@ -1,6 +1,5 @@ -import { useRecoilState } from 'recoil'; -import { Switch } from '@librechat/client'; -import { useLocalize } from '~/hooks'; +import { useRecoilValue } from 'recoil'; +import ToggleSwitch from '../../ToggleSwitch'; import store from '~/store'; export default function CacheTTSSwitch({ @@ -8,28 +7,15 @@ export default function CacheTTSSwitch({ }: { onCheckedChange?: (value: boolean) => void; }) { - const localize = useLocalize(); - const [cacheTTS, setCacheTTS] = useRecoilState(store.cacheTTS); - const [textToSpeech] = useRecoilState(store.textToSpeech); - - const handleCheckedChange = (value: boolean) => { - setCacheTTS(value); - if (onCheckedChange) { - onCheckedChange(value); - } - }; + const textToSpeech = useRecoilValue(store.textToSpeech); return ( -
-
{localize('com_nav_enable_cache_tts')}
- -
+ ); } diff --git a/client/src/components/Nav/SettingsTabs/Speech/TTS/CloudBrowserVoicesSwitch.tsx b/client/src/components/Nav/SettingsTabs/Speech/TTS/CloudBrowserVoicesSwitch.tsx index 6a10806baa..9fa57bf90a 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/TTS/CloudBrowserVoicesSwitch.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/TTS/CloudBrowserVoicesSwitch.tsx @@ -1,6 +1,5 @@ -import { useRecoilState } from 'recoil'; -import { Switch } from '@librechat/client'; -import { useLocalize } from '~/hooks'; +import { useRecoilValue } from 'recoil'; +import ToggleSwitch from '../../ToggleSwitch'; import store from '~/store'; export default function CloudBrowserVoicesSwitch({ @@ -8,30 +7,15 @@ export default function CloudBrowserVoicesSwitch({ }: { onCheckedChange?: (value: boolean) => void; }) { - const localize = useLocalize(); - const [cloudBrowserVoices, setCloudBrowserVoices] = useRecoilState( - store.cloudBrowserVoices, - ); - const [textToSpeech] = useRecoilState(store.textToSpeech); - - const handleCheckedChange = (value: boolean) => { - setCloudBrowserVoices(value); - if (onCheckedChange) { - onCheckedChange(value); - } - }; + const textToSpeech = useRecoilValue(store.textToSpeech); return ( -
-
{localize('com_nav_enable_cloud_browser_voice')}
- -
+ ); } diff --git a/client/src/components/Nav/SettingsTabs/Speech/TTS/EngineTTSDropdown.tsx b/client/src/components/Nav/SettingsTabs/Speech/TTS/EngineTTSDropdown.tsx index 95d45671b3..a5a576ba92 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/TTS/EngineTTSDropdown.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/TTS/EngineTTSDropdown.tsx @@ -23,9 +23,11 @@ const EngineTTSDropdown: React.FC = ({ external }) => { setEngineTTS(value); }; + const labelId = 'engine-tts-dropdown-label'; + return (
-
{localize('com_nav_engine')}
+
{localize('com_nav_engine')}
= ({ external }) => { sizeClasses="w-[180px]" testId="EngineTTSDropdown" className="z-50" + aria-labelledby={labelId} />
); diff --git a/client/src/components/Nav/SettingsTabs/Speech/TTS/PlaybackRate.tsx b/client/src/components/Nav/SettingsTabs/Speech/TTS/PlaybackRate.tsx index 571055a377..fee956e2f2 100755 --- a/client/src/components/Nav/SettingsTabs/Speech/TTS/PlaybackRate.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/TTS/PlaybackRate.tsx @@ -13,7 +13,7 @@ export default function DecibelSelector() { return (
-
{localize('com_nav_playback_rate')}
+
{localize('com_nav_playback_rate')}
({localize('com_endpoint_default_with_num', { 0: '1' })}) @@ -29,6 +29,7 @@ export default function DecibelSelector() { step={0.1} className="ml-4 flex h-4 w-24" disabled={!textToSpeech} + aria-labelledby="playback-rate-label" />
setPlaybackRate(value ? value[0] : 0)} min={0.1} max={2} + aria-labelledby="playback-rate-label" className={cn( defaultTextProps, cn( diff --git a/client/src/components/Nav/SettingsTabs/Speech/TTS/TextToSpeechSwitch.tsx b/client/src/components/Nav/SettingsTabs/Speech/TTS/TextToSpeechSwitch.tsx index b9a4ad1665..f4c499eb78 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/TTS/TextToSpeechSwitch.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/TTS/TextToSpeechSwitch.tsx @@ -1,6 +1,4 @@ -import { useRecoilState } from 'recoil'; -import { Switch } from '@librechat/client'; -import { useLocalize } from '~/hooks'; +import ToggleSwitch from '../../ToggleSwitch'; import store from '~/store'; export default function TextToSpeechSwitch({ @@ -8,28 +6,13 @@ export default function TextToSpeechSwitch({ }: { onCheckedChange?: (value: boolean) => void; }) { - const localize = useLocalize(); - const [TextToSpeech, setTextToSpeech] = useRecoilState(store.textToSpeech); - - const handleCheckedChange = (value: boolean) => { - setTextToSpeech(value); - if (onCheckedChange) { - onCheckedChange(value); - } - }; - return ( -
-
- {localize('com_nav_text_to_speech')} -
- -
+ ); } diff --git a/client/src/components/Nav/SettingsTabs/ToggleSwitch.tsx b/client/src/components/Nav/SettingsTabs/ToggleSwitch.tsx index 64c8062ca7..391ab0a494 100644 --- a/client/src/components/Nav/SettingsTabs/ToggleSwitch.tsx +++ b/client/src/components/Nav/SettingsTabs/ToggleSwitch.tsx @@ -11,6 +11,9 @@ interface ToggleSwitchProps { hoverCardText?: LocalizeKey; switchId: string; onCheckedChange?: (value: boolean) => void; + showSwitch?: boolean; + disabled?: boolean; + strongLabel?: boolean; } const ToggleSwitch: React.FC = ({ @@ -19,6 +22,9 @@ const ToggleSwitch: React.FC = ({ hoverCardText, switchId, onCheckedChange, + showSwitch = true, + disabled = false, + strongLabel = false, }) => { const [switchState, setSwitchState] = useRecoilState(stateAtom); const localize = useLocalize(); @@ -28,10 +34,18 @@ const ToggleSwitch: React.FC = ({ onCheckedChange?.(value); }; + const labelId = `${switchId}-label`; + + if (!showSwitch) { + return null; + } + return (
-
{localize(localizationKey)}
+
+ {strongLabel ? {localize(localizationKey)} : localize(localizationKey)} +
{hoverCardText && }
= ({ onCheckedChange={handleCheckedChange} className="ml-4" data-testid={switchId} + aria-labelledby={labelId} + disabled={disabled} />
); diff --git a/client/src/components/Nav/SettingsTabs/index.ts b/client/src/components/Nav/SettingsTabs/index.ts index 9eab047c86..911cbaa98a 100644 --- a/client/src/components/Nav/SettingsTabs/index.ts +++ b/client/src/components/Nav/SettingsTabs/index.ts @@ -1,9 +1,8 @@ -export { default as General } from './General/General'; export { default as Chat } from './Chat/Chat'; export { default as Data } from './Data/Data'; -export { default as Commands } from './Commands/Commands'; -export { RevokeKeysButton } from './Data/RevokeKeysButton'; -export { default as Account } from './Account/Account'; -export { default as Balance } from './Balance/Balance'; export { default as Speech } from './Speech/Speech'; +export { default as Balance } from './Balance/Balance'; +export { default as General } from './General/General'; +export { default as Account } from './Account/Account'; +export { default as Commands } from './Commands/Commands'; export { default as Personalization } from './Personalization'; diff --git a/client/src/components/Prompts/Groups/ChatGroupItem.tsx b/client/src/components/Prompts/Groups/ChatGroupItem.tsx index a69ff16be3..586535f7cd 100644 --- a/client/src/components/Prompts/Groups/ChatGroupItem.tsx +++ b/client/src/components/Prompts/Groups/ChatGroupItem.tsx @@ -71,10 +71,12 @@ function ChatGroupItem({ { e.stopPropagation(); setPreviewDialogOpen(true); }} - className="w-full cursor-pointer rounded-lg text-text-secondary hover:bg-surface-hover focus:bg-surface-hover disabled:cursor-not-allowed" + className="w-full cursor-pointer rounded-lg text-text-primary hover:bg-surface-hover focus:bg-surface-hover disabled:cursor-not-allowed" > - {canEdit && ( { e.stopPropagation(); onEditClick(e); }} > - diff --git a/client/src/components/Prompts/Groups/DashGroupItem.tsx b/client/src/components/Prompts/Groups/DashGroupItem.tsx index 3e906fba8c..d10def66c6 100644 --- a/client/src/components/Prompts/Groups/DashGroupItem.tsx +++ b/client/src/components/Prompts/Groups/DashGroupItem.tsx @@ -89,7 +89,7 @@ function DashGroupItemComponent({ group, instanceProjectId }: DashGroupItemProps onKeyDown={handleKeyDown} role="button" tabIndex={0} - aria-label={`${group.name} prompt group`} + aria-label={`${group.name} Prompt, ${localize('com_ui_category')}: ${group.category ?? ''}`} >
diff --git a/client/src/components/Prompts/Groups/FilterPrompts.tsx b/client/src/components/Prompts/Groups/FilterPrompts.tsx index 7f2b4d301d..f74800bdf8 100644 --- a/client/src/components/Prompts/Groups/FilterPrompts.tsx +++ b/client/src/components/Prompts/Groups/FilterPrompts.tsx @@ -87,7 +87,7 @@ export default function FilterPrompts({ className = '' }: { className?: string } value={categoryFilter || SystemCategories.ALL} onChange={onSelect} options={filterOptions} - className="bg-transparent" + className="rounded-lg bg-transparent" icon={} label="Filter: " ariaLabel={localize('com_ui_filter_prompts')} diff --git a/client/src/components/Prompts/Groups/List.tsx b/client/src/components/Prompts/Groups/List.tsx index dca07b07b0..2dc8c5265e 100644 --- a/client/src/components/Prompts/Groups/List.tsx +++ b/client/src/components/Prompts/Groups/List.tsx @@ -40,7 +40,7 @@ export default function List({
)} -
+
{isLoading && isChatRoute && ( diff --git a/client/src/components/Prompts/PreviewPrompt.tsx b/client/src/components/Prompts/PreviewPrompt.tsx index b16443cb75..6193bd9e5d 100644 --- a/client/src/components/Prompts/PreviewPrompt.tsx +++ b/client/src/components/Prompts/PreviewPrompt.tsx @@ -14,7 +14,7 @@ const PreviewPrompt = ({ return ( -
+
diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index c4b8481398..2fc5623f4d 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -1,6 +1,6 @@ { - "chat_direction_left_to_right": "something needs to go here. was empty", - "chat_direction_right_to_left": "something needs to go here. was empty", + "chat_direction_left_to_right": "Left to Right", + "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_start": "The AI has started their reply.", @@ -408,7 +408,6 @@ "com_nav_auto_scroll": "Auto-Scroll to latest message on chat open", "com_nav_auto_send_prompts": "Auto-send Prompts", "com_nav_auto_send_text": "Auto send text", - "com_nav_auto_send_text_disabled": "set -1 to disable", "com_nav_auto_transcribe_audio": "Auto transcribe audio", "com_nav_automatic_playback": "Autoplay Latest Message", "com_nav_balance": "Balance", @@ -573,6 +572,7 @@ "com_nav_slash_command_description": "Toggle command \"/\" for selecting a prompt via keyboard", "com_nav_speech_to_text": "Speech to Text", "com_nav_stop_generating": "Stop generating", + "com_nav_setting_delay": "Delay (s)", "com_nav_text_to_speech": "Text to Speech", "com_nav_theme": "Theme", "com_nav_theme_dark": "Dark", @@ -761,6 +761,7 @@ "com_ui_close": "Close", "com_ui_close_menu": "Close Menu", "com_ui_close_window": "Close Window", + "com_ui_close_settings": "Close Settings", "com_ui_code": "Code", "com_ui_collapse_chat": "Collapse Chat", "com_ui_command_placeholder": "Optional: Enter a command for the prompt or name will be used", @@ -950,8 +951,9 @@ "com_ui_image_edited": "Image edited", "com_ui_image_gen": "Image Gen", "com_ui_import": "Import", - "com_ui_import_conversation_error": "There was an error importing your conversations", - "com_ui_import_conversation_file_type_error": "Unsupported import type", + "com_ui_import_conversation_error": "There was an error while importing your conversations", + "com_ui_import_conversation_file_type_error": "Error with file type. Please select a valid JSON file.", + "com_ui_import_conversation_upload_error": "Error uploading file. Please try again.", "com_ui_import_conversation_info": "Import conversations from a JSON file", "com_ui_import_conversation_success": "Conversations imported successfully", "com_ui_include_shadcnui": "Include shadcn/ui components instructions", @@ -1077,6 +1079,7 @@ "com_ui_prompts_allow_create": "Allow creating Prompts", "com_ui_prompts_allow_share": "Allow sharing Prompts", "com_ui_prompts_allow_use": "Allow using Prompts", + "com_ui_prompt_groups": "Prompt Groups List", "com_ui_provider": "Provider", "com_ui_quality": "Quality", "com_ui_read_aloud": "Read aloud", @@ -1279,5 +1282,21 @@ "com_ui_x_selected": "{{0}} selected", "com_ui_yes": "Yes", "com_ui_zoom": "Zoom", - "com_user_message": "You" + "com_user_message": "You", + "com_ui_rotate": "Rotate", + "com_ui_reset": "Reset", + "com_ui_zoom_in": "Zoom in", + "com_ui_zoom_out": "Zoom out", + "com_ui_zoom_level": "Zoom level", + "com_ui_rotate_90": "Rotate 90 degrees", + "com_ui_reset_adjustments": "Reset adjustments", + "com_ui_editor_instructions": "Drag the image to reposition • Use zoom slider or buttons to adjust size", + "com_ui_save_key_success": "API key saved successfully", + "com_ui_save_key_error": "Failed to save API key. Please try again.", + "com_ui_revoke_key_success": "API key revoked successfully", + "com_ui_revoke_key_error": "Failed to revoke API key. Please try again.", + "com_ui_key_required": "API key is required", + "com_ui_max_file_size": "PNG, JPG or JPEG (max {{0}})", + "com_ui_upload_avatar_label": "Upload avatar image", + "com_ui_file_input_avatar_label": "File input for avatar" } diff --git a/packages/client/src/components/Dropdown.tsx b/packages/client/src/components/Dropdown.tsx index 5a4d3f2b20..536bbc5829 100644 --- a/packages/client/src/components/Dropdown.tsx +++ b/packages/client/src/components/Dropdown.tsx @@ -16,6 +16,7 @@ interface DropdownProps { iconOnly?: boolean; renderValue?: (option: Option) => React.ReactNode; ariaLabel?: string; + 'aria-labelledby'?: string; portal?: boolean; } @@ -37,6 +38,7 @@ const Dropdown: React.FC = ({ iconOnly = false, renderValue, ariaLabel, + 'aria-labelledby': ariaLabelledBy, portal = true, }) => { const handleChange = (value: string) => { @@ -77,6 +79,7 @@ const Dropdown: React.FC = ({ )} data-testid={testId} aria-label={ariaLabel} + aria-labelledby={ariaLabelledBy} >
{icon} diff --git a/packages/client/src/components/DropdownMenu.tsx b/packages/client/src/components/DropdownMenu.tsx index b317806826..4c050a2713 100644 --- a/packages/client/src/components/DropdownMenu.tsx +++ b/packages/client/src/components/DropdownMenu.tsx @@ -1,191 +1,225 @@ import * as React from 'react'; import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; -import { Check, ChevronRight, Circle } from 'lucide-react'; +import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'; import { cn } from '~/utils'; -const DropdownMenu = DropdownMenuPrimitive.Root; +function DropdownMenu({ ...props }: React.ComponentProps) { + return ; +} -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ; +} -const DropdownMenuGroup = DropdownMenuPrimitive.Group; +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ; +} -const DropdownMenuPortal = DropdownMenuPrimitive.Portal; +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} -const DropdownMenuSub = DropdownMenuPrimitive.Sub; +function DropdownMenuGroup({ ...props }: React.ComponentProps) { + return ; +} -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; - -const DropdownMenuSubTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean; - } ->(({ className = '', inset, children, ...props }, ref) => ( - - {children} - - -)); -DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; - -const DropdownMenuSubContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className = '', ...props }, ref) => ( - -)); -DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; - -const DropdownMenuContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className = '', sideOffset = 4, ...props }, ref) => ( - - & { + inset?: boolean; + variant?: 'default' | 'destructive'; +}) { + return ( + - -)); -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; - -const DropdownMenuItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean; - } ->(({ className = '', inset, ...props }, ref) => ( - -)); -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; - -const DropdownMenuCheckboxItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className = '', children, checked, ...props }, ref) => ( - - - - - - - {children} - -)); -DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; - -const DropdownMenuRadioItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className = '', children, ...props }, ref) => ( - - - - - - - {children} - -)); -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; - -const DropdownMenuLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean; - } ->(({ className = '', inset, ...props }, ref) => ( - -)); -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; - -const DropdownMenuSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className = '', ...props }, ref) => ( - -)); -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; - -const DropdownMenuShortcut = ({ - className = '', - ...props -}: React.HTMLAttributes) => { - return ( - ); -}; -DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'; +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) { + return ( + + ); +} + +function DropdownMenuSub({ ...props }: React.ComponentProps) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} export { DropdownMenu, + DropdownMenuPortal, DropdownMenuTrigger, DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, DropdownMenuItem, DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, DropdownMenuRadioItem, - DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, - DropdownMenuGroup, - DropdownMenuPortal, DropdownMenuSub, - DropdownMenuSubContent, DropdownMenuSubTrigger, - DropdownMenuRadioGroup, + DropdownMenuSubContent, }; diff --git a/packages/client/src/components/InfoHoverCard.tsx b/packages/client/src/components/InfoHoverCard.tsx index ab43b6dd18..5b45666807 100644 --- a/packages/client/src/components/InfoHoverCard.tsx +++ b/packages/client/src/components/InfoHoverCard.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { CircleHelpIcon } from 'lucide-react'; import { HoverCard, HoverCardTrigger, HoverCardPortal, HoverCardContent } from './HoverCard'; import { ESide } from '~/common'; @@ -8,15 +9,23 @@ type InfoHoverCardProps = { }; const InfoHoverCard = ({ side, text }: InfoHoverCardProps) => { + const [isOpen, setIsOpen] = useState(false); + return ( - - - {' '} + + setIsOpen(true)} + onBlur={() => setIsOpen(false)} + aria-label={text} + > +
-

{text}

+ {text}
diff --git a/packages/client/src/components/Label.tsx b/packages/client/src/components/Label.tsx index 54f75fb2a8..d250e47e3f 100644 --- a/packages/client/src/components/Label.tsx +++ b/packages/client/src/components/Label.tsx @@ -13,7 +13,7 @@ const Label = React.forwardRef< {...props} {...{ className: cn( - 'block w-full break-all text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-200', + 'block w-full break-all text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-200', className, ), }} diff --git a/packages/client/src/components/Slider.tsx b/packages/client/src/components/Slider.tsx index 4be0f20039..3845b8901f 100644 --- a/packages/client/src/components/Slider.tsx +++ b/packages/client/src/components/Slider.tsx @@ -2,37 +2,56 @@ import * as React from 'react'; import * as SliderPrimitive from '@radix-ui/react-slider'; import { cn } from '~/utils'; -const Slider = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - className?: string; - onDoubleClick?: () => void; - } ->(({ className, onDoubleClick, ...props }, ref) => ( - & { + className?: string; + onDoubleClick?: () => void; + 'aria-describedby'?: string; +} & ( + | { 'aria-label': string; 'aria-labelledby'?: never } + | { 'aria-labelledby': string; 'aria-label'?: never } + | { 'aria-label': string; 'aria-labelledby': string } + ); + +const Slider = React.forwardRef, SliderProps>( + ( + { + className, onDoubleClick, - }} - > - - - - ( + - -)); + > + + + + + + ), +); Slider.displayName = SliderPrimitive.Root.displayName; export { Slider };