Mishig coyotte508 HF staff nsarrazin HF staff victor HF staff commited on
Commit
10dbbd6
1 Parent(s): 714ff2c

[Assistants] Filter on names (#841)

Browse files

* [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>

src/lib/server/database.ts CHANGED
@@ -117,6 +117,7 @@ client.on("open", () => {
117
  assistants.createIndex({ userCount: 1 }).catch(console.error);
118
  assistants.createIndex({ featured: 1, userCount: -1 }).catch(console.error);
119
  assistants.createIndex({ modelId: 1, userCount: -1 }).catch(console.error);
 
120
  reports.createIndex({ assistantId: 1 }).catch(console.error);
121
  reports.createIndex({ createdBy: 1, assistantId: 1 }).catch(console.error);
122
  });
 
117
  assistants.createIndex({ userCount: 1 }).catch(console.error);
118
  assistants.createIndex({ featured: 1, userCount: -1 }).catch(console.error);
119
  assistants.createIndex({ modelId: 1, userCount: -1 }).catch(console.error);
120
+ assistants.createIndex({ searchTokens: 1 }).catch(console.error);
121
  reports.createIndex({ assistantId: 1 }).catch(console.error);
122
  reports.createIndex({ createdBy: 1, assistantId: 1 }).catch(console.error);
123
  });
src/lib/types/Assistant.ts CHANGED
@@ -14,4 +14,5 @@ export interface Assistant extends Timestamps {
14
  preprompt: string;
15
  userCount?: number;
16
  featured?: boolean;
 
17
  }
 
14
  preprompt: string;
15
  userCount?: number;
16
  featured?: boolean;
17
+ searchTokens: string[];
18
  }
src/lib/utils/debounce.ts ADDED
@@ -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 ADDED
@@ -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 CHANGED
@@ -3,6 +3,7 @@ import { ENABLE_ASSISTANTS } from "$env/static/private";
3
  import { collections } from "$lib/server/database.js";
4
  import type { Assistant } from "$lib/types/Assistant";
5
  import type { User } from "$lib/types/User";
 
6
  import { error, redirect } from "@sveltejs/kit";
7
  import type { Filter } from "mongodb";
8
 
@@ -16,6 +17,7 @@ export const load = async ({ url, locals }) => {
16
  const modelId = url.searchParams.get("modelId");
17
  const pageIndex = parseInt(url.searchParams.get("p") ?? "0");
18
  const username = url.searchParams.get("user");
 
19
  const createdByCurrentUser = locals.user?.username && locals.user.username === username;
20
 
21
  let user: Pick<User, "_id"> | null = null;
@@ -34,6 +36,7 @@ export const load = async ({ url, locals }) => {
34
  ...(modelId && { modelId }),
35
  ...(!createdByCurrentUser && { userCount: { $gt: 1 } }),
36
  ...(user ? { createdById: user._id } : { featured: true }),
 
37
  };
38
  const assistants = await collections.assistants
39
  .find(filter)
@@ -49,5 +52,6 @@ export const load = async ({ url, locals }) => {
49
  selectedModel: modelId ?? "",
50
  numTotalItems,
51
  numItemsPerPage: NUM_PER_PAGE,
 
52
  };
53
  };
 
3
  import { collections } from "$lib/server/database.js";
4
  import type { Assistant } from "$lib/types/Assistant";
5
  import type { User } from "$lib/types/User";
6
+ import { generateQueryTokens } from "$lib/utils/searchTokens.js";
7
  import { error, redirect } from "@sveltejs/kit";
8
  import type { Filter } from "mongodb";
9
 
 
17
  const modelId = url.searchParams.get("modelId");
18
  const pageIndex = parseInt(url.searchParams.get("p") ?? "0");
19
  const username = url.searchParams.get("user");
20
+ const query = url.searchParams.get("q")?.trim() ?? null;
21
  const createdByCurrentUser = locals.user?.username && locals.user.username === username;
22
 
23
  let user: Pick<User, "_id"> | null = null;
 
36
  ...(modelId && { modelId }),
37
  ...(!createdByCurrentUser && { userCount: { $gt: 1 } }),
38
  ...(user ? { createdById: user._id } : { featured: true }),
