🖼️ style: Conversation Menu and Dialogs update (#3601)

* feat: new dropdown

* fix: maintain popover active when open

* fix: update DeleteButton and ShareButton component to use useState for managing dialog state

* BREAKING: style improvement of base Button component

* style: update export button

* a11y: ExportAndShareButton

* add border

* quick style fix

* fix: flick issue on convo

* fix: DropDown opens when renaming

* chore: update radix-ui/react-dropdown-menu to latest

* small fix

* style: bookmarks update

* reorder export modal

* feat: imporved dropdowns

* style: a lot of changes; header, bookmarks, export, nav, convo, convoOptions

* fix: small style issues

* fix: button

* fix: bookmarks header menu

* fix: dropdown close glitch

* feat: Improve accessibility and keyboard navigation in ModelSpec component

* fix: Nav related type issues

* style: ConvoOptions theming and focus ring

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Marco Beretta 2024-08-16 10:30:14 +02:00 committed by GitHub
parent 7f50d2f7c0
commit 96581d56df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 2627 additions and 1821 deletions

View file

@ -1,21 +1,17 @@
import { useRecoilValue } from 'recoil';
import { useParams } from 'react-router-dom';
import { useState, useRef, useMemo } from 'react';
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { Constants } from 'librechat-data-provider';
import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import { Check, X } from 'lucide-react';
import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react';
import { useConversations, useNavigateToConvo, useMediaQuery } from '~/hooks';
import { useUpdateConversationMutation } from '~/data-provider';
import EndpointIcon from '~/components/Endpoints/EndpointIcon';
import { useConversations, useNavigateToConvo } from '~/hooks';
import { NotificationSeverity } from '~/common';
import { ArchiveIcon } from '~/components/svg';
import { useToastContext } from '~/Providers';
import ArchiveButton from './ArchiveButton';
import DropDownMenu from './DropDownMenu';
import DeleteButton from './DeleteButton';
import RenameButton from './RenameButton';
import HoverToggle from './HoverToggle';
import ShareButton from './ShareButton';
import { ConvoOptions } from './ConvoOptions';
import { cn } from '~/utils';
import store from '~/store';
@ -28,7 +24,6 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
const activeConvos = useRecoilValue(store.allConversationsSelector);
const { data: endpointsConfig } = useGetEndpointsQuery();
const { navigateWithLastTools } = useNavigateToConvo();
const { data: startupConfig } = useGetStartupConfig();
const { refreshConversations } = useConversations();
const { showToast } = useToastContext();
const { conversationId, title } = conversation;
@ -36,6 +31,7 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
const [titleInput, setTitleInput] = useState(title);
const [renaming, setRenaming] = useState(false);
const [isPopoverActive, setIsPopoverActive] = useState(false);
const isSmallScreen = useMediaQuery('(max-width: 768px)');
const clickHandler = async (event: React.MouseEvent<HTMLAnchorElement>) => {
if (event.button === 0 && (event.ctrlKey || event.metaKey)) {
@ -44,7 +40,7 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
}
event.preventDefault();
if (currentConvoId === conversationId) {
if (currentConvoId === conversationId || isPopoverActive) {
return;
}
@ -57,17 +53,17 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
};
const renameHandler = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setIsPopoverActive(false);
setTitleInput(title);
setRenaming(true);
setTimeout(() => {
if (!inputRef.current) {
return;
}
inputRef.current.focus();
}, 25);
};
useEffect(() => {
if (renaming && inputRef.current) {
inputRef.current.focus();
}
}, [renaming]);
const onRename = (e: MouseEvent<HTMLButtonElement> | FocusEvent<HTMLInputElement> | KeyEvent) => {
e.preventDefault();
setRenaming(false);
@ -99,6 +95,12 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
}
};
const cancelRename = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setTitleInput(title);
setRenaming(false);
};
const isActiveConvo =
currentConvoId === conversationId ||
(isLatestConvo && currentConvoId === 'new' && activeConvos[0] && activeConvos[0] !== 'new');
@ -106,95 +108,77 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
return (
<div
className={cn(
'hover:bg-token-sidebar-surface-secondary group relative rounded-lg active:opacity-90',
'group relative mt-2 flex h-9 items-center rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700',
isActiveConvo ? 'bg-gray-200 dark:bg-gray-700' : '',
isSmallScreen ? 'h-12' : '',
)}
>
{renaming ? (
<div className="absolute inset-0 z-50 flex w-full items-center rounded-lg bg-gray-200 p-1.5 dark:bg-gray-700">
<div className="absolute inset-0 z-20 flex w-full items-center rounded-lg bg-gray-200 p-1.5 dark:bg-gray-700">
<input
ref={inputRef}
type="text"
className="w-full rounded border border-blue-500 bg-transparent p-0.5 text-sm leading-tight outline-none"
className="w-full rounded bg-transparent p-0.5 text-sm leading-tight outline-none"
value={titleInput}
onChange={(e) => setTitleInput(e.target.value)}
onBlur={onRename}
onKeyDown={handleKeyDown}
/>
<div className="flex gap-1">
<button onClick={cancelRename}>
<X className="transition-color h-4 w-4 duration-200 ease-in-out hover:opacity-70" />
</button>
<button onClick={onRename}>
<Check className="transition-color h-4 w-4 duration-200 ease-in-out hover:opacity-70" />
</button>
</div>
</div>
) : (
<HoverToggle
isActiveConvo={isActiveConvo}
<a
href={`/c/${conversationId}`}
data-testid="convo-item"
onClick={clickHandler}
className={cn(
'flex grow cursor-pointer items-center gap-2 overflow-hidden whitespace-nowrap break-all rounded-lg px-2 py-2',
isActiveConvo ? 'bg-gray-200 dark:bg-gray-700' : '',
)}
title={title}
>
<EndpointIcon
conversation={conversation}
endpointsConfig={endpointsConfig}
size={20}
context="menu-item"
/>
{!renaming && (
<div className="relative line-clamp-1 flex-1 grow overflow-hidden">{title}</div>
)}
{isActiveConvo ? (
<div
className={cn(
'absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l',
!renaming ? 'from-gray-200 from-40% to-transparent dark:from-gray-700' : '',
)}
/>
) : (
<div className="absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l from-gray-50 from-0% to-transparent group-hover:from-gray-200 group-hover:from-40% dark:from-gray-850 dark:group-hover:from-gray-700" />
)}
</a>
)}
<div
className={cn(
'mr-2',
isPopoverActive || isActiveConvo ? 'flex' : 'hidden group-hover:flex',
)}
>
<ConvoOptions
conversation={conversation}
retainView={retainView}
renameHandler={renameHandler}
isPopoverActive={isPopoverActive}
setIsPopoverActive={setIsPopoverActive}
>
<DropDownMenu>
{startupConfig && startupConfig.sharedLinksEnabled && (
<ShareButton
conversationId={conversationId}
title={title}
appendLabel={true}
className="mb-[3.5px]"
setPopoverActive={setIsPopoverActive}
/>
)}
<RenameButton
renaming={renaming}
onRename={onRename}
renameHandler={renameHandler}
appendLabel={true}
className="mb-[3.5px]"
/>
<DeleteButton
conversationId={conversationId}
retainView={retainView}
renaming={renaming}
title={title}
appendLabel={true}
className="group m-1.5 mt-[3.5px] flex w-full cursor-pointer items-center gap-2 rounded p-2.5 text-sm hover:bg-gray-200 focus-visible:bg-gray-200 focus-visible:outline-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-600 dark:focus-visible:bg-gray-600"
/>
</DropDownMenu>
<ArchiveButton
className="z-50 hover:text-black dark:hover:text-white"
conversationId={conversationId}
retainView={retainView}
shouldArchive={true}
icon={<ArchiveIcon className="hover:text-gray-400" />}
/>
</HoverToggle>
)}
<a
href={`/c/${conversationId}`}
data-testid="convo-item"
onClick={clickHandler}
className={cn(
isActiveConvo || isPopoverActive
? 'group relative mt-2 flex cursor-pointer items-center gap-2 break-all rounded-lg bg-gray-200 px-2 py-2 active:opacity-50 dark:bg-gray-700'
: 'group relative mt-2 flex grow cursor-pointer items-center gap-2 overflow-hidden whitespace-nowrap break-all rounded-lg px-2 py-2 hover:bg-gray-200 active:opacity-50 dark:hover:bg-gray-700',
!isActiveConvo && !renaming ? 'peer-hover:bg-gray-200 dark:peer-hover:bg-gray-800' : '',
)}
title={title}
>
<EndpointIcon
conversation={conversation}
endpointsConfig={endpointsConfig}
size={20}
context="menu-item"
isActiveConvo={isActiveConvo}
/>
{!renaming && (
<div className="relative line-clamp-1 max-h-5 flex-1 grow overflow-hidden">{title}</div>
)}
{isActiveConvo ? (
<div
className={cn(
'absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l',
!renaming ? 'from-gray-200 from-60% to-transparent dark:from-gray-700' : '',
)}
/>
) : (
<div className="absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l from-gray-50 from-0% to-transparent group-hover:from-gray-200 group-hover:from-60% dark:from-gray-850 dark:group-hover:from-gray-700" />
)}
</a>
</div>
</div>
);
}