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

Co-Authored-By: G30 <50341825+silentoplayz@users.noreply.github.com>
This commit is contained in:
Timothy Jaeryang Baek 2025-08-06 20:55:58 +04:00
parent d1a654019b
commit 041da26756
3 changed files with 171 additions and 34 deletions

View File

@ -617,8 +617,34 @@ class ChatTable:
if word.startswith("tag:") 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 = [ 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) search_text = " ".join(search_text_words)
@ -626,9 +652,20 @@ class ChatTable:
with get_db() as db: with get_db() as db:
query = db.query(Chat).filter(Chat.user_id == user_id) 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) 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()) query = query.order_by(Chat.updated_at.desc())
# Check if the database dialect is either 'sqlite' or 'postgresql' # Check if the database dialect is either 'sqlite' or 'postgresql'

View File

@ -42,7 +42,7 @@
} }
const loadChatPreview = async (selectedIdx) => { const loadChatPreview = async (selectedIdx) => {
if (!chatList || chatList.length === 0) { if (!chatList || chatList.length === 0 || chatList[selectedIdx] === undefined) {
selectedChat = null; selectedChat = null;
messages = null; messages = null;
history = null; history = null;
@ -139,6 +139,11 @@
}; };
const onKeyDown = (e) => { const onKeyDown = (e) => {
const searchOptions = document.getElementById('search-options-container');
if (searchOptions) {
return;
}
if (e.code === 'Escape') { if (e.code === 'Escape') {
show = false; show = false;
onClose(); onClose();

View File

@ -15,6 +15,7 @@
export let onKeydown = (e) => {}; export let onKeydown = (e) => {};
let selectedIdx = 0; let selectedIdx = 0;
let selectedOption = null;
let lastWord = ''; let lastWord = '';
$: lastWord = value ? value.split(' ').at(-1) : value; $: lastWord = value ? value.split(' ').at(-1) : value;
@ -23,39 +24,115 @@
{ {
name: 'tag:', name: 'tag:',
description: $i18n.t('search for tags') 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 focused = false;
let loading = false; let loading = false;
let hovering = false;
let filteredOptions = options; let filteredOptions = options;
$: filteredOptions = options.filter((option) => { $: filteredOptions = options.filter((option) => {
return option.name.startsWith(lastWord); return option.name.startsWith(lastWord);
}); });
let filteredTags = []; let filteredItems = [];
$: filteredTags = lastWord.startsWith('tag:')
? [ $: if (lastWord && lastWord !== null) {
initItems();
}
const initItems = async () => {
console.log('initItems', lastWord);
loading = true;
await tick();
if (lastWord.startsWith('tag:')) {
filteredItems = [
...$tags, ...$tags,
{ {
id: 'none', id: 'none',
name: $i18n.t('Untagged') name: $i18n.t('Untagged')
} }
].filter((tag) => { ]
const tagName = lastWord.slice(4); .filter((tag) => {
if (tagName) { const tagName = lastWord.slice(4);
const tagId = tagName.replace(' ', '_').toLowerCase(); if (tagName) {
const tagId = tagName.replace(' ', '_').toLowerCase();
if (tag.id !== tagId) { if (tag.id !== tagId) {
return tag.id.startsWith(tagId); return tag.id.startsWith(tagId);
} else {
return false;
}
} else { } 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 () => { const initTags = async () => {
loading = true; loading = true;
@ -99,6 +176,7 @@
id="search-input" 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" 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')} placeholder={placeholder ? placeholder : $i18n.t('Search')}
autocomplete="off"
bind:value bind:value
on:input={() => { on:input={() => {
dispatch('input'); dispatch('input');
@ -108,13 +186,15 @@
initTags(); initTags();
}} }}
on:blur={() => { on:blur={() => {
focused = false; if (!hovering) {
focused = false;
}
}} }}
on:keydown={(e) => { on:keydown={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
if (filteredTags.length > 0) { if (filteredItems.length > 0) {
const tagElement = document.getElementById(`search-tag-${selectedIdx}`); const itemElement = document.getElementById(`search-item-${selectedIdx}`);
tagElement.click(); itemElement.click();
return; return;
} }
@ -131,10 +211,18 @@
} else if (e.key === 'ArrowDown') { } else if (e.key === 'ArrowDown') {
e.preventDefault(); e.preventDefault();
if (filteredTags.length > 0) { if (filteredItems.length > 0) {
selectedIdx = Math.min(selectedIdx + 1, filteredTags.length - 1); if (selectedIdx === filteredItems.length - 1) {
focused = false;
} else {
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
}
} else { } 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 { } else {
// if the user types something, reset to the top selection. // if the user types something, reset to the top selection.
@ -159,48 +247,53 @@
{/if} {/if}
</div> </div>
{#if focused && (filteredOptions.length > 0 || filteredTags.length > 0)} {#if focused && (filteredOptions.length > 0 || filteredItems.length > 0)}
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <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" 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" id="search-options-container"
in:fade={{ duration: 50 }} in:fade={{ duration: 50 }}
on:mouseenter={() => { on:mouseenter={() => {
hovering = true;
selectedIdx = null; selectedIdx = null;
}} }}
on:mouseleave={() => { on:mouseleave={() => {
selectedIdx = 0; selectedIdx = 0;
hovering = false;
}} }}
> >
<div class="px-2 py-2 text-xs group"> <div class="px-2 py-2 text-xs group">
{#if filteredTags.length > 0} {#if filteredItems.length > 0}
<div class="px-1 font-medium dark:text-gray-300 text-gray-700 mb-1">Tags</div> <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"> <div class="max-h-60 overflow-auto">
{#each filteredTags as tag, tagIdx} {#each filteredItems as item, itemIdx}
<button <button
class=" px-1.5 py-0.5 flex gap-1 hover:bg-gray-100 dark:hover:bg-gray-900 w-full rounded {selectedIdx === 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' ? 'bg-gray-100 dark:bg-gray-900'
: ''}" : ''}"
id="search-tag-{tagIdx}" id="search-item-{itemIdx}"
on:click|stopPropagation={async () => { on:click|stopPropagation={async () => {
const words = value.split(' '); const words = value.split(' ');
words.pop(); words.pop();
words.push(`tag:${tag.id} `); words.push(`${item.type}:${item.id} `);
value = words.join(' '); value = words.join(' ');
filteredItems = [];
dispatch('input'); dispatch('input');
}} }}
> >
<div class="dark:text-gray-300 text-gray-700 font-medium line-clamp-1 shrink-0"> <div class="dark:text-gray-300 text-gray-700 font-medium line-clamp-1 shrink-0">
{tag.name} {item.name}
</div> </div>
<div class=" text-gray-500 line-clamp-1"> <div class=" text-gray-500 line-clamp-1">
{tag.id} {item.id}
</div> </div>
</button> </button>
{/each} {/each}
@ -222,7 +315,9 @@
const words = value.split(' '); const words = value.split(' ');
words.pop(); words.pop();
words.push('tag:'); words.push(`${option.name}`);
selectedOption = option.name.replace(':', '');
value = words.join(' '); value = words.join(' ');