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(
marked.parse(
sanitizeResponseContent( sanitizeResponseContent(
models[selectedModelIdx]?.info?.meta?.description models[selectedModelIdx]?.info?.meta?.description
).replaceAll('\n', '<br>') ).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,33 +536,18 @@
{/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
forceMount
trapFocus={false}
preventScroll={false}
side="bottom"
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 })}
{#if open}
<div <div
{...wrapperProps} use:portal
style="{wrapperProps.style ?? ''}{$mobile bind:this={contentElement}
? '; left: 0.5rem !important; width: calc(100vw - 1rem) !important;' style="position: fixed; z-index: 9999; top: {dropdownPosition.top}px; left: {dropdownPosition.left}px;{$mobile
? ` width: ${dropdownPosition.width}px;`
: ''}" : ''}"
> >
<div <div
{...props} class="z-40 {$mobile
class="{props.class} z-40 {$mobile
? `w-full` ? `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" : `${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 transition:flyAndScale
@@ -533,10 +571,7 @@
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,
filteredItems.length - 1
);
} else if (e.code === 'ArrowUp') { } else if (e.code === 'ArrowUp') {
e.stopPropagation(); e.stopPropagation();
selectedModelIdx = Math.max(selectedModelIdx - 1, 0); selectedModelIdx = Math.max(selectedModelIdx - 1, 0);
@@ -810,7 +845,4 @@
</div> </div>
</div> </div>
{/if} {/if}
{/snippet} </div>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
+4 -2
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(
marked.parse(
sanitizeResponseContent( sanitizeResponseContent(
models[selectedModelIdx]?.info?.meta?.description ?? '' models[selectedModelIdx]?.info?.meta?.description ?? ''
).replaceAll('\n', '<br>') ).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.'