File size: 11,870 Bytes
0c3e3b2
a1a6daf
 
 
0c3e3b2
 
 
 
 
 
d433b55
0c3e3b2
 
 
 
 
 
 
 
 
 
 
 
 
69c6804
20d8f14
14ef8d0
0c3e3b2
a1a6daf
 
 
0c3e3b2
a1a6daf
0c3e3b2
20d8f14
 
0c3e3b2
a1a6daf
 
0c3e3b2
a1a6daf
0c3e3b2
a1a6daf
2adc19f
0c3e3b2
 
 
 
 
 
 
 
 
 
 
 
 
d433b55
0c3e3b2
 
 
 
 
a1a6daf
0c3e3b2
 
 
 
 
 
 
 
 
 
d433b55
0c3e3b2
 
 
 
 
 
2adc19f
 
d433b55
2adc19f
 
 
 
 
 
0c3e3b2
d433b55
0c3e3b2
 
 
 
 
 
d433b55
0c3e3b2
 
 
d433b55
a1a6daf
 
 
 
 
d433b55
a1a6daf
 
 
 
0c3e3b2
 
57b36aa
0c3e3b2
 
 
 
 
 
 
 
 
 
 
888795c
0c3e3b2
 
 
 
 
888795c
 
9105b0f
942fdc7
9105b0f
31f8c3d
 
 
 
 
 
888795c
0c3e3b2
2adc19f
 
a1a6daf
2adc19f
 
 
d433b55
14ef8d0
a1a6daf
14ef8d0
 
 
 
 
 
 
 
 
 
 
 
 
 
0c3e3b2
 
20d8f14
0c3e3b2
 
 
 
c86de3d
0c3e3b2
d433b55
0c3e3b2
 
a1a6daf
0c3e3b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d433b55
0c3e3b2
 
 
 
 
 
 
 
 
 
 
 
 
d433b55
0c3e3b2
 
 
a1a6daf
0c3e3b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a1a6daf
0c3e3b2
 
 
888795c
0c3e3b2
 
 
 
a1a6daf
0c3e3b2
888795c
0c3e3b2
 
 
 
 
 
20d8f14
 
 
 
 
 
 
 
0c3e3b2
d433b55
3f9e008
9f3980a
a1a6daf
 
9f3980a
 
69c6804
 
 
2adc19f
 
ebe5cbc
0c3e3b2
a1a6daf
 
 
632d693
0c3e3b2
 
 
 
632d693
0c3e3b2
ebe5cbc
0c3e3b2
 
 
 
a29a950
0c3e3b2
 
632d693
 
 
 
 
3f9e008
632d693
0c3e3b2
 
 
a1a6daf
 
 
 
0c3e3b2
 
 
 
4065a4c
 
 
 
 
0c3e3b2
 
3f9e008
632d693
 
0c3e3b2
 
9f3980a
0c3e3b2
31f8c3d
 
 
 
 
0c3e3b2
 
 
 
 
 
 
 
 
 
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
<script lang="ts">
	import { createBubbler } from "svelte/legacy";

	const bubble = createBubbler();
	import type { PageData } from "./$types";

	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 CarbonSearch from "~icons/carbon/search";
	import Pagination from "$lib/components/Pagination.svelte";
	import { getHref } from "$lib/utils/getHref";
	import { debounce } from "$lib/utils/debounce";
	import { isDesktop } from "$lib/utils/isDesktop";
	import { SortKey } from "$lib/types/Assistant";
	import ToolLogo from "$lib/components/ToolLogo.svelte";
	import { ReviewStatus } from "$lib/types/Review";
	import { useSettingsStore } from "$lib/stores/settings";
	import { loginModalOpen } from "$lib/stores/loginModal";

	interface Props {
		data: PageData;
	}

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

	const settings = useSettingsStore();

	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 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 sortTools = () => {
		const newUrl = getHref(page.url, {
			newKeys: { sort: sortValue },
			existingKeys: { behaviour: "delete", keys: ["p"] },
		});
		goto(newUrl);
	};

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

	const goToActiveUrl = () => {
		return getHref(page.url, {
			newKeys: { active: "true" },
			existingKeys: { behaviour: "delete_except", keys: ["active", "sort"] },
		});
	};

	const goToCommunity = () => {
		return getHref(page.url, {
			existingKeys: { behaviour: "delete_except", keys: ["sort", "q"] },
		});
	};
	let activeOnly = $derived(page.url.searchParams.get("active") === "true");
	let tools = $derived(
		data.tools.filter((t) =>
			activeOnly ? data.settings.tools.some((toolId) => toolId === t._id.toString()) : true
		)
	);
	let toolsCreator = $derived(page.url.searchParams.get("user"));
	let createdByMe = $derived(data.user?.username && data.user.username === toolsCreator);
	let currentModelSupportTools = $derived(
		data.models.find((m) => m.id === $settings.activeModel)?.tools ?? false
	);
</script>

