File size: 12,547 Bytes
797e348
 
 
a8a9533
5a5937a
 
797e348
 
d433b55
797e348
 
b1a2ff0
f66535d
 
 
3a99eb3
5340dfb
10b8070
 
0c913b6
3a99eb3
f66535d
5340dfb
d2d38a7
9264459
c704d63
1b76365
69c6804
14ef8d0
797e348
a1a6daf
 
 
797e348
a1a6daf
 
d433b55
a1a6daf
f66535d
5340dfb
a1a6daf
 
a826c5b
a1a6daf
 
5ca011a
 
 
d433b55
5ca011a
 
 
 
 
5340dfb
797e348
d433b55
f66535d
 
 
c704d63
797e348
 
d2d38a7
c704d63
 
 
 
 
a826c5b
 
 
 
 
 
 
 
d433b55
80f57fd
 
 
5340dfb
c704d63
a1a6daf
c704d63
a826c5b
 
 
 
 
 
5340dfb
 
1b76365
d433b55
1b76365
 
 
 
 
 
d2d38a7
797e348
 
5a5937a
 
 
 
 
 
 
 
 
 
 
a8a9533
d433b55
5a5937a
d433b55
5a5937a
 
 
57b36aa
797e348
63368bc
 
b1a2ff0
 
 
 
 
 
 
 
888795c
b1a2ff0
 
 
 
63368bc
888795c
797e348
 
5a5937a
9b6bf77
a1a6daf
888795c
797e348
 
 
 
 
 
5ca011a
 
a1a6daf
5ca011a
 
 
d433b55
14ef8d0
a1a6daf
14ef8d0
 
 
 
 
 
 
 
 
 
 
 
 
 
797e348
f66535d
1b76365
f66535d
 
 
 
 
 
d433b55
5340dfb
f66535d
a1a6daf
f66535d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9a16516
f66535d
d433b55
5340dfb
f66535d
a1a6daf
f66535d
 
 
 
 
 
 
9a16516
 
d433b55
9a16516
5340dfb
9a16516
a1a6daf
5340dfb
9a16516
 
 
 
 
 
f66535d
5340dfb
06feee8
5340dfb
 
 
 
 
a826c5b
a1a6daf
5340dfb
 
 
888795c
5340dfb
 
1b76365
 
a1a6daf
888795c
a81de8e
1b76365
 
e96b46c
1b76365
f66535d
 
 
 
9264459
 
 
32b059e
 
9264459
d2d38a7
5ca011a
55af7c5
 
 
a1a6daf
d2d38a7
 
 
 
 
 
 
797e348
3a99eb3
 
 
 
 
 
9a16516
3a99eb3
9264459
10b8070
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9264459
797e348
 
22d10ff
797e348
 
 
 
 
06feee8
797e348
 
 
 
 
 
 
 
 
06feee8
797e348
 
 
5a5937a
797e348
 
f66535d
797e348
 
 
 
 
d2d38a7
797e348
 
 
 
