diff --git a/src/app/library/page.tsx b/src/app/library/page.tsx index 9c40b2b..78a2c36 100644 --- a/src/app/library/page.tsx +++ b/src/app/library/page.tsx @@ -1,10 +1,12 @@ 'use client'; import DeleteChat from '@/components/DeleteChat'; +import BatchDeleteChats from '@/components/BatchDeleteChats'; import { cn, formatTimeDifference } from '@/lib/utils'; -import { BookOpenText, ClockIcon, Delete, ScanEye } from 'lucide-react'; +import { BookOpenText, Check, ClockIcon, Delete, ScanEye, Search, X } from 'lucide-react'; import Link from 'next/link'; import { useEffect, useState } from 'react'; +import { toast } from 'sonner'; export interface Chat { id: string; @@ -15,7 +17,13 @@ export interface Chat { const Page = () => { const [chats, setChats] = useState([]); + const [filteredChats, setFilteredChats] = useState([]); const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + const [selectionMode, setSelectionMode] = useState(false); + const [selectedChats, setSelectedChats] = useState([]); + const [hoveredChatId, setHoveredChatId] = useState(null); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); useEffect(() => { const fetchChats = async () => { @@ -31,12 +39,71 @@ const Page = () => { const data = await res.json(); setChats(data.chats); + setFilteredChats(data.chats); setLoading(false); }; fetchChats(); }, []); + useEffect(() => { + if (searchQuery.trim() === '') { + setFilteredChats(chats); + } else { + const filtered = chats.filter((chat) => + chat.title.toLowerCase().includes(searchQuery.toLowerCase()) + ); + setFilteredChats(filtered); + } + }, [searchQuery, chats]); + + const handleSearchChange = (e: React.ChangeEvent) => { + setSearchQuery(e.target.value); + }; + + const clearSearch = () => { + setSearchQuery(''); + }; + + const toggleSelectionMode = () => { + setSelectionMode(!selectionMode); + setSelectedChats([]); + }; + + const toggleChatSelection = (chatId: string) => { + if (selectedChats.includes(chatId)) { + setSelectedChats(selectedChats.filter(id => id !== chatId)); + } else { + setSelectedChats([...selectedChats, chatId]); + } + }; + + const selectAllChats = () => { + if (selectedChats.length === filteredChats.length) { + setSelectedChats([]); + } else { + setSelectedChats(filteredChats.map(chat => chat.id)); + } + }; + + const deleteSelectedChats = () => { + if (selectedChats.length === 0) return; + setIsDeleteDialogOpen(true); + }; + + const handleBatchDeleteComplete = () => { + setSelectedChats([]); + setSelectionMode(false); + }; + + const updateChatsAfterDelete = (newChats: Chat[]) => { + setChats(newChats); + setFilteredChats(newChats.filter(chat => + searchQuery.trim() === '' || + chat.title.toLowerCase().includes(searchQuery.toLowerCase()) + )); + }; + return loading ? (
{

Library


+ + {/* Search Box */} +
+
+ +
+ + {searchQuery && ( + + )} +
+ + {/* Thread Count and Selection Controls */} +
+ {!selectionMode ? ( +
+
+ You have {chats.length} threads in Perplexica +
+ +
+ ) : ( +
+
+ {selectedChats.length} selected thread{selectedChats.length !== 1 ? 's' : ''} +
+
+ + + + + +
+
+ )} +
- {chats.length === 0 && ( -
+ + {filteredChats.length === 0 && ( +

- No chats found. + {searchQuery ? 'No threads found matching your search.' : 'No threads found.'}

)} - {chats.length > 0 && ( + + {filteredChats.length > 0 && (
- {chats.map((chat, i) => ( + {filteredChats.map((chat, i) => (
setHoveredChatId(chat.id)} + onMouseLeave={() => setHoveredChatId(null)} > - - {chat.title} - +
+ {/* Checkbox - visible when in selection mode or when hovering */} + {(selectionMode || hoveredChatId === chat.id) && ( +
{ + e.preventDefault(); + if (!selectionMode) setSelectionMode(true); + toggleChatSelection(chat.id); + }} + > +
+ {selectedChats.includes(chat.id) && ( + + )} +
+
+ )} + + {/* Chat Title */} + { + if (selectionMode) { + e.preventDefault(); + toggleChatSelection(chat.id); + } + }} + > + {chat.title} + +
+
@@ -97,16 +277,30 @@ const Page = () => { {formatTimeDifference(new Date(), chat.createdAt)} Ago

- + + {/* Delete button - only visible when not in selection mode */} + {!selectionMode && ( + + )}
))}
)} + + {/* Batch Delete Confirmation Dialog */} +
); }; diff --git a/src/components/BatchDeleteChats.tsx b/src/components/BatchDeleteChats.tsx new file mode 100644 index 0000000..209efe7 --- /dev/null +++ b/src/components/BatchDeleteChats.tsx @@ -0,0 +1,118 @@ +import { + Description, + Dialog, + DialogBackdrop, + DialogPanel, + DialogTitle, + Transition, + TransitionChild, +} from '@headlessui/react'; +import { Fragment, useState } from 'react'; +import { toast } from 'sonner'; +import { Chat } from '@/app/library/page'; + +interface BatchDeleteChatsProps { + chatIds: string[]; + chats: Chat[]; + setChats: (chats: Chat[]) => void; + onComplete: () => void; + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; +} + +const BatchDeleteChats = ({ + chatIds, + chats, + setChats, + onComplete, + isOpen, + setIsOpen, +}: BatchDeleteChatsProps) => { + const [loading, setLoading] = useState(false); + + const handleDelete = async () => { + if (chatIds.length === 0) return; + + setLoading(true); + try { + for (const chatId of chatIds) { + await fetch(`${process.env.NEXT_PUBLIC_API_URL}/chats/${chatId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); + } + + const newChats = chats.filter(chat => !chatIds.includes(chat.id)); + setChats(newChats); + + toast.success(`${chatIds.length} thread${chatIds.length > 1 ? 's' : ''} deleted`); + onComplete(); + } catch (err: any) { + toast.error('Failed to delete threads'); + } finally { + setIsOpen(false); + setLoading(false); + } + }; + + return ( + + { + if (!loading) { + setIsOpen(false); + } + }} + > + +
+
+ + + + Delete Confirmation + + + Are you sure you want to delete {chatIds.length} selected thread{chatIds.length !== 1 ? 's' : ''}? + +
+ + +
+
+
+
+
+
+
+ ); +}; + +export default BatchDeleteChats;