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