Rename chat (#673)

Fixes #102
This commit is contained in:
Will Chen
2025-07-21 17:44:56 -07:00
committed by GitHub
parent c5fdbeb90f
commit de21c6ff25
7 changed files with 338 additions and 81 deletions

View File

@@ -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<number | null>(null);
const [renameChatTitle, setRenameChatTitle] = useState("");
// Delete dialog state
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [deleteChatId, setDeleteChatId] = useState<number | null>(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 (
<SidebarGroup className="overflow-y-auto h-[calc(100vh-112px)]">
<SidebarGroupLabel>Recent Chats</SidebarGroupLabel>
<SidebarGroupContent>
<div className="flex flex-col space-y-4">
<Button
onClick={handleNewChat}
variant="outline"
className="flex items-center justify-start gap-2 mx-2 py-3"
>
<PlusCircle size={16} />
<span>New Chat</span>
</Button>
<>
<SidebarGroup className="overflow-y-auto h-[calc(100vh-112px)]">
<SidebarGroupLabel>Recent Chats</SidebarGroupLabel>
<SidebarGroupContent>
<div className="flex flex-col space-y-4">
<Button
onClick={handleNewChat}
variant="outline"
className="flex items-center justify-start gap-2 mx-2 py-3"
>
<PlusCircle size={16} />
<span>New Chat</span>
</Button>
{loading ? (
<div className="py-3 px-4 text-sm text-gray-500">
Loading chats...
</div>
) : chats.length === 0 ? (
<div className="py-3 px-4 text-sm text-gray-500">
No chats found
</div>
) : (
<SidebarMenu className="space-y-1">
{chats.map((chat) => (
<SidebarMenuItem key={chat.id} className="mb-1">
<div className="flex w-[175px] items-center">
<Button
variant="ghost"
onClick={() =>
handleChatClick({ chatId: chat.id, appId: chat.appId })
}
className={`justify-start w-full text-left py-3 pr-1 hover:bg-sidebar-accent/80 ${
selectedChatId === chat.id
? "bg-sidebar-accent text-sidebar-accent-foreground"
: ""
}`}
>
<div className="flex flex-col w-full">
<span className="truncate">
{chat.title || "New Chat"}
</span>
<span className="text-xs text-gray-500">
{formatDistanceToNow(new Date(chat.createdAt), {
addSuffix: true,
})}
</span>
</div>
</Button>
{selectedChatId === chat.id && (
<DropdownMenu
onOpenChange={(open) => setIsDropdownOpen(open)}
{loading ? (
<div className="py-3 px-4 text-sm text-gray-500">
Loading chats...
</div>
) : chats.length === 0 ? (
<div className="py-3 px-4 text-sm text-gray-500">
No chats found
</div>
) : (
<SidebarMenu className="space-y-1">
{chats.map((chat) => (
<SidebarMenuItem key={chat.id} className="mb-1">
<div className="flex w-[175px] items-center">
<Button
variant="ghost"
onClick={() =>
handleChatClick({
chatId: chat.id,
appId: chat.appId,
})
}
className={`justify-start w-full text-left py-3 pr-1 hover:bg-sidebar-accent/80 ${
selectedChatId === chat.id
? "bg-sidebar-accent text-sidebar-accent-foreground"
: ""
}`}
>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="ml-1 w-4"
onClick={(e) => e.stopPropagation()}
<div className="flex flex-col w-full">
<span className="truncate">
{chat.title || "New Chat"}
</span>
<span className="text-xs text-gray-500">
{formatDistanceToNow(new Date(chat.createdAt), {
addSuffix: true,
})}
</span>
</div>
</Button>
{selectedChatId === chat.id && (
<DropdownMenu
modal={false}
onOpenChange={(open) => setIsDropdownOpen(open)}
>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="ml-1 w-4"
onClick={(e) => e.stopPropagation()}
>
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="space-y-1 p-2"
>
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
variant="destructive"
onClick={() => handleDeleteChat(chat.id)}
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete Chat</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</SidebarMenuItem>
))}
</SidebarMenu>
)}
</div>
</SidebarGroupContent>
</SidebarGroup>
<DropdownMenuItem
onClick={() =>
handleRenameChat(chat.id, chat.title || "")
}
className="px-3 py-2"
>
<Edit3 className="mr-2 h-4 w-4" />
<span>Rename Chat</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
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"
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete Chat</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</SidebarMenuItem>
))}
</SidebarMenu>
)}
</div>
</SidebarGroupContent>
</SidebarGroup>
{/* Rename Chat Dialog */}
{renameChatId !== null && (
<RenameChatDialog
chatId={renameChatId}
currentTitle={renameChatTitle}
isOpen={isRenameDialogOpen}
onOpenChange={handleRenameDialogClose}
onRename={refreshChats}
/>
)}
{/* Delete Chat Dialog */}
<DeleteChatDialog
isOpen={isDeleteDialogOpen}
onOpenChange={setIsDeleteDialogOpen}
onConfirmDelete={handleConfirmDelete}
chatTitle={deleteChatTitle}
/>
</>
);
}