🖼️ 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,8 +1,8 @@
import { useState, type FC } from 'react';
import { type FC } from 'react';
import { useRecoilValue } from 'recoil';
import { useLocation } from 'react-router-dom';
import { TConversation } from 'librechat-data-provider';
import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover';
import { Menu, MenuButton, MenuItems } from '@headlessui/react';
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
import { BookmarkContext } from '~/Providers/BookmarkContext';
import { useGetConversationTags } from '~/data-provider';
@ -25,8 +25,6 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags }: BookmarkNavProps)
const activeConvo = useRecoilValue(store.conversationByIndex(0));
const globalConvo = useRecoilValue(store.conversation) ?? ({} as TConversation);
const [open, setIsOpen] = useState(false);
let conversation: TConversation | null | undefined;
if (location.state?.from?.pathname.includes('/chat')) {
conversation = globalConvo;
@ -35,38 +33,29 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags }: BookmarkNavProps)
}
return (
<Root open={open} onOpenChange={setIsOpen}>
<Trigger asChild>
<button
className={cn(
'relative mt-1 flex h-10 w-full cursor-pointer items-center gap-1 rounded-lg border-border-light bg-transparent px-1 py-2 text-text-primary transition-colors duration-200 focus-within:bg-surface-hover hover:bg-surface-hover',
open ? 'bg-surface-hover' : '',
)}
id="show-bookmarks"
data-testid="show-bookmarks"
title={localize('com_ui_bookmarks')}
>
<div className="relative flex h-8 w-8 items-center justify-center rounded-full p-1 text-text-primary">
{tags.length > 0 ? (
<BookmarkFilledIcon className="h-5 w-5" />
) : (
<BookmarkIcon className="h-5 w-5" />
<Menu as="div" className="group relative">
{({ open }) => (
<>
<MenuButton
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' : '',
)}
</div>
<div className="grow overflow-hidden whitespace-nowrap text-left text-sm text-text-primary">
{tags.length > 0 ? tags.join(', ') : localize('com_ui_bookmarks')}
</div>
</button>
</Trigger>
<Portal>
<div className="fixed left-0 top-0 z-auto translate-x-[268px] translate-y-[50px]">
<Content
side="bottom"
align="start"
className="mt-2 max-h-96 min-w-[240px] overflow-y-auto rounded-lg border border-border-medium bg-surface-primary-alt text-text-primary shadow-lg lg:max-h-96"
data-testid="bookmark-menu"
>
<div className="relative flex h-8 w-8 items-center justify-center rounded-full p-1 text-text-primary">
{tags.length > 0 ? (
<BookmarkFilledIcon className="h-5 w-5" />
) : (
<BookmarkIcon className="h-5 w-5" />
)}
</div>
<div className="grow overflow-hidden whitespace-nowrap text-left text-sm text-text-primary">
{tags.length > 0 ? tags.join(', ') : localize('com_ui_bookmarks')}
</div>
</MenuButton>
<MenuItems className="absolute left-0 top-full z-[100] mt-1 w-full translate-y-0 overflow-hidden rounded-lg bg-header-primary p-1.5 shadow-lg outline-none">
{data && conversation && (
// Display bookmarks and highlight the selected tag
<BookmarkContext.Provider value={{ bookmarks: data.filter((tag) => tag.count > 0) }}>
<BookmarkNavItems
// Currently selected conversation
@ -78,10 +67,10 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags }: BookmarkNavProps)
/>
</BookmarkContext.Provider>
)}
</Content>
</div>
</Portal>
</Root>
</MenuItems>
</>
)}
</Menu>
);
};

View file

