diff --git a/backend/apps/webui/models/chats.py b/backend/apps/webui/models/chats.py index abde4f2b3..d504b18c3 100644 --- a/backend/apps/webui/models/chats.py +++ b/backend/apps/webui/models/chats.py @@ -250,7 +250,7 @@ class ChatTable: user_id: str, include_archived: bool = False, skip: int = 0, - limit: int = 50, + limit: int = -1, ) -> List[ChatTitleIdResponse]: with get_db() as db: query = db.query(Chat).filter_by(user_id=user_id) @@ -260,9 +260,10 @@ class ChatTable: all_chats = ( query.order_by(Chat.updated_at.desc()) # limit cols - .with_entities( - Chat.id, Chat.title, Chat.updated_at, Chat.created_at - ).all() + .with_entities(Chat.id, Chat.title, Chat.updated_at, Chat.created_at) + .limit(limit) + .offset(skip) + .all() ) # result has to be destrctured from sqlalchemy `row` and mapped to a dict since the `ChatModel`is not the returned dataclass. return [ diff --git a/backend/apps/webui/routers/chats.py b/backend/apps/webui/routers/chats.py index ec8e72e0b..6e89722d3 100644 --- a/backend/apps/webui/routers/chats.py +++ b/backend/apps/webui/routers/chats.py @@ -43,9 +43,15 @@ router = APIRouter() @router.get("/", response_model=List[ChatTitleIdResponse]) @router.get("/list", response_model=List[ChatTitleIdResponse]) async def get_session_user_chat_list( - user=Depends(get_verified_user), skip: int = 0, limit: int = 50 + user=Depends(get_verified_user), page: Optional[int] = None ): - return Chats.get_chat_title_id_list_by_user_id(user.id, skip=skip, limit=limit) + if page is not None: + limit = 60 + skip = (page - 1) * limit + + return Chats.get_chat_title_id_list_by_user_id(user.id, skip=skip, limit=limit) + else: + return Chats.get_chat_title_id_list_by_user_id(user.id) ############################ diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts index b046f1b10..8f4f81aea 100644 --- a/src/lib/apis/chats/index.ts +++ b/src/lib/apis/chats/index.ts @@ -32,10 +32,15 @@ export const createNewChat = async (token: string, chat: object) => { return res; }; -export const getChatList = async (token: string = '') => { +export const getChatList = async (token: string = '', page: number | null = null) => { let error = null; + const searchParams = new URLSearchParams(); - const res = await fetch(`${WEBUI_API_BASE_URL}/chats/`, { + if (page !== null) { + searchParams.append('page', `${page}`); + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/?${searchParams.toString()}`, { method: 'GET', headers: { Accept: 'application/json', diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 253dada19..2c42c2046 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -25,7 +25,8 @@ user, socket, showCallOverlay, - tools + tools, + currentChatPage } from '$lib/stores'; import { convertMessagesToHistory, @@ -421,7 +422,9 @@ params: params, files: chatFiles }); - await chats.set(await getChatList(localStorage.token)); + + currentChatPage.set(1); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); } } }; @@ -467,7 +470,9 @@ params: params, files: chatFiles }); - await chats.set(await getChatList(localStorage.token)); + + currentChatPage.set(1); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); } } }; @@ -627,7 +632,9 @@ tags: [], timestamp: Date.now() }); - await chats.set(await getChatList(localStorage.token)); + + currentChatPage.set(1); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); await chatId.set(chat.id); } else { await chatId.set('local'); @@ -703,7 +710,9 @@ }) ); - await chats.set(await getChatList(localStorage.token)); + currentChatPage.set(1); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); + return _responses; }; @@ -949,7 +958,9 @@ params: params, files: chatFiles }); - await chats.set(await getChatList(localStorage.token)); + + currentChatPage.set(1); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); } } } else { @@ -1216,7 +1227,9 @@ params: params, files: chatFiles }); - await chats.set(await getChatList(localStorage.token)); + + currentChatPage.set(1); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); } } } else { @@ -1381,7 +1394,9 @@ if ($settings.saveChatHistory ?? true) { chat = await updateChatById(localStorage.token, _chatId, { title: _title }); - await chats.set(await getChatList(localStorage.token)); + + currentChatPage.set(1); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); } }; diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index 319f0f170..9e3c147b1 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -1,6 +1,6 @@ + +
+ +
diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 49a0e2ebb..be2ebb093 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -11,7 +11,9 @@ showSidebar, mobile, showArchivedChats, - pinnedChats + pinnedChats, + scrollPaginationEnabled, + currentChatPage } from '$lib/stores'; import { onMount, getContext, tick } from 'svelte'; @@ -34,6 +36,8 @@ import UserMenu from './Sidebar/UserMenu.svelte'; import ChatItem from './Sidebar/ChatItem.svelte'; import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; + import Spinner from '../common/Spinner.svelte'; + import Loader from '../common/Loader.svelte'; const BREAKPOINT = 768; @@ -50,6 +54,10 @@ let filteredChatList = []; + // Pagination variables + let chatListLoading = false; + let allChatsLoaded = false; + $: filteredChatList = $chats.filter((chat) => { if (search === '') { return true; @@ -70,6 +78,29 @@ } }); + const enablePagination = async () => { + // Reset pagination variables + currentChatPage.set(1); + allChatsLoaded = false; + await chats.set(await getChatList(localStorage.token, $currentChatPage)); + + // Enable pagination + scrollPaginationEnabled.set(true); + }; + + const loadMoreChats = async () => { + chatListLoading = true; + + currentChatPage.set($currentChatPage + 1); + const newChatList = await getChatList(localStorage.token, $currentChatPage); + + // once the bottom of the list has been reached (no results) there is no need to continue querying + allChatsLoaded = newChatList.length === 0; + await chats.set([...$chats, ...newChatList]); + + chatListLoading = false; + }; + onMount(async () => { mobile.subscribe((e) => { if ($showSidebar && e) { @@ -82,9 +113,8 @@ }); showSidebar.set(window.innerWidth > BREAKPOINT); - await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned')); - await chats.set(await getChatList(localStorage.token)); + await enablePagination(); let touchstart; let touchend; @@ -185,7 +215,11 @@ await tick(); goto('/'); } - await chats.set(await getChatList(localStorage.token)); + + allChatsLoaded = false; + currentChatPage.set(1); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); + await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned')); } }; @@ -410,7 +444,10 @@ class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm bg-transparent dark:text-gray-300 outline-none" placeholder={$i18n.t('Search')} bind:value={search} - on:focus={() => { + on:focus={async () => { + // TODO: migrate backend for more scalable search mechanism + scrollPaginationEnabled.set(false); + await chats.set(await getChatList(localStorage.token)); // when searching, load all chats enrichChatsWithContent($chats); }} /> @@ -422,7 +459,7 @@