11'use client' ;
22
33import DeleteChat from '@/components/DeleteChat' ;
4+ import BatchDeleteChats from '@/components/BatchDeleteChats' ;
45import { cn , formatTimeDifference } from '@/lib/utils' ;
5- import { BookOpenText , ClockIcon , Delete , ScanEye } from 'lucide-react' ;
6+ import { BookOpenText , Check , ClockIcon , Delete , ScanEye , Search , X } from 'lucide-react' ;
67import Link from 'next/link' ;
78import { useEffect , useState } from 'react' ;
9+ import { toast } from 'sonner' ;
810
911export interface Chat {
1012 id : string ;
@@ -15,7 +17,13 @@ export interface Chat {
1517
1618const Page = ( ) => {
1719 const [ chats , setChats ] = useState < Chat [ ] > ( [ ] ) ;
20+ const [ filteredChats , setFilteredChats ] = useState < Chat [ ] > ( [ ] ) ;
1821 const [ loading , setLoading ] = useState ( true ) ;
22+ const [ searchQuery , setSearchQuery ] = useState ( '' ) ;
23+ const [ selectionMode , setSelectionMode ] = useState ( false ) ;
24+ const [ selectedChats , setSelectedChats ] = useState < string [ ] > ( [ ] ) ;
25+ const [ hoveredChatId , setHoveredChatId ] = useState < string | null > ( null ) ;
26+ const [ isDeleteDialogOpen , setIsDeleteDialogOpen ] = useState ( false ) ;
1927
2028 useEffect ( ( ) => {
2129 const fetchChats = async ( ) => {
@@ -31,12 +39,71 @@ const Page = () => {
3139 const data = await res . json ( ) ;
3240
3341 setChats ( data . chats ) ;
42+ setFilteredChats ( data . chats ) ;
3443 setLoading ( false ) ;
3544 } ;
3645
3746 fetchChats ( ) ;
3847 } , [ ] ) ;
3948
49+ useEffect ( ( ) => {
50+ if ( searchQuery . trim ( ) === '' ) {
51+ setFilteredChats ( chats ) ;
52+ } else {
53+ const filtered = chats . filter ( ( chat ) =>
54+ chat . title . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) )
55+ ) ;
56+ setFilteredChats ( filtered ) ;
57+ }
58+ } , [ searchQuery , chats ] ) ;
59+
60+ const handleSearchChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
61+ setSearchQuery ( e . target . value ) ;
62+ } ;
63+
64+ const clearSearch = ( ) => {
65+ setSearchQuery ( '' ) ;
66+ } ;
67+
68+ const toggleSelectionMode = ( ) => {
69+ setSelectionMode ( ! selectionMode ) ;
70+ setSelectedChats ( [ ] ) ;
71+ } ;
72+
73+ const toggleChatSelection = ( chatId : string ) => {
74+ if ( selectedChats . includes ( chatId ) ) {
75+ setSelectedChats ( selectedChats . filter ( id => id !== chatId ) ) ;
76+ } else {
77+ setSelectedChats ( [ ...selectedChats , chatId ] ) ;
78+ }
79+ } ;
80+
81+ const selectAllChats = ( ) => {
82+ if ( selectedChats . length === filteredChats . length ) {
83+ setSelectedChats ( [ ] ) ;
84+ } else {
85+ setSelectedChats ( filteredChats . map ( chat => chat . id ) ) ;
86+ }
87+ } ;
88+
89+ const deleteSelectedChats = ( ) => {
90+ if ( selectedChats . length === 0 ) return ;
91+ setIsDeleteDialogOpen ( true ) ;
92+ } ;
93+
94+ const handleBatchDeleteComplete = ( ) => {
95+ setSelectedChats ( [ ] ) ;
96+ setSelectionMode ( false ) ;
97+ } ;
98+
99+ const updateChatsAfterDelete = ( newChats : Chat [ ] ) => {
100+ setChats ( newChats ) ;
101+ setFilteredChats ( newChats . filter ( chat =>
102+ searchQuery . trim ( ) === '' ||
103+ chat . title . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) )
104+ ) ) ;
105+ } ;
106+
40107 return loading ? (
41108 < div className = "flex flex-row items-center justify-center min-h-screen" >
42109 < svg
@@ -64,49 +131,176 @@ const Page = () => {
64131 < h1 className = "text-3xl font-medium p-2" > Library</ h1 >
65132 </ div >
66133 < hr className = "border-t border-[#2B2C2C] my-4 w-full" />
134+
135+ { /* Search Box */ }
136+ < div className = "relative mt-6 mb-6" >
137+ < div className = "absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none" >
138+ < Search className = "w-5 h-5 text-black/50 dark:text-white/50" />
139+ </ div >
140+ < input
141+ type = "text"
142+ className = "block w-full p-2 pl-10 pr-10 bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 rounded-md text-black dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500"
143+ placeholder = "Search your threads..."
144+ value = { searchQuery }
145+ onChange = { handleSearchChange }
146+ />
147+ { searchQuery && (
148+ < button
149+ onClick = { clearSearch }
150+ className = "absolute inset-y-0 right-0 flex items-center pr-3"
151+ >
152+ < X className = "w-5 h-5 text-black/50 dark:text-white/50 hover:text-black dark:hover:text-white" />
153+ </ button >
154+ ) }
155+ </ div >
156+
157+ { /* Thread Count and Selection Controls */ }
158+ < div className = "mb-4" >
159+ { ! selectionMode ? (
160+ < div className = "flex items-center justify-between" >
161+ < div className = "text-black/70 dark:text-white/70" >
162+ You have { chats . length } threads in Perplexica
163+ </ div >
164+ < button
165+ onClick = { toggleSelectionMode }
166+ className = "text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white text-sm transition duration-200"
167+ >
168+ Select
169+ </ button >
170+ </ div >
171+ ) : (
172+ < div className = "flex items-center justify-between" >
173+ < div className = "text-black/70 dark:text-white/70" >
174+ { selectedChats . length } selected thread{ selectedChats . length !== 1 ? 's' : '' }
175+ </ div >
176+ < div className = "flex space-x-4" >
177+ < button
178+ onClick = { selectAllChats }
179+ className = "text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white text-sm transition duration-200"
180+ >
181+ { selectedChats . length === filteredChats . length ? 'Deselect all' : 'Select all' }
182+ </ button >
183+
184+ < button
185+ onClick = { toggleSelectionMode }
186+ className = "text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white text-sm transition duration-200"
187+ >
188+ Cancel
189+ </ button >
190+
191+ < button
192+ onClick = { deleteSelectedChats }
193+ disabled = { selectedChats . length === 0 }
194+ className = { cn (
195+ "text-sm transition duration-200" ,
196+ selectedChats . length === 0
197+ ? "text-red-400/50 hover:text-red-500/50 cursor-not-allowed"
198+ : "text-red-400 hover:text-red-500 cursor-pointer"
199+ ) }
200+ >
201+ Delete Selected
202+ </ button >
203+ </ div >
204+ </ div >
205+ ) }
206+ </ div >
67207 </ div >
68- { chats . length === 0 && (
69- < div className = "flex flex-row items-center justify-center min-h-screen" >
208+
209+ { filteredChats . length === 0 && (
210+ < div className = "flex flex-row items-center justify-center min-h-[50vh]" >
70211 < p className = "text-black/70 dark:text-white/70 text-sm" >
71- No chats found.
212+ { searchQuery ? ' No threads found matching your search.' : 'No threads found.' }
72213 </ p >
73214 </ div >
74215 ) }
75- { chats . length > 0 && (
216+
217+ { filteredChats . length > 0 && (
76218 < div className = "flex flex-col pb-20 lg:pb-2" >
77- { chats . map ( ( chat , i ) => (
219+ { filteredChats . map ( ( chat , i ) => (
78220 < div
79221 className = { cn (
80222 'flex flex-col space-y-4 py-6' ,
81- i !== chats . length - 1
223+ i !== filteredChats . length - 1
82224 ? 'border-b border-white-200 dark:border-dark-200'
83225 : '' ,
84226 ) }
85227 key = { i }
228+ onMouseEnter = { ( ) => setHoveredChatId ( chat . id ) }
229+ onMouseLeave = { ( ) => setHoveredChatId ( null ) }
86230 >
87- < Link
88- href = { `/c/${ chat . id } ` }
89- className = "text-black dark:text-white lg:text-xl font-medium truncate transition duration-200 hover:text-[#24A0ED] dark:hover:text-[#24A0ED] cursor-pointer"
90- >
91- { chat . title }
92- </ Link >
231+ < div className = "flex items-center" >
232+ { /* Checkbox - visible when in selection mode or when hovering */ }
233+ { ( selectionMode || hoveredChatId === chat . id ) && (
234+ < div
235+ className = "mr-3 cursor-pointer"
236+ onClick = { ( e ) => {
237+ e . preventDefault ( ) ;
238+ if ( ! selectionMode ) setSelectionMode ( true ) ;
239+ toggleChatSelection ( chat . id ) ;
240+ } }
241+ >
242+ < div className = { cn (
243+ "w-5 h-5 border rounded flex items-center justify-center transition-colors" ,
244+ selectedChats . includes ( chat . id )
245+ ? "bg-blue-500 border-blue-500"
246+ : "border-gray-400 dark:border-gray-600"
247+ ) } >
248+ { selectedChats . includes ( chat . id ) && (
249+ < Check className = "w-4 h-4 text-white" />
250+ ) }
251+ </ div >
252+ </ div >
253+ ) }
254+
255+ { /* Chat Title */ }
256+ < Link
257+ href = { `/c/${ chat . id } ` }
258+ className = { cn (
259+ "text-black dark:text-white lg:text-xl font-medium truncate transition duration-200 hover:text-[#24A0ED] dark:hover:text-[#24A0ED] cursor-pointer" ,
260+ selectionMode && "pointer-events-none text-black dark:text-white hover:text-black dark:hover:text-white"
261+ ) }
262+ onClick = { ( e ) => {
263+ if ( selectionMode ) {
264+ e . preventDefault ( ) ;
265+ toggleChatSelection ( chat . id ) ;
266+ }
267+ } }
268+ >
269+ { chat . title }
270+ </ Link >
271+ </ div >
272+
93273 < div className = "flex flex-row items-center justify-between w-full" >
94274 < div className = "flex flex-row items-center space-x-1 lg:space-x-1.5 text-black/70 dark:text-white/70" >
95275 < ClockIcon size = { 15 } />
96276 < p className = "text-xs" >
97277 { formatTimeDifference ( new Date ( ) , chat . createdAt ) } Ago
98278 </ p >
99279 </ div >
100- < DeleteChat
101- chatId = { chat . id }
102- chats = { chats }
103- setChats = { setChats }
104- />
280+
281+ { /* Delete button - only visible when not in selection mode */ }
282+ { ! selectionMode && (
283+ < DeleteChat
284+ chatId = { chat . id }
285+ chats = { chats }
286+ setChats = { updateChatsAfterDelete }
287+ />
288+ ) }
105289 </ div >
106290 </ div >
107291 ) ) }
108292 </ div >
109293 ) }
294+
295+ { /* Batch Delete Confirmation Dialog */ }
296+ < BatchDeleteChats
297+ chatIds = { selectedChats }
298+ chats = { chats }
299+ setChats = { updateChatsAfterDelete }
300+ onComplete = { handleBatchDeleteComplete }
301+ isOpen = { isDeleteDialogOpen }
302+ setIsOpen = { setIsDeleteDialogOpen }
303+ />
110304 </ div >
111305 ) ;
112306} ;
0 commit comments