39
+ ...(query && { searchTokens: { $all: generateQueryTokens(query) } }),
40
  };
41
  const assistants = await collections.assistants
42
  .find(filter)
 
52
  selectedModel: modelId ?? "",
53
  numTotalItems,
54
  numItemsPerPage: NUM_PER_PAGE,
55
+ query,
56
  };
57
  };
src/routes/assistants/+page.svelte CHANGED
@@ -4,6 +4,7 @@
4
  import { PUBLIC_APP_ASSETS, PUBLIC_ORIGIN } from "$env/static/public";
5
  import { isHuggingChat } from "$lib/utils/isHuggingChat";
6
 
 
7
  import { goto } from "$app/navigation";
8
  import { base } from "$app/paths";
9
  import { page } from "$app/stores";
@@ -14,9 +15,11 @@
14
  import CarbonArrowUpRight from "~icons/carbon/arrow-up-right";
15
  import CarbonEarthAmerica from "~icons/carbon/earth-americas-filled";
16
  import CarbonUserMultiple from "~icons/carbon/user-multiple";
 
17
  import Pagination from "$lib/components/Pagination.svelte";
18
  import { formatUserCount } from "$lib/utils/formatUserCount";
19
  import { getHref } from "$lib/utils/getHref";
 
20
  import { useSettingsStore } from "$lib/stores/settings";
21
 
22
  export let data: PageData;
@@ -24,6 +27,10 @@
24
  $: assistantsCreator = $page.url.searchParams.get("user");
25
  $: createdByMe = data.user?.username && data.user.username === assistantsCreator;
26
 
 
 
 
 
