Merge branch 'coleam00:main' into main
Browse files
app/components/sidebar/Menu.client.tsx
CHANGED
@@ -8,6 +8,7 @@ import { cubicEasingFn } from '~/utils/easings';
|
|
8 |
import { logger } from '~/utils/logger';
|
9 |
import { HistoryItem } from './HistoryItem';
|
10 |
import { binDates } from './date-binning';
|
|
|
11 |
|
12 |
const menuVariants = {
|
13 |
closed: {
|
@@ -39,6 +40,11 @@ export function Menu() {
|
|
39 |
const [open, setOpen] = useState(false);
|
40 |
const [dialogContent, setDialogContent] = useState<DialogContent>(null);
|
41 |
|
|
|
|
|
|
|
|
|
|
|
42 |
const loadEntries = useCallback(() => {
|
43 |
if (db) {
|
44 |
getAll(db)
|
@@ -115,11 +121,11 @@ export function Menu() {
|
|
115 |
initial="closed"
|
116 |
animate={open ? 'open' : 'closed'}
|
117 |
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"
|
119 |
>
|
120 |
<div className="flex items-center h-[var(--header-height)]">{/* Placeholder */}</div>
|
121 |
<div className="flex-1 flex flex-col h-full w-full overflow-hidden">
|
122 |
-
<div className="p-4">
|
123 |
<a
|
124 |
href="/"
|
125 |
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() {
|
|
128 |
Start new chat
|
129 |
</a>
|
130 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
<div className="text-bolt-elements-textPrimary font-medium pl-6 pr-5 my-2">Your Chats</div>
|
132 |
<div className="flex-1 overflow-auto pl-4 pr-5 pb-5">
|
133 |
-
{
|
|
|
|
|
|
|
|
|
134 |
<DialogRoot open={dialogContent !== null}>
|
135 |
-
{binDates(
|
136 |
<div key={category} className="mt-4 first:mt-0 space-y-1">
|
137 |
<div className="text-bolt-elements-textTertiary sticky top-0 z-1 bg-bolt-elements-background-depth-2 pl-2 pt-2 pb-1">
|
138 |
{category}
|
|
|
8 |
import { logger } from '~/utils/logger';
|
9 |
import { HistoryItem } from './HistoryItem';
|
10 |
import { binDates } from './date-binning';
|
11 |
+
import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
|
12 |
|
13 |
const menuVariants = {
|
14 |
closed: {
|
|
|
40 |
const [open, setOpen] = useState(false);
|
41 |
const [dialogContent, setDialogContent] = useState<DialogContent>(null);
|
42 |
|
43 |
+
const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({
|
44 |
+
items: list,
|
45 |
+
searchFields: ['description'],
|
46 |
+
});
|
47 |
+
|
48 |
const loadEntries = useCallback(() => {
|
49 |
if (db) {
|
50 |
getAll(db)
|
|
|
121 |
initial="closed"
|
122 |
animate={open ? 'open' : 'closed'}
|
123 |
variants={menuVariants}
|
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"
|
125 |
>
|
126 |
<div className="flex items-center h-[var(--header-height)]">{/* Placeholder */}</div>
|
127 |
<div className="flex-1 flex flex-col h-full w-full overflow-hidden">
|
128 |
+
<div className="p-4 select-none">
|
129 |
<a
|
130 |
href="/"
|
131 |
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"
|
|
|
134 |
Start new chat
|
135 |
</a>
|
136 |
</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>
|
148 |
<div className="text-bolt-elements-textPrimary font-medium pl-6 pr-5 my-2">Your Chats</div>
|
149 |
<div className="flex-1 overflow-auto pl-4 pr-5 pb-5">
|
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 |
+
)}
|
155 |
<DialogRoot open={dialogContent !== null}>
|
156 |
+
{binDates(filteredList).map(({ category, items }) => (
|
157 |
<div key={category} className="mt-4 first:mt-0 space-y-1">
|
158 |
<div className="text-bolt-elements-textTertiary sticky top-0 z-1 bg-bolt-elements-background-depth-2 pl-2 pt-2 pb-1">
|
159 |
{category}
|
app/lib/hooks/useSearchFilter.ts
ADDED
@@ -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 |
+
}
|
app/utils/constants.ts
CHANGED
@@ -283,9 +283,9 @@ const getOllamaBaseUrl = () => {
|
|
283 |
};
|
284 |
|
285 |
async function getOllamaModels(): Promise<ModelInfo[]> {
|
286 |
-
if (typeof window === 'undefined') {
|
287 |
-
return [];
|
288 |
-
}
|
289 |
|
290 |
try {
|
291 |
const baseUrl = getOllamaBaseUrl();
|
|
|
283 |
};
|
284 |
|
285 |
async function getOllamaModels(): Promise<ModelInfo[]> {
|
286 |
+
//if (typeof window === 'undefined') {
|
287 |
+
//return [];
|
288 |
+
//}
|
289 |
|
290 |
try {
|
291 |
const baseUrl = getOllamaBaseUrl();
|