From def5444ea3d1bfba18f808991e471d52eda3b78e Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek <tim@openwebui.com> Date: Mon, 31 Mar 2025 16:51:42 -0700 Subject: [PATCH] feat: hide base model --- .../components/admin/Settings/Models.svelte | 170 +++++++-- .../admin/Settings/Models/ModelMenu.svelte | 116 +++++++ .../chat/ModelSelector/Selector.svelte | 324 +++++++++--------- src/lib/components/icons/Eye.svelte | 20 ++ 4 files changed, 434 insertions(+), 196 deletions(-) create mode 100644 src/lib/components/admin/Settings/Models/ModelMenu.svelte create mode 100644 src/lib/components/icons/Eye.svelte diff --git a/src/lib/components/admin/Settings/Models.svelte b/src/lib/components/admin/Settings/Models.svelte index 96ed282d5..0fa069891 100644 --- a/src/lib/components/admin/Settings/Models.svelte +++ b/src/lib/components/admin/Settings/Models.svelte @@ -29,6 +29,12 @@ import Wrench from '$lib/components/icons/Wrench.svelte'; import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte'; import ManageModelsModal from './Models/ManageModelsModal.svelte'; + import ModelMenu from '$lib/components/admin/Settings/Models/ModelMenu.svelte'; + import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte'; + import EyeSlash from '$lib/components/icons/EyeSlash.svelte'; + import Eye from '$lib/components/icons/Eye.svelte'; + + let shiftKey = false; let importFiles; let modelsImportInputElement: HTMLInputElement; @@ -146,8 +152,62 @@ ); }; + const hideModelHandler = async (model) => { + model.meta = { + ...model.meta, + hidden: !(model?.meta?.hidden ?? false) + }; + + console.log(model); + + toast.success( + model.meta.hidden + ? $i18n.t(`Model {{name}} is now hidden`, { + name: model.id + }) + : $i18n.t(`Model {{name}} is now visible`, { + name: model.id + }) + ); + + upsertModelHandler(model); + }; + + const exportModelHandler = async (model) => { + let blob = new Blob([JSON.stringify([model])], { + type: 'application/json' + }); + saveAs(blob, `${model.id}-${Date.now()}.json`); + }; + onMount(async () => { - init(); + await init(); + + const onKeyDown = (event) => { + if (event.key === 'Shift') { + shiftKey = true; + } + }; + + const onKeyUp = (event) => { + if (event.key === 'Shift') { + shiftKey = false; + } + }; + + const onBlur = () => { + shiftKey = false; + }; + + window.addEventListener('keydown', onKeyDown); + window.addEventListener('keyup', onKeyUp); + window.addEventListener('blur-sm', onBlur); + + return () => { + window.removeEventListener('keydown', onKeyDown); + window.removeEventListener('keyup', onKeyUp); + window.removeEventListener('blur-sm', onBlur); + }; }); </script> @@ -211,7 +271,10 @@ {#if models.length > 0} {#each filteredModels as model, modelIdx (model.id)} <div - class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-lg transition" + class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-lg transition {model + ?.meta?.hidden + ? 'opacity-50 dark:opacity-50' + : ''}" id="model-item-{model.id}" > <button @@ -261,41 +324,78 @@ </div> </button> <div class="flex flex-row gap-0.5 items-center self-center"> - <button - class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" - type="button" - on:click={() => { - selectedModelId = model.id; - }} - > - <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="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" - /> - </svg> - </button> - - <div class="ml-1"> - <Tooltip - content={(model?.is_active ?? true) ? $i18n.t('Enabled') : $i18n.t('Disabled')} - > - <Switch - bind:state={model.is_active} - on:change={async () => { - toggleModelHandler(model); + {#if shiftKey} + <Tooltip content={model?.meta?.hidden ? $i18n.t('Show') : $i18n.t('Hide')}> + <button + class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" + type="button" + on:click={() => { + hideModelHandler(model); }} - /> + > + {#if model?.meta?.hidden} + <EyeSlash /> + {:else} + <Eye /> + {/if} + </button> </Tooltip> - </div> + {:else} + <button + class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" + type="button" + on:click={() => { + selectedModelId = model.id; + }} + > + <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="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" + /> + </svg> + </button> + + <ModelMenu + user={$user} + {model} + exportHandler={() => { + exportModelHandler(model); + }} + hideHandler={() => { + hideModelHandler(model); + }} + onClose={() => {}} + > + <button + class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" + type="button" + > + <EllipsisHorizontal className="size-5" /> + </button> + </ModelMenu> + + <div class="ml-1"> + <Tooltip + content={(model?.is_active ?? true) ? $i18n.t('Enabled') : $i18n.t('Disabled')} + > + <Switch + bind:state={model.is_active} + on:change={async () => { + toggleModelHandler(model); + }} + /> + </Tooltip> + </div> + {/if} </div> </div> {/each} diff --git a/src/lib/components/admin/Settings/Models/ModelMenu.svelte b/src/lib/components/admin/Settings/Models/ModelMenu.svelte new file mode 100644 index 000000000..88465e42e --- /dev/null +++ b/src/lib/components/admin/Settings/Models/ModelMenu.svelte @@ -0,0 +1,116 @@ +<script lang="ts"> + import { DropdownMenu } from 'bits-ui'; + import { flyAndScale } from '$lib/utils/transitions'; + import { getContext } from 'svelte'; + + import Dropdown from '$lib/components/common/Dropdown.svelte'; + import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; + import Pencil from '$lib/components/icons/Pencil.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import Tags from '$lib/components/chat/Tags.svelte'; + import Share from '$lib/components/icons/Share.svelte'; + import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte'; + import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte'; + import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte'; + import ArrowUpCircle from '$lib/components/icons/ArrowUpCircle.svelte'; + + import { config } from '$lib/stores'; + + const i18n = getContext('i18n'); + + export let user; + export let model; + + export let exportHandler: Function; + export let hideHandler: Function; + + export let onClose: Function; + + let show = false; +</script> + +<Dropdown + bind:show + on:change={(e) => { + if (e.detail === false) { + onClose(); + } + }} +> + <Tooltip content={$i18n.t('More')}> + <slot /> + </Tooltip> + + <div slot="content"> + <DropdownMenu.Content + class="w-full max-w-[160px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm" + sideOffset={-2} + side="bottom" + align="start" + transition={flyAndScale} + > + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + hideHandler(); + }} + > + {#if model?.meta?.hidden ?? false} + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" + /> + </svg> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" + /> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" + /> + </svg> + {/if} + + <div class="flex items-center"> + {#if model?.meta?.hidden ?? false} + {$i18n.t('Show Model')} + {:else} + {$i18n.t('Hide Model')} + {/if} + </div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + exportHandler(); + }} + > + <ArrowDownTray /> + + <div class="flex items-center">{$i18n.t('Export')}</div> + </DropdownMenu.Item> + </DropdownMenu.Content> + </div> +</Dropdown> diff --git a/src/lib/components/chat/ModelSelector/Selector.svelte b/src/lib/components/chat/ModelSelector/Selector.svelte index 226f5b1bd..47f06f8f5 100644 --- a/src/lib/components/chat/ModelSelector/Selector.svelte +++ b/src/lib/components/chat/ModelSelector/Selector.svelte @@ -458,174 +458,176 @@ {/if} {#each filteredItems as item, index} - <button - aria-label="model-item" - 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 === - selectedModelIdx - ? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent' - : ''}" - data-arrow-selected={index === selectedModelIdx} - data-value={item.value} - on:click={() => { - value = item.value; - selectedModelIdx = index; + {#if !(item.model?.info?.meta?.hidden ?? false)} + <button + aria-label="model-item" + 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 === + selectedModelIdx + ? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent' + : ''}" + data-arrow-selected={index === selectedModelIdx} + data-value={item.value} + on:click={() => { + value = item.value; + selectedModelIdx = index; - 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> - - {#if item.model.owned_by === 'ollama' && (item.model.ollama?.details?.parameter_size ?? '') !== ''} - <div class="flex ml-1 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} - </div> - </Tooltip> - </div> - </div> - </div> - - <!-- {JSON.stringify(item.info)} --> - - {#if item.model?.direct} - <Tooltip content={`${'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.owned_by === 'openai'} - <Tooltip content={`${'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" - > + 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} - <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> + <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> - </div> + <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" + /> - {#if value === item.value} - <div class="ml-auto pl-2 pr-2 md:pr-0"> - <Check /> + <div class="flex items-center line-clamp-1"> + <div class="line-clamp-1"> + {item.label} + </div> + + {#if item.model.owned_by === 'ollama' && (item.model.ollama?.details?.parameter_size ?? '') !== ''} + <div class="flex ml-1 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} + </div> + </Tooltip> + </div> + </div> + </div> + + <!-- {JSON.stringify(item.info)} --> + + {#if item.model?.direct} + <Tooltip content={`${'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.owned_by === 'openai'} + <Tooltip content={`${'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> - {/if} - </button> + + {#if value === item.value} + <div class="ml-auto pl-2 pr-2 md:pr-0"> + <Check /> + </div> + {/if} + </button> + {/if} {:else} <div> <div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100"> diff --git a/src/lib/components/icons/Eye.svelte b/src/lib/components/icons/Eye.svelte new file mode 100644 index 000000000..5af95a9e7 --- /dev/null +++ b/src/lib/components/icons/Eye.svelte @@ -0,0 +1,20 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" + /> + <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /> +</svg>