open-webui/src/lib/components/layout/Sidebar/SearchInput.svelte

322 lines
7.5 KiB
Svelte
Raw Normal View History

2024-10-15 00:31:52 +00:00
<script lang="ts">
import { getAllTags } from '$lib/apis/chats';
2024-10-15 00:31:52 +00:00
import { tags } from '$lib/stores';
import { getContext, createEventDispatcher, onMount, onDestroy, tick } from 'svelte';
import { fade } from 'svelte/transition';
2025-06-25 17:42:18 +00:00
import Search from '$lib/components/icons/Search.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
2024-10-15 00:31:52 +00:00
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
export let placeholder = '';
export let value = '';
export let showClearButton = false;
2025-05-20 19:47:41 +00:00
export let onKeydown = (e) => {};
2024-10-15 00:31:52 +00:00
let selectedIdx = 0;
let selectedOption = null;
2024-10-15 00:31:52 +00:00
let lastWord = '';
$: lastWord = value ? value.split(' ').at(-1) : value;
let options = [
{
name: 'tag:',
description: $i18n.t('search for tags')
},
{
name: 'pinned:',
description: $i18n.t('search for pinned chats')
},
{
name: 'shared:',
description: $i18n.t('search for shared chats')
},
{
name: 'archived:',
description: $i18n.t('search for archived chats')
2024-10-15 00:31:52 +00:00
}
];
let focused = false;
let loading = false;
2024-10-15 00:31:52 +00:00
let hovering = false;
2024-10-15 00:31:52 +00:00
let filteredOptions = options;
$: filteredOptions = options.filter((option) => {
return option.name.startsWith(lastWord);
});
let filteredItems = [];
$: if (lastWord && lastWord !== null) {
initItems();
}
const initItems = async () => {
console.log('initItems', lastWord);
loading = true;
await tick();
if (lastWord.startsWith('tag:')) {
filteredItems = [
2024-10-20 04:16:59 +00:00
...$tags,
{
id: 'none',
name: $i18n.t('Untagged')
}
]
.filter((tag) => {
const tagName = lastWord.slice(4);
if (tagName) {
const tagId = tagName.replace(' ', '_').toLowerCase();
if (tag.id !== tagId) {
return tag.id.startsWith(tagId);
} else {
return false;
}
2024-10-15 00:31:52 +00:00
} else {
return true;
2024-10-15 00:31:52 +00:00
}
})
.map((tag) => {
return {
id: tag.id,
name: tag.name,
type: 'tag'
};
});
} else if (lastWord.startsWith('pinned:')) {
filteredItems = [
{
id: 'true',
name: 'true',
type: 'pinned'
},
{
id: 'false',
name: 'false',
type: 'pinned'
2024-10-15 00:31:52 +00:00
}
];
} else if (lastWord.startsWith('shared:')) {
filteredItems = [
{
id: 'true',
name: 'true',
type: 'shared'
},
{
id: 'false',
name: 'false',
type: 'shared'
}
];
} else if (lastWord.startsWith('archived:')) {
filteredItems = [
{
id: 'true',
name: 'true',
type: 'archived'
},
{
id: 'false',
name: 'false',
type: 'archived'
}
];
} else {
filteredItems = [];
}
loading = false;
};
2024-10-15 00:31:52 +00:00
const initTags = async () => {
loading = true;
2025-08-06 19:15:08 +00:00
await tags.set(await getAllTags(localStorage.token));
loading = false;
};
const clearSearchInput = () => {
value = '';
dispatch('input');
};
2024-10-15 00:31:52 +00:00
</script>
2024-11-07 05:45:48 +00:00
<div class="px-1 mb-1 flex justify-center space-x-2 relative z-10" id="search-container">
2024-10-15 00:31:52 +00:00
<div class="flex w-full rounded-xl" id="chat-search">
2025-05-18 21:39:33 +00:00
<div class="self-center py-2 rounded-l-xl bg-transparent dark:text-gray-300">
2025-06-25 18:04:40 +00:00
<Search />
2024-10-15 00:31:52 +00:00
</div>
<input
2025-07-19 14:06:59 +00:00
id="search-input"
2025-03-20 20:55:13 +00:00
class="w-full rounded-r-xl py-1.5 pl-2.5 text-sm bg-transparent dark:text-gray-300 outline-hidden"
2024-10-15 00:31:52 +00:00
placeholder={placeholder ? placeholder : $i18n.t('Search')}
autocomplete="off"
2024-10-15 00:31:52 +00:00
bind:value
on:input={() => {
dispatch('input');
}}
on:focus={() => {
2025-08-06 19:15:08 +00:00
hovering = false;
2024-10-15 00:31:52 +00:00
focused = true;
initTags();
2024-10-15 00:31:52 +00:00
}}
2025-07-19 14:06:59 +00:00
on:blur={() => {
if (!hovering) {
focused = false;
}
2025-07-19 14:06:59 +00:00
}}
2024-10-15 00:31:52 +00:00
on:keydown={(e) => {
if (e.key === 'Enter') {
if (filteredItems.length > 0) {
const itemElement = document.getElementById(`search-item-${selectedIdx}`);
itemElement.click();
2024-10-15 00:31:52 +00:00
return;
}
if (filteredOptions.length > 0) {
const optionElement = document.getElementById(`search-option-${selectedIdx}`);
optionElement.click();
return;
}
}
if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIdx = Math.max(0, selectedIdx - 1);
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (filteredItems.length > 0) {
if (selectedIdx === filteredItems.length - 1) {
focused = false;
} else {
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
}
2024-10-15 00:31:52 +00:00
} else {
if (selectedIdx === filteredOptions.length - 1) {
focused = false;
} else {
selectedIdx = Math.min(selectedIdx + 1, filteredOptions.length - 1);
}
2024-10-15 00:31:52 +00:00
}
} else {
// if the user types something, reset to the top selection.
selectedIdx = 0;
}
2025-05-20 19:47:41 +00:00
if (!document.getElementById('search-options-container')) {
onKeydown(e);
}
2024-10-15 00:31:52 +00:00
}}
/>
2025-03-20 20:55:13 +00:00
{#if showClearButton && value}
2025-05-18 21:39:33 +00:00
<div class="self-center pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent">
2025-03-20 20:55:13 +00:00
<button
class="p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
on:click={clearSearchInput}
>
2025-03-20 20:55:13 +00:00
<XMark className="size-3" strokeWidth="2" />
</button>
</div>
{/if}
2024-10-15 00:31:52 +00:00
</div>
{#if focused && (filteredOptions.length > 0 || filteredItems.length > 0)}
2024-10-15 00:31:52 +00:00
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
2025-04-03 04:23:45 +00:00
class="absolute top-0 mt-8 left-0 right-1 border border-gray-100 dark:border-gray-900 bg-gray-50 dark:bg-gray-950 rounded-lg z-10 shadow-lg"
2025-05-20 19:47:41 +00:00
id="search-options-container"
2024-10-15 00:31:52 +00:00
in:fade={{ duration: 50 }}
on:mouseenter={() => {
hovering = true;
2024-10-15 00:31:52 +00:00
selectedIdx = null;
}}
on:mouseleave={() => {
hovering = false;
2025-08-06 19:15:08 +00:00
selectedIdx = 0;
2024-10-15 00:31:52 +00:00
}}
>
<div class="px-2 py-2 text-xs group">
{#if filteredItems.length > 0}
<div class="px-1 font-medium dark:text-gray-300 text-gray-700 mb-1 capitalize">
{selectedOption}
</div>
2024-10-15 00:31:52 +00:00
2024-10-20 03:34:17 +00:00
<div class="max-h-60 overflow-auto">
{#each filteredItems as item, itemIdx}
2024-10-15 00:31:52 +00:00
<button
class=" px-1.5 py-0.5 flex gap-1 hover:bg-gray-100 dark:hover:bg-gray-900 w-full rounded {selectedIdx ===
itemIdx
2024-10-15 00:31:52 +00:00
? 'bg-gray-100 dark:bg-gray-900'
: ''}"
id="search-item-{itemIdx}"
2024-10-15 01:53:29 +00:00
on:click|stopPropagation={async () => {
2024-10-15 00:31:52 +00:00
const words = value.split(' ');
words.pop();
words.push(`${item.type}:${item.id} `);
2024-10-15 00:31:52 +00:00
value = words.join(' ');
filteredItems = [];
2024-10-15 00:31:52 +00:00
dispatch('input');
}}
>
2025-02-18 02:51:40 +00:00
<div class="dark:text-gray-300 text-gray-700 font-medium line-clamp-1 shrink-0">
{item.name}
2024-10-20 03:58:19 +00:00
</div>
2024-10-15 00:31:52 +00:00
<div class=" text-gray-500 line-clamp-1">
{item.id}
2024-10-15 00:31:52 +00:00
</div>
</button>
{/each}
</div>
{:else if filteredOptions.length > 0}
<div class="px-1 font-medium dark:text-gray-300 text-gray-700 mb-1">
{$i18n.t('Search options')}
</div>
2024-10-15 00:31:52 +00:00
2024-10-20 03:34:17 +00:00
<div class=" max-h-60 overflow-auto">
2024-10-15 00:31:52 +00:00
{#each filteredOptions as option, optionIdx}
<button
class=" px-1.5 py-0.5 flex gap-1 hover:bg-gray-100 dark:hover:bg-gray-900 w-full rounded {selectedIdx ===
optionIdx
? 'bg-gray-100 dark:bg-gray-900'
: ''}"
id="search-option-{optionIdx}"
2024-10-15 01:53:29 +00:00
on:click|stopPropagation={async () => {
2024-10-15 00:31:52 +00:00
const words = value.split(' ');
words.pop();
words.push(`${option.name}`);
selectedOption = option.name.replace(':', '');
2024-10-15 00:31:52 +00:00
value = words.join(' ');
dispatch('input');
}}
>
<div class="dark:text-gray-300 text-gray-700 font-medium">{option.name}</div>
<div class=" text-gray-500 line-clamp-1">
{option.description}
</div>
</button>
{/each}
</div>
{/if}
</div>
</div>
{/if}
</div>