0c913b6
 
 
 
 
797e348
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
<script lang="ts">
	import type { PageData } from "./$types";

	import { env as envPublic } from "$env/dynamic/public";
	import { isHuggingChat } from "$lib/utils/isHuggingChat";

	import { goto } from "$app/navigation";
	import { base } from "$app/paths";
	import { page } from "$app/state";

	import CarbonAdd from "~icons/carbon/add";
	import CarbonHelpFilled from "~icons/carbon/help-filled";
	import CarbonClose from "~icons/carbon/close";
	import CarbonArrowUpRight from "~icons/carbon/arrow-up-right";
	import CarbonEarthAmerica from "~icons/carbon/earth-americas-filled";
	import CarbonUserMultiple from "~icons/carbon/user-multiple";
	import CarbonSearch from "~icons/carbon/search";
	import CarbonTools from "~icons/carbon/tools";

	import Pagination from "$lib/components/Pagination.svelte";
	import { formatUserCount } from "$lib/utils/formatUserCount";
	import { getHref } from "$lib/utils/getHref";
	import { debounce } from "$lib/utils/debounce";
	import { useSettingsStore } from "$lib/stores/settings";
	import IconInternet from "$lib/components/icons/IconInternet.svelte";
	import { isDesktop } from "$lib/utils/isDesktop";
	import { SortKey } from "$lib/types/Assistant";
	import { ReviewStatus } from "$lib/types/Review";
	import { loginModalOpen } from "$lib/stores/loginModal";

	interface Props {
		data: PageData;
	}

	let { data = $bindable() }: Props = $props();

	let assistantsCreator = $derived(page.url.searchParams.get("user"));
	let createdByMe = $derived(data.user?.username && data.user.username === assistantsCreator);

	const SEARCH_DEBOUNCE_DELAY = 400;
	let filterInputEl: HTMLInputElement | undefined = $state();
	let filterValue = $state(data.query);
	let isFilterInPorgress = false;
	let sortValue = $state(data.sort as SortKey);
	let showUnfeatured = $state(data.showUnfeatured);

	const toggleShowUnfeatured = () => {
		showUnfeatured = !showUnfeatured;
		const newUrl = getHref(page.url, {
			newKeys: { showUnfeatured: showUnfeatured ? "true" : undefined },
			existingKeys: { behaviour: "delete", keys: [] },
		});
		goto(newUrl);
	};

	const onModelChange = (e: Event) => {
		const newUrl = getHref(page.url, {
			newKeys: { modelId: (e.target as HTMLSelectElement).value },
			existingKeys: { behaviour: "delete_except", keys: ["user"] },
		});
		resetFilter();
		goto(newUrl);
	};

	const resetFilter = () => {
		filterValue = "";
		isFilterInPorgress = false;
	};

	const filterOnName = debounce(async (value: string) => {
		filterValue = value;

		if (isFilterInPorgress) {
			return;
		}

		isFilterInPorgress = true;
		const newUrl = getHref(page.url, {
			newKeys: { q: value },
			existingKeys: { behaviour: "delete", keys: ["p"] },
		});
		await goto(newUrl);
		if (isDesktop(window)) {
			setTimeout(() => filterInputEl?.focus(), 0);
		}
		isFilterInPorgress = false;

		// there was a new filter query before server returned response
		if (filterValue !== value) {
			filterOnName(filterValue);
		}
	}, SEARCH_DEBOUNCE_DELAY);

	const sortAssistants = () => {
		const newUrl = getHref(page.url, {
			newKeys: { sort: sortValue },
			existingKeys: { behaviour: "delete", keys: ["p"] },
		});
		goto(newUrl);
	};

	const settings = useSettingsStore();
</script>

<svelte:head>
	{#if isHuggingChat}
		<title>HuggingChat - Assistants</title>
		<meta property="og:title" content="HuggingChat - Assistants" />
		<meta property="og:type" content="link" />
		<meta
			property="og:description"
			content="Browse HuggingChat assistants made by the community."
		/>
		<meta
			property="og:image"
			content="{envPublic.PUBLIC_ORIGIN ||
				page.url.origin}{base}/{envPublic.PUBLIC_APP_ASSETS}/assistants-thumbnail.png"
		/>
		<meta property="og:url" content={page.url.href} />
	{/if}
</svelte:head>

