enh: search chat preview

This commit is contained in:
Timothy Jaeryang Baek 2025-07-19 18:06:59 +04:00
parent c94ce71ca3
commit 71ee33bf82
3 changed files with 219 additions and 66 deletions

View file

@ -26,8 +26,14 @@
return 'w-[30rem]'; return 'w-[30rem]';
} else if (size === 'md') { } else if (size === 'md') {
return 'w-[42rem]'; return 'w-[42rem]';
} else { } else if (size === 'lg') {
return 'w-[56rem]'; return 'w-[56rem]';
} else if (size === 'xl') {
return 'w-[70rem]';
} else if (size === '2xl') {
return 'w-[84rem]';
} else if (size === '3xl') {
return 'w-[100rem]';
} }
}; };

View file

@ -1,16 +1,19 @@
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { getContext, onMount } from 'svelte'; import { getContext, onDestroy, onMount, tick } from 'svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
import Modal from '$lib/components/common/Modal.svelte'; import Modal from '$lib/components/common/Modal.svelte';
import SearchInput from './Sidebar/SearchInput.svelte'; import SearchInput from './Sidebar/SearchInput.svelte';
import { getChatList, getChatListBySearchText } from '$lib/apis/chats'; import { getChatById, getChatList, getChatListBySearchText } from '$lib/apis/chats';
import Spinner from '../common/Spinner.svelte'; import Spinner from '../common/Spinner.svelte';
import dayjs from '$lib/dayjs'; import dayjs from '$lib/dayjs';
import calendar from 'dayjs/plugin/calendar'; import calendar from 'dayjs/plugin/calendar';
import Loader from '../common/Loader.svelte'; import Loader from '../common/Loader.svelte';
import { createMessagesList } from '$lib/utils';
import { user } from '$lib/stores';
import Messages from '../chat/Messages.svelte';
dayjs.extend(calendar); dayjs.extend(calendar);
export let show = false; export let show = false;
@ -28,6 +31,58 @@
let selectedIdx = 0; let selectedIdx = 0;
let selectedChat = null;
let selectedModels = [''];
let history = null;
let messages = null;
$: loadChatPreview(selectedIdx);
const loadChatPreview = async (selectedIdx) => {
if (!chatList || chatList.length === 0) {
selectedChat = null;
messages = null;
history = null;
selectedModels = [''];
return;
}
const chatId = chatList[selectedIdx].id;
const chat = await getChatById(localStorage.token, chatId).catch(async (error) => {
return null;
});
if (chat) {
if (chat?.chat?.history) {
selectedModels =
(chat?.chat?.models ?? undefined) !== undefined
? chat?.chat?.models
: [chat?.chat?.models ?? ''];
history = chat?.chat?.history;
messages = createMessagesList(chat?.chat?.history, chat?.chat?.history?.currentId);
// scroll to the bottom of the messages container
await tick();
const messagesContainerElement = document.getElementById('chat-preview');
if (messagesContainerElement) {
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
}
} else {
messages = [];
}
} else {
toast.error($i18n.t('Failed to load chat preview'));
selectedChat = null;
messages = null;
history = null;
selectedModels = [''];
return;
}
};
const searchHandler = async () => { const searchHandler = async () => {
if (searchDebounceTimeout) { if (searchDebounceTimeout) {
clearTimeout(searchDebounceTimeout); clearTimeout(searchDebounceTimeout);
@ -76,12 +131,69 @@
searchHandler(); searchHandler();
}; };
const onKeyDown = (e) => {
if (e.code === 'Escape') {
show = false;
onClose();
} else if (e.code === 'Enter' && (chatList ?? []).length > 0) {
const item = document.querySelector(`[data-arrow-selected="true"]`);
if (item) {
item?.click();
}
show = false;
return;
} else if (e.code === 'ArrowDown') {
const searchInput = document.getElementById('search-input');
if (searchInput) {
// check if focused on the search input
if (document.activeElement === searchInput) {
searchInput.blur();
selectedIdx = 0;
return;
}
}
selectedIdx = Math.min(selectedIdx + 1, (chatList ?? []).length - 1);
} else if (e.code === 'ArrowUp') {
if (selectedIdx === 0) {
const searchInput = document.getElementById('search-input');
if (searchInput) {
// check if focused on the search input
if (document.activeElement !== searchInput) {
searchInput.focus();
selectedIdx = 0;
return;
}
}
}
selectedIdx = Math.max(selectedIdx - 1, 0);
} else {
selectedIdx = 0;
}
const item = document.querySelector(`[data-arrow-selected="true"]`);
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
};
onMount(() => { onMount(() => {
init(); init();
document.addEventListener('keydown', onKeyDown);
});
onDestroy(() => {
if (searchDebounceTimeout) {
clearTimeout(searchDebounceTimeout);
}
document.removeEventListener('keydown', onKeyDown);
}); });
</script> </script>
<Modal size="md" bind:show> <Modal size="xl" bind:show>
<div class="py-2.5 dark:text-gray-300 text-gray-700"> <div class="py-2.5 dark:text-gray-300 text-gray-700">
<div class="px-3.5 pb-1.5"> <div class="px-3.5 pb-1.5">
<SearchInput <SearchInput
@ -116,7 +228,10 @@
<!-- <hr class="border-gray-100 dark:border-gray-850 my-1" /> --> <!-- <hr class="border-gray-100 dark:border-gray-850 my-1" /> -->
<div class="flex flex-col overflow-y-auto h-80 scrollbar-hidden px-3 pb-1"> <div class="flex px-3 pb-1">
<div
class="flex flex-col overflow-y-auto h-96 md:h-[40rem] max-h-full scrollbar-hidden w-full flex-1"
>
{#if chatList} {#if chatList}
{#if chatList.length === 0} {#if chatList.length === 0}
<div class="text-xs text-gray-500 dark:text-gray-400 text-center px-5"> <div class="text-xs text-gray-500 dark:text-gray-400 text-center px-5">
@ -201,5 +316,33 @@
</div> </div>
{/if} {/if}
</div> </div>
<div
id="chat-preview"
class="hidden md:flex md:flex-1 w-full overflow-y-auto h-96 md:h-[40rem] scrollbar-hidden"
>
{#if messages === null}
<div
class="w-full h-full flex justify-center items-center text-gray-500 dark:text-gray-400 text-sm"
>
{$i18n.t('Select a conversation to preview')}
</div>
{:else}
<div class="w-full h-full flex flex-col">
<Messages
className="h-full flex pt-4 pb-8 w-full"
user={$user}
readOnly={true}
{selectedModels}
bind:history
bind:messages
autoScroll={true}
sendPrompt={() => {}}
continueResponse={() => {}}
regenerateResponse={() => {}}
/>
</div>
{/if}
</div>
</div>
</div> </div>
</Modal> </Modal>

View file

@ -96,6 +96,7 @@
</div> </div>
<input <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')}
bind:value bind:value
@ -106,6 +107,9 @@
focused = true; focused = true;
initTags(); initTags();
}} }}
on:blur={() => {
focused = false;
}}
on:keydown={(e) => { on:keydown={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
if (filteredTags.length > 0) { if (filteredTags.length > 0) {