mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 04:15:25 +00:00
enh: search chat preview
This commit is contained in:
parent
c94ce71ca3
commit
71ee33bf82
3 changed files with 219 additions and 66 deletions
|
|
@ -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]';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,23 +228,26 @@
|
||||||
|
|
||||||
<!-- <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">
|
||||||
{#if chatList}
|
<div
|
||||||
{#if chatList.length === 0}
|
class="flex flex-col overflow-y-auto h-96 md:h-[40rem] max-h-full scrollbar-hidden w-full flex-1"
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400 text-center px-5">
|
>
|
||||||
{$i18n.t('No results found')}
|
{#if chatList}
|
||||||
</div>
|
{#if chatList.length === 0}
|
||||||
{/if}
|
<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)}
|
{#each chatList as chat, idx (chat.id)}
|
||||||
{#if idx === 0 || (idx > 0 && chat.time_range !== chatList[idx - 1].time_range)}
|
{#if idx === 0 || (idx > 0 && chat.time_range !== chatList[idx - 1].time_range)}
|
||||||
<div
|
<div
|
||||||
class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
|
class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
|
||||||
? ''
|
? ''
|
||||||
: 'pt-5'} pb-2 px-2"
|
: 'pt-5'} pb-2 px-2"
|
||||||
>
|
>
|
||||||
{$i18n.t(chat.time_range)}
|
{$i18n.t(chat.time_range)}
|
||||||
<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
|
<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
|
||||||
{$i18n.t('Today')}
|
{$i18n.t('Today')}
|
||||||
{$i18n.t('Yesterday')}
|
{$i18n.t('Yesterday')}
|
||||||
{$i18n.t('Previous 7 days')}
|
{$i18n.t('Previous 7 days')}
|
||||||
|
|
@ -150,56 +265,84 @@
|
||||||
{$i18n.t('November')}
|
{$i18n.t('November')}
|
||||||
{$i18n.t('December')}
|
{$i18n.t('December')}
|
||||||
-->
|
-->
|
||||||
</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}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
<div class=" pl-3 shrink-0 text-gray-500 dark:text-gray-400 text-xs">
|
<a
|
||||||
{dayjs(chat?.updated_at * 1000).calendar()}
|
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 ===
|
||||||
</div>
|
idx
|
||||||
</a>
|
? 'bg-gray-50 dark:bg-gray-850'
|
||||||
{/each}
|
: ''}"
|
||||||
|
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>
|
||||||
|
|
||||||
{#if !allChatsLoaded}
|
<div class=" pl-3 shrink-0 text-gray-500 dark:text-gray-400 text-xs">
|
||||||
<Loader
|
{dayjs(chat?.updated_at * 1000).calendar()}
|
||||||
on:visible={(e) => {
|
</div>
|
||||||
if (!chatListLoading) {
|
</a>
|
||||||
loadMoreChats();
|
{/each}
|
||||||
}
|
|
||||||
}}
|
{#if !allChatsLoaded}
|
||||||
>
|
<Loader
|
||||||
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
|
on:visible={(e) => {
|
||||||
<Spinner className=" size-4" />
|
if (!chatListLoading) {
|
||||||
<div class=" ">Loading...</div>
|
loadMoreChats();
|
||||||
</div>
|
}
|
||||||
</Loader>
|
}}
|
||||||
|
>
|
||||||
|
<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}
|
{/if}
|
||||||
{:else}
|
</div>
|
||||||
<div class="w-full h-full flex justify-center items-center">
|
<div
|
||||||
<Spinner className="size-5" />
|
id="chat-preview"
|
||||||
</div>
|
class="hidden md:flex md:flex-1 w-full overflow-y-auto h-96 md:h-[40rem] scrollbar-hidden"
|
||||||
{/if}
|
>
|
||||||
|
{#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>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue