mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-23 10:46:12 +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
|
|
@ -3,6 +3,7 @@ import { isAssistantsEndpoint } from 'librechat-data-provider';
|
|||
import type { TConversation } from 'librechat-data-provider';
|
||||
import { useChatContext, useAddedChatContext } from '~/Providers';
|
||||
import { mainTextareaId } from '~/common';
|
||||
import { Button } from '~/components/ui';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
function AddMultiConvo({ className = '' }: { className?: string }) {
|
||||
|
|
@ -32,16 +33,15 @@ function AddMultiConvo({ className = '' }: { className?: string }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
id="add-multi-conversation-button"
|
||||
aria-label="Add multi-conversation"
|
||||
onClick={clickHandler}
|
||||
className={cn(
|
||||
'group m-1.5 flex w-fit cursor-pointer items-center rounded text-sm hover:bg-border-medium focus-visible:bg-border-medium focus-visible:outline-offset-2',
|
||||
className,
|
||||
)}
|
||||
variant="outline"
|
||||
className={cn('h-10 w-10 p-0 transition-all duration-300 ease-in-out', className)}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,64 +1,95 @@
|
|||
import { useState } from 'react';
|
||||
import { Upload } from 'lucide-react';
|
||||
import { Upload, Share2 } from 'lucide-react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import DropDownMenu from '~/components/Conversations/DropDownMenu';
|
||||
import ShareButton from '~/components/Conversations/ShareButton';
|
||||
import HoverToggle from '~/components/Conversations/HoverToggle';
|
||||
import { ShareButton } from '~/components/Conversations/ConvoOptions';
|
||||
import { Button, DropdownPopup } from '~/components/ui';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import ExportButton from './ExportButton';
|
||||
import { ExportModal } from '../Nav';
|
||||
import store from '~/store';
|
||||
|
||||
export default function ExportAndShareMenu({
|
||||
isSharedButtonEnabled,
|
||||
className = '',
|
||||
}: {
|
||||
isSharedButtonEnabled: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
|
||||
const conversation = useRecoilValue(store.conversationByIndex(0));
|
||||
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
||||
const [showExports, setShowExports] = useState(false);
|
||||
const [showShareDialog, setShowShareDialog] = useState(false);
|
||||
|
||||
const exportable =
|
||||
conversation &&
|
||||
conversation.conversationId &&
|
||||
conversation.conversationId != null &&
|
||||
conversation.conversationId !== 'new' &&
|
||||
conversation.conversationId !== 'search';
|
||||
|
||||
if (!exportable) {
|
||||
if (exportable === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isActiveConvo = exportable;
|
||||
const onOpenChange = (value: boolean) => {
|
||||
setShowExports(value);
|
||||
};
|
||||
|
||||
const shareHandler = () => {
|
||||
setIsPopoverActive(false);
|
||||
setShowShareDialog(true);
|
||||
};
|
||||
|
||||
const exportHandler = () => {
|
||||
setIsPopoverActive(false);
|
||||
setShowExports(true);
|
||||
};
|
||||
|
||||
const dropdownItems = [
|
||||
{
|
||||
label: localize('com_endpoint_export'),
|
||||
onClick: exportHandler,
|
||||
icon: <Upload className="icon-md mr-2 dark:text-gray-300" />,
|
||||
},
|
||||
{
|
||||
label: localize('com_ui_share'),
|
||||
onClick: shareHandler,
|
||||
icon: <Share2 className="icon-md mr-2 dark:text-gray-300" />,
|
||||
show: isSharedButtonEnabled,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<HoverToggle
|
||||
isActiveConvo={!!isActiveConvo}
|
||||
isPopoverActive={isPopoverActive}
|
||||
setIsPopoverActive={setIsPopoverActive}
|
||||
className={className}
|
||||
>
|
||||
<DropDownMenu
|
||||
icon={<Upload />}
|
||||
tooltip={localize('com_endpoint_export_share')}
|
||||
className="pointer-cursor relative z-50 flex h-[40px] min-w-4 flex-none flex-col items-center justify-center rounded-md border border-gray-100 bg-white px-3 text-left hover:bg-gray-50 focus:outline-none focus:ring-0 focus:ring-offset-0 radix-state-open:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700 sm:text-sm"
|
||||
>
|
||||
{conversation && conversation.conversationId && (
|
||||
<>
|
||||
<ExportButton conversation={conversation} setPopoverActive={setIsPopoverActive} />
|
||||
{isSharedButtonEnabled && (
|
||||
<ShareButton
|
||||
conversationId={conversation.conversationId}
|
||||
title={conversation.title ?? ''}
|
||||
appendLabel={true}
|
||||
className="mb-[3.5px]"
|
||||
setPopoverActive={setIsPopoverActive}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropDownMenu>
|
||||
</HoverToggle>
|
||||
<>
|
||||
<DropdownPopup
|
||||
isOpen={isPopoverActive}
|
||||
setIsOpen={setIsPopoverActive}
|
||||
trigger={
|
||||
<Button
|
||||
id="export-menu-button"
|
||||
aria-label="Export options"
|
||||
variant="outline"
|
||||
className="mr-4 h-10 w-10 p-0 transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<Upload className="icon-md dark:text-gray-300" aria-hidden="true" focusable="false" />
|
||||
</Button>
|
||||
}
|
||||
items={dropdownItems}
|
||||
anchor="bottom end"
|
||||
/>
|
||||
{showShareDialog && conversation.conversationId != null && (
|
||||
<ShareButton
|
||||
conversationId={conversation.conversationId}
|
||||
title={conversation.title ?? ''}
|
||||
showShareDialog={showShareDialog}
|
||||
setShowShareDialog={setShowShareDialog}
|
||||
/>
|
||||
)}
|
||||
{showExports && (
|
||||
<ExportModal
|
||||
open={showExports}
|
||||
onOpenChange={onOpenChange}
|
||||
conversation={conversation}
|
||||
aria-label="Export conversation modal"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { useState } from 'react';
|
||||
import type { TConversation } from 'librechat-data-provider';
|
||||
import { Upload } from 'lucide-react';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { ExportModal } from '../Nav';
|
||||
|
||||
function ExportButton({
|
||||
conversation,
|
||||
setPopoverActive,
|
||||
}: {
|
||||
conversation: TConversation;
|
||||
setPopoverActive: (value: boolean) => void;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
|
||||
const [showExports, setShowExports] = useState(false);
|
||||
|
||||
const clickHandler = () => {
|
||||
setShowExports(true);
|
||||
};
|
||||
|
||||
const onOpenChange = (value: boolean) => {
|
||||
setShowExports(value);
|
||||
setPopoverActive(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={clickHandler}
|
||||
className="group m-1.5 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"
|
||||
>
|
||||
<Upload size={16} /> {localize('com_nav_export')}
|
||||
</button>
|
||||
{showExports && (
|
||||
<ExportModal open={showExports} onOpenChange={onOpenChange} conversation={conversation} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExportButton;
|
||||
|
|
@ -12,31 +12,31 @@ export default function Footer({ className }: { className?: string }) {
|
|||
const privacyPolicy = config?.interface?.privacyPolicy;
|
||||
const termsOfService = config?.interface?.termsOfService;
|
||||
|
||||
const privacyPolicyRender = privacyPolicy?.externalUrl && (
|
||||
const privacyPolicyRender = privacyPolicy?.externalUrl != null && (
|
||||
<a
|
||||
className=" text-gray-600 underline dark:text-gray-300"
|
||||
className="text-text-secondary underline"
|
||||
href={privacyPolicy.externalUrl}
|
||||
target={privacyPolicy.openNewTab ? '_blank' : undefined}
|
||||
target={privacyPolicy.openNewTab === true ? '_blank' : undefined}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{localize('com_ui_privacy_policy')}
|
||||
</a>
|
||||
);
|
||||
|
||||
const termsOfServiceRender = termsOfService?.externalUrl && (
|
||||
const termsOfServiceRender = termsOfService?.externalUrl != null && (
|
||||
<a
|
||||
className=" text-gray-600 underline dark:text-gray-300"
|
||||
className="text-text-secondary underline"
|
||||
href={termsOfService.externalUrl}
|
||||
target={termsOfService.openNewTab ? '_blank' : undefined}
|
||||
target={termsOfService.openNewTab === true ? '_blank' : undefined}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{localize('com_ui_terms_of_service')}
|
||||
</a>
|
||||
);
|
||||
|
||||
if (config?.analyticsGtmId) {
|
||||
if (config?.analyticsGtmId != null) {
|
||||
const tagManagerArgs = {
|
||||
gtmId: config?.analyticsGtmId,
|
||||
gtmId: config.analyticsGtmId,
|
||||
};
|
||||
TagManager.initialize(tagManagerArgs);
|
||||
}
|
||||
|
|
@ -54,19 +54,22 @@ export default function Footer({ className }: { className?: string }) {
|
|||
<React.Fragment key={`main-content-part-${index}`}>
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
a: (props) => {
|
||||
const { ['node']: _, href, ...otherProps } = props;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
a: ({ node: _n, href, children, ...otherProps }) => {
|
||||
return (
|
||||
<a
|
||||
className=" text-gray-600 underline dark:text-gray-300"
|
||||
className="text-text-secondary underline"
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
{...otherProps}
|
||||
/>
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
p: ({ node, ...props }) => <span {...props} />,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
p: ({ node: _n, ...props }) => <span {...props} />,
|
||||
}}
|
||||
>
|
||||
{text.trim()}
|
||||
|
|
@ -81,8 +84,8 @@ export default function Footer({ className }: { className?: string }) {
|
|||
return (
|
||||
<div
|
||||
className={
|
||||
className ||
|
||||
'relative flex items-center justify-center gap-2 px-2 py-2 text-center text-xs text-gray-600 dark:text-gray-300 md:px-[60px]'
|
||||
className ??
|
||||
'relative flex items-center justify-center gap-2 px-2 py-2 text-center text-xs text-text-primary md:px-[60px]'
|
||||
}
|
||||
role="contentinfo"
|
||||
>
|
||||
|
|
@ -92,7 +95,7 @@ export default function Footer({ className }: { className?: string }) {
|
|||
<React.Fragment key={`footer-element-${index}`}>
|
||||
{contentRender}
|
||||
{!isLastElement && (
|
||||
<div key={`separator-${index}`} className="h-2 border-r-[1px] border-gray-300" />
|
||||
<div key={`separator-${index}`} className="h-2 border-r-[1px] border-border-medium" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -29,17 +29,16 @@ export default function Header() {
|
|||
<div className="flex items-center gap-2">
|
||||
{!navVisible && <HeaderNewChat />}
|
||||
{interfaceConfig.endpointsMenu && <EndpointsMenu />}
|
||||
{modelSpecs?.length > 0 && <ModelSpecsMenu modelSpecs={modelSpecs} />}
|
||||
{modelSpecs.length > 0 && <ModelSpecsMenu modelSpecs={modelSpecs} />}
|
||||
{<HeaderOptions interfaceConfig={interfaceConfig} />}
|
||||
{interfaceConfig.presets && <PresetsMenu />}
|
||||
<BookmarkMenu />
|
||||
<AddMultiConvo />
|
||||
{isSmallScreen && (
|
||||
<ExportAndShareMenu
|
||||
isSharedButtonEnabled={startupConfig?.sharedLinksEnabled ?? false}
|
||||
className="pl-0"
|
||||
/>
|
||||
)}
|
||||
<BookmarkMenu />
|
||||
<AddMultiConvo />
|
||||
</div>
|
||||
{!isSmallScreen && (
|
||||
<ExportAndShareMenu isSharedButtonEnabled={startupConfig?.sharedLinksEnabled ?? false} />
|
||||
|
|
|
|||
|
|
@ -33,12 +33,12 @@ export default function OptionsPopover({
|
|||
(_target) => {
|
||||
const target = _target as Element;
|
||||
if (
|
||||
target?.id === 'presets-button' ||
|
||||
(target?.parentNode instanceof Element && target.parentNode.id === 'presets-button')
|
||||
target.id === 'presets-button' ||
|
||||
(target.parentNode instanceof Element && target.parentNode.id === 'presets-button')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const tagName = target?.tagName;
|
||||
const tagName = target.tagName;
|
||||
return tagName === 'path' || tagName === 'svg' || tagName === 'circle';
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,78 +1,34 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
import { BookmarkPlusIcon } from 'lucide-react';
|
||||
import type { FC } from 'react';
|
||||
import type { TConversation } from 'librechat-data-provider';
|
||||
import { BookmarkItems, BookmarkEditDialog } from '~/components/Bookmarks';
|
||||
import { useTagConversationMutation } from '~/data-provider';
|
||||
import { useLocalize, useBookmarkSuccess } from '~/hooks';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { BookmarkItems, BookmarkItem } from '~/components/Bookmarks';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export const BookmarkMenuItems: FC<{
|
||||
conversation: TConversation;
|
||||
tags: string[];
|
||||
setTags: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
}> = ({ conversation, tags, setTags }) => {
|
||||
const { showToast } = useToastContext();
|
||||
handleToggleOpen?: () => Promise<void>;
|
||||
handleSubmit: (tag?: string) => Promise<void>;
|
||||
}> = ({
|
||||
tags,
|
||||
handleSubmit,
|
||||
handleToggleOpen = async () => {
|
||||
('');
|
||||
},
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
|
||||
const conversationId = conversation?.conversationId ?? '';
|
||||
const onSuccess = useBookmarkSuccess(conversationId);
|
||||
|
||||
const { mutateAsync } = useTagConversationMutation(conversationId);
|
||||
const handleSubmit = useCallback(
|
||||
async (tag: string): Promise<void> => {
|
||||
if (tags !== undefined && conversationId) {
|
||||
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],
|
||||
);
|
||||
|
||||
return (
|
||||
<BookmarkItems
|
||||
ctx="header"
|
||||
tags={tags}
|
||||
handleSubmit={handleSubmit}
|
||||
header={
|
||||
<div>
|
||||
<BookmarkEditDialog
|
||||
conversation={conversation}
|
||||
tags={tags}
|
||||
setTags={setTags}
|
||||
trigger={
|
||||
<div
|
||||
role="menuitem"
|
||||
className="group m-1.5 flex cursor-pointer gap-2 rounded px-2 !pr-3.5 pb-2.5 pt-3 text-sm !opacity-100 hover:bg-header-hover focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div className="flex grow items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<BookmarkPlusIcon className="size-4" />
|
||||
<div className="break-all">{localize('com_ui_bookmarks_new')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<BookmarkItem
|
||||
tag={localize('com_ui_bookmarks_new')}
|
||||
data-testid="bookmark-item-new"
|
||||
handleSubmit={handleToggleOpen}
|
||||
selected={false}
|
||||
icon={<BookmarkPlusIcon className="size-4" aria-label="Add Bookmark" />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const MenuItem: FC<MenuItemProps> = ({
|
|||
const expiryTime = getExpiry();
|
||||
|
||||
const clickHandler = () => {
|
||||
if (!expiryTime) {
|
||||
if (expiryTime == null) {
|
||||
setDialogOpen(true);
|
||||
}
|
||||
if (onClick) {
|
||||
|
|
@ -60,6 +60,12 @@ const MenuItem: FC<MenuItemProps> = ({
|
|||
{...rest}
|
||||
onClick={clickHandler}
|
||||
aria-label={title}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
clickHandler();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex grow items-center justify-between gap-2">
|
||||
<div>
|
||||
|
|
@ -78,7 +84,7 @@ const MenuItem: FC<MenuItemProps> = ({
|
|||
className={cn(
|
||||
'invisible flex gap-x-1 group-hover:visible',
|
||||
selected ? 'visible' : '',
|
||||
expiryTime
|
||||
expiryTime != null
|
||||
? 'w-full rounded-lg p-2 hover:bg-gray-200 dark:hover:bg-gray-900'
|
||||
: '',
|
||||
)}
|
||||
|
|
@ -88,10 +94,15 @@ const MenuItem: FC<MenuItemProps> = ({
|
|||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<div className={cn('invisible group-hover:visible', expiryTime ? 'text-xs' : '')}>
|
||||
<div
|
||||
className={cn(
|
||||
'invisible group-hover:visible',
|
||||
expiryTime != null ? 'text-xs' : '',
|
||||
)}
|
||||
>
|
||||
{localize('com_endpoint_config_key')}
|
||||
</div>
|
||||
<Settings className={cn(expiryTime ? 'icon-sm' : 'icon-md stroke-1')} />
|
||||
<Settings className={cn(expiryTime != null ? 'icon-sm' : 'icon-md stroke-1')} />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ const PresetItems: FC<{
|
|||
<div className="flex h-full grow items-center justify-end gap-2">
|
||||
<label
|
||||
htmlFor="default-preset"
|
||||
className="w-40 truncate rounded bg-transparent py-1 text-xs font-medium font-normal text-gray-600 transition-colors dark:bg-transparent dark:text-gray-300 sm:w-72"
|
||||
className="w-40 truncate rounded bg-transparent py-1 text-xs font-medium text-gray-600 transition-colors dark:bg-transparent dark:text-gray-300 sm:w-72"
|
||||
>
|
||||
{defaultPreset
|
||||
? `${localize('com_endpoint_preset_default_item')} ${defaultPreset.title}`
|
||||
|
|
@ -55,7 +55,7 @@ const PresetItems: FC<{
|
|||
<DialogTrigger asChild>
|
||||
<label
|
||||
htmlFor="file-upload"
|
||||
className="mr-1 flex h-[32px] cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium font-normal text-gray-600 transition-colors hover:bg-gray-100 hover:text-red-700 dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-green-500 dark:hover:text-red-700"
|
||||
className="mr-1 flex h-[32px] cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-red-700 dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-red-700"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
|
|
@ -159,7 +159,7 @@ const PresetItems: FC<{
|
|||
<PinIcon unpin={defaultPreset?.presetId === preset.presetId} />
|
||||
</button>
|
||||
<button
|
||||
className="m-0 h-full rounded-md p-2 text-gray-400 hover:text-gray-700 dark:bg-gray-600 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:group-hover:visible"
|
||||
className="m-0 h-full rounded-md p-2 text-gray-400 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:group-hover:visible"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
@ -169,7 +169,7 @@ const PresetItems: FC<{
|
|||
<EditIcon />
|
||||
</button>
|
||||
<button
|
||||
className="m-0 h-full rounded-md p-2 text-gray-400 hover:text-gray-600 dark:bg-gray-600 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:group-hover:visible"
|
||||
className="m-0 h-full rounded-md p-2 text-gray-400 hover:text-gray-600 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:group-hover:visible"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue