Skip to content

Commit 502df01

Browse files
add duplicate to chat actions dropdown
1 parent 7807a4c commit 502df01

File tree

3 files changed

+82
-4
lines changed

3 files changed

+82
-4
lines changed

packages/web/src/app/[domain]/chat/components/chatActionsDropdown.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
'use client';
22

33
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
4-
import { PencilIcon, TrashIcon } from "lucide-react";
4+
import { CopyIcon, PencilIcon, TrashIcon } from "lucide-react";
55

66
interface ChatActionsDropdownProps {
77
children: React.ReactNode;
88
onRenameClick: () => void;
9+
onDuplicateClick: () => void;
910
onDeleteClick: () => void;
1011
align?: "start" | "center" | "end";
1112
}
1213

1314
export const ChatActionsDropdown = ({
1415
children,
1516
onRenameClick,
17+
onDuplicateClick,
1618
onDeleteClick,
1719
align = "start",
1820
}: ChatActionsDropdownProps) => {
@@ -35,6 +37,16 @@ export const ChatActionsDropdown = ({
3537
<PencilIcon className="w-4 h-4 mr-2" />
3638
Rename
3739
</DropdownMenuItem>
40+
<DropdownMenuItem
41+
className="cursor-pointer"
42+
onClick={(e) => {
43+
e.stopPropagation();
44+
onDuplicateClick();
45+
}}
46+
>
47+
<CopyIcon className="w-4 h-4 mr-2" />
48+
Duplicate
49+
</DropdownMenuItem>
3850
<DropdownMenuItem
3951
className="cursor-pointer text-destructive focus:text-destructive"
4052
onClick={(e) => {

packages/web/src/app/[domain]/chat/components/chatName.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
import { useToast } from "@/components/hooks/use-toast";
44
import { Button } from "@/components/ui/button";
5-
import { deleteChat, updateChatName } from "@/features/chat/actions";
5+
import { deleteChat, duplicateChat, updateChatName } from "@/features/chat/actions";
66
import { isServiceError } from "@/lib/utils";
77
import { ChevronDown } from "lucide-react";
8-
import { useRouter } from "next/navigation";
8+
import { useParams, useRouter } from "next/navigation";
99
import { useCallback, useState } from "react";
1010
import { ChatActionsDropdown } from "./chatActionsDropdown";
1111
import { DeleteChatDialog } from "./deleteChatDialog";
12+
import { DuplicateChatDialog } from "./duplicateChatDialog";
1213
import { RenameChatDialog } from "./renameChatDialog";
1314
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants";
1415

@@ -20,9 +21,11 @@ interface ChatNameProps {
2021

2122
export const ChatName = ({ name, id, isOwner = false }: ChatNameProps) => {
2223
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
24+
const [isDuplicateDialogOpen, setIsDuplicateDialogOpen] = useState(false);
2325
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
2426
const { toast } = useToast();
2527
const router = useRouter();
28+
const params = useParams<{ domain: string }>();
2629

2730
const onRenameChat = useCallback(async (newName: string): Promise<boolean> => {
2831
const response = await updateChatName({
@@ -62,6 +65,23 @@ export const ChatName = ({ name, id, isOwner = false }: ChatNameProps) => {
6265
}
6366
}, [id, toast, router]);
6467

68+
const onDuplicateChat = useCallback(async (newName: string): Promise<string | null> => {
69+
const response = await duplicateChat({ chatId: id, newName });
70+
71+
if (isServiceError(response)) {
72+
toast({
73+
description: `❌ Failed to duplicate chat. Reason: ${response.message}`
74+
});
75+
return null;
76+
} else {
77+
toast({
78+
description: `✅ Chat duplicated successfully`
79+
});
80+
router.push(`/${params.domain}/chat/${response.id}`);
81+
return response.id;
82+
}
83+
}, [id, toast, router, params.domain]);
84+
6585
return (
6686
<>
6787
<div className="flex flex-row gap-1 items-center">
@@ -71,6 +91,7 @@ export const ChatName = ({ name, id, isOwner = false }: ChatNameProps) => {
7191
{isOwner && (
7292
<ChatActionsDropdown
7393
onRenameClick={() => setIsRenameDialogOpen(true)}
94+
onDuplicateClick={() => setIsDuplicateDialogOpen(true)}
7495
onDeleteClick={() => setIsDeleteDialogOpen(true)}
7596
align="center"
7697
>
@@ -95,6 +116,12 @@ export const ChatName = ({ name, id, isOwner = false }: ChatNameProps) => {
95116
onOpenChange={setIsDeleteDialogOpen}
96117
onDelete={onDeleteChat}
97118
/>
119+
<DuplicateChatDialog
120+
isOpen={isDuplicateDialogOpen}
121+
onOpenChange={setIsDuplicateDialogOpen}
122+
onDuplicate={onDuplicateChat}
123+
currentName={name ?? ""}
124+
/>
98125
</>
99126
)
100127
}

packages/web/src/app/[domain]/chat/components/chatSidePanel.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ResizablePanel } from "@/components/ui/resizable";
77
import { ScrollArea } from "@/components/ui/scroll-area";
88
import { Separator } from "@/components/ui/separator";
99
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
10-
import { deleteChat, updateChatName } from "@/features/chat/actions";
10+
import { deleteChat, duplicateChat, updateChatName } from "@/features/chat/actions";
1111
import { cn, isServiceError } from "@/lib/utils";
1212
import { CirclePlusIcon, EllipsisIcon } from "lucide-react";
1313
import { ChatActionsDropdown } from "./chatActionsDropdown";
@@ -21,6 +21,7 @@ import { ImperativePanelHandle } from "react-resizable-panels";
2121
import { useChatId } from "../useChatId";
2222
import { RenameChatDialog } from "./renameChatDialog";
2323
import { DeleteChatDialog } from "./deleteChatDialog";
24+
import { DuplicateChatDialog } from "./duplicateChatDialog";
2425
import Link from "next/link";
2526
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants";
2627

@@ -48,6 +49,8 @@ export const ChatSidePanel = ({
4849
const chatId = useChatId();
4950
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
5051
const [chatIdToRename, setChatIdToRename] = useState<string | null>(null);
52+
const [isDuplicateDialogOpen, setIsDuplicateDialogOpen] = useState(false);
53+
const [chatIdToDuplicate, setChatIdToDuplicate] = useState<string | null>(null);
5154
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
5255
const [chatIdToDelete, setChatIdToDelete] = useState<string | null>(null);
5356

@@ -114,6 +117,27 @@ export const ChatSidePanel = ({
114117
}
115118
}, [chatId, router, toast]);
116119

120+
const onDuplicateChat = useCallback(async (newName: string, chatIdToDuplicate: string): Promise<string | null> => {
121+
if (!chatIdToDuplicate) {
122+
return null;
123+
}
124+
125+
const response = await duplicateChat({ chatId: chatIdToDuplicate, newName });
126+
127+
if (isServiceError(response)) {
128+
toast({
129+
description: `❌ Failed to duplicate chat. Reason: ${response.message}`
130+
});
131+
return null;
132+
} else {
133+
toast({
134+
description: `✅ Chat duplicated successfully`
135+
});
136+
router.push(`/${SINGLE_TENANT_ORG_DOMAIN}/chat/${response.id}`);
137+
return response.id;
138+
}
139+
}, [router, toast]);
140+
117141
return (
118142
<>
119143
<ResizablePanel
@@ -175,6 +199,10 @@ export const ChatSidePanel = ({
175199
setChatIdToRename(chat.id);
176200
setIsRenameDialogOpen(true);
177201
}}
202+
onDuplicateClick={() => {
203+
setChatIdToDuplicate(chat.id);
204+
setIsDuplicateDialogOpen(true);
205+
}}
178206
onDeleteClick={() => {
179207
setChatIdToDelete(chat.id);
180208
setIsDeleteDialogOpen(true);
@@ -244,6 +272,17 @@ export const ChatSidePanel = ({
244272
return false;
245273
}}
246274
/>
275+
<DuplicateChatDialog
276+
isOpen={isDuplicateDialogOpen}
277+
onOpenChange={setIsDuplicateDialogOpen}
278+
onDuplicate={async (newName) => {
279+
if (chatIdToDuplicate) {
280+
return await onDuplicateChat(newName, chatIdToDuplicate);
281+
}
282+
return null;
283+
}}
284+
currentName={chatHistory?.find((chat) => chat.id === chatIdToDuplicate)?.name ?? ""}
285+
/>
247286
</>
248287
)
249288
}

0 commit comments

Comments
 (0)