Kaynağa Gözat

feat: add pinned, shared and archived tags functionality for chat search moda

Co-Authored-By: G30 <50341825+silentoplayz@users.noreply.github.com>
Timothy Jaeryang Baek 2 ay önce
ebeveyn
işleme
041da26756

+ 39 - 2
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'

+ 6 - 1
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();

+ 127 - 32
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();
-
-					if (tag.id !== tagId) {
-						return tag.id.startsWith(tagId);
+			]
+				.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;
+						}
 					} 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}
 	</div>
 
-	{#if focused && (filteredOptions.length > 0 || filteredTags.length > 0)}
+	{#if focused && (filteredOptions.length > 0 || filteredItems.length > 0)}
 		<!-- svelte-ignore a11y-no-static-element-interactions -->
 		<div
 			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"
 			id="search-options-container"
 			in:fade={{ duration: 50 }}
 			on:mouseenter={() => {
+				hovering = true;
 				selectedIdx = null;
 			}}
 			on:mouseleave={() => {
 				selectedIdx = 0;
+				hovering = false;
 			}}
 		>
 			<div class="px-2 py-2 text-xs group">
-				{#if filteredTags.length > 0}
-					<div class="px-1 font-medium dark:text-gray-300 text-gray-700 mb-1">Tags</div>
+				{#if filteredItems.length > 0}
+					<div class="px-1 font-medium dark:text-gray-300 text-gray-700 mb-1 capitalize">
+						{selectedOption}
+					</div>
 
 					<div class="max-h-60 overflow-auto">
-						{#each filteredTags as tag, tagIdx}
+						{#each filteredItems as item, itemIdx}
 							<button
 								class=" px-1.5 py-0.5 flex gap-1 hover:bg-gray-100 dark:hover:bg-gray-900 w-full rounded {selectedIdx ===
-								tagIdx
+								itemIdx
 									? 'bg-gray-100 dark:bg-gray-900'
 									: ''}"
-								id="search-tag-{tagIdx}"
+								id="search-item-{itemIdx}"
 								on:click|stopPropagation={async () => {
 									const words = value.split(' ');
 
 									words.pop();
-									words.push(`tag:${tag.id} `);
+									words.push(`${item.type}:${item.id} `);
 
 									value = words.join(' ');
 
+									filteredItems = [];
 									dispatch('input');
 								}}
 							>
 								<div class="dark:text-gray-300 text-gray-700 font-medium line-clamp-1 shrink-0">
-									{tag.name}
+									{item.name}
 								</div>
 
 								<div class=" text-gray-500 line-clamp-1">
-									{tag.id}
+									{item.id}
 								</div>
 							</button>
 						{/each}
@@ -222,7 +315,9 @@
 									const words = value.split(' ');
 
 									words.pop();
-									words.push('tag:');
+									words.push(`${option.name}`);
+
+									selectedOption = option.name.replace(':', '');
 
 									value = words.join(' ');