27
  const onModelChange = (e: Event) => {
28
  const newUrl = getHref($page.url, {
29
  newKeys: { modelId: (e.target as HTMLSelectElement).value },
@@ -32,6 +39,18 @@
32
  goto(newUrl);
33
  };
34
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  const settings = useSettingsStore();
36
  </script>
37
 
@@ -99,7 +118,7 @@
99
  {assistantsCreator}'s Assistants
100
  <a
101
  href={getHref($page.url, {
102
- existingKeys: { behaviour: "delete", keys: ["user", "modelId", "p"] },
103
  })}
104
  class="group"
105
  ><CarbonClose
@@ -119,7 +138,7 @@
119
  {:else}
120
  <a
121
  href={getHref($page.url, {
122
- existingKeys: { behaviour: "delete", keys: ["user", "modelId", "p"] },
123
  })}
124
  class="flex items-center gap-1.5 rounded-full border px-3 py-1 {!assistantsCreator
125
  ? 'border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white'
@@ -132,9 +151,9 @@
132
  <a
133
  href={getHref($page.url, {
134
  newKeys: { user: data.user.username },
135
- existingKeys: { behaviour: "delete", keys: ["modelId", "p"] },
136
  })}
137
- class="flex items-center gap-1.5 rounded-full border px-3 py-1 {assistantsCreator &&
138
  createdByMe
139
  ? 'border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white'
140
  : 'border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-300'}"
@@ -142,6 +161,21 @@
142
  </a>
143
  {/if}
144
  {/if}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  </div>
146
 
147
  <div class="mt-8 grid grid-cols-2 gap-3 sm:gap-5 md:grid-cols-3 lg:grid-cols-4">
 
4
  import { PUBLIC_APP_ASSETS, PUBLIC_ORIGIN } from "$env/static/public";
5
  import { isHuggingChat } from "$lib/utils/isHuggingChat";
6
 
7
+ import { tick } from "svelte";
8
  import { goto } from "$app/navigation";
9
  import { base } from "$app/paths";
10
  import { page } from "$app/stores";
 
15
  import CarbonArrowUpRight from "~icons/carbon/arrow-up-right";
16
  import CarbonEarthAmerica from "~icons/carbon/earth-americas-filled";
17
  import CarbonUserMultiple from "~icons/carbon/user-multiple";
18
+ import CarbonSearch from "~icons/carbon/search";
19
  import Pagination from "$lib/components/Pagination.svelte";
20
  import { formatUserCount } from "$lib/utils/formatUserCount";
21
  import { getHref } from "$lib/utils/getHref";
22
+ import { debounce } from "$lib/utils/debounce";
23
  import { useSettingsStore } from "$lib/stores/settings";
24
 
25
  export let data: PageData;
 
27
  $: assistantsCreator = $page.url.searchParams.get("user");
28
  $: createdByMe = data.user?.username && data.user.username === assistantsCreator;
29
 
30
+ const SEARCH_DEBOUNCE_DELAY = 400;
31
+ let filterInputEl: HTMLInputElement;
32
+ let searchDisabled = false;
33
+
34
  const onModelChange = (e: Event) => {
35
  const newUrl = getHref($page.url, {
36
  newKeys: { modelId: (e.target as HTMLSelectElement).value },
 
39
  goto(newUrl);
40
  };
41
 
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
+
54
  const settings = useSettingsStore();
55
  </script>
56
 
 
118
  {assistantsCreator}'s Assistants
119
  <a
120
  href={getHref($page.url, {
121
+ existingKeys: { behaviour: "delete", keys: ["user", "modelId", "p", "q"] },
122
  })}
123
  class="group"
124
  ><CarbonClose
 
138
  {:else}
139
  <a
140
  href={getHref($page.url, {
141
+ existingKeys: { behaviour: "delete", keys: ["user", "modelId", "p", "q"] },
142
  })}
143
  class="flex items-center gap-1.5 rounded-full border px-3 py-1 {!assistantsCreator
144
  ? 'border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white'
 
151
  <a
152
  href={getHref($page.url, {
153
  newKeys: { user: data.user.username },
154
+ existingKeys: { behaviour: "delete", keys: ["modelId", "p", "q"] },
155
  })}
156
+ class="flex items-center gap-1.5 truncate rounded-full border px-3 py-1 {assistantsCreator &&
157
  createdByMe
158
  ? 'border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white'
159
  : 'border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-300'}"
 
161
  </a>
162
  {/if}
163
  {/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>
179
  </div>
180
 
181
  <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 CHANGED
@@ -8,6 +8,7 @@ import { z } from "zod";
8
  import { sha256 } from "$lib/utils/sha256";
9
 
10
  import sharp from "sharp";
 
11
 
12
  const newAsssistantSchema = z.object({
13
  name: z.string().min(1),
@@ -130,6 +131,7 @@ export const actions: Actions = {
130
  exampleInputs,
131
  avatar: deleteAvatar ? undefined : hash ?? assistant.avatar,
132
  updatedAt: new Date(),
 
133
  },
134
  }
135
  );
 
8
  import { sha256 } from "$lib/utils/sha256";
9
 
10
  import sharp from "sharp";
11
+ import { generateSearchTokens } from "$lib/utils/searchTokens";
12
 
13
  const newAsssistantSchema = z.object({
14
  name: z.string().min(1),
 
131
  exampleInputs,
132
  avatar: deleteAvatar ? undefined : hash ?? assistant.avatar,
133
  updatedAt: new Date(),
134
+ searchTokens: generateSearchTokens(parse.data.name),
135
  },
136
  }
137
  );
src/routes/settings/assistants/new/+page.server.ts CHANGED
@@ -7,6 +7,7 @@ import { ObjectId } from "mongodb";
7
  import { z } from "zod";
8
  import { sha256 } from "$lib/utils/sha256";
9
  import sharp from "sharp";
 
10
 
11
  const newAsssistantSchema = z.object({
12
  name: z.string().min(1),
@@ -99,6 +100,7 @@ export const actions: Actions = {
99
  updatedAt: new Date(),
100
  userCount: 1,
101
  featured: false,
 
102
  });
103
 
104
  // add insertedId to user settings
 
7
  import { z } from "zod";
8
  import { sha256 } from "$lib/utils/sha256";
9
  import sharp from "sharp";
10
+ import { generateSearchTokens } from "$lib/utils/searchTokens";
11
 
12
  const newAsssistantSchema = z.object({
13
  name: z.string().min(1),
 
100
  updatedAt: new Date(),
101
  userCount: 1,
102
  featured: false,
103
+ searchTokens: generateSearchTokens(parse.data.name),
104
  });
105
 
106
  // add insertedId to user settings