<div class="scrollbar-custom h-full overflow-y-auto py-12 max-sm:pt-8 md:py-24">
	<div class="pt-42 mx-auto flex flex-col px-5 xl:w-[60rem] 2xl:w-[64rem]">
		<div class="flex items-center">
			<h1 class="text-2xl font-bold">Assistants</h1>
			{#if isHuggingChat}
				<div class="5 ml-1.5 rounded-lg text-xxs uppercase text-gray-500 dark:text-gray-500">
					beta
				</div>
				<a
					href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions/357"
					class="ml-auto dark:text-gray-400 dark:hover:text-gray-300"
					target="_blank"
					aria-label="Hub discussion about assistants"
				>
					<CarbonHelpFilled />
				</a>
			{/if}
		</div>
		<h2 class="text-gray-500">Popular assistants made by the community</h2>
		<div class="mt-6 flex justify-between gap-2 max-sm:flex-col sm:items-center">
			<select
				class="mt-1 h-[34px] rounded-lg border border-gray-300 bg-gray-50 px-2 text-sm text-gray-900 focus:border-blue-700 focus:ring-blue-700 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400"
				bind:value={data.selectedModel}
				onchange={onModelChange}
				aria-label="Filter assistants by model"
			>
				<option value="">All models</option>
				{#each data.models.filter((model) => !model.unlisted) as model}
					<option value={model.name}>{model.name}</option>
				{/each}
			</select>
			{#if data.user?.isAdmin}
				<label class="mr-auto flex items-center gap-1 text-red-500" title="Admin only feature">
					<input type="checkbox" checked={showUnfeatured} onchange={toggleShowUnfeatured} />
					Show unfeatured assistants
				</label>
			{/if}
			{#if page.data.loginRequired && !data.user}
				<button
					onclick={() => {
						$loginModalOpen = true;
					}}
					class="flex items-center gap-1 whitespace-nowrap rounded-lg border bg-white py-1 pl-1.5 pr-2.5 shadow-sm hover:bg-gray-50 hover:shadow-none dark:border-gray-600 dark:bg-gray-700 dark:hover:bg-gray-700"
				>
					<CarbonAdd />Create new assistant
				</button>
			{:else}
				<a
					href={`${base}/settings/assistants/new`}
					class="flex items-center gap-1 whitespace-nowrap rounded-lg border bg-white py-1 pl-1.5 pr-2.5 shadow-sm hover:bg-gray-50 hover:shadow-none dark:border-gray-600 dark:bg-gray-700 dark:hover:bg-gray-700"
				>
					<CarbonAdd />Create new assistant
				</a>
			{/if}
		</div>

		<div class="mt-7 flex flex-wrap items-center gap-x-2 gap-y-3 text-sm">
			{#if assistantsCreator && !createdByMe}
				<div
					class="flex items-center gap-1.5 rounded-full border border-gray-300 bg-gray-50 px-3 py-1 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
				>
					{assistantsCreator}'s Assistants
					<a
						href={getHref(page.url, {
							existingKeys: { behaviour: "delete", keys: ["user", "modelId", "p", "q"] },
						})}
						onclick={resetFilter}
						class="group"
						><CarbonClose
							class="text-xs group-hover:text-gray-800 dark:group-hover:text-gray-300"
						/></a
					>
				</div>
				{#if isHuggingChat}
					<a
						href="https://hf.co/{assistantsCreator}"
						target="_blank"
						class="ml-auto flex items-center text-xs text-gray-500 underline hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-300"
						><CarbonArrowUpRight class="mr-1 flex-none text-[0.58rem]" target="_blank" />View {assistantsCreator}
						on HF</a
					>
				{/if}
			{:else}
				<a
					href={getHref(page.url, {
						existingKeys: { behaviour: "delete", keys: ["user", "modelId", "p", "q"] },
					})}
					onclick={resetFilter}
					class="flex items-center gap-1.5 rounded-full border px-3 py-1 {!assistantsCreator
						? 'border-gray-300 bg-gray-50  dark:border-gray-600 dark:bg-gray-700 dark:text-white'
						: 'border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-300'}"
				>
					<CarbonEarthAmerica class="text-xs" />
					Community
				</a>
				{#if data.user?.username}
					<a
						href={getHref(page.url, {
							newKeys: { user: data.user.username },
							existingKeys: { behaviour: "delete", keys: ["modelId", "p", "q"] },
						})}
						onclick={resetFilter}
						class="flex items-center gap-1.5 truncate rounded-full border px-3 py-1 {assistantsCreator &&
						createdByMe
							? 'border-gray-300 bg-gray-50  dark:border-gray-600 dark:bg-gray-700 dark:text-white'
							: 'border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-300'}"
						>{data.user.username}
					</a>
				{/if}
			{/if}
			<div
				class="relative ml-auto flex h-[30px] w-40 items-center rounded-full border px-2 has-[:focus]:border-gray-400 dark:border-gray-600 sm:w-64"
			>
				<CarbonSearch class="pointer-events-none absolute left-2 text-xs text-gray-400" />
				<input
					class="h-[30px] w-full bg-transparent pl-5 focus:outline-none"
					placeholder="Filter by name"
					value={filterValue}
					oninput={(e) => filterOnName(e.currentTarget.value)}
					bind:this={filterInputEl}
					maxlength="150"
					type="search"
					aria-label="Filter assistants by name"
				/>
			</div>
			<select
				bind:value={sortValue}
				onchange={sortAssistants}
				aria-label="Sort assistants"
				class="rounded-lg border border-gray-300 bg-gray-50 px-2 py-1 text-sm text-gray-900 focus:border-blue-700 focus:ring-blue-700 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400"
			>
				<option value={SortKey.TRENDING}>{SortKey.TRENDING}</option>
				<option value={SortKey.POPULAR}>{SortKey.POPULAR}</option>
			</select>
		</div>

		<div class="mt-8 grid grid-cols-2 gap-3 sm:gap-5 md:grid-cols-3 lg:grid-cols-4">
			{#each data.assistants as assistant (assistant._id)}
				{@const hasRag =
					assistant?.rag?.allowAllDomains ||
					!!assistant?.rag?.allowedDomains?.length ||
					!!assistant?.rag?.allowedLinks?.length ||
					!!assistant?.dynamicPrompt}

				<button
					class="relative flex flex-col items-center justify-center overflow-hidden text-balance rounded-xl border bg-gray-50/50 px-4 py-6 text-center shadow hover:bg-gray-50 hover:shadow-inner dark:border-gray-800/70 dark:bg-gray-950/20 dark:hover:bg-gray-950/40 max-sm:px-4 sm:h-64 sm:pb-4 xl:pt-8
					{!(assistant.review === ReviewStatus.APPROVED) && !createdByMe && data.user?.isAdmin
						? 'border !border-red-500/30'
						: ''}"
					onclick={() => {
						if (data.settings.assistants.includes(assistant._id.toString())) {
							settings.instantSet({ activeModel: assistant._id.toString() });
							goto(`${base}` || "/");
						} else {
							goto(`${base}/assistant/${assistant._id}`);
						}
					}}
				>
					{#if assistant.userCount && assistant.userCount > 1}
						<div
							class="absolute right-3 top-3 flex items-center gap-1 text-xs text-gray-400"
							title="Number of users"
						>
							<CarbonUserMultiple class="text-xxs" />{formatUserCount(assistant.userCount)}
						</div>
					{/if}

					<div class="absolute left-3 top-3 flex items-center gap-1 text-xs text-gray-400">
						{#if assistant.tools?.length}
							<div
								class="grid size-5 place-items-center rounded-full bg-purple-500/10"
								title="This assistant uses the websearch."
							>
								<CarbonTools class="text-xs text-purple-600" />
							</div>
						{/if}
						{#if hasRag}
							<div
								class="grid size-5 place-items-center rounded-full bg-blue-500/10"
								title="This assistant uses the websearch."
							>
								<IconInternet classNames="text-sm text-blue-600" />
							</div>
						{/if}
					</div>

					{#if assistant.avatar}
						<img
							src="{base}/settings/assistants/{assistant._id}/avatar.jpg"
							alt="Avatar"
							class="mb-2 aspect-square size-12 flex-none rounded-full object-cover sm:mb-6 sm:size-20"
						/>
					{:else}
						<div
							class="mb-2 flex aspect-square size-12 flex-none items-center justify-center rounded-full bg-gray-300 text-2xl font-bold uppercase text-gray-500 dark:bg-gray-800 sm:mb-6 sm:size-20"
						>
							{assistant.name[0]}
						</div>
					{/if}
					<h3
						class="mb-2 line-clamp-2 max-w-full break-words text-center text-[.8rem] font-semibold leading-snug sm:text-sm"
					>
						{assistant.name}
					</h3>
					<p class="line-clamp-4 text-xs text-gray-700 dark:text-gray-400 sm:line-clamp-2">
						{assistant.description}
					</p>
					{#if assistant.createdByName}
						<p class="mt-auto pt-2 text-xs text-gray-400 dark:text-gray-500">
							Created by <a
								class="hover:underline"
								href="{base}/assistants?user={assistant.createdByName}"
							>
								{assistant.createdByName}
							</a>
						</p>
					{/if}
				</button>
			{:else}
				No assistants found
			{/each}
		</div>
		<Pagination
			classNames="w-full flex justify-center mt-14 mb-4"
			numItemsPerPage={data.numItemsPerPage}
			numTotalItems={data.numTotalItems}
		/>
	</div>
</div>