This commit is contained in:
Timothy Jaeryang Baek
2026-04-02 22:54:17 -05:00
parent 53eadb7df7
commit 253f416de3
5 changed files with 382 additions and 344 deletions
@@ -97,11 +97,13 @@
<div <div
class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400 line-clamp-3 markdown" class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400 line-clamp-3 markdown"
> >
{@html DOMPurify.sanitize(marked.parse( {@html DOMPurify.sanitize(
sanitizeResponseContent( marked.parse(
models[selectedModelIdx]?.info?.meta?.description sanitizeResponseContent(
).replaceAll('\n', '<br>') models[selectedModelIdx]?.info?.meta?.description
))} ).replaceAll('\n', '<br>')
)
)}
</div> </div>
{#if models[selectedModelIdx]?.info?.meta?.user} {#if models[selectedModelIdx]?.info?.meta?.user}
<div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500"> <div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500">
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { marked } from 'marked'; import { marked } from 'marked';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
@@ -63,6 +62,64 @@
let tagsContainerElement; let tagsContainerElement;
let show = false; let show = false;
let triggerElement: HTMLElement | null = null;
let contentElement: HTMLElement | null = null;
let dropdownPosition = { top: 0, left: 0, width: 0 };
const portal = (node: HTMLElement) => {
document.body.appendChild(node);
return {
destroy() {
node.remove();
}
};
};
const updatePosition = () => {
if (!show || !triggerElement) return;
const rect = triggerElement.getBoundingClientRect();
dropdownPosition = {
top: rect.bottom + 2,
left: $mobile ? 8 : rect.left,
width: $mobile ? window.innerWidth - 16 : 0
};
};
const toggleOpen = () => {
show = !show;
if (show) {
searchValue = '';
listScrollTop = 0;
resetView();
updatePosition();
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
} else {
document.getElementById(`model-selector-${id}-button`)?.blur();
}
};
const handlePointerDown = (e: PointerEvent) => {
if (!show) return;
const target = e.target as Node;
if (
(triggerElement && triggerElement.contains(target)) ||
(contentElement && contentElement.contains(target))
) {
return;
}
show = false;
document.getElementById(`model-selector-${id}-button`)?.blur();
};
const handleKeydown = (e: KeyboardEvent) => {
if (show && e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
show = false;
document.getElementById(`model-selector-${id}-button`)?.blur();
}
};
let tags = []; let tags = [];
let selectedModel = ''; let selectedModel = '';
@@ -437,30 +494,26 @@
}} }}
/> />
<DropdownMenu.Root <svelte:window
bind:open={show} on:pointerdown={handlePointerDown}
onOpenChange={(open) => { on:keydown={handleKeydown}
if (open) { on:resize={updatePosition}
searchValue = ''; />
listScrollTop = 0;
resetView(); <div class="relative w-full">
} <button
}} bind:this={triggerElement}
onOpenChangeComplete={(open) => {
if (!open) {
// Replaces the old closeFocus={false} behavior - prevent focus jump back to trigger
document.getElementById(`model-selector-${id}-button`)?.blur();
}
}}
>
<DropdownMenu.Trigger
class="relative w-full {($settings?.highContrastMode ?? false) class="relative w-full {($settings?.highContrastMode ?? false)
? '' ? ''
: 'outline-hidden focus:outline-hidden'}" : 'outline-hidden focus:outline-hidden'}"
aria-label={selectedModel aria-label={selectedModel
? $i18n.t('Selected model: {{modelName}}', { modelName: selectedModel.label }) ? $i18n.t('Selected model: {{modelName}}', { modelName: selectedModel.label })
: placeholder} : placeholder}
aria-haspopup="listbox"
aria-expanded={show}
id="model-selector-{id}-button" id="model-selector-{id}-button"
type="button"
on:click={toggleOpen}
> >
<div <div
class="flex w-full text-left px-0.5 bg-transparent truncate {triggerClassName} justify-between {($settings?.highContrastMode ?? class="flex w-full text-left px-0.5 bg-transparent truncate {triggerClassName} justify-between {($settings?.highContrastMode ??
@@ -483,334 +536,313 @@
{/if} {/if}
<ChevronDown className=" self-center ml-2 size-3" strokeWidth="2.5" /> <ChevronDown className=" self-center ml-2 size-3" strokeWidth="2.5" />
</div> </div>
</DropdownMenu.Trigger> </button>
<DropdownMenu.Portal> {#if show}
<DropdownMenu.Content <div
forceMount use:portal
trapFocus={false} bind:this={contentElement}
preventScroll={false} style="position: fixed; z-index: 9999; top: {dropdownPosition.top}px; left: {dropdownPosition.left}px;{$mobile
side="bottom" ? ` width: ${dropdownPosition.width}px;`
align={$mobile ? 'center' : 'start'} : ''}"
sideOffset={2}
alignOffset={-1}
onOpenAutoFocus={(e) => {
e.preventDefault();
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
}}
> >
{#snippet child({ wrapperProps, props, open })} <div
{#if open} class="z-40 {$mobile
<div ? `w-full`
{...wrapperProps} : `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-2xl bg-white dark:bg-gray-850 dark:text-white shadow-lg outline-hidden"
style="{wrapperProps.style ?? ''}{$mobile transition:flyAndScale
? '; left: 0.5rem !important; width: calc(100vw - 1rem) !important;' >
: ''}" <slot>
> {#if searchEnabled}
<div <div class="flex items-center gap-2.5 px-4.5 pt-3.5 mb-1.5">
{...props} <Search className="size-4" strokeWidth="2.5" />
class="{props.class} z-40 {$mobile
? `w-full`
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-2xl bg-white dark:bg-gray-850 dark:text-white shadow-lg outline-hidden"
transition:flyAndScale
>
<slot>
{#if searchEnabled}
<div class="flex items-center gap-2.5 px-4.5 pt-3.5 mb-1.5">
<Search className="size-4" strokeWidth="2.5" />
<input <input
id="model-search-input" id="model-search-input"
bind:value={searchValue} bind:value={searchValue}
class="w-full text-sm bg-transparent outline-hidden" class="w-full text-sm bg-transparent outline-hidden"
placeholder={searchPlaceholder} placeholder={searchPlaceholder}
autocomplete="off" autocomplete="off"
aria-label={$i18n.t('Search In Models')} aria-label={$i18n.t('Search In Models')}
on:keydown={(e) => { on:keydown={(e) => {
if (e.code === 'Enter' && filteredItems.length > 0) { if (e.code === 'Enter' && filteredItems.length > 0) {
value = filteredItems[selectedModelIdx].value; value = filteredItems[selectedModelIdx].value;
show = false; show = false;
return; // dont need to scroll on selection return; // dont need to scroll on selection
} else if (e.code === 'ArrowDown') { } else if (e.code === 'ArrowDown') {
e.stopPropagation(); e.stopPropagation();
selectedModelIdx = Math.min( selectedModelIdx = Math.min(selectedModelIdx + 1, filteredItems.length - 1);
selectedModelIdx + 1, } else if (e.code === 'ArrowUp') {
filteredItems.length - 1 e.stopPropagation();
); selectedModelIdx = Math.max(selectedModelIdx - 1, 0);
} else if (e.code === 'ArrowUp') { } else {
e.stopPropagation(); // if the user types something, reset to the top selection.
selectedModelIdx = Math.max(selectedModelIdx - 1, 0); selectedModelIdx = 0;
} else { }
// if the user types something, reset to the top selection.
selectedModelIdx = 0;
}
const item = document.querySelector(`[data-arrow-selected="true"]`); const item = document.querySelector(`[data-arrow-selected="true"]`);
item?.scrollIntoView({ item?.scrollIntoView({
block: 'center', block: 'center',
inline: 'nearest', inline: 'nearest',
behavior: 'instant' behavior: 'instant'
}); });
}} }}
/> />
</div> </div>
{/if} {/if}
<div class="px-2"> <div class="px-2">
{#if tags && items.filter((item) => !(item.model?.info?.meta?.hidden ?? false)).length > 0} {#if tags && items.filter((item) => !(item.model?.info?.meta?.hidden ?? false)).length > 0}
<div <div
class=" flex w-full bg-white dark:bg-gray-850 overflow-x-auto scrollbar-none font-[450] mb-0.5" class=" flex w-full bg-white dark:bg-gray-850 overflow-x-auto scrollbar-none font-[450] mb-0.5"
on:wheel={(e) => { on:wheel={(e) => {
if (e.deltaY !== 0) { if (e.deltaY !== 0) {
e.preventDefault(); e.preventDefault();
e.currentTarget.scrollLeft += e.deltaY; e.currentTarget.scrollLeft += e.deltaY;
} }
}}
>
<div
class="flex gap-1 w-fit text-center text-sm rounded-full bg-transparent px-1.5 whitespace-nowrap"
bind:this={tagsContainerElement}
>
{#if items.find((item) => item.model?.connection_type === 'local') || items.find((item) => item.model?.connection_type === 'external') || items.find((item) => item.model?.direct) || tags.length > 0}
<button
class="min-w-fit outline-none px-1.5 py-0.5 {selectedTag === '' &&
selectedConnectionType === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
aria-pressed={selectedTag === '' && selectedConnectionType === ''}
on:click={() => {
selectedConnectionType = '';
selectedTag = '';
}} }}
> >
<div {$i18n.t('All')}
class="flex gap-1 w-fit text-center text-sm rounded-full bg-transparent px-1.5 whitespace-nowrap" </button>
bind:this={tagsContainerElement}
>
{#if items.find((item) => item.model?.connection_type === 'local') || items.find((item) => item.model?.connection_type === 'external') || items.find((item) => item.model?.direct) || tags.length > 0}
<button
class="min-w-fit outline-none px-1.5 py-0.5 {selectedTag === '' &&
selectedConnectionType === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
aria-pressed={selectedTag === '' && selectedConnectionType === ''}
on:click={() => {
selectedConnectionType = '';
selectedTag = '';
}}
>
{$i18n.t('All')}
</button>
{/if}
{#if items.find((item) => item.model?.connection_type === 'local')}
<button
class="min-w-fit outline-none px-1.5 py-0.5 {selectedConnectionType ===
'local'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
aria-pressed={selectedConnectionType === 'local'}
on:click={() => {
selectedTag = '';
selectedConnectionType = 'local';
}}
>
{$i18n.t('Local')}
</button>
{/if}
{#if items.find((item) => item.model?.connection_type === 'external')}
<button
class="min-w-fit outline-none px-1.5 py-0.5 {selectedConnectionType ===
'external'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
aria-pressed={selectedConnectionType === 'external'}
on:click={() => {
selectedTag = '';
selectedConnectionType = 'external';
}}
>
{$i18n.t('External')}
</button>
{/if}
{#if items.find((item) => item.model?.direct)}
<button
class="min-w-fit outline-none px-1.5 py-0.5 {selectedConnectionType ===
'direct'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
aria-pressed={selectedConnectionType === 'direct'}
on:click={() => {
selectedTag = '';
selectedConnectionType = 'direct';
}}
>
{$i18n.t('Direct')}
</button>
{/if}
{#each tags as tag}
<Tooltip content={tag}>
<button
class="min-w-fit outline-none px-1.5 py-0.5 {selectedTag === tag
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
aria-pressed={selectedTag === tag}
on:click={() => {
selectedConnectionType = '';
selectedTag = tag;
}}
>
{tag.length > 16 ? `${tag.slice(0, 16)}...` : tag}
</button>
</Tooltip>
{/each}
</div>
</div>
{/if}
</div>
<div class="px-2.5 group relative">
{#if filteredItems.length === 0}
{#if items.length === 0 && $user?.role === 'admin'}
<div class="flex flex-col items-start justify-center py-6 px-4 text-start">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1">
{$i18n.t('No models available')}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mb-4">
{$i18n.t('Connect to an AI provider to start chatting')}
</div>
<a
href="/admin/settings/connections"
class="px-4 py-1.5 rounded-xl text-xs font-medium bg-gray-900 dark:bg-white text-white dark:text-gray-900 hover:bg-gray-800 dark:hover:bg-gray-100 transition"
on:click={() => {
show = false;
}}
>
{$i18n.t('Manage Connections')}
</a>
</div>
{:else}
<div class="">
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
{$i18n.t('No results found')}
</div>
</div>
{/if}
{:else}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="max-h-64 overflow-y-auto"
role="listbox"
aria-label={$i18n.t('Available models')}
bind:this={listContainer}
on:scroll={() => {
listScrollTop = listContainer.scrollTop;
}}
>
<div style="height: {visibleStart * ITEM_HEIGHT}px;" />
{#each filteredItems.slice(visibleStart, visibleEnd) as item, i (item.value)}
{@const index = visibleStart + i}
<ModelItem
{selectedModelIdx}
{item}
{index}
{value}
{pinModelHandler}
{unloadModelHandler}
{deleteModelHandler}
onClick={() => {
value = item.value;
selectedModelIdx = index;
show = false;
}}
/>
{/each}
<div style="height: {(filteredItems.length - visibleEnd) * ITEM_HEIGHT}px;" />
</div>
{/if} {/if}
{#if !(searchValue.trim() in $MODEL_DOWNLOAD_POOL) && searchValue && ollamaVersion && $user?.role === 'admin'} {#if items.find((item) => item.model?.connection_type === 'local')}
<Tooltip <button
content={$i18n.t(`Pull "{{searchValue}}" from Ollama.com`, { class="min-w-fit outline-none px-1.5 py-0.5 {selectedConnectionType ===
searchValue: searchValue 'local'
})} ? ''
placement="top-start" : 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
aria-pressed={selectedConnectionType === 'local'}
on:click={() => {
selectedTag = '';
selectedConnectionType = 'local';
}}
> >
{$i18n.t('Local')}
</button>
{/if}
{#if items.find((item) => item.model?.connection_type === 'external')}
<button
class="min-w-fit outline-none px-1.5 py-0.5 {selectedConnectionType ===
'external'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
aria-pressed={selectedConnectionType === 'external'}
on:click={() => {
selectedTag = '';
selectedConnectionType = 'external';
}}
>
{$i18n.t('External')}
</button>
{/if}
{#if items.find((item) => item.model?.direct)}
<button
class="min-w-fit outline-none px-1.5 py-0.5 {selectedConnectionType ===
'direct'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
aria-pressed={selectedConnectionType === 'direct'}
on:click={() => {
selectedTag = '';
selectedConnectionType = 'direct';
}}
>
{$i18n.t('Direct')}
</button>
{/if}
{#each tags as tag}
<Tooltip content={tag}>
<button <button
class="flex w-full 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" class="min-w-fit outline-none px-1.5 py-0.5 {selectedTag === tag
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
aria-pressed={selectedTag === tag}
on:click={() => { on:click={() => {
pullModelHandler(); selectedConnectionType = '';
selectedTag = tag;
}} }}
> >
<div class=" truncate"> {tag.length > 16 ? `${tag.slice(0, 16)}...` : tag}
{$i18n.t(`Pull "{{searchValue}}" from Ollama.com`, {
searchValue: searchValue
})}
</div>
</button> </button>
</Tooltip> </Tooltip>
{/if}
{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
<div
class="flex w-full justify-between font-medium select-none rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 rounded-xl cursor-pointer data-highlighted:bg-muted"
>
<div class="flex">
<div class="mr-2.5 translate-y-0.5">
<Spinner />
</div>
<div class="flex flex-col self-start">
<div class="flex gap-1">
<div class="line-clamp-1">
Downloading "{model}"
</div>
<div class="shrink-0">
{'pullProgress' in $MODEL_DOWNLOAD_POOL[model]
? `(${$MODEL_DOWNLOAD_POOL[model].pullProgress}%)`
: ''}
</div>
</div>
{#if 'digest' in $MODEL_DOWNLOAD_POOL[model] && $MODEL_DOWNLOAD_POOL[model].digest}
<div class="-mt-1 h-fit text-[0.7rem] dark:text-gray-500 line-clamp-1">
{$MODEL_DOWNLOAD_POOL[model].digest}
</div>
{/if}
</div>
</div>
<div class="mr-2 ml-1 translate-y-0.5">
<Tooltip content={$i18n.t('Cancel')}>
<button
class="text-gray-800 dark:text-gray-100"
aria-label={$i18n.t('Cancel download of {{model}}', { model: model })}
on:click={() => {
cancelModelPullHandler(model);
}}
>
<svg
class="w-4 h-4 text-gray-800 dark:text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18 17.94 6M18 18 6.06 6"
/>
</svg>
</button>
</Tooltip>
</div>
</div>
{/each} {/each}
</div> </div>
</div>
<div class="pb-2.5"></div> {/if}
<div class="hidden w-[42rem]" />
<div class="hidden w-[32rem]" />
</slot>
</div>
</div> </div>
{/if}
{/snippet} <div class="px-2.5 group relative">
</DropdownMenu.Content> {#if filteredItems.length === 0}
</DropdownMenu.Portal> {#if items.length === 0 && $user?.role === 'admin'}
</DropdownMenu.Root> <div class="flex flex-col items-start justify-center py-6 px-4 text-start">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1">
{$i18n.t('No models available')}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mb-4">
{$i18n.t('Connect to an AI provider to start chatting')}
</div>
<a
href="/admin/settings/connections"
class="px-4 py-1.5 rounded-xl text-xs font-medium bg-gray-900 dark:bg-white text-white dark:text-gray-900 hover:bg-gray-800 dark:hover:bg-gray-100 transition"
on:click={() => {
show = false;
}}
>
{$i18n.t('Manage Connections')}
</a>
</div>
{:else}
<div class="">
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
{$i18n.t('No results found')}
</div>
</div>
{/if}
{:else}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="max-h-64 overflow-y-auto"
role="listbox"
aria-label={$i18n.t('Available models')}
bind:this={listContainer}
on:scroll={() => {
listScrollTop = listContainer.scrollTop;
}}
>
<div style="height: {visibleStart * ITEM_HEIGHT}px;" />
{#each filteredItems.slice(visibleStart, visibleEnd) as item, i (item.value)}
{@const index = visibleStart + i}
<ModelItem
{selectedModelIdx}
{item}
{index}
{value}
{pinModelHandler}
{unloadModelHandler}
{deleteModelHandler}
onClick={() => {
value = item.value;
selectedModelIdx = index;
show = false;
}}
/>
{/each}
<div style="height: {(filteredItems.length - visibleEnd) * ITEM_HEIGHT}px;" />
</div>
{/if}
{#if !(searchValue.trim() in $MODEL_DOWNLOAD_POOL) && searchValue && ollamaVersion && $user?.role === 'admin'}
<Tooltip
content={$i18n.t(`Pull "{{searchValue}}" from Ollama.com`, {
searchValue: searchValue
})}
placement="top-start"
>
<button
class="flex w-full 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"
on:click={() => {
pullModelHandler();
}}
>
<div class=" truncate">
{$i18n.t(`Pull "{{searchValue}}" from Ollama.com`, {
searchValue: searchValue
})}
</div>
</button>
</Tooltip>
{/if}
{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
<div
class="flex w-full justify-between font-medium select-none rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 rounded-xl cursor-pointer data-highlighted:bg-muted"
>
<div class="flex">
<div class="mr-2.5 translate-y-0.5">
<Spinner />
</div>
<div class="flex flex-col self-start">
<div class="flex gap-1">
<div class="line-clamp-1">
Downloading "{model}"
</div>
<div class="shrink-0">
{'pullProgress' in $MODEL_DOWNLOAD_POOL[model]
? `(${$MODEL_DOWNLOAD_POOL[model].pullProgress}%)`
: ''}
</div>
</div>
{#if 'digest' in $MODEL_DOWNLOAD_POOL[model] && $MODEL_DOWNLOAD_POOL[model].digest}
<div class="-mt-1 h-fit text-[0.7rem] dark:text-gray-500 line-clamp-1">
{$MODEL_DOWNLOAD_POOL[model].digest}
</div>
{/if}
</div>
</div>
<div class="mr-2 ml-1 translate-y-0.5">
<Tooltip content={$i18n.t('Cancel')}>
<button
class="text-gray-800 dark:text-gray-100"
aria-label={$i18n.t('Cancel download of {{model}}', { model: model })}
on:click={() => {
cancelModelPullHandler(model);
}}
>
<svg
class="w-4 h-4 text-gray-800 dark:text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18 17.94 6M18 18 6.06 6"
/>
</svg>
</button>
</Tooltip>
</div>
</div>
{/each}
</div>
<div class="pb-2.5"></div>
<div class="hidden w-[42rem]" />
<div class="hidden w-[32rem]" />
</slot>
</div>
</div>
{/if}
</div>
+7 -5
View File
@@ -175,11 +175,13 @@
<div <div
class="mt-0.5 px-2 text-sm font-normal text-gray-500 dark:text-gray-400 line-clamp-2 max-w-xl markdown" class="mt-0.5 px-2 text-sm font-normal text-gray-500 dark:text-gray-400 line-clamp-2 max-w-xl markdown"
> >
{@html DOMPurify.sanitize(marked.parse( {@html DOMPurify.sanitize(
sanitizeResponseContent( marked.parse(
models[selectedModelIdx]?.info?.meta?.description ?? '' sanitizeResponseContent(
).replaceAll('\n', '<br>') models[selectedModelIdx]?.info?.meta?.description ?? ''
))} ).replaceAll('\n', '<br>')
)
)}
</div> </div>
</Tooltip> </Tooltip>
+3 -1
View File
@@ -874,7 +874,9 @@
</a> </a>
{/if} {/if}
</div> </div>
<div class="flex-1 px-3.5 md:pl-0 md:pr-4.5 md:min-h-[min(42rem,calc(100dvh-10rem))] max-h-[min(42rem,calc(100dvh-10rem))] overflow-y-auto"> <div
class="flex-1 px-3.5 md:pl-0 md:pr-4.5 md:min-h-[min(42rem,calc(100dvh-10rem))] max-h-[min(42rem,calc(100dvh-10rem))] overflow-y-auto"
>
{#if selectedTab === 'general'} {#if selectedTab === 'general'}
<General <General
{getModels} {getModels}
@@ -41,9 +41,9 @@
style="white-space: pre-wrap;" style="white-space: pre-wrap;"
> >
{#if ($config?.ui?.pending_user_overlay_content ?? '').trim() !== ''} {#if ($config?.ui?.pending_user_overlay_content ?? '').trim() !== ''}
{@html DOMPurify.sanitize(marked.parse( {@html DOMPurify.sanitize(
($config?.ui?.pending_user_overlay_content ?? '').replace(/\n/g, '<br>') marked.parse(($config?.ui?.pending_user_overlay_content ?? '').replace(/\n/g, '<br>'))
))} )}
{:else} {:else}
{$i18n.t('Your account status is currently pending activation.')}{'\n'}{$i18n.t( {$i18n.t('Your account status is currently pending activation.')}{'\n'}{$i18n.t(
'To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.' 'To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.'