Spaces:
Configuration error
Configuration error
| <script lang="ts"> | |
| import { toast } from 'svelte-sonner'; | |
| 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 { 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 { config, user } from '$lib/stores'; | |
| import Messages from '../chat/Messages.svelte'; | |
| import { goto } from '$app/navigation'; | |
| import PencilSquare from '../icons/PencilSquare.svelte'; | |
| import PageEdit from '../icons/PageEdit.svelte'; | |
| dayjs.extend(calendar); | |
| export let show = false; | |
| export let onClose = () => {}; | |
| let actions = [ | |
| { | |
| label: $i18n.t('Start a new conversation'), | |
| onClick: async () => { | |
| await goto(`/${query ? `?q=${query}` : ''}`); | |
| show = false; | |
| onClose(); | |
| }, | |
| icon: PencilSquare | |
| } | |
| ]; | |
| let query = ''; | |
| let page = 1; | |
| let chatList = null; | |
| let chatListLoading = false; | |
| let allChatsLoaded = false; | |
| let searchDebounceTimeout; | |
| let selectedIdx = null; | |
| let selectedChat = null; | |
| let selectedModels = ['']; | |
| let history = null; | |
| let messages = null; | |
| $: if (!chatListLoading && chatList) { | |
| loadChatPreview(selectedIdx); | |
| } | |
| const loadChatPreview = async (selectedIdx) => { | |
| if (!chatList || chatList.length === 0 || selectedIdx === null) { | |
| selectedChat = null; | |
| messages = null; | |
| history = null; | |
| selectedModels = ['']; | |
| return; | |
| } | |
| const selectedChatIdx = selectedIdx - actions.length; | |
| if (selectedChatIdx < 0 || selectedChatIdx >= chatList.length) { | |
| selectedChat = null; | |
| messages = null; | |
| history = null; | |
| selectedModels = ['']; | |
| return; | |
| } | |
| const chatId = chatList[selectedChatIdx].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 (!show) { | |
| return; | |
| } | |
| if (searchDebounceTimeout) { | |
| clearTimeout(searchDebounceTimeout); | |
| } | |
| page = 1; | |
| chatList = null; | |
| if (query === '') { | |
| chatList = await getChatList(localStorage.token, page); | |
| } else { | |
| searchDebounceTimeout = setTimeout(async () => { | |
| chatList = await getChatListBySearchText(localStorage.token, query, page); | |
| if ((chatList ?? []).length === 0) { | |
| allChatsLoaded = true; | |
| } else { | |
| allChatsLoaded = false; | |
| } | |
| }, 500); | |
| } | |
| selectedChat = null; | |
| messages = null; | |
| history = null; | |
| selectedModels = ['']; | |
| if ((chatList ?? []).length === 0) { | |
| allChatsLoaded = true; | |
| } else { | |
| allChatsLoaded = false; | |
| } | |
| }; | |
| const loadMoreChats = async () => { | |
| chatListLoading = true; | |
| page += 1; | |
| let newChatList = []; | |
| if (query) { | |
| newChatList = await getChatListBySearchText(localStorage.token, query, page); | |
| } else { | |
| newChatList = await getChatList(localStorage.token, page); | |
| } | |
| // once the bottom of the list has been reached (no results) there is no need to continue querying | |
| allChatsLoaded = newChatList.length === 0; | |
| if (newChatList.length > 0) { | |
| chatList = [...chatList, ...newChatList]; | |
| } | |
| chatListLoading = false; | |
| }; | |
| $: if (show) { | |
| searchHandler(); | |
| } | |
| const onKeyDown = (e) => { | |
| const searchOptions = document.getElementById('search-options-container'); | |
| if (searchOptions || !show) { | |
| return; | |
| } | |
| if (e.code === 'Escape') { | |
| show = false; | |
| onClose(); | |
| } else if (e.code === 'Enter') { | |
| 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 + actions.length); | |
| } 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); | |
| } | |
| const item = document.querySelector(`[data-arrow-selected="true"]`); | |
| item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' }); | |
| }; | |
| onMount(() => { | |
| actions = [ | |
| ...actions, | |
| ...(($config?.features?.enable_notes ?? false) && | |
| ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true)) | |
| ? [ | |
| { | |
| label: $i18n.t('Create a new note'), | |
| onClick: async () => { | |
| await goto(`/notes?content=${query}`); | |
| show = false; | |
| onClose(); | |
| }, | |
| icon: PageEdit | |
| } | |
| ] | |
| : []) | |
| ]; | |
| document.addEventListener('keydown', onKeyDown); | |
| }); | |
| onDestroy(() => { | |
| if (searchDebounceTimeout) { | |
| clearTimeout(searchDebounceTimeout); | |
| } | |
| document.removeEventListener('keydown', onKeyDown); | |
| }); | |
| </script> | |
| <Modal size="xl" bind:show> | |
| <div class="py-3 dark:text-gray-300 text-gray-700"> | |
| <div class="px-4 pb-1.5"> | |
| <SearchInput | |
| bind:value={query} | |
| on:input={searchHandler} | |
| placeholder={$i18n.t('Search')} | |
| showClearButton={true} | |
| onFocus={() => { | |
| selectedIdx = null; | |
| messages = null; | |
| }} | |
| onKeydown={(e) => { | |
| console.log('e', e); | |
| 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') { | |
| selectedIdx = Math.min(selectedIdx + 1, (chatList ?? []).length - 1 + actions.length); | |
| } else if (e.code === 'ArrowUp') { | |
| 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' }); | |
| }} | |
| /> | |
| </div> | |
| <!-- <hr class="border-gray-50 dark:border-gray-850 my-1" /> --> | |
| <div class="flex px-4 pb-1"> | |
| <div | |
| class="flex flex-col overflow-y-auto h-96 md:h-[40rem] max-h-full scrollbar-hidden w-full flex-1 pr-2" | |
| > | |
| <div class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium pb-2 px-2"> | |
| {$i18n.t('Actions')} | |
| </div> | |
| {#each actions as action, idx (action.label)} | |
| <button | |
| class=" w-full flex items-center rounded-xl text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedIdx === | |
| idx | |
| ? 'bg-gray-50 dark:bg-gray-850' | |
| : ''}" | |
| data-arrow-selected={selectedIdx === idx ? 'true' : undefined} | |
| dragabble="false" | |
| on:mouseenter={() => { | |
| selectedIdx = idx; | |
| }} | |
| on:click={async () => { | |
| await action.onClick(); | |
| }} | |
| > | |
| <div class="pr-2"> | |
| <svelte:component this={action.icon} /> | |
| </div> | |
| <div class=" flex-1 text-left"> | |
| <div class="text-ellipsis line-clamp-1 w-full"> | |
| {$i18n.t(action.label)} | |
| </div> | |
| </div> | |
| </button> | |
| {/each} | |
| {#if chatList} | |
| <hr class="border-gray-50 dark:border-gray-850 my-3" /> | |
| {#if chatList.length === 0} | |
| <div class="text-xs text-gray-500 dark:text-gray-400 text-center px-5 py-4"> | |
| {$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): | |
| {$i18n.t('Today')} | |
| {$i18n.t('Yesterday')} | |
| {$i18n.t('Previous 7 days')} | |
| {$i18n.t('Previous 30 days')} | |
| {$i18n.t('January')} | |
| {$i18n.t('February')} | |
| {$i18n.t('March')} | |
| {$i18n.t('April')} | |
| {$i18n.t('May')} | |
| {$i18n.t('June')} | |
| {$i18n.t('July')} | |
| {$i18n.t('August')} | |
| {$i18n.t('September')} | |
| {$i18n.t('October')} | |
| {$i18n.t('November')} | |
| {$i18n.t('December')} | |
| --> | |
| </div> | |
| {/if} | |
| <a | |
| class=" w-full flex justify-between items-center rounded-xl text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedIdx === | |
| idx + actions.length | |
| ? 'bg-gray-50 dark:bg-gray-850' | |
| : ''}" | |
| href="/c/{chat.id}" | |
| draggable="false" | |
| data-arrow-selected={selectedIdx === idx + actions.length ? 'true' : undefined} | |
| on:mouseenter={() => { | |
| selectedIdx = idx + actions.length; | |
| }} | |
| on:click={async () => { | |
| await goto(`/c/${chat.id}`); | |
| show = false; | |
| onClose(); | |
| }} | |
| > | |
| <div class=" flex-1"> | |
| <div class="text-ellipsis line-clamp-1 w-full"> | |
| {chat?.title} | |
| </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="w-full flex justify-center py-4 text-xs animate-pulse items-center gap-2"> | |
| <Spinner className=" size-4" /> | |
| <div class=" ">{$i18n.t('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" | |
| > | |
| {$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" | |
| chatId={`chat-preview-${selectedChat?.id ?? ''}`} | |
| user={$user} | |
| readOnly={true} | |
| {selectedModels} | |
| bind:history | |
| bind:messages | |
| autoScroll={true} | |
| sendMessage={() => {}} | |
| continueResponse={() => {}} | |
| regenerateResponse={() => {}} | |
| /> | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| </div> | |
| </Modal> | |