mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-15 15:08:10 +01:00
🖼️ 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:
parent
7f50d2f7c0
commit
96581d56df
62 changed files with 2627 additions and 1821 deletions
|
|
@ -1,106 +1,128 @@
|
|||
import { useState, type FC } from 'react';
|
||||
import { useState, type FC, useCallback } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Constants } 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 { useConversationTagsQuery, useTagConversationMutation } from '~/data-provider';
|
||||
import { BookmarkMenuItems } from './Bookmarks/BookmarkMenuItems';
|
||||
import { BookmarkContext } from '~/Providers/BookmarkContext';
|
||||
import { useLocalize, useBookmarkSuccess } from '~/hooks';
|
||||
import { BookmarkEditDialog } from '~/components/Bookmarks';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { useBookmarkSuccess } from '~/hooks';
|
||||
import { Spinner } from '~/components';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
const BookmarkMenu: FC = () => {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
|
||||
const conversation = useRecoilValue(store.conversationByIndex(0));
|
||||
const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined;
|
||||
const conversationId = conversation?.conversationId ?? '';
|
||||
const onSuccess = useBookmarkSuccess(conversationId);
|
||||
const [tags, setTags] = useState<string[]>(conversation?.tags || []);
|
||||
|
||||
const [open, setIsOpen] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { mutateAsync, isLoading } = useTagConversationMutation(conversationId);
|
||||
|
||||
const { data } = useConversationTagsQuery();
|
||||
|
||||
const isActiveConvo =
|
||||
const isActiveConvo = Boolean(
|
||||
conversation &&
|
||||
conversationId &&
|
||||
conversationId !== Constants.NEW_CONVO &&
|
||||
conversationId !== 'search';
|
||||
conversationId &&
|
||||
conversationId !== Constants.NEW_CONVO &&
|
||||
conversationId !== 'search',
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (tag?: string): Promise<void> => {
|
||||
if (tag === undefined || tag === '' || !conversationId) {
|
||||
showToast({
|
||||
message: 'Invalid tag or conversationId',
|
||||
severity: NotificationSeverity.ERROR,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newTags = tags.includes(tag) ? tags.filter((t) => t !== tag) : [...tags, tag];
|
||||
await mutateAsync(
|
||||
{
|
||||
tags: newTags,
|
||||
},
|
||||
{
|
||||
onSuccess: (newTags: string[]) => {
|
||||
setTags(newTags);
|
||||
onSuccess(newTags);
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: 'Error adding bookmark',
|
||||
severity: NotificationSeverity.ERROR,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
[tags, conversationId, mutateAsync, setTags, onSuccess, showToast],
|
||||
);
|
||||
|
||||
if (!isActiveConvo) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const onOpenChange = async (open: boolean) => {
|
||||
if (!open) {
|
||||
setIsOpen(open);
|
||||
return;
|
||||
}
|
||||
if (open && tags && tags.length > 0) {
|
||||
setIsOpen(open);
|
||||
} else {
|
||||
if (conversation && conversationId) {
|
||||
await mutateAsync(
|
||||
{
|
||||
tags: [Constants.SAVED_TAG as 'Saved'],
|
||||
},
|
||||
{
|
||||
onSuccess: (newTags: string[]) => {
|
||||
setTags(newTags);
|
||||
onSuccess(newTags);
|
||||
},
|
||||
onError: () => {
|
||||
console.error('Error adding bookmark');
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderButtonContent = () => {
|
||||
if (isLoading) {
|
||||
return <Spinner />;
|
||||
return <Spinner aria-label="Spinner" />;
|
||||
}
|
||||
if (tags && tags.length > 0) {
|
||||
return <BookmarkFilledIcon className="icon-sm" />;
|
||||
if (tags.length > 0) {
|
||||
return <BookmarkFilledIcon className="icon-sm" aria-label="Filled Bookmark" />;
|
||||
}
|
||||
return <BookmarkIcon className="icon-sm" />;
|
||||
return <BookmarkIcon className="icon-sm" aria-label="Bookmark" />;
|
||||
};
|
||||
|
||||
const handleToggleOpen = () => {
|
||||
setOpen(!open);
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
return (
|
||||
<Root open={open} onOpenChange={onOpenChange}>
|
||||
<Trigger asChild>
|
||||
<button
|
||||
id="header-bookmarks-menu"
|
||||
className={cn(
|
||||
'pointer-cursor relative flex flex-col rounded-md border border-border-light bg-transparent text-left focus:outline-none focus:ring-0 sm:text-sm',
|
||||
'hover:bg-header-button-hover radix-state-open:bg-header-button-hover',
|
||||
'z-50 flex h-[40px] min-w-4 flex-none items-center justify-center px-3 focus:outline-offset-2 focus:ring-0 focus-visible:ring-2 focus-visible:ring-ring-primary ',
|
||||
)}
|
||||
title={localize('com_ui_bookmarks')}
|
||||
>
|
||||
{renderButtonContent()}
|
||||
</button>
|
||||
</Trigger>
|
||||
<Portal>
|
||||
<Content
|
||||
className="mt-2 grid max-h-[500px] w-full min-w-[240px] overflow-y-auto rounded-lg border border-border-medium bg-header-primary text-text-primary shadow-lg"
|
||||
side="bottom"
|
||||
align="start"
|
||||
>
|
||||
{data && conversation && (
|
||||
<BookmarkContext.Provider value={{ bookmarks: data }}>
|
||||
<BookmarkMenuItems conversation={conversation} tags={tags ?? []} setTags={setTags} />
|
||||
</BookmarkContext.Provider>
|
||||
)}
|
||||
</Content>
|
||||
</Portal>
|
||||
</Root>
|
||||
<>
|
||||
<Menu as="div" className="group relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<MenuButton
|
||||
aria-label="Add bookmarks"
|
||||
className={cn(
|
||||
'mt-text-sm flex size-10 items-center justify-center gap-2 rounded-lg border border-border-light text-sm transition-colors duration-200 hover:bg-surface-hover',
|
||||
open ? 'bg-surface-hover' : '',
|
||||
)}
|
||||
data-testid="bookmark-menu"
|
||||
>
|
||||
{renderButtonContent()}
|
||||
</MenuButton>
|
||||
<MenuItems
|
||||
anchor="bottom start"
|
||||
className="overflow-hidden rounded-lg bg-header-primary p-1.5 shadow-lg outline-none"
|
||||
>
|
||||
<BookmarkContext.Provider value={{ bookmarks: data || [] }}>
|
||||
<BookmarkMenuItems
|
||||
handleToggleOpen={handleToggleOpen}
|
||||
tags={tags}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
</BookmarkContext.Provider>
|
||||
</MenuItems>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
<BookmarkEditDialog
|
||||
conversation={conversation}
|
||||
tags={tags}
|
||||
setTags={setTags}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue