Skip to content

Commit 10dbbd6

Browse files
Mishigcoyotte508nsarrazingary149
authored
[Assistants] Filter on names (#841)
* [Assistants] Filter on names * Add `$text` index on `assistant.name` (#844) * add maxlength * better experience * use `$meta: "textScore"` * Update src/routes/assistants/+page.server.ts Co-authored-by: Eliott C. <coyotte508@gmail.com> * null, not undefined * [Assistants] Filter on names (using searchTokens) (#873) Filter with `searchTokens` * input * rm extra whitespace * hide UI before migration * rm ad-hoc migration --------- Co-authored-by: Eliott C. <coyotte508@gmail.com> Co-authored-by: Nathan Sarrazin <sarrazin.nathan@gmail.com> Co-authored-by: Victor Mustar <victor.mustar@gmail.com>
1 parent 714ff2c commit 10dbbd6

File tree

8 files changed

+98
-4
lines changed

8 files changed

+98
-4
lines changed

src/lib/server/database.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ client.on("open", () => {
117117
assistants.createIndex({ userCount: 1 }).catch(console.error);
118118
assistants.createIndex({ featured: 1, userCount: -1 }).catch(console.error);
119119
assistants.createIndex({ modelId: 1, userCount: -1 }).catch(console.error);
120+
assistants.createIndex({ searchTokens: 1 }).catch(console.error);
120121
reports.createIndex({ assistantId: 1 }).catch(console.error);
121122
reports.createIndex({ createdBy: 1, assistantId: 1 }).catch(console.error);
122123
});

src/lib/types/Assistant.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ export interface Assistant extends Timestamps {
1414
preprompt: string;
1515
userCount?: number;
1616
featured?: boolean;
17+
searchTokens: string[];
1718
}

src/lib/utils/debounce.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* A debounce function that works in both browser and Nodejs.
3+
* For pure Nodejs work, prefer the `Debouncer` class.
4+
*/
5+
export function debounce<T extends unknown[]>(
6+
callback: (...rest: T) => unknown,
7+
limit: number
8+
): (...rest: T) => void {
9+
let timer: ReturnType<typeof setTimeout>;
10+
11+
return function (...rest) {
12+
clearTimeout(timer);
13+
timer = setTimeout(() => {
14+
callback(...rest);
15+
}, limit);
16+
};
17+
}

src/lib/utils/searchTokens.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const PUNCTUATION_REGEX = /\p{P}/gu;
2+
3+
function removeDiacritics(s: string, form: "NFD" | "NFKD" = "NFD"): string {
4+
return s.normalize(form).replace(/[\u0300-\u036f]/g, "");
5+
}
6+
7+
export function generateSearchTokens(value: string): string[] {
8+
const fullTitleToken = removeDiacritics(value)
9+
.replace(PUNCTUATION_REGEX, "")
10+
.replaceAll(/\s+/g, "")
11+
.toLowerCase();
12+
return [
13+
...new Set([
14+
...removeDiacritics(value)
15+
.split(/\s+/)
16+
.map((word) => word.replace(PUNCTUATION_REGEX, "").toLowerCase())
17+
.filter((word) => word.length),
18+
...(fullTitleToken.length ? [fullTitleToken] : []),
19+
]),
20+
];
21+
}
22+
23+
function escapeForRegExp(s: string): string {
24+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
25+
}
26+
27+
export function generateQueryTokens(query: string): RegExp[] {
28+
return removeDiacritics(query)
29+
.split(/\s+/)
30+
.map((word) => word.replace(PUNCTUATION_REGEX, "").toLowerCase())
31+
.filter((word) => word.length)
32+
.map((token) => new RegExp(`^${escapeForRegExp(token)}`));
33+
}

src/routes/assistants/+page.server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ENABLE_ASSISTANTS } from "$env/static/private";
33
import { collections } from "$lib/server/database.js";
44
import type { Assistant } from "$lib/types/Assistant";
55
import type { User } from "$lib/types/User";
6+
import { generateQueryTokens } from "$lib/utils/searchTokens.js";
67
import { error, redirect } from "@sveltejs/kit";
78
import type { Filter } from "mongodb";
89

@@ -16,6 +17,7 @@ export const load = async ({ url, locals }) => {
1617
const modelId = url.searchParams.get("modelId");
1718
const pageIndex = parseInt(url.searchParams.get("p") ?? "0");
1819
const username = url.searchParams.get("user");
20+
const query = url.searchParams.get("q")?.trim() ?? null;
1921
const createdByCurrentUser = locals.user?.username && locals.user.username === username;
2022

2123
let user: Pick<User, "_id"> | null = null;
@@ -34,6 +36,7 @@ export const load = async ({ url, locals }) => {
3436
...(modelId && { modelId }),
3537
...(!createdByCurrentUser && { userCount: { $gt: 1 } }),
3638
...(user ? { createdById: user._id } : { featured: true }),
39+
...(query && { searchTokens: { $all: generateQueryTokens(query) } }),
3740
};
3841
const assistants = await collections.assistants
3942
.find(filter)
@@ -49,5 +52,6 @@ export const load = async ({ url, locals }) => {
4952
selectedModel: modelId ?? "",
5053
numTotalItems,
5154
numItemsPerPage: NUM_PER_PAGE,
55+
query,
5256
};
5357
};

src/routes/assistants/+page.svelte

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { PUBLIC_APP_ASSETS, PUBLIC_ORIGIN } from "$env/static/public";
55
import { isHuggingChat } from "$lib/utils/isHuggingChat";
66
7+
import { tick } from "svelte";
78
import { goto } from "$app/navigation";
89
import { base } from "$app/paths";
910
import { page } from "$app/stores";
@@ -14,16 +15,22 @@
1415
import CarbonArrowUpRight from "~icons/carbon/arrow-up-right";
1516
import CarbonEarthAmerica from "~icons/carbon/earth-americas-filled";
1617
import CarbonUserMultiple from "~icons/carbon/user-multiple";
18+
import CarbonSearch from "~icons/carbon/search";
1719
import Pagination from "$lib/components/Pagination.svelte";
1820
import { formatUserCount } from "$lib/utils/formatUserCount";
1921
import { getHref } from "$lib/utils/getHref";
22+
import { debounce } from "$lib/utils/debounce";
2023
import { useSettingsStore } from "$lib/stores/settings";
2124
2225
export let data: PageData;
2326
2427
$: assistantsCreator = $page.url.searchParams.get("user");
2528
$: createdByMe = data.user?.username && data.user.username === assistantsCreator;
2629
30+
const SEARCH_DEBOUNCE_DELAY = 400;
31+
let filterInputEl: HTMLInputElement;
32+
let searchDisabled = false;
33+
2734
const onModelChange = (e: Event) => {
2835
const newUrl = getHref($page.url, {
2936
newKeys: { modelId: (e.target as HTMLSelectElement).value },
@@ -32,6 +39,18 @@
3239
goto(newUrl);
3340
};
3441
42+
const filterOnName = debounce(async (e: Event) => {
43+
searchDisabled = true;
44+
const value = (e.target as HTMLInputElement).value;
45+
const newUrl = getHref($page.url, { newKeys: { q: value } });
46+
await goto(newUrl);
47+
setTimeout(async () => {
48+
searchDisabled = false;
49+
await tick();
50+
filterInputEl.focus();
51+
}, 0);
52+
}, SEARCH_DEBOUNCE_DELAY);
53+
3554
const settings = useSettingsStore();
3655
</script>
3756

@@ -99,7 +118,7 @@
99118
{assistantsCreator}'s Assistants
100119
<a
101120
href={getHref($page.url, {
102-
existingKeys: { behaviour: "delete", keys: ["user", "modelId", "p"] },
121+
existingKeys: { behaviour: "delete", keys: ["user", "modelId", "p", "q"] },
103122
})}
104123
class="group"
105124
><CarbonClose
@@ -119,7 +138,7 @@
119138
{:else}
120139
<a
121140
href={getHref($page.url, {
122-
existingKeys: { behaviour: "delete", keys: ["user", "modelId", "p"] },
141+
existingKeys: { behaviour: "delete", keys: ["user", "modelId", "p", "q"] },
123142
})}
124143
class="flex items-center gap-1.5 rounded-full border px-3 py-1 {!assistantsCreator
125144
? 'border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white'
@@ -132,16 +151,31 @@
132151
<a
133152
href={getHref($page.url, {
134153
newKeys: { user: data.user.username },
135-
existingKeys: { behaviour: "delete", keys: ["modelId", "p"] },
154+
existingKeys: { behaviour: "delete", keys: ["modelId", "p", "q"] },
136155
})}
137-
class="flex items-center gap-1.5 rounded-full border px-3 py-1 {assistantsCreator &&
156+
class="flex items-center gap-1.5 truncate rounded-full border px-3 py-1 {assistantsCreator &&
138157
createdByMe
139158
? 'border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white'
140159
: 'border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-300'}"
141160
>{data.user.username}
142161
</a>
143162
{/if}
144163
{/if}
164+
<div
165+
class="relative ml-auto flex hidden h-[30px] w-40 items-center rounded-full border px-2 has-[:focus]:border-gray-400 sm:w-64 dark:border-gray-600"
166+
>
167+
<CarbonSearch class="pointer-events-none absolute left-2 text-xs text-gray-400" />
168+
<input
169+
class="h-[30px] w-full bg-transparent pl-5 focus:outline-none"
170+
placeholder="Filter by name"
171+
value={data.query}
172+
on:input={filterOnName}
173+
bind:this={filterInputEl}
174+
maxlength="150"
175+
type="search"
176+
disabled={searchDisabled}
177+
/>
178+
</div>
145179
</div>
146180

147181
<div class="mt-8 grid grid-cols-2 gap-3 sm:gap-5 md:grid-cols-3 lg:grid-cols-4">

src/routes/settings/assistants/[assistantId]/edit/+page.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { z } from "zod";
88
import { sha256 } from "$lib/utils/sha256";
99

1010
import sharp from "sharp";
11+
import { generateSearchTokens } from "$lib/utils/searchTokens";
1112

1213
const newAsssistantSchema = z.object({
1314
name: z.string().min(1),
@@ -130,6 +131,7 @@ export const actions: Actions = {
130131
exampleInputs,
131132
avatar: deleteAvatar ? undefined : hash ?? assistant.avatar,
132133
updatedAt: new Date(),
134+
searchTokens: generateSearchTokens(parse.data.name),
133135
},
134136
}
135137
);

src/routes/settings/assistants/new/+page.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ObjectId } from "mongodb";
77
import { z } from "zod";
88
import { sha256 } from "$lib/utils/sha256";
99
import sharp from "sharp";
10+
import { generateSearchTokens } from "$lib/utils/searchTokens";
1011

1112
const newAsssistantSchema = z.object({
1213
name: z.string().min(1),
@@ -99,6 +100,7 @@ export const actions: Actions = {
99100
updatedAt: new Date(),
100101
userCount: 1,
101102
featured: false,
103+
searchTokens: generateSearchTokens(parse.data.name),
102104
});
103105

104106
// add insertedId to user settings

0 commit comments

Comments
 (0)