@ -43,9 +43,8 @@ const BookmarkNavItems: FC<{
return (
<div className="flex flex-col">
<BookmarkItem
ctx="nav"
tag={localize('com_ui_no_bookmarks')}
data-testid="bookmark-item-clear"
data-testid="bookmark-item-no-bookmarks"
handleSubmit={() => Promise.resolve()}
selected={false}
icon={'🤔'}
@ -57,18 +56,15 @@ const BookmarkNavItems: FC<{
return (
<div className="flex flex-col">
<BookmarkItems
ctx="nav"
tags={tags}
handleSubmit={handleSubmit}
highlightSelected={true}
header={
<BookmarkItem
ctx="nav"
tag="Clear all"
tag={localize('com_ui_clear_all')}
data-testid="bookmark-item-clear"
handleSubmit={clear}
selected={false}
icon={<CrossCircledIcon className="h-4 w-4" />}
icon={<CrossCircledIcon className="size-4" />}
/>
}
/>

View file

@ -1,9 +1,9 @@
import filenamify from 'filenamify';
import { useEffect, useState } from 'react';
import type { TConversation } from 'librechat-data-provider';
import { Dialog, DialogButton, Input, Label, Checkbox, Dropdown } from '~/components/ui';
import { OGDialog, Button, Input, Label, Checkbox, Dropdown } from '~/components/ui';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useLocalize, useExportConversation } from '~/hooks';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { cn, defaultTextProps } from '~/utils';
export default function ExportModal({
@ -62,8 +62,8 @@ export default function ExportModal({
});
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTemplate
<OGDialog open={open} onOpenChange={onOpenChange}>
<OGDialogTemplate
title={localize('com_nav_export_conversation')}
className="max-w-full sm:max-w-2xl"
main={
@ -164,16 +164,13 @@ export default function ExportModal({
}
buttons={
<>
<DialogButton
onClick={exportConversation}
className="dark:hover:gray-400 border-gray-700 bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600"
>
<Button onClick={exportConversation} variant="success">
{localize('com_endpoint_export')}
</DialogButton>
</Button>
</>
}
selection={undefined}
/>
</Dialog>
</OGDialog>
);
}

View file

@ -24,7 +24,13 @@ import NewChat from './NewChat';
import { cn } from '~/utils';
import store from '~/store';
const Nav = ({ navVisible, setNavVisible }) => {
const Nav = ({
navVisible,
setNavVisible,
}: {
navVisible: boolean;
setNavVisible: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
const localize = useLocalize();
const { conversationId } = useParams();
const { isAuthenticated } = useAuthContext();
@ -80,7 +86,9 @@ const Nav = ({ navVisible, setNavVisible }) => {
setShowLoading,
hasNextPage: searchQuery ? searchQueryRes?.hasNextPage : hasNextPage,
fetchNextPage: searchQuery ? searchQueryRes?.fetchNextPage : fetchNextPage,
isFetchingNextPage: searchQuery ? searchQueryRes?.isFetchingNextPage : isFetchingNextPage,
isFetchingNextPage: searchQuery
? searchQueryRes?.isFetchingNextPage ?? false
: isFetchingNextPage,
});
const conversations = useMemo(
@ -155,24 +163,37 @@ const Nav = ({ navVisible, setNavVisible }) => {
onMouseLeave={handleMouseLeave}
ref={containerRef}
>
<NewChat
toggleNav={itemToggleNav}
subHeaders={
<>
{isSearchEnabled && <SearchBar clearSearch={clearSearch} />}
<BookmarkNav tags={tags} setTags={setTags} />
</>
}
/>
{isSmallScreen == true ? (
<div className="pt-3.5">
{isSearchEnabled === true && (
<SearchBar clearSearch={clearSearch} isSmallScreen={isSmallScreen} />
)}
<BookmarkNav tags={tags} setTags={setTags} />
</div>
) : (
<NewChat
toggleNav={itemToggleNav}
subHeaders={
<>
{isSearchEnabled === true && (
<SearchBar
clearSearch={clearSearch}
isSmallScreen={isSmallScreen}
/>
)}
<BookmarkNav tags={tags} setTags={setTags} />
</>
}
/>
)}
<Conversations
conversations={conversations}
moveToTop={moveToTop}
toggleNav={itemToggleNav}
/>
{(isFetchingNextPage || showLoading) && (
<Spinner
className={cn('m-1 mx-auto mb-4 h-4 w-4 text-black dark:text-white')}
/>
<Spinner className={cn('m-1 mx-auto mb-4 h-4 w-4 text-text-primary')} />
)}
</div>
<NavLinks />

View file

@ -54,7 +54,7 @@ function NavLinks() {
<UserIcon />
</div>
) : (
<img className="rounded-full" src={user?.avatar || avatarSrc} alt="avatar" />
<img className="rounded-full" src={user.avatar || avatarSrc} alt="avatar" />
)}
</div>
</div>
@ -68,7 +68,7 @@ function NavLinks() {
<Transition
as={Fragment}
enter="transition ease-out duration-110 transform"
enter="transition ease-out duration-100 transform"
enterFrom="translate-y-2 opacity-0"
enterTo="translate-y-0 opacity-100"
leave="transition ease-in duration-100 transform"

View file

@ -82,14 +82,14 @@ export default function NewChat({
return (
<TooltipProvider delayDuration={250}>
<Tooltip>
<div className="sticky left-0 right-0 top-0 z-20 bg-gray-50 pt-3.5 dark:bg-gray-850">
<div className="sticky left-0 right-0 top-0 z-20 bg-surface-primary-alt pt-3.5">
<div className="pb-0.5 last:pb-0" style={{ transform: 'none' }}>
<a
href="/"
tabIndex={0}
data-testid="nav-new-chat"
onClick={clickHandler}
className="group flex h-10 items-center gap-2 rounded-lg px-2 font-medium hover:bg-gray-200 dark:hover:bg-gray-700"
className="group flex h-10 items-center gap-2 rounded-lg px-2 font-medium transition-colors duration-200 hover:bg-surface-hover"
aria-label={localize('com_ui_new_chat')}
>
<NewChatButtonIcon conversation={conversation} />
@ -104,7 +104,7 @@ export default function NewChat({
aria-label="nav-new-chat-btn"
className="text-token-text-primary"
>
<NewChatIcon className="h-[18px] w-[18px]" />
<NewChatIcon className="size-5" />
</button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={20}>

View file

@ -10,10 +10,11 @@ import store from '~/store';
type SearchBarProps = {
clearSearch: () => void;
isSmallScreen?: boolean;
};
const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) => {
const { clearSearch } = props;
const { clearSearch, isSmallScreen } = props;
const queryClient = useQueryClient();
const clearConvoState = store.useClearConvoState();
const setSearchQuery = useSetRecoilState(store.searchQuery);
@ -58,7 +59,10 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
return (
<div
ref={ref}
className="relative mt-1 flex h-10 cursor-pointer items-center gap-3 rounded-lg border-border-medium px-3 py-2 text-text-primary transition-colors duration-200 focus-within:bg-surface-hover hover:bg-surface-hover dark:focus-within:bg-surface-hover"
className={cn(
'relative mt-1 flex h-10 cursor-pointer items-center gap-3 rounded-lg border-border-medium px-3 py-2 text-text-primary transition-colors duration-200 focus-within:bg-surface-hover hover:bg-surface-hover dark:focus-within:bg-surface-hover',
isSmallScreen === true ? 'h-16 rounded-2xl' : '',
)}
>
{<Search className="absolute left-3 h-4 w-4" />}
<input
@ -77,8 +81,9 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
/>
<X
className={cn(
'absolute right-[7px] h-5 w-5 cursor-pointer transition-opacity duration-1000',
'absolute right-[7px] h-5 w-5 cursor-pointer transition-opacity duration-200',
showClearIcon ? 'opacity-100' : 'opacity-0',
isSmallScreen === true ? 'right-[16px]' : '',
)}
onClick={clearText}
/>

View file

@ -94,7 +94,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<Tabs.Trigger
tabIndex={0}
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
@ -108,7 +108,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<Tabs.Trigger
tabIndex={0}
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
@ -122,7 +122,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<Tabs.Trigger
tabIndex={0}
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
@ -136,7 +136,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<Tabs.Trigger
tabIndex={0}
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
@ -150,7 +150,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<Tabs.Trigger
tabIndex={0}
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
@ -164,7 +164,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<Tabs.Trigger
tabIndex={0}
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
@ -178,7 +178,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<Tabs.Trigger
tabIndex={0}
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',

View file

@ -1,6 +1,6 @@
import { useLocalize } from '~/hooks';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { Dialog, DialogTrigger } from '~/components/ui';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { OGDialog, OGDialogTrigger } from '~/components/ui';
import ArchivedChatsTable from './ArchivedChatsTable';
@ -10,19 +10,19 @@ export default function ArchivedChats() {
return (
<div className="flex items-center justify-between">
<div>{localize('com_nav_archived_chats')}</div>
<Dialog>
<DialogTrigger asChild>
<OGDialog>
<OGDialogTrigger asChild>
<button className="btn btn-neutral relative ">
{localize('com_nav_archived_chats_manage')}
</button>
</DialogTrigger>
<DialogTemplate
</OGDialogTrigger>
<OGDialogTemplate
title={localize('com_nav_archived_chats')}
className="max-w-[1000px]"
showCancelButton={false}
main={<ArchivedChatsTable />}
/>
</Dialog>
</OGDialog>
</div>
);
}

View file

@ -2,8 +2,8 @@ import { useMemo, useState } from 'react';
import { MessageCircle, ArchiveRestore } from 'lucide-react';
import { useConversationsInfiniteQuery } from '~/data-provider';
import { useAuthContext, useLocalize, useNavScrolling } from '~/hooks';
import ArchiveButton from '~/components/Conversations/ArchiveButton';
import DeleteButton from '~/components/Conversations/DeleteButton';
import ArchiveButton from '~/components/Conversations/ConvoOptions/ArchiveButton';
import DeleteButton from '~/components/Conversations/ConvoOptions/DeleteButton';
import { Spinner } from '~/components/svg';
import { cn } from '~/utils';
import { ConversationListResponse } from 'librechat-data-provider';
@ -90,10 +90,7 @@ export default function ArchivedChatsTable({ className }: { className?: string }
<DeleteButton
conversationId={conversation.conversationId}
retainView={moveToTop}
renaming={false}
title={conversation.title}
appendLabel={false}
className="group ml-4 flex w-full cursor-pointer items-center items-center gap-2 rounded 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"
title={conversation.title ?? ''}
/>
</div>
</>