mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-26 12:24:10 +01:00
♿️ fix: Accessibility, UI consistency, dialog & avatar refactors (#9975)
* 🔧 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
This commit is contained in:
parent
bcd97aad2f
commit
a5189052ec
56 changed files with 1158 additions and 857 deletions
|
|
@ -31,12 +31,12 @@ export const ClearChats = () => {
|
|||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="font-light">{localize('com_nav_clear_all_chats')}</Label>
|
||||
<Label id="clear-all-chats-label">{localize('com_nav_clear_all_chats')}</Label>
|
||||
<OGDialog open={open} onOpenChange={setOpen}>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
aria-labelledby="clear-all-chats-label"
|
||||
variant="destructive"
|
||||
className="flex items-center justify-center rounded-lg transition-colors duration-200"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{localize('com_ui_delete')}
|
||||
|
|
@ -47,7 +47,7 @@ export const ClearChats = () => {
|
|||
title={localize('com_nav_confirm_clear')}
|
||||
className="max-w-[450px]"
|
||||
main={
|
||||
<Label className="text-left text-sm font-medium">
|
||||
<Label className="break-words">
|
||||
{localize('com_nav_clear_conversation_confirm_message')}
|
||||
</Label>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import { useOnClickOutside } from '@librechat/client';
|
||||
import ImportConversations from './ImportConversations';
|
||||
import { RevokeAllKeys } from './RevokeAllKeys';
|
||||
import { RevokeKeys } from './RevokeKeys';
|
||||
import { DeleteCache } from './DeleteCache';
|
||||
import { ClearChats } from './ClearChats';
|
||||
import SharedLinks from './SharedLinks';
|
||||
|
|
@ -20,7 +20,7 @@ function Data() {
|
|||
<SharedLinks />
|
||||
</div>
|
||||
<div className="pb-3">
|
||||
<RevokeAllKeys />
|
||||
<RevokeKeys />
|
||||
</div>
|
||||
<div className="pb-3">
|
||||
<DeleteCache />
|
||||
|
|
|
|||
|
|
@ -38,14 +38,14 @@ export const DeleteCache = ({ disabled = false }: { disabled?: boolean }) => {
|
|||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="font-light">{localize('com_nav_delete_cache_storage')}</Label>
|
||||
<Label id="delete-cache-label">{localize('com_nav_delete_cache_storage')}</Label>
|
||||
<OGDialog open={open} onOpenChange={setOpen}>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="flex items-center justify-center rounded-lg transition-colors duration-200"
|
||||
onClick={() => setOpen(true)}
|
||||
disabled={disabled || isCacheEmpty}
|
||||
aria-labelledby="delete-cache-label"
|
||||
>
|
||||
{localize('com_ui_delete')}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -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<HTMLInputElement>(null);
|
||||
|
||||
const { showToast } = useToastContext();
|
||||
const [, setErrors] = useState<string[]>([]);
|
||||
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<HTMLInputElement>) => {
|
||||
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<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
handleFiles(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportClick = () => {
|
||||
const handleImportClick = useCallback(() => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
handleImportClick();
|
||||
}
|
||||
};
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
handleImportClick();
|
||||
}
|
||||
},
|
||||
[handleImportClick],
|
||||
);
|
||||
|
||||
const isImportDisabled = isUploading;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>{localize('com_ui_import_conversation_info')}</div>
|
||||
<button
|
||||
<Label id="import-conversation-label">{localize('com_ui_import_conversation_info')}</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleImportClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={!allowImport}
|
||||
disabled={isImportDisabled}
|
||||
aria-label={localize('com_ui_import')}
|
||||
className="btn btn-neutral relative"
|
||||
aria-labelledby="import-conversation-label"
|
||||
>
|
||||
{allowImport ? (
|
||||
<Import className="mr-1 flex h-4 w-4 items-center stroke-1" />
|
||||
) : (
|
||||
{isUploading ? (
|
||||
<Spinner className="mr-1 w-4" />
|
||||
) : (
|
||||
<Import className="mr-1 flex h-4 w-4 items-center stroke-1" />
|
||||
)}
|
||||
<span>{localize('com_ui_import')}</span>
|
||||
</button>
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Label } from '@librechat/client';
|
||||
import { RevokeKeysButton } from './RevokeKeysButton';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export const RevokeAllKeys = () => {
|
||||
const localize = useLocalize();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="font-light">{localize('com_ui_revoke_info')}</Label>
|
||||
<RevokeKeysButton all={true} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
72
client/src/components/Nav/SettingsTabs/Data/RevokeKeys.tsx
Normal file
72
client/src/components/Nav/SettingsTabs/Data/RevokeKeys.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="flex items-center justify-between">
|
||||
<Label id="revoke-info-label">{localize('com_ui_revoke_info')}</Label>
|
||||
|
||||
<OGDialog open={open} onOpenChange={setOpen}>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => setOpen(true)}
|
||||
disabled={disabled}
|
||||
aria-labelledby="revoke-info-label"
|
||||
>
|
||||
{localize('com_ui_revoke')}
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_ui_revoke_keys')}
|
||||
className="max-w-[450px]"
|
||||
main={
|
||||
<Label className="text-left text-sm font-medium">
|
||||
{localize('com_ui_revoke_keys_confirm')}
|
||||
</Label>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: onClick,
|
||||
selectClasses:
|
||||
'bg-destructive text-white transition-all duration-200 hover:bg-destructive/80',
|
||||
selectText: isLoading ? <Spinner /> : localize('com_ui_revoke'),
|
||||
}}
|
||||
/>
|
||||
</OGDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 (
|
||||
<OGDialog open={open} onOpenChange={setOpen}>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="flex items-center justify-center rounded-lg transition-colors duration-200"
|
||||
onClick={() => setOpen(true)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{localize('com_ui_revoke')}
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={dialogTitle}
|
||||
className="max-w-[450px]"
|
||||
main={<Label className="text-left text-sm font-medium">{dialogMessage}</Label>}
|
||||
selection={{
|
||||
selectHandler: onClick,
|
||||
selectClasses:
|
||||
'bg-destructive text-white transition-all duration-200 hover:bg-destructive/80',
|
||||
selectText: isLoading ? <Spinner /> : localize('com_ui_revoke'),
|
||||
}}
|
||||
/>
|
||||
</OGDialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -286,11 +286,13 @@ export default function SharedLinks() {
|
|||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>{localize('com_nav_shared_links')}</div>
|
||||
<Label id="shared-links-label">{localize('com_nav_shared_links')}</Label>
|
||||
|
||||
<OGDialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<OGDialogTrigger asChild onClick={() => setIsOpen(true)}>
|
||||
<Button variant="outline">{localize('com_ui_manage')}</Button>
|
||||
<Button aria-labelledby="shared-links-label" variant="outline">
|
||||
{localize('com_ui_manage')}
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
|
||||
<OGDialogContent
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue