feat: Implement Conversation Duplication & UI Improvements (#5036)

* feat(ui): enhance conversation components and add duplication

- feat: add conversation duplication functionality
- fix: resolve OGDialogTemplate display issues
- style: improve mobile dropdown component design
- chore: standardize shared link title formatting

* style: update active item background color in select-item

* feat(conversation): add duplicate conversation functionality and UI integration

* feat(conversation): enable title renaming on double-click and improve input focus styles

* fix(conversation): remove "(Copy)" suffix from duplicated conversation title in logging

* fix(RevokeKeysButton): correct className duration property for smoother transitions

* refactor(conversation): ensure proper parent-child relationships and timestamps when message cloning

---------

Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>
This commit is contained in:
Danny Avila 2024-12-18 11:10:34 -05:00 committed by GitHub
parent 649c7a6032
commit e8bde332c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 717 additions and 85 deletions

View file

@ -53,6 +53,7 @@ export default function Conversation({
}
event.preventDefault();
if (currentConvoId === conversationId || isPopoverActive) {
return;
}
@ -155,7 +156,7 @@ export default function Conversation({
<input
ref={inputRef}
type="text"
className="w-full rounded bg-transparent p-0.5 text-sm leading-tight outline-none"
className="w-full rounded bg-transparent p-0.5 text-sm leading-tight focus-visible:outline-none"
value={titleInput ?? ''}
onChange={(e) => setTitleInput(e.target.value)}
onKeyDown={handleKeyDown}
@ -199,7 +200,17 @@ export default function Conversation({
size={20}
context="menu-item"
/>
<div className="relative line-clamp-1 flex-1 grow overflow-hidden">{title}</div>
<div
className="relative line-clamp-1 flex-1 grow overflow-hidden"
onDoubleClick={(e) => {
e.preventDefault();
e.stopPropagation();
setTitleInput(title);
setRenaming(true);
}}
>
{title}
</div>
{isActiveConvo ? (
<div className="absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l" />
) : (
@ -215,16 +226,17 @@ export default function Conversation({
: 'hidden group-focus-within:flex group-hover:flex',
)}
>
<ConvoOptions
title={title}
renaming={renaming}
retainView={retainView}
renameHandler={renameHandler}
isActiveConvo={isActiveConvo}
conversationId={conversationId}
isPopoverActive={isPopoverActive}
setIsPopoverActive={setIsPopoverActive}
/>
{!renaming && (
<ConvoOptions
title={title}
retainView={retainView}
renameHandler={renameHandler}
isActiveConvo={isActiveConvo}
conversationId={conversationId}
isPopoverActive={isPopoverActive}
setIsPopoverActive={setIsPopoverActive}
/>
)}
</div>
</div>
);

View file

@ -1,9 +1,11 @@
import { useState, useId } from 'react';
import * as Ariakit from '@ariakit/react';
import { Ellipsis, Share2, Archive, Pen, Trash } from 'lucide-react';
import * as Menu from '@ariakit/react/menu';
import { Ellipsis, Share2, Copy, Archive, Pen, Trash } from 'lucide-react';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { MouseEvent } from 'react';
import { useLocalize, useArchiveHandler } from '~/hooks';
import { useLocalize, useArchiveHandler, useNavigateToConvo } from '~/hooks';
import { useToastContext, useChatContext } from '~/Providers';
import { useDuplicateConversationMutation } from '~/data-provider';
import { DropdownPopup } from '~/components/ui';
import DeleteButton from './DeleteButton';
import ShareButton from './ShareButton';
@ -12,7 +14,6 @@ import { cn } from '~/utils';
export default function ConvoOptions({
conversationId,
title,
renaming,
retainView,
renameHandler,
isPopoverActive,
@ -21,7 +22,6 @@ export default function ConvoOptions({
}: {
conversationId: string | null;
title: string | null;
renaming: boolean;
retainView: () => void;
renameHandler: (e: MouseEvent) => void;
isPopoverActive: boolean;
@ -29,10 +29,37 @@ export default function ConvoOptions({
isActiveConvo: boolean;
}) {
const localize = useLocalize();
const { index } = useChatContext();
const { data: startupConfig } = useGetStartupConfig();
const archiveHandler = useArchiveHandler(conversationId, true, retainView);
const { navigateToConvo } = useNavigateToConvo(index);
const { showToast } = useToastContext();
const [showShareDialog, setShowShareDialog] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const archiveHandler = useArchiveHandler(conversationId, true, retainView);
const duplicateConversation = useDuplicateConversationMutation({
onSuccess: (data) => {
if (data != null) {
navigateToConvo(data.conversation);
showToast({
message: localize('com_ui_duplication_success'),
status: 'success',
});
}
},
onMutate: () => {
showToast({
message: localize('com_ui_duplication_processing'),
status: 'info',
});
},
onError: () => {
showToast({
message: localize('com_ui_duplication_error'),
status: 'error',
});
},
});
const shareHandler = () => {
setIsPopoverActive(false);
@ -44,27 +71,39 @@ export default function ConvoOptions({
setShowDeleteDialog(true);
};
const duplicateHandler = () => {
setIsPopoverActive(false);
duplicateConversation.mutate({
conversationId: conversationId ?? '',
});
};
const dropdownItems = [
{
label: localize('com_ui_rename'),
onClick: renameHandler,
icon: <Pen className="icon-md mr-2 text-text-secondary" />,
},
{
label: localize('com_ui_share'),
onClick: shareHandler,
icon: <Share2 className="icon-md mr-2 text-text-secondary" />,
icon: <Share2 className="icon-sm mr-2 text-text-primary" />,
show: startupConfig && startupConfig.sharedLinksEnabled,
},
{
label: localize('com_ui_rename'),
onClick: renameHandler,
icon: <Pen className="icon-sm mr-2 text-text-primary" />,
},
{
label: localize('com_ui_duplicate'),
onClick: duplicateHandler,
icon: <Copy className="icon-sm mr-2 text-text-primary" />,
},
{
label: localize('com_ui_archive'),
onClick: archiveHandler,
icon: <Archive className="icon-md mr-2 text-text-secondary" />,
icon: <Archive className="icon-sm mr-2 text-text-primary" />,
},
{
label: localize('com_ui_delete'),
onClick: deleteHandler,
icon: <Trash className="icon-md mr-2 text-text-secondary" />,
icon: <Trash className="icon-sm mr-2 text-text-primary" />,
},
];
@ -76,7 +115,7 @@ export default function ConvoOptions({
isOpen={isPopoverActive}
setIsOpen={setIsPopoverActive}
trigger={
<Ariakit.MenuButton
<Menu.MenuButton
id="conversation-menu-button"
aria-label={localize('com_nav_convo_menu_options')}
className={cn(
@ -84,11 +123,10 @@ export default function ConvoOptions({
isActiveConvo === true
? 'opacity-100'
: 'opacity-0 focus:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100 data-[open]:opacity-100',
renaming === true ? 'pointer-events-none opacity-0' : '',
)}
>
<Ellipsis className="icon-md text-text-secondary" aria-hidden={true} />
</Ariakit.MenuButton>
</Menu.MenuButton>
}
items={dropdownItems}
menuId={menuId}

View file

@ -6,7 +6,6 @@ import { useUpdateSharedLinkMutation } from '~/data-provider';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { Spinner } from '~/components/svg';
import { Button } from '~/components/ui';
import { useLocalize } from '~/hooks';
export default function SharedLinkButton({
@ -112,7 +111,7 @@ export default function SharedLinkButton({
onClick={() => {
handlers.handler();
}}
className="btn btn-primary flex items-center"
className="btn btn-primary flex items-center justify-center"
>
{isCopying && (
<>