import { useState, useId, useRef, memo, useCallback, useMemo } from 'react'; import * as Menu from '@ariakit/react/menu'; import { useParams, useNavigate } from 'react-router-dom'; import { DropdownPopup, Spinner, useToastContext } from '@librechat/client'; import { Ellipsis, Share2, CopyPlus, Archive, Pen, Trash } from 'lucide-react'; import type { MouseEvent } from 'react'; import { useDuplicateConversationMutation, useGetStartupConfig, useArchiveConvoMutation, } from '~/data-provider'; import { useLocalize, useNavigateToConvo, useNewConvo } from '~/hooks'; import { NotificationSeverity } from '~/common'; import { useChatContext } from '~/Providers'; import DeleteButton from './DeleteButton'; import ShareButton from './ShareButton'; import { cn } from '~/utils'; function ConvoOptions({ conversationId, title, retainView, renameHandler, isPopoverActive, setIsPopoverActive, isActiveConvo, }: { conversationId: string | null; title: string | null; retainView: () => void; renameHandler: (e: MouseEvent) => void; isPopoverActive: boolean; setIsPopoverActive: React.Dispatch>; isActiveConvo: boolean; }) { const localize = useLocalize(); const { index } = useChatContext(); const { data: startupConfig } = useGetStartupConfig(); const { navigateToConvo } = useNavigateToConvo(index); const { showToast } = useToastContext(); const navigate = useNavigate(); const { conversationId: currentConvoId } = useParams(); const { newConversation } = useNewConvo(); const shareButtonRef = useRef(null); const deleteButtonRef = useRef(null); const [showShareDialog, setShowShareDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [announcement, setAnnouncement] = useState(''); const archiveConvoMutation = useArchiveConvoMutation(); const duplicateConversation = useDuplicateConversationMutation({ onSuccess: (data) => { navigateToConvo(data.conversation); showToast({ message: localize('com_ui_duplication_success'), status: 'success', }); setIsPopoverActive(false); }, onMutate: () => { showToast({ message: localize('com_ui_duplication_processing'), status: 'info', }); }, onError: () => { showToast({ message: localize('com_ui_duplication_error'), status: 'error', }); }, }); const isDuplicateLoading = duplicateConversation.isLoading; const isArchiveLoading = archiveConvoMutation.isLoading; const handleShareClick = useCallback(() => { setShowShareDialog(true); }, []); const handleDeleteClick = useCallback(() => { setShowDeleteDialog(true); }, []); const handleArchiveClick = useCallback(async () => { const convoId = conversationId ?? ''; if (!convoId) { return; } archiveConvoMutation.mutate( { conversationId: convoId, isArchived: true }, { onSuccess: () => { setAnnouncement(localize('com_ui_convo_archived')); setTimeout(() => { setAnnouncement(''); }, 10000); if (currentConvoId === convoId || currentConvoId === 'new') { newConversation(); navigate('/c/new', { replace: true }); } retainView(); setIsPopoverActive(false); }, onError: () => { showToast({ message: localize('com_ui_archive_error'), severity: NotificationSeverity.ERROR, showIcon: true, }); }, }, ); }, [ conversationId, currentConvoId, archiveConvoMutation, navigate, newConversation, retainView, setIsPopoverActive, showToast, localize, ]); const handleDuplicateClick = useCallback(() => { duplicateConversation.mutate({ conversationId: conversationId ?? '', }); }, [conversationId, duplicateConversation]); const dropdownItems = useMemo( () => [ { label: localize('com_ui_share'), onClick: handleShareClick, icon: , show: startupConfig && startupConfig.sharedLinksEnabled, hideOnClick: false, ref: shareButtonRef, ariaHasPopup: 'dialog' as const, ariaControls: 'share-conversation-dialog', }, { label: localize('com_ui_rename'), onClick: renameHandler, icon: , }, { label: localize('com_ui_duplicate'), onClick: handleDuplicateClick, hideOnClick: false, icon: isDuplicateLoading ? ( ) : ( ), }, { label: localize('com_ui_archive'), onClick: handleArchiveClick, hideOnClick: false, icon: isArchiveLoading ? ( ) : ( ), }, { label: localize('com_ui_delete'), onClick: handleDeleteClick, icon: , hideOnClick: false, ref: deleteButtonRef, ariaHasPopup: 'dialog' as const, ariaControls: 'delete-conversation-dialog', }, ], [ localize, handleShareClick, startupConfig, renameHandler, handleDuplicateClick, isDuplicateLoading, handleArchiveClick, isArchiveLoading, handleDeleteClick, ], ); const menuId = useId(); return ( <> {announcement} ) => { e.stopPropagation(); }} onKeyDown={(e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); } }} > } items={dropdownItems} menuId={menuId} className="z-30" /> {showShareDialog && ( )} {showDeleteDialog && ( )} ); } export default memo(ConvoOptions, (prevProps, nextProps) => { return ( prevProps.conversationId === nextProps.conversationId && prevProps.title === nextProps.title && prevProps.isPopoverActive === nextProps.isPopoverActive && prevProps.isActiveConvo === nextProps.isActiveConvo ); });