Skip to content

Commit abec687

Browse files
AndlerRLmerivercapBran18
authored
feat(masterbots.ai): chat sidebar filtering (#264)
* sidebar refactor with ai * fix: sidebar AI V - Prev Jun (#262) * fix:semistable * fix:stable v * impr:delete nonused component * fix: upt category filtering * fix typo --------- Co-authored-by: Roberto Lucas <andler.dev@gmail.com> * feat: sidebar state * fix(masterbots.ai): logic typo * fix(masterbots.ai): ts typo --------- Co-authored-by: Jun Dam <jun@bitcash.org> Co-authored-by: Brandon Fernández <31634868+Bran18@users.noreply.github.com>
1 parent 9a6333b commit abec687

File tree

17 files changed

+559
-359
lines changed

17 files changed

+559
-359
lines changed

apps/masterbots.ai/components/layout/sidebar/sidebar-actions.tsx

Lines changed: 92 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { useRouter } from 'next/navigation'
44
import * as React from 'react'
55
import { toast } from 'react-hot-toast'
6-
76
import { ServerActionResult, type Chat } from '@/types/types'
87
import {
98
AlertDialog,
@@ -16,13 +15,14 @@ import {
1615
AlertDialogTitle
1716
} from '@/components/ui/alert-dialog'
1817
import { Button } from '@/components/ui/button'
19-
import { IconShare, IconSpinner, IconTrash } from '@/components/ui/icons'
18+
import { IconShare, IconSpinner, IconTrash, IconCheck } from '@/components/ui/icons'
2019
import { ChatShareDialog } from '@/components/routes/chat/chat-share-dialog'
2120
import {
2221
Tooltip,
2322
TooltipContent,
2423
TooltipTrigger
2524
} from '@/components/ui/tooltip'
25+
import { useSidebar } from '@/lib/hooks/use-sidebar'
2626

2727
interface SidebarActionsProps {
2828
chat: Chat
@@ -39,41 +39,101 @@ export function SidebarActions({
3939
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)
4040
const [shareDialogOpen, setShareDialogOpen] = React.useState(false)
4141
const [isRemovePending, startRemoveTransition] = React.useTransition()
42+
const {
43+
isFilterMode,
44+
selectedChats,
45+
setSelectedChats,
46+
filterValue
47+
} = useSidebar()
48+
49+
const isSelected = selectedChats.includes(chat.id)
50+
const isVisible = !filterValue || chat.title.toLowerCase().includes(filterValue.toLowerCase())
51+
52+
const handleSelect = React.useCallback(() => {
53+
setSelectedChats(prev =>
54+
isSelected
55+
? prev.filter(id => id !== chat.id)
56+
: [...prev, chat.id]
57+
)
58+
}, [chat.id, isSelected, setSelectedChats])
59+
60+
const handleDelete = React.useCallback(async () => {
61+
startRemoveTransition(async () => {
62+
const result = await removeChat({
63+
id: chat.id,
64+
path: chat.path
65+
})
66+
67+
if (result && 'error' in result) {
68+
toast.error(result.error)
69+
return
70+
}
71+
72+
setDeleteDialogOpen(false)
73+
router.refresh()
74+
router.push('/')
75+
toast.success('Chat deleted')
76+
77+
// Remove the chat from selected chats if it was selected
78+
if (isSelected) {
79+
setSelectedChats(prev => prev.filter(id => id !== chat.id))
80+
}
81+
})
82+
}, [chat.id, chat.path, removeChat, router, isSelected, setSelectedChats])
83+
84+
if (!isVisible) return null
4285

4386
return (
4487
<>
4588
<div className="space-x-1">
46-
<Tooltip>
47-
<TooltipTrigger asChild>
48-
<Button
49-
variant="ghost"
50-
className="p-0 size-6 hover:bg-background"
51-
onClick={() => setShareDialogOpen(true)}
52-
>
53-
<IconShare />
54-
<span className="sr-only">Share</span>
55-
</Button>
56-
</TooltipTrigger>
57-
<TooltipContent>Share chat</TooltipContent>
58-
</Tooltip>
59-
<Tooltip>
60-
<TooltipTrigger asChild>
61-
<Button
62-
variant="ghost"
63-
className="p-0 size-6 hover:bg-background"
64-
disabled={isRemovePending}
65-
onClick={() => setDeleteDialogOpen(true)}
66-
>
67-
<IconTrash />
68-
<span className="sr-only">Delete</span>
69-
</Button>
70-
</TooltipTrigger>
71-
<TooltipContent>Delete chat</TooltipContent>
72-
</Tooltip>
89+
{isFilterMode ? (
90+
<Tooltip>
91+
<TooltipTrigger asChild>
92+
<Button
93+
variant="ghost"
94+
className="p-0 size-6 hover:bg-background"
95+
onClick={handleSelect}
96+
>
97+
{isSelected ? <IconCheck /> : <IconShare />}
98+
<span className="sr-only">{isSelected ? 'Deselect' : 'Select'}</span>
99+
</Button>
100+
</TooltipTrigger>
101+
<TooltipContent>{isSelected ? 'Deselect chat' : 'Select chat'}</TooltipContent>
102+
</Tooltip>
103+
) : (
104+
<>
105+
<Tooltip>
106+
<TooltipTrigger asChild>
107+
<Button
108+
variant="ghost"
109+
className="p-0 size-6 hover:bg-background"
110+
onClick={() => setShareDialogOpen(true)}
111+
>
112+
<IconShare />
113+
<span className="sr-only">Share</span>
114+
</Button>
115+
</TooltipTrigger>
116+
<TooltipContent>Share chat</TooltipContent>
117+
</Tooltip>
118+
<Tooltip>
119+
<TooltipTrigger asChild>
120+
<Button
121+
variant="ghost"
122+
className="p-0 size-6 hover:bg-background"
123+
disabled={isRemovePending}
124+
onClick={() => setDeleteDialogOpen(true)}
125+
>
126+
<IconTrash />
127+
<span className="sr-only">Delete</span>
128+
</Button>
129+
</TooltipTrigger>
130+
<TooltipContent>Delete chat</TooltipContent>
131+
</Tooltip>
132+
</>
133+
)}
73134
</div>
74135
<ChatShareDialog
75136
chat={chat}
76-
// shareChat={shareChat}
77137
open={shareDialogOpen}
78138
onOpenChange={setShareDialogOpen}
79139
onCopy={() => setShareDialogOpen(false)}
@@ -93,26 +153,7 @@ export function SidebarActions({
93153
</AlertDialogCancel>
94154
<AlertDialogAction
95155
disabled={isRemovePending}
96-
onClick={event => {
97-
event.preventDefault()
98-
// @ts-ignore
99-
startRemoveTransition(async () => {
100-
const result = await removeChat({
101-
id: chat.id,
102-
path: chat.path
103-
})
104-
105-
if (result && 'error' in result) {
106-
toast.error(result.error)
107-
return
108-
}
109-
110-
setDeleteDialogOpen(false)
111-
router.refresh()
112-
router.push('/')
113-
toast.success('Chat deleted')
114-
})
115-
}}
156+
onClick={handleDelete}
116157
>
117158
{isRemovePending && <IconSpinner className="mr-2 animate-spin" />}
118159
Delete
@@ -122,4 +163,4 @@ export function SidebarActions({
122163
</AlertDialog>
123164
</>
124165
)
125-
}
166+
}
Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,53 @@
1-
// import { ChatHistory } from '@/components/chat-history'
2-
import { getCategories } from '@/services/hasura'
3-
import { getServerSession } from 'next-auth'
1+
'use client'
2+
43
import SidebarLink from '@/components/layout/sidebar/sidebar-link'
4+
import { useSidebar } from '@/lib/hooks/use-sidebar'
5+
import { getCategories } from '@/services/hasura'
6+
import { Category } from 'mb-genql'
7+
import { useEffect, useMemo, useState } from 'react'
8+
9+
export function SidebarCategoryGeneral() {
10+
const [categories, setCategories] = useState<Category[]>([])
11+
const [isLoading, setIsLoading] = useState(true)
12+
const { filterValue, selectedCategories, isFilterMode } = useSidebar()
13+
14+
useEffect(() => {
15+
async function fetchCategories() {
16+
try {
17+
const fetchedCategories = await getCategories()
18+
setCategories(fetchedCategories)
19+
} catch (error) {
20+
console.error('Error fetching categories:', error)
21+
} finally {
22+
setIsLoading(false)
23+
}
24+
}
25+
fetchCategories()
26+
}, [])
27+
28+
const filteredCategories = useMemo(() =>
29+
isFilterMode
30+
? categories
31+
: categories.filter(category =>
32+
category.name.toLowerCase().includes(filterValue.toLowerCase()) ||
33+
category.chatbots.some(chatbot =>
34+
chatbot.chatbot.name.toLowerCase().includes(filterValue.toLowerCase())
35+
)
36+
).filter(category => selectedCategories.includes(category.categoryId)),
37+
[categories, filterValue, isFilterMode]
38+
)
39+
40+
if (isLoading) return <div className="p-4 text-center">Loading categories...</div>
41+
if (!filteredCategories.length) return <div className="p-4 text-center">No matching categories found</div>
42+
if (!selectedCategories.length) return <div className="p-4 text-center">No categories selected</div>
543

6-
export async function SidebarGeneralCategory() {
7-
const session = await getServerSession()
8-
if (!session) return null
9-
const categories = await getCategories()
1044
return (
11-
<ul>
12-
{categories.map((category, key) => (
13-
<li key={key}>
14-
<SidebarLink category={category} />
45+
<ul className="space-y-2">
46+
{filteredCategories.map((category) => (
47+
<li key={category.categoryId}>
48+
<SidebarLink category={category} isFilterMode={isFilterMode} />
1549
</li>
1650
))}
1751
</ul>
1852
)
19-
}
53+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use client'
2+
3+
import * as React from 'react'
4+
import { Input } from '@/components/ui/input'
5+
import { Button } from '@/components/ui/button'
6+
import { IconChatSearch, IconFilter, IconClose } from '@/components/ui/icons'
7+
import { cn } from '@/lib/utils'
8+
import { useSidebar } from '@/lib/hooks/use-sidebar'
9+
10+
interface FilterInputProps {
11+
className?: string
12+
}
13+
14+
export function FilterInput({ className }: FilterInputProps) {
15+
const {
16+
filterValue,
17+
setFilterValue,
18+
isFilterMode,
19+
setIsFilterMode
20+
} = useSidebar()
21+
22+
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
23+
setFilterValue(event.target.value)
24+
}
25+
26+
const handleClearFilter = () => {
27+
setFilterValue('')
28+
}
29+
30+
const handleFilterModeToggle = () => {
31+
setIsFilterMode(prev => !prev)
32+
}
33+
34+
return (
35+
<div className={cn('flex items-center space-x-2', className)}>
36+
<div className="relative flex-1">
37+
<Input
38+
type="text"
39+
placeholder="Search..."
40+
value={filterValue}
41+
onChange={handleInputChange}
42+
className="pr-12"
43+
aria-label="Filter bots"
44+
/>
45+
<IconChatSearch className="absolute -translate-y-1/2 right-2 top-1/2 text-muted-foreground" />
46+
{filterValue && (
47+
<button
48+
onClick={handleClearFilter}
49+
className="absolute -translate-y-1/2 right-8 top-1/2 text-muted-foreground hover:text-gray-700 dark:hover:text-gray-300"
50+
aria-label="Clear filter"
51+
>
52+
<IconClose className="w-4 h-4" />
53+
</button>
54+
)}
55+
</div>
56+
<Button
57+
size="icon"
58+
variant={isFilterMode ? 'default' : 'outline'}
59+
onClick={handleFilterModeToggle}
60+
aria-label="Toggle filter mode"
61+
>
62+
<IconFilter className={cn('size-4', isFilterMode && 'text-white')} />
63+
</Button>
64+
</div>
65+
)
66+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use client'
2+
3+
import React from 'react'
4+
import { FilterInput } from '@/components/layout/sidebar/sidebar-filter-input'
5+
6+
export function SidebarHeader() {
7+
8+
return (
9+
<div className="p-4 space-y-2">
10+
<FilterInput />
11+
</div>
12+
)
13+
}

0 commit comments

Comments
 (0)