瀏覽代碼

enh: search chat preview

Timothy Jaeryang Baek 2 月之前
父節點
當前提交
71ee33bf82

+ 7 - 1
src/lib/components/common/Modal.svelte

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

+ 207 - 64
src/lib/components/layout/SearchModal.svelte

@@ -1,16 +1,19 @@
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
-	import { getContext, onMount } from 'svelte';
+	import { getContext, onDestroy, onMount, tick } from 'svelte';
 	const i18n = getContext('i18n');
 
 	import Modal from '$lib/components/common/Modal.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 dayjs from '$lib/dayjs';
 	import calendar from 'dayjs/plugin/calendar';
 	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);
 
 	export let show = false;
@@ -28,6 +31,58 @@
 
 	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 () => {
 		if (searchDebounceTimeout) {
 			clearTimeout(searchDebounceTimeout);
@@ -76,12 +131,69 @@
 		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(() => {
 		init();
+
+		document.addEventListener('keydown', onKeyDown);
+	});
+
+	onDestroy(() => {
+		if (searchDebounceTimeout) {
+			clearTimeout(searchDebounceTimeout);
+		}
+		document.removeEventListener('keydown', onKeyDown);
 	});
 </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="px-3.5 pb-1.5">
 			<SearchInput
@@ -116,23 +228,26 @@
 
 		<!-- <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">
-			{#if chatList}
-				{#if chatList.length === 0}
-					<div class="text-xs text-gray-500 dark:text-gray-400 text-center px-5">
-						{$i18n.t('No results found')}
-					</div>
-				{/if}
+		<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.length === 0}
+						<div class="text-xs text-gray-500 dark:text-gray-400 text-center px-5">
+							{$i18n.t('No results found')}
+						</div>
+					{/if}
 
-				{#each chatList as chat, idx (chat.id)}
-					{#if idx === 0 || (idx > 0 && chat.time_range !== chatList[idx - 1].time_range)}
-						<div
-							class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
-								? ''
-								: 'pt-5'} pb-2 px-2"
-						>
-							{$i18n.t(chat.time_range)}
-							<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
+					{#each chatList as chat, idx (chat.id)}
+						{#if idx === 0 || (idx > 0 && chat.time_range !== chatList[idx - 1].time_range)}
+							<div
+								class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
+									? ''
+									: 'pt-5'} pb-2 px-2"
+							>
+								{$i18n.t(chat.time_range)}
+								<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
 							{$i18n.t('Today')}
 							{$i18n.t('Yesterday')}
 							{$i18n.t('Previous 7 days')}
@@ -150,56 +265,84 @@
 							{$i18n.t('November')}
 							{$i18n.t('December')}
 							-->
-						</div>
-					{/if}
+							</div>
+						{/if}
 
-					<a
-						class=" w-full flex justify-between items-center rounded-lg text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedIdx ===
-						idx
-							? 'bg-gray-50 dark:bg-gray-850'
-							: ''}"
-						href="/c/{chat.id}"
-						draggable="false"
-						data-arrow-selected={selectedIdx === idx ? 'true' : undefined}
-						on:mouseenter={() => {
-							selectedIdx = idx;
-						}}
-						on:click={() => {
-							show = false;
-							onClose();
-						}}
-					>
-						<div class=" flex-1">
-							<div class="text-ellipsis line-clamp-1 w-full">
-								{chat?.title}
+						<a
+							class=" w-full flex justify-between items-center rounded-lg text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedIdx ===
+							idx
+								? 'bg-gray-50 dark:bg-gray-850'
+								: ''}"
+							href="/c/{chat.id}"
+							draggable="false"
+							data-arrow-selected={selectedIdx === idx ? 'true' : undefined}
+							on:mouseenter={() => {
+								selectedIdx = idx;
+							}}
+							on:click={() => {
+								show = false;
+								onClose();
+							}}
+						>
+							<div class=" flex-1">
+								<div class="text-ellipsis line-clamp-1 w-full">
+									{chat?.title}
+								</div>
 							</div>
-						</div>
 
-						<div class=" pl-3 shrink-0 text-gray-500 dark:text-gray-400 text-xs">
-							{dayjs(chat?.updated_at * 1000).calendar()}
-						</div>
-					</a>
-				{/each}
-
-				{#if !allChatsLoaded}
-					<Loader
-						on:visible={(e) => {
-							if (!chatListLoading) {
-								loadMoreChats();
-							}
-						}}
+							<div class=" pl-3 shrink-0 text-gray-500 dark:text-gray-400 text-xs">
+								{dayjs(chat?.updated_at * 1000).calendar()}
+							</div>
+						</a>
+					{/each}
+
+					{#if !allChatsLoaded}
+						<Loader
+							on:visible={(e) => {
+								if (!chatListLoading) {
+									loadMoreChats();
+								}
+							}}
+						>
+							<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
+								<Spinner className=" size-4" />
+								<div class=" ">Loading...</div>
+							</div>
+						</Loader>
+					{/if}
+				{:else}
+					<div class="w-full h-full flex justify-center items-center">
+						<Spinner className="size-5" />
+					</div>
+				{/if}
+			</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"
 					>
-						<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
-							<Spinner className=" size-4" />
-							<div class=" ">Loading...</div>
-						</div>
-					</Loader>
+						{$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}
-			{:else}
-				<div class="w-full h-full flex justify-center items-center">
-					<Spinner className="size-5" />
-				</div>
-			{/if}
+			</div>
 		</div>
 	</div>
 </Modal>

+ 4 - 0
src/lib/components/layout/Sidebar/SearchInput.svelte

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