enzostvs's picture
enzostvs HF Staff
think process
b9ff581
<script lang="ts">
import type { Snippet } from 'svelte';
import { Check, ExternalLink, Frown, MessageCircle, Plus, Save, X } from '@lucide/svelte';
import type { ChatModel } from '$lib/helpers/types';
import { formatPricingPerToken, getProviderName } from '$lib/index.js';
import { Button } from '$lib/components/ui/button';
import { Slider } from '$lib/components/ui/slider';
import Input from '$lib/components/ui/input/input.svelte';
import * as Select from '$lib/components/ui/select/index.js';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import { PROVIDER_SELECTION_MODES } from '$lib/consts';
import Separator from '$lib/components/ui/separator/separator.svelte';
import Switch from '$lib/components/ui/switch/switch.svelte';
import { fade } from 'svelte/transition';
import Spinner from '../loading/Spinner.svelte';
import { modelsState, saveModelSettings } from '$lib/state/models.svelte';
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
let {
model: modelId,
children,
selectedModels,
onToggleModel
}: {
model: string;
children: Snippet;
selectedModels: string[];
onToggleModel?: (model: string) => void;
} = $props();
let model = $state<ChatModel | null>(modelsState.models.find((m) => m.id === modelId) ?? null);
let temperature = $state<number | undefined>(undefined);
let max_tokens = $state<number | undefined>(undefined);
let top_p = $state<number | undefined>(undefined);
let provider = $state<string>('auto');
let search = $state<string>('');
$effect(() => {
if (model) {
temperature = model.temperature ?? undefined;
max_tokens = model.max_tokens ?? undefined;
top_p = model.top_p ?? undefined;
provider = model.provider ?? 'auto';
}
});
let open = $state<boolean>(false);
let loading = $state<boolean>(false);
let modelsFiltered = $derived(
search
? modelsState.models.filter((m) => m.id?.toLowerCase()?.includes(search.toLowerCase()))
: modelsState.models
);
let maxContentLength = $derived(
provider === 'auto'
? model?.providers[0]?.context_length
: model?.providers.find((p) => p.provider === provider)?.context_length
);
async function handleSave() {
if (loading) return;
loading = true;
await new Promise((resolve) => setTimeout(resolve, 600));
saveModelSettings(model?.id!, { temperature, top_p, max_tokens, provider });
loading = false;
open = false;
}
</script>
<Dialog.Root bind:open>
<Dialog.Trigger>{@render children?.()}</Dialog.Trigger>
<Dialog.Content class="max-w-3xl! gap-0! p-0!">
<Dialog.Header class="mb-0 gap-1! rounded-none border-b p-5">
<Dialog.Title>Model Settings</Dialog.Title>
<Dialog.Description>Manage your model settings.</Dialog.Description>
</Dialog.Header>
<section
class="flex min-h-0 items-stretch justify-start overflow-hidden lg:max-h-[50vh] lg:min-h-[50vh]"
>
<aside
class="flex min-h-0 max-w-64 min-w-64 flex-1 flex-col overflow-hidden overflow-y-auto border-r border-border bg-accent p-3.5 max-lg:hidden dark:bg-gray-900/50"
>
<div class="min-h-0 flex-1">
<div class="relative">
<Input
type="text"
class="mb-3 text-xs!"
placeholder="Search models"
autofocus={false}
tabindex={-1}
bind:value={search}
/>
{#if search}
<Button
variant="outline"
size="icon-2xs"
class="absolute top-1.5 right-1.5"
onclick={() => (search = '')}
>
<X class="size-4" />
</Button>
{/if}
</div>
<div class="space-y-0.5">
{#if modelsFiltered.length === 0}
<div class="mt-5 flex flex-col items-center gap-2">
<p
class="flex flex-col items-center gap-2 text-center text-xs text-muted-foreground"
>
<span
class="mx-auto flex size-8 items-center justify-center rounded-full bg-linear-to-b from-blue-500/20 to-blue-500/5 text-blue-600"
>
<Frown class="size-4" />
</span>
No models found
</p>
<Button variant="outline" size="2xs" class="mt-3" onclick={() => (search = '')}
>Clear search</Button
>
</div>
{/if}
{#each modelsFiltered as mdl, index}
<button
class="{mdl.id === model?.id
? 'from-blue-500/20! to-blue-500/5! dark:from-blue-500/50! dark:to-blue-500/20!'
: 'hover:from-accent-foreground/10 hover:to-accent-foreground/0 dark:hover:from-blue-500/20! dark:hover:to-blue-500/5!'} flex w-full cursor-pointer items-center justify-between gap-2 rounded-md bg-linear-to-r from-transparent to-transparent px-2.5 py-2 text-left text-xs transition-all duration-300"
onclick={() => (model = mdl)}
>
<span class="flex items-center gap-2 truncate">
<img
src={`https://huggingface.co/api/avatars/${mdl.owned_by}`}
alt={mdl.owned_by}
class="size-4 rounded"
/>
<span class="truncate text-ellipsis">
{mdl.owned_by ?? ''}/
<span class="font-medium">
{mdl.id.split('/').pop() ?? mdl.id}
</span>
</span>
</span>
{#if selectedModels.includes(mdl.id)}
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<span
class="flex size-[18px] items-center justify-center rounded bg-indigo-500/10 text-indigo-500"
>
<MessageCircle class="size-2.5 fill-indigo-500" />
</span>
</Tooltip.Trigger>
<Tooltip.Content>
<p class="flex items-center gap-1">
<Check class="size-3.5" /> This model will be used in the chat
</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
</button>
{/each}
</div>
</div>
</aside>
<article class="min-h-0 flex-1 overflow-y-auto">
{#if model}
<header
class="mb-4 flex items-center justify-between gap-3 border-b border-border bg-white px-5 py-3 dark:bg-gray-900/50"
>
<div class="flex items-center justify-start gap-3">
<img
src={`https://huggingface.co/api/avatars/${model.owned_by}`}
alt={model.owned_by}
class="size-8 rounded-md"
/>
<div>
<p class="text-sm font-medium">{model.id.split('/').pop() ?? model.id}</p>
<p class="text-xs text-muted-foreground">by {model.owned_by}</p>
</div>
</div>
<a href={`https://huggingface.co/${model.id}`} target="_blank">
<Button variant="outline" size="icon-xs">
<ExternalLink class="size-4" />
</Button>
</a>
</header>
<main class="mt-0 space-y-5 px-4 pb-5">
<div class="rounded-lg border border-border p-3.5">
<h4 class="text-sm leading-none font-medium">Inference provider</h4>
<p class="mt-0.5 text-xs text-muted-foreground">
Choose which Inference Provider to use with this model
</p>
<Select.Root type="single" bind:value={provider}>
<Select.Trigger
class="mt-3 flex w-full items-center justify-between gap-2 capitalize"
>
<div class="flex items-center gap-2">
{#if PROVIDER_SELECTION_MODES.find((m) => m.value === provider)}
{@const mode = PROVIDER_SELECTION_MODES.find((m) => m.value === provider)!}
<div class="flex size-5 items-center justify-center rounded {mode.class}">
<mode.icon class="size-3 {mode.iconClass}" />
</div>
<p>
{mode.label}
{#if mode.description}
<span class="text-xs text-muted-foreground lowercase italic"
>({mode.description})</span
>
{/if}
</p>
{:else}
<img
src={`https://huggingface.co/api/avatars/${provider}`}
alt={provider}
class="size-4 rounded"
/>
{provider}
{/if}
</div>
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.GroupHeading>Selection mode</Select.GroupHeading>
{#each PROVIDER_SELECTION_MODES as mode}
<Select.Item value={mode.value}>
<div class="flex size-5 items-center justify-center rounded {mode.class}">
<mode.icon class="size-3 {mode.iconClass}" />
</div>
<p>
{mode.label}
{#if mode.description}
<span class="text-xs text-muted-foreground lowercase italic"
>({mode.description})</span
>
{/if}
</p>
</Select.Item>
{/each}
</Select.Group>
<Select.Separator />
<Select.Group>
<Select.GroupHeading>Specific provider</Select.GroupHeading>
{#each model.providers as provider}
{@const providerName = getProviderName(provider.provider)}
<Select.Item value={provider.provider}>
<div class="flex items-center gap-2 capitalize">
<img
src={`https://huggingface.co/api/avatars/${providerName}`}
alt={providerName}
class="size-4 rounded"
/>
{providerName}
</div>
{#if formatPricingPerToken(provider.pricing)}
<span class="text-xs text-muted-foreground">
{formatPricingPerToken(provider.pricing)}
</span>
{/if}
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
</div>
<div class="grid gap-3 rounded-lg border border-border p-3.5">
<div class="space-y-2">
<div class="flex items-center justify-between gap-2">
<div>
<h4 class="text-sm leading-none font-medium">Temperature</h4>
<p class="mt-0.5 text-xs text-muted-foreground">
Tunes the creativity vs. predictability trade-off.
</p>
</div>
<Switch
checked={temperature !== undefined}
onCheckedChange={(value) => {
if (value) {
temperature = 0.5;
} else {
temperature = undefined;
}
}}
/>
</div>
{#if temperature !== undefined}
<div class="flex items-center gap-2" transition:fade={{ duration: 100 }}>
<Slider
type="single"
bind:value={temperature}
min={0}
max={2}
step={0.01}
class="mt-2"
/>
<Input
type="number"
min={0}
max={2}
step={0.1}
bind:value={temperature}
class="h-7! w-24!"
/>
</div>
{/if}
</div>
<Separator />
<div class="space-y-2">
<div class="flex items-center justify-between gap-2">
<div>
<h4 class="text-sm leading-none font-medium">Max Tokens</h4>
<p class="mt-0.5 text-xs text-muted-foreground">
Sets the absolute limit for generated content length.
</p>
</div>
<Switch
checked={max_tokens !== undefined}
onCheckedChange={(value) => {
if (value) {
max_tokens = (maxContentLength ?? 32_000) / 2;
} else {
max_tokens = undefined;
}
}}
/>
</div>
{#if max_tokens !== undefined}
<div class="flex items-center gap-2" transition:fade={{ duration: 100 }}>
<Slider
type="single"
bind:value={max_tokens}
min={0}
max={maxContentLength ?? 32_000}
step={256}
class="mt-2"
/>
<Input
type="number"
min={0}
max={maxContentLength ?? 32_000}
step={256}
bind:value={max_tokens}
class="h-7! w-24!"
/>
</div>
{/if}
</div>
<Separator />
<div class="space-y-2">
<div class="flex items-center justify-between gap-2">
<div>
<h4 class="text-sm leading-none font-medium">Top-P</h4>
<p class="mt-0.5 text-xs text-muted-foreground">
Dynamically filters token selection by probability mass.
</p>
</div>
<Switch
checked={top_p !== undefined}
onCheckedChange={(value) => {
if (value) {
top_p = 0.5;
} else {
top_p = undefined;
}
}}
/>
</div>
{#if top_p !== undefined}
<div class="flex items-center gap-2" transition:fade={{ duration: 100 }}>
<Slider
type="single"
bind:value={top_p}
min={0}
max={2}
step={0.01}
class="mt-2"
/>
<Input
type="number"
min={0}
max={2}
step={0.1}
bind:value={top_p}
class="h-7! w-24!"
/>
</div>
{/if}
</div>
</div>
<div class="flex items-center justify-end gap-3">
<Button variant="outline" class="flex-1" onclick={() => onToggleModel?.(model?.id!)}>
<MessageCircle class="size-4" />
{#if selectedModels.includes(model?.id!)}
Remove from chat
{:else}
Use this model
{/if}
</Button>
<Button class="flex-1" onclick={handleSave}>
{#if loading}
<Spinner className="text-lg" />
Saving...
{:else}
<Save />
Save settings
{/if}
</Button>
</div>
</main>
{/if}
</article>
</section>
</Dialog.Content>
</Dialog.Root>