Spaces:
Running
Running
[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. <[email protected]>
* 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. <[email protected]>
Co-authored-by: Nathan Sarrazin <[email protected]>
Co-authored-by: Victor Mustar <[email protected]>
- src/lib/server/database.ts +1 -0
- src/lib/types/Assistant.ts +1 -0
- src/lib/utils/debounce.ts +17 -0
- src/lib/utils/searchTokens.ts +33 -0
- src/routes/assistants/+page.server.ts +4 -0
- src/routes/assistants/+page.svelte +38 -4
- src/routes/settings/assistants/[assistantId]/edit/+page.server.ts +2 -0
- src/routes/settings/assistants/new/+page.server.ts +2 -0
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
|