mirror of
https://github.com/open-webui/open-webui
synced 2025-06-26 18:26:48 +00:00
refac: styling
This commit is contained in:
parent
7f75acff96
commit
5e91c2c1fe
264
src/lib/components/chat/ModelSelector/ModelItem.svelte
Normal file
264
src/lib/components/chat/ModelSelector/ModelItem.svelte
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
import { getContext, tick } from 'svelte';
|
||||||
|
import dayjs from '$lib/dayjs';
|
||||||
|
|
||||||
|
import { mobile, pinnedModels, user } from '$lib/stores';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
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 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-label="model-item"
|
||||||
|
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-lg 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">
|
||||||
|
{#if $mobile && (item?.model?.tags ?? []).length > 0}
|
||||||
|
<div class="flex gap-0.5 self-start h-full mb-1.5 -translate-x-1">
|
||||||
|
{#each item.model?.tags.sort((a, b) => a.name.localeCompare(b.name)) as tag}
|
||||||
|
<div
|
||||||
|
class=" text-xs font-bold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||||
|
>
|
||||||
|
{tag.name}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="flex items-center min-w-fit">
|
||||||
|
<div class="line-clamp-1">
|
||||||
|
<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 ?? '/static/favicon.png'}
|
||||||
|
alt="Model"
|
||||||
|
class="rounded-full size-5 flex items-center mr-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex items-center line-clamp-1">
|
||||||
|
<div class="line-clamp-1">
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#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?.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}
|
||||||
|
|
||||||
|
{#if !$mobile && (item?.model?.tags ?? []).length > 0}
|
||||||
|
<div
|
||||||
|
class="flex gap-0.5 self-center items-center h-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-bold px-1 rounded-sm uppercase bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||||
|
>
|
||||||
|
{tag.name}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ml-auto pl-2 pr-1 flex items-center gap-1.5">
|
||||||
|
{#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}
|
||||||
|
toggleSidebarHandler={() => {
|
||||||
|
pinnedModels.set([...new Set([...$pinnedModels, item.model.id])]);
|
||||||
|
}}
|
||||||
|
copyLinkHandler={() => {
|
||||||
|
copyLinkHandler(item.model);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="flex items-center"
|
||||||
|
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>
|
79
src/lib/components/chat/ModelSelector/ModelItemMenu.svelte
Normal file
79
src/lib/components/chat/ModelSelector/ModelItemMenu.svelte
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu } from 'bits-ui';
|
||||||
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import Link from '$lib/components/icons/Link.svelte';
|
||||||
|
import Eye from '$lib/components/icons/Eye.svelte';
|
||||||
|
import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
|
||||||
|
import { pinnedModels } from '$lib/stores';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
export let model;
|
||||||
|
|
||||||
|
export let toggleSidebarHandler: Function = () => {};
|
||||||
|
export let copyLinkHandler: Function = () => {};
|
||||||
|
|
||||||
|
export let onClose: Function = () => {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenu.Root
|
||||||
|
bind:open={show}
|
||||||
|
closeFocus={false}
|
||||||
|
onOpenChange={(state) => {
|
||||||
|
if (state === false) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
typeahead={false}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<Tooltip content={$i18n.t('More')} className=" group-hover/item:opacity-100 opacity-0">
|
||||||
|
<slot />
|
||||||
|
</Tooltip>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<DropdownMenu.Content
|
||||||
|
class="w-full max-w-[180px] text-sm rounded-xl px-1 py-1.5 z-[9999999] bg-white dark:bg-gray-850 dark:text-white shadow-lg"
|
||||||
|
sideOffset={-2}
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
transition={flyAndScale}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2"
|
||||||
|
on:click={() => {
|
||||||
|
toggleSidebarHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if ($pinnedModels ?? []).includes(model?.id)}
|
||||||
|
<EyeSlash />
|
||||||
|
{:else}
|
||||||
|
<Eye />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
{#if ($pinnedModels ?? []).includes(model?.id)}
|
||||||
|
{$i18n.t('Hide from Sidebar')}
|
||||||
|
{:else}
|
||||||
|
{$i18n.t('Keep in Sidebar')}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2"
|
||||||
|
on:click={() => {
|
||||||
|
copyLinkHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link />
|
||||||
|
|
||||||
|
<div class="flex items-center">{$i18n.t('Copy Link')}</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
@ -3,12 +3,13 @@
|
|||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
|
import dayjs from '$lib/dayjs';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
import { flyAndScale } from '$lib/utils/transitions';
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
|
||||||
import Check from '$lib/components/icons/Check.svelte';
|
|
||||||
import Search from '$lib/components/icons/Search.svelte';
|
|
||||||
|
|
||||||
import { deleteModel, getOllamaVersion, pullModel, unloadModel } from '$lib/apis/ollama';
|
import { deleteModel, getOllamaVersion, pullModel, unloadModel } from '$lib/apis/ollama';
|
||||||
|
|
||||||
@ -25,14 +26,14 @@
|
|||||||
import { capitalizeFirstLetter, sanitizeResponseContent, splitStream } from '$lib/utils';
|
import { capitalizeFirstLetter, sanitizeResponseContent, splitStream } from '$lib/utils';
|
||||||
import { getModels } from '$lib/apis';
|
import { getModels } from '$lib/apis';
|
||||||
|
|
||||||
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||||
|
import Check from '$lib/components/icons/Check.svelte';
|
||||||
|
import Search from '$lib/components/icons/Search.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
import ChatBubbleOval from '$lib/components/icons/ChatBubbleOval.svelte';
|
import ChatBubbleOval from '$lib/components/icons/ChatBubbleOval.svelte';
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import dayjs from '$lib/dayjs';
|
import ModelItem from './ModelItem.svelte';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
||||||
import ArrowUpTray from '$lib/components/icons/ArrowUpTray.svelte';
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@ -494,210 +495,19 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#each filteredItems as item, index}
|
{#each filteredItems as item, index}
|
||||||
<button
|
<ModelItem
|
||||||
aria-label="model-item"
|
{selectedModelIdx}
|
||||||
class="flex 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-lg cursor-pointer data-highlighted:bg-muted {index ===
|
{item}
|
||||||
selectedModelIdx
|
{index}
|
||||||
? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent'
|
{value}
|
||||||
: ''}"
|
{unloadModelHandler}
|
||||||
data-arrow-selected={index === selectedModelIdx}
|
onClick={() => {
|
||||||
data-value={item.value}
|
|
||||||
on:click={() => {
|
|
||||||
value = item.value;
|
value = item.value;
|
||||||
selectedModelIdx = index;
|
selectedModelIdx = index;
|
||||||
|
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
{#if $mobile && (item?.model?.tags ?? []).length > 0}
|
|
||||||
<div class="flex gap-0.5 self-start h-full mb-1.5 -translate-x-1">
|
|
||||||
{#each item.model?.tags.sort((a, b) => a.name.localeCompare(b.name)) as tag}
|
|
||||||
<div
|
|
||||||
class=" text-xs font-bold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
|
||||||
>
|
|
||||||
{tag.name}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="flex items-center min-w-fit">
|
|
||||||
<div class="line-clamp-1">
|
|
||||||
<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 ?? '/static/favicon.png'}
|
|
||||||
alt="Model"
|
|
||||||
class="rounded-full size-5 flex items-center mr-2"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex items-center line-clamp-1">
|
|
||||||
<div class="line-clamp-1">
|
|
||||||
{item.label}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#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?.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}
|
|
||||||
|
|
||||||
{#if !$mobile && (item?.model?.tags ?? []).length > 0}
|
|
||||||
<div
|
|
||||||
class="flex gap-0.5 self-center items-center h-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-bold px-1 rounded-sm uppercase bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
|
||||||
>
|
|
||||||
{tag.name}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ml-auto pl-2 pr-1 flex gap-1.5 items-center">
|
|
||||||
{#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">
|
|
||||||
<button
|
|
||||||
class="flex"
|
|
||||||
on:click={() => {
|
|
||||||
unloadModelHandler(item.value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ArrowUpTray className="size-3" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if value === item.value}
|
|
||||||
<div>
|
|
||||||
<Check className="size-3" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
|
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
|
||||||
|
@ -21,7 +21,9 @@
|
|||||||
channels,
|
channels,
|
||||||
socket,
|
socket,
|
||||||
config,
|
config,
|
||||||
isApp
|
isApp,
|
||||||
|
pinnedModels,
|
||||||
|
models
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import { onMount, getContext, tick, onDestroy } from 'svelte';
|
import { onMount, getContext, tick, onDestroy } from 'svelte';
|
||||||
|
|
||||||
@ -644,6 +646,51 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if ($pinnedModels ?? []).length > 0}
|
||||||
|
<div class="py-2">
|
||||||
|
{#each $pinnedModels as modelId (modelId)}
|
||||||
|
<div class="px-1.5 flex justify-center text-gray-800 dark:text-gray-200">
|
||||||
|
<a
|
||||||
|
class="grow flex items-center space-x-3 rounded-lg px-2 py-[7px] hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||||
|
href="/?model={modelId}"
|
||||||
|
on:click={() => {
|
||||||
|
selectedChatId = null;
|
||||||
|
chatId.set('');
|
||||||
|
|
||||||
|
if ($mobile) {
|
||||||
|
showSidebar.set(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
draggable="false"
|
||||||
|
>
|
||||||
|
<div class="self-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-[1.1rem]"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M13.5 16.875h3.375m0 0h3.375m-3.375 0V13.5m0 3.375v3.375M6 10.5h2.25a2.25 2.25 0 0 0 2.25-2.25V6a2.25 2.25 0 0 0-2.25-2.25H6A2.25 2.25 0 0 0 3.75 6v2.25A2.25 2.25 0 0 0 6 10.5Zm0 9.75h2.25A2.25 2.25 0 0 0 10.5 18v-2.25a2.25 2.25 0 0 0-2.25-2.25H6a2.25 2.25 0 0 0-2.25 2.25V18A2.25 2.25 0 0 0 6 20.25Zm9.75-9.75H18a2.25 2.25 0 0 0 2.25-2.25V6A2.25 2.25 0 0 0 18 3.75h-2.25A2.25 2.25 0 0 0 13.5 6v2.25a2.25 2.25 0 0 0 2.25 2.25Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex self-center translate-y-[0.5px]">
|
||||||
|
<div class=" self-center font-medium text-sm font-primary">
|
||||||
|
{$models.find((model) => model.id === modelId)?.name ?? modelId}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="relative flex flex-col flex-1 overflow-y-auto overflow-x-hidden {$temporaryChatEnabled
|
class="relative flex flex-col flex-1 overflow-y-auto overflow-x-hidden {$temporaryChatEnabled
|
||||||
? 'opacity-20'
|
? 'opacity-20'
|
||||||
|
@ -52,6 +52,7 @@ export const pinnedChats = writable([]);
|
|||||||
export const tags = writable([]);
|
export const tags = writable([]);
|
||||||
|
|
||||||
export const models: Writable<Model[]> = writable([]);
|
export const models: Writable<Model[]> = writable([]);
|
||||||
|
export const pinnedModels = writable([]);
|
||||||
|
|
||||||
export const prompts: Writable<null | Prompt[]> = writable(null);
|
export const prompts: Writable<null | Prompt[]> = writable(null);
|
||||||
export const knowledge: Writable<null | Document[]> = writable(null);
|
export const knowledge: Writable<null | Document[]> = writable(null);
|
||||||
|
Loading…
Reference in New Issue
Block a user