🎨 refactor: UI stlye (#4438)

* feat: Refactor ChatForm and StopButton components for improved styling and localization

* feat: Refactor AudioRecorder, ChatForm, AttachFile, and SendButton components for improved styling and layout

* feat: Add RevokeAllKeys component and update styling for buttons and inputs

* feat: Refactor ClearChats component and update ClearConvos functionality for improved clarity and user experience

* feat: Remove ClearConvos component and update related imports and functionality in Avatar and DeleteCacheButton components

* feat: Rename DeleteCacheButton to DeleteCache and update related imports; enhance confirmation message in localization

* feat: Update ChatForm layout for RTL support and improve component structure

* feat: Adjust ChatForm layout for improved RTL support and alignment

* feat: Refactor Bookmark components to use new UI elements and improve styling

* feat: Update FileSearch and ShareAgent components for improved button styling and layout

* feat: Update ChatForm and TextareaHeader styles for improved UI consistency

* feat: Refactor Nav components for improved styling and layout adjustments

* feat: Update button sizes and padding for improved UI consistency across chat components

* feat: Remove ClearChatsButton test file as part of code cleanup
This commit is contained in:
Marco Beretta 2024-10-19 14:30:52 +02:00 committed by GitHub
parent 20fb7f05ae
commit 8f3de7d11f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 471 additions and 564 deletions

View file

@ -30,7 +30,7 @@ function AccountSettings() {
<Select.Select
aria-label={localize('com_nav_account_settings')}
data-testid="nav-user"
className="duration-350 mt-text-sm flex h-auto w-full items-center gap-2 rounded-xl p-2 text-sm transition-all duration-200 ease-in-out hover:bg-accent"
className="mt-text-sm flex h-auto w-full items-center gap-2 rounded-xl p-2 text-sm transition-all duration-200 ease-in-out hover:bg-accent"
>
<div className="-ml-0.9 -mt-0.8 h-8 w-8 flex-shrink-0">
<div className="relative flex">

View file

@ -41,7 +41,7 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags, isSmallScreen }: Boo
className={cn(
'mt-text-sm flex h-10 w-full items-center gap-2 rounded-lg p-2 text-sm transition-colors duration-200 hover:bg-surface-hover',
open ? 'bg-surface-hover' : '',
isSmallScreen ? 'h-14 rounded-2xl' : '',
isSmallScreen ? 'h-12' : '',
)}
data-testid="bookmark-menu"
>

View file

@ -1,53 +0,0 @@
import { useState } from 'react';
import { useClearConversationsMutation } from 'librechat-data-provider/react-query';
import { useLocalize, useConversation, useConversations } from '~/hooks';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { ClearChatsButton } from './SettingsTabs';
import { Dialog } from '~/components/ui';
const ClearConvos = ({ open, onOpenChange }) => {
const { newConversation } = useConversation();
const { refreshConversations } = useConversations();
const clearConvosMutation = useClearConversationsMutation();
const [confirmClear, setConfirmClear] = useState(false);
const localize = useLocalize();
// Clear all conversations
const clearConvos = () => {
if (confirmClear) {
clearConvosMutation.mutate(
{},
{
onSuccess: () => {
newConversation();
refreshConversations();
},
},
);
setConfirmClear(false);
} else {
setConfirmClear(true);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTemplate
title={localize('com_nav_clear_conversation')}
className="w-11/12 max-w-[650px] sm:w-3/4 md:w-3/4 lg:w-3/4"
headerClassName="border-none"
description={localize('com_nav_clear_conversation_confirm_message')}
buttons={
<ClearChatsButton
showText={false}
confirmClear={confirmClear}
onClick={clearConvos}
className="w-[77px]"
/>
}
/>
</Dialog>
);
};
export default ClearConvos;

View file

@ -168,36 +168,22 @@ const Nav = ({
onMouseLeave={handleMouseLeave}
ref={containerRef}
>
{isSmallScreen == true ? (
<div className="pt-3.5">
{isSearchEnabled === true && (
<SearchBar clearSearch={clearSearch} isSmallScreen={isSmallScreen} />
)}
{hasAccessToBookmarks === true && (
<NewChat
toggleNav={itemToggleNav}
isSmallScreen={isSmallScreen}
subHeaders={
<>
{isSearchEnabled === true && (
<SearchBar clearSearch={clearSearch} isSmallScreen={isSmallScreen} />
)}
<BookmarkNav
tags={tags}
setTags={setTags}
isSmallScreen={isSmallScreen}
/>
)}
</div>
) : (
<NewChat
toggleNav={itemToggleNav}
subHeaders={
<>
{isSearchEnabled === true && (
<SearchBar clearSearch={clearSearch} isSmallScreen={isSmallScreen} />
)}
<BookmarkNav
tags={tags}
setTags={setTags}
isSmallScreen={isSmallScreen}
/>
</>
}
/>
)}
</>
}
/>
<Conversations
conversations={conversations}

View file

@ -8,6 +8,7 @@ import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
import { useLocalize, useNewConvo } from '~/hooks';
import { NewChatIcon } from '~/components/svg';
import { cn } from '~/utils';
import store from '~/store';
const NewChatButtonIcon = ({ conversation }: { conversation: TConversation | null }) => {
@ -57,10 +58,12 @@ export default function NewChat({
index = 0,
toggleNav,
subHeaders,
isSmallScreen,
}: {
index?: number;
toggleNav: () => void;
subHeaders?: React.ReactNode;
isSmallScreen: boolean;
}) {
/** Note: this component needs an explicit index passed if using more than one */
const { newConversation: newConvo } = useNewConvo(index);
@ -86,7 +89,10 @@ export default function NewChat({
tabIndex={0}
data-testid="nav-new-chat"
onClick={clickHandler}
className="group flex h-10 items-center gap-2 rounded-lg px-2 font-medium transition-colors duration-200 hover:bg-surface-hover"
className={cn(
'group flex h-10 items-center gap-2 rounded-lg px-2 font-medium transition-colors duration-200 hover:bg-surface-hover',
isSmallScreen ? 'h-14' : '',
)}
aria-label={localize('com_ui_new_chat')}
>
<NewChatButtonIcon conversation={conversation} />

View file

@ -76,11 +76,11 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<div className={cn('fixed inset-0 flex w-screen items-center justify-center p-4')}>
<DialogPanel
className={cn(
'min-h-[600px] overflow-hidden rounded-xl rounded-b-lg bg-background pb-6 shadow-2xl backdrop-blur-2xl animate-in sm:rounded-lg md:min-h-[373px] md:w-[680px]',
'min-h-[600px] overflow-hidden rounded-xl rounded-b-lg bg-background pb-6 shadow-2xl backdrop-blur-2xl animate-in sm:rounded-2xl md:min-h-[373px] md:w-[680px]',
)}
>
<DialogTitle
className="mb-3 flex items-center justify-between border-b border-border-medium p-6 pb-5 text-left"
className="mb-1 flex items-center justify-between p-6 pb-5 text-left"
as="div"
>
<h2 className="text-lg font-medium leading-6 text-text-primary">

View file

@ -5,16 +5,17 @@ import AvatarEditor from 'react-avatar-editor';
import { fileConfig as defaultFileConfig, mergeFileConfig } from 'librechat-data-provider';
import type { TUser } from 'librechat-data-provider';
import {
Slider,
Button,
Spinner,
OGDialog,
OGDialogContent,
OGDialogHeader,
OGDialogTitle,
OGDialogTrigger,
Slider,
} from '~/components/ui';
} from '~/components';
import { useUploadAvatarMutation, useGetFileConfig } from '~/data-provider';
import { useToastContext } from '~/Providers';
import { Spinner } from '~/components/svg';
import { cn, formatBytes } from '~/utils';
import { useLocalize } from '~/hooks';
import store from '~/store';
@ -130,10 +131,7 @@ function Avatar() {
</OGDialogTrigger>
</div>
<OGDialogContent
className={cn('bg-surface-tertiary text-text-primary shadow-2xl md:h-auto md:w-[450px]')}
style={{ borderRadius: '12px' }}
>
<OGDialogContent className="w-11/12 max-w-sm" style={{ borderRadius: '12px' }}>
<OGDialogHeader>
<OGDialogTitle className="text-lg font-medium leading-6 text-text-primary">
{image ? localize('com_ui_preview') : localize('com_ui_upload_image')}
@ -174,10 +172,10 @@ function Avatar() {
<RotateCw className="h-5 w-5" />
</button>
</div>
<button
<Button
className={cn(
'mt-4 flex items-center rounded px-4 py-2 text-white transition-colors hover:bg-green-600 hover:text-gray-200',
isUploading ? 'cursor-not-allowed bg-green-600' : 'bg-green-500',
'btn btn-primary mt-4 flex w-full hover:bg-green-600',
isUploading ? 'cursor-not-allowed opacity-90' : '',
)}
onClick={handleUpload}
disabled={isUploading}
@ -188,24 +186,21 @@ function Avatar() {
<Upload className="mr-2 h-5 w-5" />
)}
{localize('com_ui_upload')}
</button>
</Button>
</>
) : (
<div
className="flex h-64 w-64 flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-700"
className="flex h-64 w-11/12 flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-transparent dark:border-gray-600"
onDrop={handleDrop}
onDragOver={handleDragOver}
>
<FileImage className="mb-4 h-12 w-12 text-gray-400" />
<FileImage className="mb-4 size-12 text-gray-400" />
<p className="mb-2 text-center text-sm text-gray-500 dark:text-gray-400">
{localize('com_ui_drag_drop')}
</p>
<button
onClick={openFileDialog}
className="rounded bg-gray-200 px-4 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500"
>
<Button variant="secondary" onClick={openFileDialog}>
{localize('com_ui_select_file')}
</button>
</Button>
<input
ref={fileInputRef}
type="file"

View file

@ -1,11 +1,19 @@
import { LockIcon, Trash } from 'lucide-react';
import React, { useState, useCallback } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, Input } from '~/components/ui';
import { cn, defaultTextProps, removeFocusOutlines } from '~/utils';
import {
Input,
Button,
Spinner,
OGDialog,
OGDialogContent,
OGDialogTrigger,
OGDialogHeader,
OGDialogTitle,
} from '~/components';
import { useDeleteUserMutation } from '~/data-provider';
import { Spinner, LockIcon } from '~/components/svg';
import { useAuthContext } from '~/hooks/AuthContext';
import { useLocalize } from '~/hooks';
import DangerButton from '../DangerButton';
import { cn } from '~/utils';
const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolean }) => {
const localize = useLocalize();
@ -15,14 +23,8 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea
});
const [isDialogOpen, setDialogOpen] = useState<boolean>(false);
const [deleteInput, setDeleteInput] = useState('');
const [emailInput, setEmailInput] = useState('');
const [isLocked, setIsLocked] = useState(true);
const onClick = useCallback(() => {
setDialogOpen(true);
}, []);
const handleDeleteUser = () => {
if (!isLocked) {
deleteUser(undefined);
@ -30,47 +32,38 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea
};
const handleInputChange = useCallback(
(newEmailInput: string, newDeleteInput: string) => {
(newEmailInput: string) => {
const isEmailCorrect =
newEmailInput.trim().toLowerCase() === user?.email?.trim().toLowerCase();
const isDeleteInputCorrect = newDeleteInput === 'DELETE';
setIsLocked(!(isEmailCorrect && isDeleteInputCorrect));
newEmailInput.trim().toLowerCase() === user?.email.trim().toLowerCase();
setIsLocked(!isEmailCorrect);
},
[user?.email],
);
return (
<>
<div className="flex items-center justify-between">
<span>{localize('com_nav_delete_account')}</span>
<label>
<DangerButton
id={'delete-user-account'}
disabled={disabled}
onClick={onClick}
actionTextCode="com_ui_delete"
className={cn(
'btn relative border-none bg-red-500 text-white hover:bg-red-700 dark:hover:bg-red-700',
)}
confirmClear={false}
infoTextCode={''}
dataTestIdInitial={''}
dataTestIdConfirm={''}
/>
</label>
</div>
<Dialog open={isDialogOpen} onOpenChange={() => setDialogOpen(false)}>
<DialogContent
className={cn('shadow-2xl md:h-[500px] md:w-[450px]')}
style={{ borderRadius: '12px', padding: '20px' }}
>
<DialogHeader>
<DialogTitle className="text-lg font-medium leading-6">
<OGDialog open={isDialogOpen} onOpenChange={setDialogOpen}>
<div className="flex items-center justify-between">
<span>{localize('com_nav_delete_account')}</span>
<OGDialogTrigger asChild>
<Button
variant="destructive"
className="flex items-center justify-center rounded-lg transition-colors duration-200"
onClick={() => setDialogOpen(true)}
disabled={disabled}
>
{localize('com_ui_delete')}
</Button>
</OGDialogTrigger>
</div>
<OGDialogContent className="w-11/12 max-w-2xl">
<OGDialogHeader>
<OGDialogTitle className="text-lg font-medium leading-6">
{localize('com_nav_delete_account_confirm')}
</DialogTitle>
</DialogHeader>
<div className="mb-20 text-sm text-black dark:text-white">
<ul>
</OGDialogTitle>
</OGDialogHeader>
<div className="mb-8 text-sm text-black dark:text-white">
<ul className="font-semibold text-amber-600">
<li>{localize('com_nav_delete_warning')}</li>
<li>{localize('com_nav_delete_data_info')}</li>
</ul>
@ -80,28 +73,14 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea
{renderInput(
localize('com_nav_delete_account_email_placeholder'),
'email-confirm-input',
user?.email || '',
(e) => {
setEmailInput(e.target.value);
handleInputChange(e.target.value, deleteInput);
},
)}
</div>
<div className="mb-4">
{renderInput(
localize('com_nav_delete_account_confirm_placeholder'),
'delete-confirm-input',
'',
(e) => {
setDeleteInput(e.target.value);
handleInputChange(emailInput, e.target.value);
},
user?.email ?? '',
(e) => handleInputChange(e.target.value),
)}
</div>
{renderDeleteButton(handleDeleteUser, isDeleting, isLocked, localize)}
</div>
</DialogContent>
</Dialog>
</OGDialogContent>
</OGDialog>
</>
);
};
@ -113,17 +92,10 @@ const renderInput = (
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
) => (
<div className="mb-4">
<label className="mb-1 block text-sm font-medium text-black dark:text-white">{label}</label>
<Input
id={id}
onChange={onChange}
placeholder={value}
className={cn(
defaultTextProps,
'h-10 max-h-10 w-full max-w-full rounded-md bg-white px-3 py-2',
removeFocusOutlines,
)}
/>
<label className="mb-1 block text-sm font-medium text-black dark:text-white" htmlFor={id}>
{label}
</label>
<Input id={id} onChange={onChange} placeholder={value} />
</div>
);
@ -135,12 +107,8 @@ const renderDeleteButton = (
) => (
<button
className={cn(
'mt-4 flex w-full items-center justify-center rounded-lg px-4 py-2 transition-colors duration-200',
isLocked
? 'cursor-not-allowed bg-gray-200 text-gray-300 dark:bg-gray-500 dark:text-gray-600'
: isDeleting
? 'cursor-not-allowed bg-gray-100 text-gray-700 dark:bg-gray-400 dark:text-gray-700'
: 'bg-red-700 text-white hover:bg-red-800 ',
'mt-4 flex w-full items-center justify-center rounded-lg bg-surface-tertiary px-4 py-2 transition-all duration-200',
isLocked ? 'cursor-not-allowed opacity-30' : 'bg-destructive text-destructive-foreground',
)}
onClick={handleDeleteUser}
disabled={isDeleting || isLocked}
@ -153,12 +121,12 @@ const renderDeleteButton = (
<>
{isLocked ? (
<>
<LockIcon />
<LockIcon className="size-5" />
<span className="ml-2">{localize('com_ui_locked')}</span>
</>
) : (
<>
<LockIcon />
<Trash className="size-5" />
<span className="ml-2">{localize('com_nav_delete_account_button')}</span>
</>
)}

View file

@ -1,29 +1,58 @@
import type { TDangerButtonProps } from '~/common';
import DangerButton from '../DangerButton';
import React, { useState } from 'react';
import { useClearConversationsMutation } from 'librechat-data-provider/react-query';
import { Label, Button, OGDialog, OGDialogTrigger, Spinner } from '~/components';
import { useConversation, useConversations, useLocalize } from '~/hooks';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
export const ClearChats = () => {
const localize = useLocalize();
const [open, setOpen] = useState(false);
const { newConversation } = useConversation();
const { refreshConversations } = useConversations();
const clearConvosMutation = useClearConversationsMutation();
const clearConvos = () => {
clearConvosMutation.mutate(
{},
{
onSuccess: () => {
newConversation();
refreshConversations();
},
},
);
};
export const ClearChatsButton = ({
confirmClear,
className = '',
showText = true,
mutation,
onClick,
}: Pick<
TDangerButtonProps,
'confirmClear' | 'mutation' | 'className' | 'showText' | 'onClick'
>) => {
return (
<DangerButton
id="clearConvosBtn"
mutation={mutation}
confirmClear={confirmClear}
className={className}
showText={showText}
infoTextCode="com_nav_clear_all_chats"
actionTextCode="com_ui_clear"
confirmActionTextCode="com_nav_confirm_clear"
dataTestIdInitial="clear-convos-initial"
dataTestIdConfirm="clear-convos-confirm"
onClick={onClick}
/>
<div className="flex items-center justify-between">
<Label className="font-light">{localize('com_nav_clear_all_chats')}</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)}
>
{localize('com_ui_delete')}
</Button>
</OGDialogTrigger>
<OGDialogTemplate
showCloseButton={false}
title={localize('com_nav_confirm_clear')}
className="max-w-[450px]"
main={
<Label className="text-left text-sm font-medium">
{localize('com_nav_clear_conversation_confirm_message')}
</Label>
}
selection={{
selectHandler: clearConvos,
selectClasses:
'bg-destructive text-white transition-all duration-200 hover:bg-destructive/80',
selectText: clearConvosMutation.isLoading ? <Spinner /> : localize('com_ui_delete'),
}}
/>
</OGDialog>
</div>
);
};

View file

@ -1,47 +0,0 @@
import 'test/matchMedia.mock';
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { ClearChatsButton } from './ClearChats';
import { RecoilRoot } from 'recoil';
describe('ClearChatsButton', () => {
let mockOnClick;
beforeEach(() => {
mockOnClick = jest.fn();
});
it('renders correctly', () => {
const { getByText } = render(
<RecoilRoot>
<ClearChatsButton confirmClear={false} showText={true} onClick={mockOnClick} />
</RecoilRoot>,
);
expect(getByText('Clear all chats')).toBeInTheDocument();
expect(getByText('Clear')).toBeInTheDocument();
});
it('renders confirm clear when confirmClear is true', () => {
const { getByText } = render(
<RecoilRoot>
<ClearChatsButton confirmClear={true} showText={true} onClick={mockOnClick} />
</RecoilRoot>,
);
expect(getByText('Confirm Clear')).toBeInTheDocument();
});
it('calls onClick when the button is clicked', () => {
const { getByText } = render(
<RecoilRoot>
<ClearChatsButton confirmClear={false} showText={true} onClick={mockOnClick} />
</RecoilRoot>,
);
fireEvent.click(getByText('Clear'));
expect(mockOnClick).toHaveBeenCalled();
});
});

View file

@ -1,10 +1,9 @@
import React, { useState, useRef } from 'react';
import { useClearConversationsMutation } from 'librechat-data-provider/react-query';
import { useConversation, useConversations, useOnClickOutside } from '~/hooks';
import { RevokeKeysButton } from './RevokeKeysButton';
import { DeleteCacheButton } from './DeleteCacheButton';
import ImportConversations from './ImportConversations';
import { ClearChatsButton } from './ClearChats';
import { RevokeAllKeys } from './RevokeAllKeys';
import { DeleteCache } from './DeleteCache';
import { useOnClickOutside } from '~/hooks';
import { ClearChats } from './ClearChats';
import SharedLinks from './SharedLinks';
function Data() {
@ -12,28 +11,6 @@ function Data() {
const [confirmClearConvos, setConfirmClearConvos] = useState(false);
useOnClickOutside(dataTabRef, () => confirmClearConvos && setConfirmClearConvos(false), []);
const { newConversation } = useConversation();
const { refreshConversations } = useConversations();
const clearConvosMutation = useClearConversationsMutation();
const clearConvos = () => {
if (confirmClearConvos) {
console.log('Clearing conversations...');
setConfirmClearConvos(false);
clearConvosMutation.mutate(
{},
{
onSuccess: () => {
newConversation();
refreshConversations();
},
},
);
} else {
setConfirmClearConvos(true);
}
};
return (
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
@ -43,18 +20,13 @@ function Data() {
<SharedLinks />
</div>
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<RevokeKeysButton all={true} />
<RevokeAllKeys />
</div>
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<DeleteCacheButton />
<DeleteCache />
</div>
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<ClearChatsButton
confirmClear={confirmClearConvos}
onClick={clearConvos}
showText={true}
mutation={clearConvosMutation}
/>
<ClearChats />
</div>
</div>
);

View file

@ -0,0 +1,65 @@
import React, { useState, useCallback, useRef, useEffect } from 'react';
import { Label, Button, OGDialog, OGDialogTrigger, Spinner } from '~/components';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useOnClickOutside, useLocalize } from '~/hooks';
export const DeleteCache = ({ disabled = false }: { disabled?: boolean }) => {
const localize = useLocalize();
const [open, setOpen] = useState(false);
const [isCacheEmpty, setIsCacheEmpty] = useState(true);
const [confirmClear, setConfirmClear] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const contentRef = useRef(null);
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);
const checkCache = useCallback(async () => {
const cache = await caches.open('tts-responses');
const keys = await cache.keys();
setIsCacheEmpty(keys.length === 0);
}, []);
useEffect(() => {
checkCache();
}, [checkCache]);
const revokeAllUserKeys = useCallback(async () => {
setIsLoading(true);
const cache = await caches.open('tts-responses');
await cache.keys().then((keys) => Promise.all(keys.map((key) => cache.delete(key))));
setIsLoading(false);
}, []);
return (
<div className="flex items-center justify-between">
<Label className="font-light">{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}
>
{localize('com_ui_delete')}
</Button>
</OGDialogTrigger>
<OGDialogTemplate
showCloseButton={false}
title={localize('com_nav_confirm_clear')}
className="max-w-[450px]"
main={
<Label className="text-left text-sm font-medium">
{localize('com_nav_clear_cache_confirm_message')}
</Label>
}
selection={{
selectHandler: revokeAllUserKeys,
selectClasses:
'bg-destructive text-white transition-all duration-200 hover:bg-destructive/80',
selectText: isLoading ? <Spinner /> : localize('com_ui_delete'),
}}
/>
</OGDialog>
</div>
);
};

View file

@ -1,53 +0,0 @@
import React, { useState, useCallback, useRef, useEffect } from 'react';
import { useOnClickOutside } from '~/hooks';
import DangerButton from '../DangerButton';
export const DeleteCacheButton = ({
showText = true,
disabled = false,
}: {
showText?: boolean;
disabled?: boolean;
}) => {
const [confirmClear, setConfirmClear] = useState(false);
const [isCacheEmpty, setIsCacheEmpty] = useState(true);
const contentRef = useRef(null);
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);
const checkCache = useCallback(async () => {
const cache = await caches.open('tts-responses');
const keys = await cache.keys();
setIsCacheEmpty(keys.length === 0);
}, []);
useEffect(() => {
checkCache();
}, [confirmClear]);
const revokeAllUserKeys = useCallback(async () => {
if (confirmClear) {
const cache = await caches.open('tts-responses');
await cache.keys().then((keys) => Promise.all(keys.map((key) => cache.delete(key))));
setConfirmClear(false);
} else {
setConfirmClear(true);
}
}, [confirmClear]);
return (
<DangerButton
ref={contentRef}
showText={showText}
onClick={revokeAllUserKeys}
disabled={disabled || isCacheEmpty}
confirmClear={confirmClear}
id={'delete-cache'}
actionTextCode={'com_ui_delete'}
infoTextCode={'com_nav_delete_cache_storage'}
infoDescriptionCode={'com_nav_info_delete_cache_storage'}
dataTestIdInitial={'delete-cache-initial'}
dataTestIdConfirm={'delete-cache-confirm'}
/>
);
};

View file

@ -0,0 +1,15 @@
import React from 'react';
import { RevokeKeysButton } from './RevokeKeysButton';
import { Label } from '~/components/ui';
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>
);
};

View file

@ -2,64 +2,77 @@ import {
useRevokeAllUserKeysMutation,
useRevokeUserKeyMutation,
} from 'librechat-data-provider/react-query';
import React, { useState, useCallback, useRef } from 'react';
import { useOnClickOutside } from '~/hooks';
import DangerButton from '../DangerButton';
import React, { useState } from 'react';
import { Button, Label, OGDialog, OGDialogTrigger, Spinner } from '~/components';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useLocalize } from '~/hooks';
export const RevokeKeysButton = ({
showText = true,
endpoint = '',
all = false,
disabled = false,
setDialogOpen,
}: {
showText?: boolean;
endpoint?: string;
all?: boolean;
disabled?: boolean;
setDialogOpen?: (open: boolean) => void;
}) => {
const [confirmClear, setConfirmClear] = useState(false);
const localize = useLocalize();
const [open, setOpen] = useState(false);
const revokeKeyMutation = useRevokeUserKeyMutation(endpoint);
const revokeKeysMutation = useRevokeAllUserKeysMutation();
const contentRef = useRef(null);
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);
const revokeAllUserKeys = useCallback(() => {
if (confirmClear) {
revokeKeysMutation.mutate({});
setConfirmClear(false);
} else {
setConfirmClear(true);
}
}, [confirmClear, revokeKeysMutation]);
const revokeUserKey = useCallback(() => {
if (!endpoint) {
const handleSuccess = () => {
if (!setDialogOpen) {
return;
} else if (confirmClear) {
revokeKeyMutation.mutate({});
setConfirmClear(false);
} else {
setConfirmClear(true);
}
}, [confirmClear, revokeKeyMutation, endpoint]);
const onClick = all ? revokeAllUserKeys : revokeUserKey;
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', endpoint);
const dialogMessage = all
? localize('com_ui_revoke_keys_confirm')
: localize('com_ui_revoke_key_confirm');
const isLoading = revokeKeyMutation.isLoading || revokeKeysMutation.isLoading;
return (
<DangerButton
ref={contentRef}
showText={showText}
onClick={onClick}
disabled={disabled}
confirmClear={confirmClear}
id={'revoke-all-user-keys'}
actionTextCode={'com_ui_revoke'}
infoTextCode={'com_ui_revoke_info'}
infoDescriptionCode={'com_nav_info_revoke'}
dataTestIdInitial={'revoke-all-keys-initial'}
dataTestIdConfirm={'revoke-all-keys-confirm'}
mutation={all ? revokeKeysMutation : revokeKeyMutation}
/>
<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>
);
};

View file

@ -1,6 +1,5 @@
export * from './ExportConversation';
export * from './SettingsTabs/';
export { default as ClearConvos } from './ClearConvos';
export { default as MobileNav } from './MobileNav';
export { default as Nav } from './Nav';
export { default as NavLink } from './NavLink';