diff --git a/src/components/ChatList.tsx b/src/components/ChatList.tsx index bc5e904..cbe2760 100644 --- a/src/components/ChatList.tsx +++ b/src/components/ChatList.tsx @@ -1,8 +1,8 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useNavigate, useRouterState } from "@tanstack/react-router"; import { formatDistanceToNow } from "date-fns"; -import { PlusCircle, MoreVertical, Trash2 } from "lucide-react"; +import { PlusCircle, MoreVertical, Trash2, Edit3 } from "lucide-react"; import { useAtom } from "jotai"; import { selectedChatIdAtom } from "@/atoms/chatAtoms"; import { selectedAppIdAtom } from "@/atoms/appAtoms"; @@ -24,6 +24,8 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { useChats } from "@/hooks/useChats"; +import { RenameChatDialog } from "@/components/chat/RenameChatDialog"; +import { DeleteChatDialog } from "@/components/chat/DeleteChatDialog"; export function ChatList({ show }: { show?: boolean }) { const navigate = useNavigate(); @@ -34,6 +36,16 @@ export function ChatList({ show }: { show?: boolean }) { const routerState = useRouterState(); const isChatRoute = routerState.location.pathname === "/chat"; + // Rename dialog state + const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); + const [renameChatId, setRenameChatId] = useState(null); + const [renameChatTitle, setRenameChatTitle] = useState(""); + + // Delete dialog state + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [deleteChatId, setDeleteChatId] = useState(null); + const [deleteChatTitle, setDeleteChatTitle] = useState(""); + // Update selectedChatId when route changes useEffect(() => { if (isChatRoute) { @@ -108,88 +120,159 @@ export function ChatList({ show }: { show?: boolean }) { } }; + const handleDeleteChatClick = (chatId: number, chatTitle: string) => { + setDeleteChatId(chatId); + setDeleteChatTitle(chatTitle); + setIsDeleteDialogOpen(true); + }; + + const handleConfirmDelete = async () => { + if (deleteChatId !== null) { + await handleDeleteChat(deleteChatId); + setIsDeleteDialogOpen(false); + setDeleteChatId(null); + setDeleteChatTitle(""); + } + }; + + const handleRenameChat = (chatId: number, currentTitle: string) => { + setRenameChatId(chatId); + setRenameChatTitle(currentTitle); + setIsRenameDialogOpen(true); + }; + + const handleRenameDialogClose = (open: boolean) => { + setIsRenameDialogOpen(open); + if (!open) { + setRenameChatId(null); + setRenameChatTitle(""); + } + }; + return ( - - Recent Chats - -
- + <> + + Recent Chats + +
+ - {loading ? ( -
- Loading chats... -
- ) : chats.length === 0 ? ( -
- No chats found -
- ) : ( - - {chats.map((chat) => ( - -
- - - {selectedChatId === chat.id && ( - setIsDropdownOpen(open)} + {loading ? ( +
+ Loading chats... +
+ ) : chats.length === 0 ? ( +
+ No chats found +
+ ) : ( + + {chats.map((chat) => ( + +
+ + + {selectedChatId === chat.id && ( + setIsDropdownOpen(open)} + > + + + + - - - - - handleDeleteChat(chat.id)} - > - - Delete Chat - - - - )} -
-
- ))} -
- )} -
- - + + handleRenameChat(chat.id, chat.title || "") + } + className="px-3 py-2" + > + + Rename Chat + + + handleDeleteChatClick( + chat.id, + chat.title || "New Chat", + ) + } + className="px-3 py-2 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/50 focus:bg-red-50 dark:focus:bg-red-950/50" + > + + Delete Chat + + + + )} +
+ + ))} + + )} +
+
+
+ + {/* Rename Chat Dialog */} + {renameChatId !== null && ( + + )} + + {/* Delete Chat Dialog */} + + ); } diff --git a/src/components/chat/DeleteChatDialog.tsx b/src/components/chat/DeleteChatDialog.tsx new file mode 100644 index 0000000..84d1434 --- /dev/null +++ b/src/components/chat/DeleteChatDialog.tsx @@ -0,0 +1,52 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; + +interface DeleteChatDialogProps { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + onConfirmDelete: () => void; + chatTitle?: string; +} + +export function DeleteChatDialog({ + isOpen, + onOpenChange, + onConfirmDelete, + chatTitle, +}: DeleteChatDialogProps) { + return ( + + + + Delete Chat + + Are you sure you want to delete "{chatTitle || "this chat"}"? This + action cannot be undone and all messages in this chat will be + permanently lost. +
+
+ Note: Any code changes that have already been + accepted will be kept. +
+
+ + Cancel + + Delete Chat + + +
+
+ ); +} diff --git a/src/components/chat/RenameChatDialog.tsx b/src/components/chat/RenameChatDialog.tsx new file mode 100644 index 0000000..6a0f897 --- /dev/null +++ b/src/components/chat/RenameChatDialog.tsx @@ -0,0 +1,106 @@ +import { useState } from "react"; +import { IpcClient } from "@/ipc/ipc_client"; +import { showError, showSuccess } from "@/lib/toast"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; + +interface RenameChatDialogProps { + chatId: number; + currentTitle: string; + isOpen: boolean; + onOpenChange: (open: boolean) => void; + onRename: () => void; +} + +export function RenameChatDialog({ + chatId, + currentTitle, + isOpen, + onOpenChange, + onRename, +}: RenameChatDialogProps) { + const [newTitle, setNewTitle] = useState(""); + + // Reset title when dialog opens + const handleOpenChange = (open: boolean) => { + if (open) { + setNewTitle(currentTitle || ""); + } else { + setNewTitle(""); + } + onOpenChange(open); + }; + + const handleSave = async () => { + if (!newTitle.trim()) { + return; + } + + try { + await IpcClient.getInstance().updateChat({ + chatId, + title: newTitle.trim(), + }); + showSuccess("Chat renamed successfully"); + + // Call the parent's onRename callback to refresh the chat list + onRename(); + + // Close the dialog + handleOpenChange(false); + } catch (error) { + showError(`Failed to rename chat: ${(error as any).toString()}`); + } + }; + + const handleClose = () => { + handleOpenChange(false); + }; + + return ( + + + + Rename Chat + Enter a new name for this chat. + +
+
+ + setNewTitle(e.target.value)} + className="col-span-3" + placeholder="Enter chat title..." + onKeyDown={(e) => { + if (e.key === "Enter") { + handleSave(); + } + }} + /> +
+
+ + + + +
+
+ ); +} diff --git a/src/ipc/handlers/chat_handlers.ts b/src/ipc/handlers/chat_handlers.ts index 8b81bcc..bb3fa81 100644 --- a/src/ipc/handlers/chat_handlers.ts +++ b/src/ipc/handlers/chat_handlers.ts @@ -9,6 +9,7 @@ import { createLoggedHandler } from "./safe_handle"; import log from "electron-log"; import { getDyadAppPath } from "../../paths/paths"; +import { UpdateChatParams } from "../ipc_types"; const logger = log.scope("chat_handlers"); const handle = createLoggedHandler(logger); @@ -107,6 +108,10 @@ export function registerChatHandlers() { await db.delete(chats).where(eq(chats.id, chatId)); }); + handle("update-chat", async (_, { chatId, title }: UpdateChatParams) => { + await db.update(chats).set({ title }).where(eq(chats.id, chatId)); + }); + handle("delete-messages", async (_, chatId: number): Promise => { await db.delete(messages).where(eq(messages.chatId, chatId)); }); diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts index ebfcf80..40aebd1 100644 --- a/src/ipc/ipc_client.ts +++ b/src/ipc/ipc_client.ts @@ -49,6 +49,7 @@ import type { IsVercelProjectAvailableParams, SaveVercelAccessTokenParams, VercelProject, + UpdateChatParams, } from "./ipc_types"; import type { AppChatContext, ProposalResult } from "@/lib/schemas"; import { showError } from "@/lib/toast"; @@ -351,6 +352,10 @@ export class IpcClient { return this.ipcRenderer.invoke("create-chat", appId); } + public async updateChat(params: UpdateChatParams): Promise { + return this.ipcRenderer.invoke("update-chat", params); + } + public async deleteChat(chatId: number): Promise { await this.ipcRenderer.invoke("delete-chat", chatId); } diff --git a/src/ipc/ipc_types.ts b/src/ipc/ipc_types.ts index 99cd9cf..b1e4494 100644 --- a/src/ipc/ipc_types.ts +++ b/src/ipc/ipc_types.ts @@ -316,3 +316,8 @@ export interface VercelProject { name: string; framework: string | null; } + +export interface UpdateChatParams { + chatId: number; + title: string; +} diff --git a/src/preload.ts b/src/preload.ts index 58073f8..7af9474 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -79,6 +79,7 @@ const validInvokeChannels = [ "get-system-platform", "upload-to-signed-url", "delete-chat", + "update-chat", "delete-messages", "start-chat-stream", "does-release-note-exist",