diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 47b559f504..f82e1d3101 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -617,8 +617,34 @@ class ChatTable: if word.startswith("tag:") ] + is_pinned = None + if "pinned:true" in search_text_words: + is_pinned = True + elif "pinned:false" in search_text_words: + is_pinned = False + + is_archived = None + if "archived:true" in search_text_words: + is_archived = True + elif "archived:false" in search_text_words: + is_archived = False + + is_shared = None + if "shared:true" in search_text_words: + is_shared = True + elif "shared:false" in search_text_words: + is_shared = False + search_text_words = [ - word for word in search_text_words if not word.startswith("tag:") + word + for word in search_text_words + if ( + not word.startswith("tag:") + and not word.startswith("folder:") + and not word.startswith("pinned:") + and not word.startswith("archived:") + and not word.startswith("shared:") + ) ] search_text = " ".join(search_text_words) @@ -626,9 +652,20 @@ class ChatTable: with get_db() as db: query = db.query(Chat).filter(Chat.user_id == user_id) - if not include_archived: + if is_archived is not None: + query = query.filter(Chat.archived == is_archived) + elif not include_archived: query = query.filter(Chat.archived == False) + if is_pinned is not None: + query = query.filter(Chat.pinned == is_pinned) + + if is_shared is not None: + if is_shared: + query = query.filter(Chat.share_id.isnot(None)) + else: + query = query.filter(Chat.share_id.is_(None)) + query = query.order_by(Chat.updated_at.desc()) # Check if the database dialect is either 'sqlite' or 'postgresql' diff --git a/src/lib/components/layout/SearchModal.svelte b/src/lib/components/layout/SearchModal.svelte index 544906e343..02a020d9f9 100644 --- a/src/lib/components/layout/SearchModal.svelte +++ b/src/lib/components/layout/SearchModal.svelte @@ -42,7 +42,7 @@ } const loadChatPreview = async (selectedIdx) => { - if (!chatList || chatList.length === 0) { + if (!chatList || chatList.length === 0 || chatList[selectedIdx] === undefined) { selectedChat = null; messages = null; history = null; @@ -139,6 +139,11 @@ }; const onKeyDown = (e) => { + const searchOptions = document.getElementById('search-options-container'); + if (searchOptions) { + return; + } + if (e.code === 'Escape') { show = false; onClose(); diff --git a/src/lib/components/layout/Sidebar/SearchInput.svelte b/src/lib/components/layout/Sidebar/SearchInput.svelte index 0710544dce..e49f9b0359 100644 --- a/src/lib/components/layout/Sidebar/SearchInput.svelte +++ b/src/lib/components/layout/Sidebar/SearchInput.svelte @@ -15,6 +15,7 @@ export let onKeydown = (e) => {}; let selectedIdx = 0; + let selectedOption = null; let lastWord = ''; $: lastWord = value ? value.split(' ').at(-1) : value; @@ -23,39 +24,115 @@ { 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') } ]; let focused = false; let loading = false; + let hovering = false; + let filteredOptions = options; $: filteredOptions = options.filter((option) => { return option.name.startsWith(lastWord); }); - let filteredTags = []; - $: filteredTags = lastWord.startsWith('tag:') - ? [ + let filteredItems = []; + + $: if (lastWord && lastWord !== null) { + initItems(); + } + + const initItems = async () => { + console.log('initItems', lastWord); + loading = true; + await tick(); + + if (lastWord.startsWith('tag:')) { + filteredItems = [ ...$tags, { id: 'none', name: $i18n.t('Untagged') } - ].filter((tag) => { - const tagName = lastWord.slice(4); - if (tagName) { - const tagId = tagName.replace(' ', '_').toLowerCase(); + ] + .filter((tag) => { + const tagName = lastWord.slice(4); + if (tagName) { + const tagId = tagName.replace(' ', '_').toLowerCase(); - if (tag.id !== tagId) { - return tag.id.startsWith(tagId); + if (tag.id !== tagId) { + return tag.id.startsWith(tagId); + } else { + return false; + } } else { - return false; + return true; } - } else { - return true; + }) + .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' } - }) - : []; + ]; + } 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; + }; const initTags = async () => { loading = true; @@ -99,6 +176,7 @@ id="search-input" class="w-full rounded-r-xl py-1.5 pl-2.5 text-sm bg-transparent dark:text-gray-300 outline-hidden" placeholder={placeholder ? placeholder : $i18n.t('Search')} + autocomplete="off" bind:value on:input={() => { dispatch('input'); @@ -108,13 +186,15 @@ initTags(); }} on:blur={() => { - focused = false; + if (!hovering) { + focused = false; + } }} on:keydown={(e) => { if (e.key === 'Enter') { - if (filteredTags.length > 0) { - const tagElement = document.getElementById(`search-tag-${selectedIdx}`); - tagElement.click(); + if (filteredItems.length > 0) { + const itemElement = document.getElementById(`search-item-${selectedIdx}`); + itemElement.click(); return; } @@ -131,10 +211,18 @@ } else if (e.key === 'ArrowDown') { e.preventDefault(); - if (filteredTags.length > 0) { - selectedIdx = Math.min(selectedIdx + 1, filteredTags.length - 1); + if (filteredItems.length > 0) { + if (selectedIdx === filteredItems.length - 1) { + focused = false; + } else { + selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1); + } } else { - selectedIdx = Math.min(selectedIdx + 1, filteredOptions.length - 1); + if (selectedIdx === filteredOptions.length - 1) { + focused = false; + } else { + selectedIdx = Math.min(selectedIdx + 1, filteredOptions.length - 1); + } } } else { // if the user types something, reset to the top selection. @@ -159,48 +247,53 @@ {/if} - {#if focused && (filteredOptions.length > 0 || filteredTags.length > 0)} + {#if focused && (filteredOptions.length > 0 || filteredItems.length > 0)}