diff --git a/src/frontend/components/Sidebar.tsx b/src/frontend/components/Sidebar.tsx index 922717d..2950104 100644 --- a/src/frontend/components/Sidebar.tsx +++ b/src/frontend/components/Sidebar.tsx @@ -2,6 +2,7 @@ import React, { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Button, + Checkbox, Label, Modal, ModalBody, @@ -27,6 +28,7 @@ interface SidebarProps { onNewChat: () => void; onSelectChat: (chatId: string) => void; onDeleteChat: (chatId: string) => void; + onDeleteChats: (chatIds: string[]) => void; onDeleteAllChats: () => void; onRenameChat: (chatId: string, newTitle: string) => void; } @@ -41,7 +43,7 @@ function SidebarComponent({ onNewChat, onSelectChat, onDeleteChat, - // eslint-disable-next-line @typescript-eslint/no-unused-vars + onDeleteChats, onDeleteAllChats, onRenameChat, }: SidebarProps) { @@ -51,7 +53,10 @@ function SidebarComponent({ const [editingChat, setEditingChat] = useState(null); const [editTitle, setEditTitle] = useState(''); const [deletingChatId, setDeletingChatId] = useState(null); - + const [showDeleteAllDialog, setShowDeleteAllDialog] = useState(false); + const [isSelectionMode, setIsSelectionMode] = useState(false); + const [selectedChatIds, setSelectedChatIds] = useState>(new Set()); + const [showDeleteSelectedDialog, setShowDeleteSelectedDialog] = useState(false); const filteredChats = useMemo(() => { const q = searchQuery.trim().toLowerCase(); @@ -62,6 +67,39 @@ function SidebarComponent({ ); }, [chatHistory, searchQuery]); + const allFilteredSelected = + filteredChats.length > 0 && filteredChats.every((chat) => selectedChatIds.has(chat.id)); + + const exitSelectionMode = () => { + setIsSelectionMode(false); + setSelectedChatIds(new Set()); + setShowDeleteSelectedDialog(false); + }; + + const toggleChatSelected = (chatId: string) => { + setSelectedChatIds((prev) => { + const next = new Set(prev); + if (next.has(chatId)) { + next.delete(chatId); + } else { + next.add(chatId); + } + return next; + }); + }; + + const toggleSelectAllFiltered = () => { + setSelectedChatIds((prev) => { + const next = new Set(prev); + if (allFilteredSelected) { + filteredChats.forEach((chat) => next.delete(chat.id)); + } else { + filteredChats.forEach((chat) => next.add(chat.id)); + } + return next; + }); + }; + const handleRename = (chatId: string, title: string) => { setEditingChat(chatId); setEditTitle(title); @@ -73,6 +111,12 @@ function SidebarComponent({ setEditTitle(''); }; + const confirmDeleteSelected = () => { + const ids = [...selectedChatIds]; + onDeleteChats(ids); + exitSelectionMode(); + }; + return (
{/* New Chat button */} @@ -94,9 +138,67 @@ function SidebarComponent({
)} - {/* Separator + label */} + {/* Separator + label / selection toolbar */}
-

Recent chats

+ {isSelectionMode ? ( +
+
+ +

+ {selectedChatIds.size} selected +

+
+
+ + +
+
+ ) : ( +
+

+ Recent chats +

+ {chatHistory.length > 0 && ( +
+ + +
+ )} +
+ )}
{/* Chat list */} @@ -104,6 +206,7 @@ function SidebarComponent({ className="flex-1 min-h-0 overflow-y-auto chat-scroll px-1.5" role="listbox" aria-label="Chat history" + aria-multiselectable={isSelectionMode} > {filteredChats.length === 0 ? (
@@ -116,6 +219,7 @@ function SidebarComponent({
{filteredChats.map((chat) => { const isActive = currentChatId === chat.id; + const isChecked = selectedChatIds.has(chat.id); const focusNeighbor = (delta: number) => { const idx = filteredChats.findIndex((c) => c.id === chat.id); @@ -127,7 +231,11 @@ function SidebarComponent({ const onOptionKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - onSelectChat(chat.id); + if (isSelectionMode) { + toggleChatSelected(chat.id); + } else { + onSelectChat(chat.id); + } return; } if (e.key === 'ArrowDown') { @@ -147,16 +255,34 @@ function SidebarComponent({ id={`sidebar-chat-option-${chat.id}`} role="option" tabIndex={editingChat === chat.id ? -1 : 0} - aria-selected={isActive} + aria-selected={isSelectionMode ? isChecked : isActive} className={cn( 'group flex items-center gap-2 px-2.5 py-2 rounded-lg cursor-pointer transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', - isActive + isSelectionMode && isChecked ? 'bg-secondary text-foreground' - : 'text-foreground/70 hover:bg-secondary/50 hover:text-foreground' + : isActive && !isSelectionMode + ? 'bg-secondary text-foreground' + : 'text-foreground/70 hover:bg-secondary/50 hover:text-foreground' )} - onClick={() => onSelectChat(chat.id)} + onClick={() => { + if (isSelectionMode) { + toggleChatSelected(chat.id); + } else { + onSelectChat(chat.id); + } + }} onKeyDown={onOptionKeyDown} > + {isSelectionMode && ( + toggleChatSelected(chat.id)} + onClick={(e) => e.stopPropagation()} + aria-label={`Select chat: ${chat.title}`} + /> + )} + {editingChat === chat.id ? (

{chat.title}

- {isActive && activeSubAgent && ( + {isActive && activeSubAgent && !isSelectionMode && (