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