123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- <script lang="ts">
- import { marked } from 'marked';
- import { getContext, tick } from 'svelte';
- import dayjs from '$lib/dayjs';
- import { mobile, settings, user } from '$lib/stores';
- import { WEBUI_BASE_URL } from '$lib/constants';
- import Tooltip from '$lib/components/common/Tooltip.svelte';
- import { copyToClipboard, sanitizeResponseContent } from '$lib/utils';
- import ArrowUpTray from '$lib/components/icons/ArrowUpTray.svelte';
- import Check from '$lib/components/icons/Check.svelte';
- import ModelItemMenu from './ModelItemMenu.svelte';
- import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
- import { toast } from 'svelte-sonner';
- import Tag from '$lib/components/icons/Tag.svelte';
- import Label from '$lib/components/icons/Label.svelte';
- const i18n = getContext('i18n');
- export let selectedModelIdx: number = -1;
- export let item: any = {};
- export let index: number = -1;
- export let value: string = '';
- export let unloadModelHandler: (modelValue: string) => void = () => {};
- export let pinModelHandler: (modelId: string) => void = () => {};
- export let onClick: () => void = () => {};
- const copyLinkHandler = async (model) => {
- const baseUrl = window.location.origin;
- const res = await copyToClipboard(`${baseUrl}/?model=${encodeURIComponent(model.id)}`);
- if (res) {
- toast.success($i18n.t('Copied link to clipboard'));
- } else {
- toast.error($i18n.t('Failed to copy link'));
- }
- };
- let showMenu = false;
- </script>
- <button
- aria-roledescription="model-item"
- aria-label={item.label}
- class="flex group/item w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl cursor-pointer data-highlighted:bg-muted {index ===
- selectedModelIdx
- ? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent'
- : ''}"
- data-arrow-selected={index === selectedModelIdx}
- data-value={item.value}
- on:click={() => {
- onClick();
- }}
- >
- <div class="flex flex-col flex-1 gap-1.5">
- <!-- {#if (item?.model?.tags ?? []).length > 0}
- <div
- class="flex gap-0.5 self-center items-start h-full w-full translate-y-[0.5px] overflow-x-auto scrollbar-none"
- >
- {#each item.model?.tags.sort((a, b) => a.name.localeCompare(b.name)) as tag}
- <Tooltip content={tag.name} className="flex-shrink-0">
- <div
- class=" text-xs font-semibold px-1 rounded-sm uppercase bg-gray-500/20 text-gray-700 dark:text-gray-200"
- >
- {tag.name}
- </div>
- </Tooltip>
- {/each}
- </div>
- {/if} -->
- <div class="flex items-center gap-2">
- <div class="flex items-center min-w-fit">
- <Tooltip content={$user?.role === 'admin' ? (item?.value ?? '') : ''} placement="top-start">
- <img
- src={item.model?.info?.meta?.profile_image_url ??
- `${WEBUI_BASE_URL}/static/favicon.png`}
- alt="Model"
- class="rounded-full size-5 flex items-center"
- />
- </Tooltip>
- </div>
- <div class="flex items-center">
- <Tooltip content={`${item.label} (${item.value})`} placement="top-start">
- <div class="line-clamp-1">
- {item.label}
- </div>
- </Tooltip>
- </div>
- <div class=" shrink-0 flex items-center gap-2">
- {#if item.model.owned_by === 'ollama'}
- {#if (item.model.ollama?.details?.parameter_size ?? '') !== ''}
- <div class="flex items-center translate-y-[0.5px]">
- <Tooltip
- content={`${
- item.model.ollama?.details?.quantization_level
- ? item.model.ollama?.details?.quantization_level + ' '
- : ''
- }${
- item.model.ollama?.size
- ? `(${(item.model.ollama?.size / 1024 ** 3).toFixed(1)}GB)`
- : ''
- }`}
- className="self-end"
- >
- <span class=" text-xs font-medium text-gray-600 dark:text-gray-400 line-clamp-1"
- >{item.model.ollama?.details?.parameter_size ?? ''}</span
- >
- </Tooltip>
- </div>
- {/if}
- {#if item.model.ollama?.expires_at && new Date(item.model.ollama?.expires_at * 1000) > new Date()}
- <div class="flex items-center translate-y-[0.5px] px-0.5">
- <Tooltip
- content={`${$i18n.t('Unloads {{FROM_NOW}}', {
- FROM_NOW: dayjs(item.model.ollama?.expires_at * 1000).fromNow()
- })}`}
- className="self-end"
- >
- <div class=" flex items-center">
- <span class="relative flex size-2">
- <span
- class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
- />
- <span class="relative inline-flex rounded-full size-2 bg-green-500" />
- </span>
- </div>
- </Tooltip>
- </div>
- {/if}
- {/if}
- <!-- {JSON.stringify(item.info)} -->
- {#if (item?.model?.tags ?? []).length > 0}
- {#key item.model.id}
- <Tooltip elementId="tags-{item.model.id}">
- <div slot="tooltip" id="tags-{item.model.id}">
- {#each item.model?.tags.sort((a, b) => a.name.localeCompare(b.name)) as tag}
- <Tooltip content={tag.name} className="flex-shrink-0">
- <div class=" text-xs font-medium rounded-sm uppercase text-white">
- {tag.name}
- </div>
- </Tooltip>
- {/each}
- </div>
- <div class="translate-y-[1px]">
- <Tag />
- </div>
- </Tooltip>
- {/key}
- {/if}
- {#if item.model?.direct}
- <Tooltip content={`${$i18n.t('Direct')}`}>
- <div class="translate-y-[1px]">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 16 16"
- fill="currentColor"
- class="size-3"
- >
- <path
- fill-rule="evenodd"
- d="M2 2.75A.75.75 0 0 1 2.75 2C8.963 2 14 7.037 14 13.25a.75.75 0 0 1-1.5 0c0-5.385-4.365-9.75-9.75-9.75A.75.75 0 0 1 2 2.75Zm0 4.5a.75.75 0 0 1 .75-.75 6.75 6.75 0 0 1 6.75 6.75.75.75 0 0 1-1.5 0C8 10.35 5.65 8 2.75 8A.75.75 0 0 1 2 7.25ZM3.5 11a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
- clip-rule="evenodd"
- />
- </svg>
- </div>
- </Tooltip>
- {:else if item.model.connection_type === 'external'}
- <Tooltip content={`${$i18n.t('External')}`}>
- <div class="translate-y-[1px]">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 16 16"
- fill="currentColor"
- class="size-3"
- >
- <path
- fill-rule="evenodd"
- d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z"
- clip-rule="evenodd"
- />
- <path
- fill-rule="evenodd"
- d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z"
- clip-rule="evenodd"
- />
- </svg>
- </div>
- </Tooltip>
- {/if}
- {#if item.model?.info?.meta?.description}
- <Tooltip
- content={`${marked.parse(
- sanitizeResponseContent(item.model?.info?.meta?.description).replaceAll('\n', '<br>')
- )}`}
- >
- <div class=" translate-y-[1px]">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- stroke-width="1.5"
- stroke="currentColor"
- class="w-4 h-4"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
- />
- </svg>
- </div>
- </Tooltip>
- {/if}
- </div>
- </div>
- </div>
- <div class="ml-auto pl-2 pr-1 flex items-center gap-1.5 shrink-0">
- {#if $user?.role === 'admin' && item.model.owned_by === 'ollama' && item.model.ollama?.expires_at && new Date(item.model.ollama?.expires_at * 1000) > new Date()}
- <Tooltip
- content={`${$i18n.t('Eject')}`}
- className="flex-shrink-0 group-hover/item:opacity-100 opacity-0 "
- >
- <button
- class="flex"
- on:click={(e) => {
- e.preventDefault();
- e.stopPropagation();
- unloadModelHandler(item.value);
- }}
- >
- <ArrowUpTray className="size-3" />
- </button>
- </Tooltip>
- {/if}
- <ModelItemMenu
- bind:show={showMenu}
- model={item.model}
- {pinModelHandler}
- copyLinkHandler={() => {
- copyLinkHandler(item.model);
- }}
- >
- <button
- aria-label={`${$i18n.t('More Options')}`}
- class="flex"
- on:click={(e) => {
- e.preventDefault();
- e.stopPropagation();
- showMenu = !showMenu;
- }}
- >
- <EllipsisHorizontal />
- </button>
- </ModelItemMenu>
- {#if value === item.value}
- <div>
- <Check className="size-3" />
- </div>
- {/if}
- </div>
- </button>
|