Spaces:
Paused
Paused
| <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> | |