Skip to content

Commit 02f4de4

Browse files
authored
Merge pull request #440 from SujalXplores/feat/search-chats
Add search chats support
2 parents a0ba540 + b471273 commit 02f4de4

File tree

2 files changed

+77
-4
lines changed

2 files changed

+77
-4
lines changed

app/components/sidebar/Menu.client.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { cubicEasingFn } from '~/utils/easings';
88
import { logger } from '~/utils/logger';
99
import { HistoryItem } from './HistoryItem';
1010
import { binDates } from './date-binning';
11+
import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
1112

1213
const menuVariants = {
1314
closed: {
@@ -39,6 +40,11 @@ export function Menu() {
3940
const [open, setOpen] = useState(false);
4041
const [dialogContent, setDialogContent] = useState<DialogContent>(null);
4142

43+
const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({
44+
items: list,
45+
searchFields: ['description'],
46+
});
47+
4248
const loadEntries = useCallback(() => {
4349
if (db) {
4450
getAll(db)
@@ -115,11 +121,11 @@ export function Menu() {
115121
initial="closed"
116122
animate={open ? 'open' : 'closed'}
117123
variants={menuVariants}
118-
className="flex flex-col side-menu fixed top-0 w-[350px] h-full bg-bolt-elements-background-depth-2 border-r rounded-r-3xl border-bolt-elements-borderColor z-sidebar shadow-xl shadow-bolt-elements-sidebar-dropdownShadow text-sm"
124+
className="flex selection-accent flex-col side-menu fixed top-0 w-[350px] h-full bg-bolt-elements-background-depth-2 border-r rounded-r-3xl border-bolt-elements-borderColor z-sidebar shadow-xl shadow-bolt-elements-sidebar-dropdownShadow text-sm"
119125
>
120126
<div className="flex items-center h-[var(--header-height)]">{/* Placeholder */}</div>
121127
<div className="flex-1 flex flex-col h-full w-full overflow-hidden">
122-
<div className="p-4">
128+
<div className="p-4 select-none">
123129
<a
124130
href="/"
125131
className="flex gap-2 items-center bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme"
@@ -128,11 +134,26 @@ export function Menu() {
128134
Start new chat
129135
</a>
130136
</div>
137+
<div className="pl-4 pr-4 my-2">
138+
<div className="relative w-full">
139+
<input
140+
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
141+
type="search"
142+
placeholder="Search"
143+
onChange={handleSearchChange}
144+
aria-label="Search chats"
145+
/>
146+
</div>
147+
</div>
131148
<div className="text-bolt-elements-textPrimary font-medium pl-6 pr-5 my-2">Your Chats</div>
132149
<div className="flex-1 overflow-auto pl-4 pr-5 pb-5">
133-
{list.length === 0 && <div className="pl-2 text-bolt-elements-textTertiary">No previous conversations</div>}
150+
{filteredList.length === 0 && (
151+
<div className="pl-2 text-bolt-elements-textTertiary">
152+
{list.length === 0 ? 'No previous conversations' : 'No matches found'}
153+
</div>
154+
)}
134155
<DialogRoot open={dialogContent !== null}>
135-
{binDates(list).map(({ category, items }) => (
156+
{binDates(filteredList).map(({ category, items }) => (
136157
<div key={category} className="mt-4 first:mt-0 space-y-1">
137158
<div className="text-bolt-elements-textTertiary sticky top-0 z-1 bg-bolt-elements-background-depth-2 pl-2 pt-2 pb-1">
138159
{category}

app/lib/hooks/useSearchFilter.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useState, useMemo, useCallback } from 'react';
2+
import { debounce } from '~/utils/debounce';
3+
import type { ChatHistoryItem } from '~/lib/persistence';
4+
5+
interface UseSearchFilterOptions {
6+
items: ChatHistoryItem[];
7+
searchFields?: (keyof ChatHistoryItem)[];
8+
debounceMs?: number;
9+
}
10+
11+
export function useSearchFilter({
12+
items = [],
13+
searchFields = ['description'],
14+
debounceMs = 300,
15+
}: UseSearchFilterOptions) {
16+
const [searchQuery, setSearchQuery] = useState('');
17+
18+
const debouncedSetSearch = useCallback(debounce(setSearchQuery, debounceMs), []);
19+
20+
const handleSearchChange = useCallback(
21+
(event: React.ChangeEvent<HTMLInputElement>) => {
22+
debouncedSetSearch(event.target.value);
23+
},
24+
[debouncedSetSearch],
25+
);
26+
27+
const filteredItems = useMemo(() => {
28+
if (!searchQuery.trim()) {
29+
return items;
30+
}
31+
32+
const query = searchQuery.toLowerCase();
33+
34+
return items.filter((item) =>
35+
searchFields.some((field) => {
36+
const value = item[field];
37+
38+
if (typeof value === 'string') {
39+
return value.toLowerCase().includes(query);
40+
}
41+
42+
return false;
43+
}),
44+
);
45+
}, [items, searchQuery, searchFields]);
46+
47+
return {
48+
searchQuery,
49+
filteredItems,
50+
handleSearchChange,
51+
};
52+
}

0 commit comments

Comments
 (0)