Thomas G. Lopes
format
2a3aa41
raw
history blame
5.46 kB
<script lang="ts">
import type { Conversation, ModelWithTokenizer } from "$lib/types.js";
import { tick } from "svelte";
import { autofocus } from "$lib/actions/autofocus.js";
import { models } from "$lib/state/models.svelte.js";
import fuzzysearch from "$lib/utils/search.js";
import { watch } from "runed";
import IconSearch from "~icons/carbon/search";
import IconStar from "~icons/carbon/star";
import IconEye from "~icons/carbon/view";
interface Props {
onModelSelect?: (model: string) => void;
onClose?: () => void;
conversation: Conversation;
}
let { onModelSelect, onClose, conversation }: Props = $props();
let backdropEl = $state<HTMLDivElement>();
let highlightIdx = $state(-1);
let ignoreCursorHighlight = $state(false);
let containerEl = $state<HTMLDivElement>();
let query = $state("");
const trending = $derived(fuzzysearch({ needle: query, haystack: models.trending, property: "id" }));
const other = $derived(fuzzysearch({ needle: query, haystack: models.nonTrending, property: "id" }));
const queried = $derived(trending.concat(other));
function getModelIdx(model: ModelWithTokenizer) {
return queried.findIndex(m => m.id === model.id);
}
const highlighted = $derived(queried[highlightIdx]);
watch(
() => queried,
(curr, prev) => {
const prevModel = prev?.[highlightIdx];
if (prevModel) {
// maintain model selection
highlightIdx = Math.max(
0,
curr.findIndex(model => model.id === prevModel?.id)
);
} else {
highlightIdx = curr.findIndex(model => model.id === conversation.model.id);
}
scrollToResult();
}
);
function selectModel(model: ModelWithTokenizer) {
onModelSelect?.(model.id);
onClose?.();
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === "Escape") {
onClose?.();
} else if (e.key === "Enter") {
if (highlighted) selectModel(highlighted);
} else if (e.key === "ArrowUp") {
if (highlightIdx > 0) highlightIdx--;
ignoreCursorHighlight = true;
} else if (e.key === "ArrowDown") {
if (highlightIdx < queried.length - 1) highlightIdx++;
ignoreCursorHighlight = true;
} else {
return;
}
e.preventDefault();
scrollToResult();
}
async function scrollToResult() {
await tick();
const highlightedEl = document.querySelector("[data-model][data-highlighted]");
highlightedEl?.scrollIntoView({ block: "nearest" });
}
function highlightRow(idx: number) {
if (ignoreCursorHighlight) return;
highlightIdx = idx;
}
function handleBackdropClick(event: MouseEvent) {
event.stopPropagation();
if (window?.getSelection()?.toString()) {
return;
}
if (event.target === backdropEl) {
onClose?.();
}
}
</script>
<svelte:window onkeydown={handleKeydown} onmousemove={() => (ignoreCursorHighlight = false)} />
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="fixed inset-0 z-10 flex h-screen items-start justify-center bg-black/85 pt-32"
bind:this={backdropEl}
onclick={handleBackdropClick}
>
<div class="flex w-full max-w-[600px] items-start justify-center overflow-hidden p-10 text-left whitespace-nowrap">
<div
class="flex h-full w-full flex-col overflow-hidden rounded-lg border bg-white text-gray-900 shadow-md dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300"
bind:this={containerEl}
>
<div class="flex items-center border-b px-3 dark:border-gray-800">
<div class="mr-2 text-sm">
<IconSearch />
</div>
<input
use:autofocus
class="flex h-10 w-full rounded-md bg-transparent py-3 text-sm placeholder-gray-400 outline-hidden"
placeholder="Search models ..."
bind:value={query}
/>
</div>
<div class="max-h-[300px] overflow-x-hidden overflow-y-auto">
{#snippet modelEntry(model: ModelWithTokenizer, trending?: boolean)}
{@const idx = getModelIdx(model)}
{@const [nameSpace, modelName] = model.id.split("/")}
<button
class="flex w-full cursor-pointer items-center px-2 py-1.5 text-sm
data-[highlighted]:bg-gray-100 data-[highlighted]:dark:bg-gray-800"
data-highlighted={highlightIdx === idx ? true : undefined}
data-model
onmouseenter={() => highlightRow(idx)}
onclick={() => {
onModelSelect?.(model.id);
onClose?.();
}}
>
{#if trending}
<div class=" mr-1.5 size-4 text-yellow-400">
<IconStar />
</div>
{/if}
<span class="inline-flex items-center"
><span class="text-gray-500 dark:text-gray-400">{nameSpace}</span><span
class="mx-1 text-gray-300 dark:text-gray-700">/</span
><span class="text-black dark:text-white">{modelName}</span></span
>
{#if model.pipeline_tag === "image-text-to-text"}
<div
class="ml-2 grid size-5 place-items-center rounded bg-gray-500/10 text-gray-500 dark:bg-gray-500/20 dark:text-gray-300"
>
<IconEye class="size-3.5" />
</div>
{/if}
</button>
{/snippet}
{#if trending.length > 0}
<div class="px-2 py-1.5 text-xs font-medium text-gray-500">Trending</div>
{#each trending as model}
{@render modelEntry(model, true)}
{/each}
{/if}
{#if other.length > 0}
<div class="px-2 py-1.5 text-xs font-medium text-gray-500">Other models</div>
{#each other as model}
{@render modelEntry(model, false)}
{/each}
{/if}
</div>
</div>
</div>
</div>