<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">Tools</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 tools"
				>
					<CarbonHelpFilled />
				</a>
			{/if}
		</div>
		<h2 class="text-gray-500">Popular tools made by the community</h2>
		<h3 class="mt-2 w-fit text-purple-700 dark:text-purple-300">
			This feature is <span
				class="rounded-lg bg-purple-100 px-2 py-1 font-semibold dark:bg-purple-800/50"
				>experimental</span
			>. Consider
			<a
				class="underline hover:text-purple-500"
				href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions/569"
				target="_blank">sharing your feedback with us!</a
			>
		</h3>
		<div class="ml-auto mt-6 flex justify-between gap-2 max-sm:flex-col sm:items-center">
			{#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 tools
				</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 tool
				</button>
			{:else}
				<a
					href={`${base}/tools/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 tool
				</a>
			{/if}
		</div>

		<div class="mb-4 mt-7 flex flex-wrap items-center gap-x-2 gap-y-3 text-sm">
			{#if toolsCreator && !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"
				>
					{toolsCreator}'s tools
					<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/{toolsCreator}"
						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 {toolsCreator}
						on HF</a
					>
				{/if}
			{:else}
				<a
					href={goToActiveUrl()}
					class="flex items-center gap-1.5 rounded-full border px-3 py-1 {activeOnly
						? '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" />
					Active ({page.data.settings?.tools?.length})
				</a>
				<a
					href={goToCommunity()}
					class="flex items-center gap-1.5 rounded-full border px-3 py-1 {!activeOnly &&
					!toolsCreator
						? '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", "active"] },
						})}
						onclick={resetFilter}
						class="flex items-center gap-1.5 truncate rounded-full border px-3 py-1 {toolsCreator &&
						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 tools by name"
				/>
			</div>
			<select
				bind:value={sortValue}
				onchange={sortTools}
				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"
				aria-label="Sort tools"
			>
				<option value={SortKey.TRENDING}>{SortKey.TRENDING}</option>
				<option value={SortKey.POPULAR}>{SortKey.POPULAR}</option>
			</select>
		</div>

		{#if !currentModelSupportTools}
			<div class="mx-auto text-center text-sm text-purple-700 dark:text-purple-300">
				You are currently not using a model that supports tools. Activate one
				<a href="{base}/models" class="underline">here</a>.
			</div>
		{/if}

		<div class="mt-4 grid grid-cols-1 gap-3 sm:gap-5 lg:grid-cols-2">
			{#each tools as tool}
				{@const isActive = (page.data.settings?.tools ?? []).includes(tool._id.toString())}
				{@const isOfficial = !tool.createdByName}
				<div
					onclick={() => goto(`${base}/tools/${tool._id.toString()}`)}
					onkeydown={(e) => e.key === "Enter" && goto(`${base}/tools/${tool._id.toString()}`)}
					role="button"
					tabindex="0"
					class="relative flex flex-row items-center gap-4 overflow-hidden text-balance rounded-xl border bg-gray-50/50 px-4 text-center shadow hover:bg-gray-50 hover:shadow-inner dark:bg-gray-950/20 dark:hover:bg-gray-950/40 max-sm:px-4 sm:h-24 {!(
						tool.review === ReviewStatus.APPROVED
					) && !isOfficial
						? ' border-red-500/30'
						: 'dark:border-gray-800/70'}"
					class:!border-blue-600={isActive}
				>
					{#key tool.color + tool.icon}
						<ToolLogo color={tool.color} icon={tool.icon} />
					{/key}
					<div class="flex h-full w-full flex-col items-start py-2 text-left">
						<span class="font-bold">
							<span class="w-full overflow-clip">
								{tool.displayName}
							</span>
							{#if isActive}
								<span
									class="mx-1.5 inline-flex items-center rounded-full bg-blue-600 px-2 py-0.5 text-xs font-semibold text-white"
									>Active</span
								>
							{/if}
						</span>
						<span class="line-clamp-1 font-mono text-xs text-gray-400">
							{tool.baseUrl ?? "Internal tool"}
						</span>

						<p class=" line-clamp-1 w-full text-sm text-gray-600 dark:text-gray-300">
							{tool.description}
						</p>

						{#if !isOfficial}
							<p class="mt-auto text-xs text-gray-400 dark:text-gray-500">
								Added by <a
									class="hover:underline"
									href="{base}/tools?user={tool.createdByName}"
									onclick={(e) => {
										e.stopPropagation();
										bubble("click");
									}}
								>
									{tool.createdByName}
								</a>
								<span class="text-gray-300"></span>
								{#if tool.useCount === 1}
									1 run
								{:else}
									{tool.useCount} runs
								{/if}
							</p>
						{:else}
							<p class="mt-auto text-xs text-purple-700 dark:text-purple-400">
								HuggingChat official tool
							</p>
						{/if}
					</div>
				</div>
			{:else}
				{#if activeOnly}
					You don't have any active tools.
				{:else}
					No tools found
				{/if}
			{/each}
		</div>

		<Pagination
			classNames="w-full flex justify-center mt-14 mb-4"
			numItemsPerPage={data.numItemsPerPage}
			numTotalItems={data.numTotalItems}
		/>
	</div>
</div>