This commit is contained in:
Timothy Jaeryang Baek
2026-07-14 17:07:39 -04:00
parent e39ff71532
commit eb11029eac
6 changed files with 136 additions and 135 deletions
@@ -53,106 +53,103 @@
} }
</script> </script>
<DropdownMenu <div class={(filteredItems ?? []).length > 0 ? '' : 'hidden'} id="suggestions-container">
className="{(filteredItems ?? []).length > 0 <DropdownMenu className="w-72 font-sans text-xs">
? '' <div class="overflow-y-auto scrollbar-thin max-h-60">
: 'hidden'} w-72 font-sans text-xs" {#if char === '/'}
id="suggestions-container" <Prompts
> bind:this={suggestionElement}
<div class="overflow-y-auto scrollbar-thin max-h-60"> {query}
{#if char === '/'} bind:filteredItems
<Prompts onSelect={(e) => {
bind:this={suggestionElement} const { type, data } = e;
{query}
bind:filteredItems
onSelect={(e) => {
const { type, data } = e;
if (type === 'prompt') { if (type === 'prompt') {
insertTextHandler(data.content); insertTextHandler(data.content);
} }
}} }}
/> />
{:else if char === '#'} {:else if char === '#'}
<Knowledge <Knowledge
bind:this={suggestionElement} bind:this={suggestionElement}
{query} {query}
bind:filteredItems bind:filteredItems
onSelect={(e) => { onSelect={(e) => {
const { type, data } = e; const { type, data } = e;
if (type === 'knowledge') { if (type === 'knowledge') {
insertTextHandler(''); insertTextHandler('');
onUpload({ onUpload({
type: 'file', type: 'file',
data: data data: data
}); });
} else if (type === 'web') { } else if (type === 'web') {
insertTextHandler(''); insertTextHandler('');
onUpload({ onUpload({
type: 'web', type: 'web',
data: data data: data
}); });
} }
}} }}
/> />
{:else if char === '@'} {:else if char === '@'}
<Models <Models
bind:this={suggestionElement} bind:this={suggestionElement}
{query} {query}
bind:filteredItems bind:filteredItems
onSelect={(e) => { onSelect={(e) => {
const { type, data } = e; const { type, data } = e;
if (type === 'model') { if (type === 'model') {
insertTextHandler(''); insertTextHandler('');
onSelect({ onSelect({
type: 'model', type: 'model',
data: data data: data
}); });
} }
}} }}
/> />
{:else if char === '$'} {:else if char === '$'}
<Skills <Skills
bind:this={suggestionElement} bind:this={suggestionElement}
{query} {query}
bind:filteredItems bind:filteredItems
onSelect={(e) => { onSelect={(e) => {
const { type, data } = e; const { type, data } = e;
if (type === 'skill') { if (type === 'skill') {
command({ command({
id: `${data.id}|${data.name}`, id: `${data.id}|${data.name}`,
label: data.name label: data.name
}); });
onSelect({ onSelect({
type: 'skill', type: 'skill',
data: data data: data
}); });
} }
}} }}
/> />
{:else if char === ':'} {:else if char === ':'}
<Emojis <Emojis
bind:this={suggestionElement} bind:this={suggestionElement}
{query} {query}
bind:filteredItems bind:filteredItems
onSelect={(e) => { onSelect={(e) => {
const { type, data } = e; const { type, data } = e;
if (type === 'emoji') { if (type === 'emoji') {
command({ command({
id: data.name, id: data.name,
label: data.shortCodes[0] label: data.shortCodes[0]
}); });
} }
}} }}
/> />
{/if} {/if}
</div> </div>
</DropdownMenu> </DropdownMenu>
</div>
@@ -64,14 +64,15 @@
</script> </script>
{#if filteredItems.length > 0} {#if filteredItems.length > 0}
<div class="px-2 text-xs text-gray-500 py-1"> <div class="px-2 py-1 text-[11px] text-gray-500 dark:text-gray-400">
{$i18n.t('Emojis')} {$i18n.t('Emojis')}
</div> </div>
{#each filteredItems as emoji, emojiIdx} {#each filteredItems as emoji, emojiIdx}
<button <button
class="px-2.5 py-1.5 rounded-xl w-full text-left {emojiIdx === selectedIdx class="flex h-[1.6875rem] w-full items-center rounded-xl px-2 text-left text-[13px] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 {emojiIdx ===
? 'bg-gray-50 dark:bg-gray-800 selected-command-option-button' selectedIdx
? 'bg-gray-50/40 dark:bg-gray-800/40 selected-command-option-button'
: ''}" : ''}"
type="button" type="button"
on:click={() => { on:click={() => {
@@ -83,14 +84,14 @@
on:focus={() => {}} on:focus={() => {}}
data-selected={emojiIdx === selectedIdx} data-selected={emojiIdx === selectedIdx}
> >
<div class="flex items-center gap-2 text-black dark:text-gray-100"> <div class="flex min-w-0 items-center gap-2 text-black dark:text-gray-100">
<img <img
src="{WEBUI_BASE_URL}/assets/emojis/{emoji.name.toLowerCase()}.svg" src="{WEBUI_BASE_URL}/assets/emojis/{emoji.name.toLowerCase()}.svg"
alt={emoji.name} alt={emoji.name}
class="size-5 flex-shrink-0" class="size-4.5 flex-shrink-0"
loading="lazy" loading="lazy"
/> />
<div class="truncate text-sm"> <div class="truncate text-[13px]">
:{emoji.shortCodes[0]}: :{emoji.shortCodes[0]}:
</div> </div>
</div> </div>
@@ -142,7 +142,7 @@
{#if filteredItems.length > 0 || query.startsWith('http')} {#if filteredItems.length > 0 || query.startsWith('http')}
{#each filteredItems as item, idx} {#each filteredItems as item, idx}
{#if idx === 0 || item?.type !== items[idx - 1]?.type} {#if idx === 0 || item?.type !== items[idx - 1]?.type}
<div class="px-2 text-xs text-gray-500 py-1"> <div class="px-2 py-1 text-[11px] text-gray-500 dark:text-gray-400">
{#if item?.type === 'folder'} {#if item?.type === 'folder'}
{$i18n.t('Folders')} {$i18n.t('Folders')}
{:else if item?.type === 'collection'} {:else if item?.type === 'collection'}
@@ -155,9 +155,9 @@
{#if !['youtube', 'web'].includes(item.type)} {#if !['youtube', 'web'].includes(item.type)}
<button <button
class=" px-2 py-1 rounded-xl w-full text-left flex justify-between items-center {idx === class="flex h-[1.6875rem] w-full items-center justify-between rounded-xl px-2 text-left text-[13px] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 {idx ===
selectedIdx selectedIdx
? ' bg-gray-50 dark:bg-gray-800 dark:text-gray-100 selected-command-option-button' ? 'bg-gray-50/40 dark:bg-gray-800/40 dark:text-gray-100 selected-command-option-button'
: ''}" : ''}"
type="button" type="button"
on:click={() => { on:click={() => {
@@ -172,7 +172,7 @@
}} }}
data-selected={idx === selectedIdx} data-selected={idx === selectedIdx}
> >
<div class=" text-black dark:text-gray-100 flex items-center gap-1"> <div class="flex min-w-0 items-center gap-1.5 text-black dark:text-gray-100">
<Tooltip <Tooltip
content={item?.legacy content={item?.legacy
? $i18n.t('Legacy') ? $i18n.t('Legacy')
@@ -184,16 +184,16 @@
placement="top" placement="top"
> >
{#if item?.type === 'collection'} {#if item?.type === 'collection'}
<Database className="size-4" /> <Database className="size-3.5" />
{:else if item?.type === 'folder'} {:else if item?.type === 'folder'}
<Folder className="size-4" /> <Folder className="size-3.5" />
{:else} {:else}
<DocumentPage className="size-4" /> <DocumentPage className="size-3.5" />
{/if} {/if}
</Tooltip> </Tooltip>
<Tooltip content={`${decodeString(item?.name)}`} placement="top-start"> <Tooltip content={`${decodeString(item?.name)}`} placement="top-start">
<div class="line-clamp-1 flex-1"> <div class="min-w-0 flex-1 truncate">
{decodeString(item?.name)} {decodeString(item?.name)}
</div> </div>
</Tooltip> </Tooltip>
@@ -204,7 +204,7 @@
{#if isYoutubeUrl(query)} {#if isYoutubeUrl(query)}
<button <button
class="px-2 py-1 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-800 dark:text-gray-100 selected-command-option-button" class="flex h-[1.6875rem] w-full items-center rounded-xl bg-gray-50/40 px-2 text-left text-[13px] dark:bg-gray-800/40 dark:text-gray-100 selected-command-option-button"
type="button" type="button"
data-selected={selectedIdx === filteredItems.findIndex((i) => i.type === 'youtube')} data-selected={selectedIdx === filteredItems.findIndex((i) => i.type === 'youtube')}
on:click={() => { on:click={() => {
@@ -220,19 +220,19 @@
} }
}} }}
> >
<div class=" text-black dark:text-gray-100 line-clamp-1 flex items-center gap-1"> <div class="flex min-w-0 items-center gap-1.5 text-black dark:text-gray-100">
<Tooltip content={$i18n.t('YouTube')} placement="top"> <Tooltip content={$i18n.t('YouTube')} placement="top">
<Youtube className="size-4" /> <Youtube className="size-3.5" />
</Tooltip> </Tooltip>
<div class="truncate flex-1"> <div class="min-w-0 flex-1 truncate">
{query} {query}
</div> </div>
</div> </div>
</button> </button>
{:else if query.startsWith('http')} {:else if query.startsWith('http')}
<button <button
class="px-2 py-1 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-800 dark:text-gray-100 selected-command-option-button" class="flex h-[1.6875rem] w-full items-center rounded-xl bg-gray-50/40 px-2 text-left text-[13px] dark:bg-gray-800/40 dark:text-gray-100 selected-command-option-button"
type="button" type="button"
data-selected={selectedIdx === filteredItems.findIndex((i) => i.type === 'web')} data-selected={selectedIdx === filteredItems.findIndex((i) => i.type === 'web')}
on:click={() => { on:click={() => {
@@ -248,12 +248,12 @@
} }
}} }}
> >
<div class=" text-black dark:text-gray-100 line-clamp-1 flex items-center gap-1"> <div class="flex min-w-0 items-center gap-1.5 text-black dark:text-gray-100">
<Tooltip content={$i18n.t('Web')} placement="top"> <Tooltip content={$i18n.t('Web')} placement="top">
<GlobeAlt className="size-4" /> <GlobeAlt className="size-3.5" />
</Tooltip> </Tooltip>
<div class="truncate flex-1"> <div class="min-w-0 flex-1 truncate">
{query} {query}
</div> </div>
</div> </div>
@@ -60,7 +60,7 @@
}; };
</script> </script>
<div class="px-2 text-xs text-gray-500 py-1"> <div class="px-2 py-1 text-[11px] text-gray-500 dark:text-gray-400">
{$i18n.t('Models')} {$i18n.t('Models')}
</div> </div>
@@ -68,8 +68,9 @@
{#each filteredItems as model, modelIdx} {#each filteredItems as model, modelIdx}
<Tooltip content={model.id} placement="top-start"> <Tooltip content={model.id} placement="top-start">
<button <button
class="px-2.5 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx class="flex h-[1.6875rem] w-full items-center rounded-xl px-2 text-left text-[13px] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 {modelIdx ===
? 'bg-gray-50 dark:bg-gray-800 selected-command-option-button' selectedIdx
? 'bg-gray-50/40 dark:bg-gray-800/40 selected-command-option-button'
: ''}" : ''}"
type="button" type="button"
on:click={() => { on:click={() => {
@@ -81,16 +82,16 @@
on:focus={() => {}} on:focus={() => {}}
data-selected={modelIdx === selectedIdx} data-selected={modelIdx === selectedIdx}
> >
<div class="flex text-black dark:text-gray-100 line-clamp-1"> <div class="flex min-w-0 items-center text-black dark:text-gray-100">
<img <img
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`} src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
alt={model?.name ?? model.id} alt={model?.name ?? model.id}
class="rounded-full size-5 items-center mr-2" class="mr-2 size-4.5 rounded-full object-cover"
on:error={(e) => { on:error={(e) => {
e.currentTarget.src = '/favicon.png'; e.currentTarget.src = '/favicon.png';
}} }}
/> />
<div class="truncate"> <div class="min-w-0 truncate">
{model.name} {model.name}
</div> </div>
</div> </div>
@@ -57,18 +57,19 @@
}; };
</script> </script>
<div class="px-2 text-xs text-gray-500 py-1"> <div class="px-2 py-1 text-[11px] text-gray-500 dark:text-gray-400">
{$i18n.t('Prompts')} {$i18n.t('Prompts')}
</div> </div>
{#if filteredItems.length > 0} {#if filteredItems.length > 0}
<div class=" space-y-0.5 scrollbar-hidden"> <div class="scrollbar-hidden">
{#each filteredItems as promptItem, promptIdx} {#each filteredItems as promptItem, promptIdx}
<Tooltip content={promptItem.name} placement="top-start"> <Tooltip content={promptItem.name} placement="top-start">
<button <button
class=" px-3 py-1 rounded-xl w-full text-left {promptIdx === selectedPromptIdx class="flex h-[1.6875rem] w-full items-center gap-1.5 rounded-xl px-2 text-left text-[13px] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 {promptIdx ===
? ' bg-gray-50 dark:bg-gray-800 selected-command-option-button' selectedPromptIdx
: ''} truncate" ? 'bg-gray-50/40 dark:bg-gray-800/40 selected-command-option-button'
: ''}"
type="button" type="button"
on:click={() => { on:click={() => {
onSelect({ type: 'prompt', data: promptItem }); onSelect({ type: 'prompt', data: promptItem });
@@ -79,11 +80,11 @@
on:focus={() => {}} on:focus={() => {}}
data-selected={promptIdx === selectedPromptIdx} data-selected={promptIdx === selectedPromptIdx}
> >
<span class=" font-normal text-black dark:text-gray-100"> <span class="shrink-0 font-normal text-black dark:text-gray-100">
{promptItem.command} {promptItem.command}
</span> </span>
<span class=" text-xs text-gray-600 dark:text-gray-100"> <span class="min-w-0 truncate text-xs text-gray-500 dark:text-gray-400">
{promptItem.name} {promptItem.name}
</span> </span>
</button> </button>
@@ -69,7 +69,7 @@
}; };
</script> </script>
<div class="px-2 text-xs text-gray-500 py-1"> <div class="px-2 py-1 text-[11px] text-gray-500 dark:text-gray-400">
{$i18n.t('Skills')} {$i18n.t('Skills')}
</div> </div>
@@ -81,8 +81,9 @@
tippyOptions={{ maxWidth: '20rem' }} tippyOptions={{ maxWidth: '20rem' }}
> >
<button <button
class="px-2.5 py-1.5 rounded-xl w-full text-left {skillIdx === selectedIdx class="flex h-[1.6875rem] w-full items-center rounded-xl px-2 text-left text-[13px] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 {skillIdx ===
? 'bg-gray-50 dark:bg-gray-800 selected-command-option-button' selectedIdx
? 'bg-gray-50/40 dark:bg-gray-800/40 selected-command-option-button'
: ''}" : ''}"
type="button" type="button"
on:click={() => { on:click={() => {
@@ -95,13 +96,13 @@
data-selected={skillIdx === selectedIdx} data-selected={skillIdx === selectedIdx}
> >
<div class="flex w-full min-w-0 items-center text-black dark:text-gray-100"> <div class="flex w-full min-w-0 items-center text-black dark:text-gray-100">
<div class="flex items-center justify-center size-5 mr-2 shrink-0"> <div class="mr-2 flex size-4.5 shrink-0 items-center justify-center">
<Cube className="size-4" /> <Cube className="size-3.5" />
</div> </div>
<div class="truncate min-w-0 flex-1"> <div class="truncate min-w-0 flex-1">
{skill.name} {skill.name}
</div> </div>
<div class="ml-2 max-w-24 shrink-0 truncate text-xs text-gray-500"> <div class="ml-2 max-w-24 shrink-0 truncate text-xs text-gray-500 dark:text-gray-400">
{skill.id} {skill.id}
</div> </div>
</div> </div>