From 49199819db69837f31227cfef3674fbaf8abca72 Mon Sep 17 00:00:00 2001 From: Aryan Kothari <87589047+thearyadev@users.noreply.github.com> Date: Thu, 1 Aug 2024 12:24:05 -0400 Subject: [PATCH 01/19] add: stores for pagination state --- src/lib/stores/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 1b0257c4b..60a617185 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -41,6 +41,9 @@ export const showSettings = writable(false); export const showArchivedChats = writable(false); export const showChangelog = writable(false); export const showCallOverlay = writable(false); +export const scrollPaginationEnabled = writable(true); +export const pageSkip = writable(0); +export const pageLimit = writable(-1); export type Model = OpenAIModel | OllamaModel; From 519375b4c090d864395b6cacecc5c3b9bb3a0d93 Mon Sep 17 00:00:00 2001 From: Aryan Kothari <87589047+thearyadev@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:05:52 -0400 Subject: [PATCH 02/19] add: skip and limit use in query - limit default changed to -1 --- backend/apps/webui/models/chats.py | 9 +++++---- backend/apps/webui/routers/chats.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) 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 80308a451..47c7c4a87 100644 --- a/backend/apps/webui/routers/chats.py +++ b/backend/apps/webui/routers/chats.py @@ -43,7 +43,7 @@ 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), skip: int = 0, limit: int = -1 ): return Chats.get_chat_title_id_list_by_user_id(user.id, skip=skip, limit=limit) From d11961626c56c30deb165f202e842d2ce283ccff Mon Sep 17 00:00:00 2001 From: Aryan Kothari <87589047+thearyadev@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:15:49 -0400 Subject: [PATCH 03/19] add: use skip and limit in api call --- src/lib/apis/chats/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts index b046f1b10..8ff12f4b6 100644 --- a/src/lib/apis/chats/index.ts +++ b/src/lib/apis/chats/index.ts @@ -32,10 +32,10 @@ export const createNewChat = async (token: string, chat: object) => { return res; }; -export const getChatList = async (token: string = '') => { +export const getChatList = async (token: string = '', skip: number = 0, limit: number = -1) => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/chats/`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/?skip=${skip}&limit=${limit}`, { method: 'GET', headers: { Accept: 'application/json', From 62dc486c8577bef6d0c65fd3777b9425bd85a06d Mon Sep 17 00:00:00 2001 From: Aryan Kothari <87589047+thearyadev@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:19:14 -0400 Subject: [PATCH 04/19] add: add paginated scroll handler --- src/lib/components/layout/Sidebar.svelte | 77 +++++++++++++++++++++--- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 49a0e2ebb..d8434e6a2 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -11,7 +11,10 @@ showSidebar, mobile, showArchivedChats, - pinnedChats + pinnedChats, + pageSkip, + pageLimit, + scrollPaginationEnabled } from '$lib/stores'; import { onMount, getContext, tick } from 'svelte'; @@ -49,6 +52,12 @@ let showDropdown = false; let filteredChatList = []; + let paginationScrollThreashold = 0.6; + let nextPageLoading = false; + let tagView = false; + let chatPagniationComplete = false; + + pageLimit.set(20); $: filteredChatList = $chats.filter((chat) => { if (search === '') { @@ -84,7 +93,7 @@ showSidebar.set(window.innerWidth > BREAKPOINT); await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned')); - await chats.set(await getChatList(localStorage.token)); + await chats.set(await getChatList(localStorage.token, $pageSkip, $pageLimit)); let touchstart; let touchend; @@ -185,7 +194,9 @@ await tick(); goto('/'); } - await chats.set(await getChatList(localStorage.token)); + await chats.set( + await getChatList(localStorage.token, 0, $pageSkip * $pageLimit || $pageLimit) + ); await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned')); } }; @@ -235,6 +246,9 @@ ? '' : 'invisible'}" > +

+ Chats loaded: {$chats.length} +

{ + on:focus={async () => { + // loading all chats. disable pagination on scrol. + scrollPaginationEnabled.set(false); + // subsequent queries will calculate page size to rehydrate the ui. + // since every chat is already loaded, the calculation should now load all chats. + pageSkip.set(0); + pageLimit.set(-1); + await chats.set(await getChatList(localStorage.token)); // when searching, load all chats + enrichChatsWithContent($chats); }} /> @@ -422,7 +444,13 @@
{/if} -
+
{ + if (!$scrollPaginationEnabled) return; + if (tagView) return; + if (nextPageLoading) return; + if (chatPagniationComplete) return; + + const maxScroll = e.target.scrollHeight - e.target.clientHeight; + const currentPos = e.target.scrollTop; + const ratio = currentPos / maxScroll; + if (ratio >= paginationScrollThreashold) { + nextPageLoading = true; + pageSkip.set($pageSkip + 1); + // extend existing chats + const nextPageChats = await getChatList( + localStorage.token, + $pageSkip * $pageLimit, + $pageLimit + ); + // once the bottom of the list has been reached (no results) there is no need to continue querying + chatPagniationComplete = nextPageChats.length === 0; + await chats.set([...$chats, ...nextPageChats]); + nextPageLoading = false; + } + }} + > {#each filteredChatList as chat, idx} {#if idx === 0 || (idx > 0 && chat.time_range !== filteredChatList[idx - 1].time_range)}
Date: Thu, 1 Aug 2024 15:20:36 -0400 Subject: [PATCH 05/19] refactor: uses of `chats.set(...)` support pagi sidebar --- src/lib/components/chat/Chat.svelte | 31 ++++++++++++++----- src/lib/components/chat/Messages.svelte | 4 +-- src/lib/components/chat/Settings/Chats.svelte | 21 +++++++++++-- src/lib/components/chat/Tags.svelte | 13 ++++++-- .../components/layout/Sidebar/ChatItem.svelte | 20 +++++++++--- 5 files changed, 70 insertions(+), 19 deletions(-) diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 24e74e695..2da9d1a8e 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -25,7 +25,9 @@ user, socket, showCallOverlay, - tools + tools, + pageSkip, + pageLimit } from '$lib/stores'; import { convertMessagesToHistory, @@ -418,7 +420,9 @@ params: params, files: chatFiles }); - await chats.set(await getChatList(localStorage.token)); + await chats.set( + await getChatList(localStorage.token, 0, $pageSkip * $pageLimit || $pageLimit) + ); } } }; @@ -464,7 +468,9 @@ params: params, files: chatFiles }); - await chats.set(await getChatList(localStorage.token)); + await chats.set( + await getChatList(localStorage.token, 0, $pageSkip * $pageLimit || $pageLimit) + ); } } }; @@ -624,7 +630,9 @@ tags: [], timestamp: Date.now() }); - await chats.set(await getChatList(localStorage.token)); + await chats.set( + await getChatList(localStorage.token, 0, $pageSkip * $pageLimit || $pageLimit) + ); await chatId.set(chat.id); } else { await chatId.set('local'); @@ -700,7 +708,8 @@ }) ); - await chats.set(await getChatList(localStorage.token)); + await chats.set(await getChatList(localStorage.token, 0, $pageSkip * $pageLimit || $pageLimit)); + return _responses; }; @@ -947,7 +956,9 @@ params: params, files: chatFiles }); - await chats.set(await getChatList(localStorage.token)); + await chats.set( + await getChatList(localStorage.token, 0, $pageSkip * $pageLimit || $pageLimit) + ); } } } else { @@ -1220,7 +1231,9 @@ params: params, files: chatFiles }); - await chats.set(await getChatList(localStorage.token)); + await chats.set( + await getChatList(localStorage.token, 0, $pageSkip * $pageLimit || $pageLimit) + ); } } } else { @@ -1385,7 +1398,9 @@ if ($settings.saveChatHistory ?? true) { chat = await updateChatById(localStorage.token, _chatId, { title: _title }); - await chats.set(await getChatList(localStorage.token)); + await chats.set( + await getChatList(localStorage.token, 0, $pageSkip * $pageLimit || $pageLimit) + ); } }; diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index e46e93143..c798de923 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 07dac20f0..70360a46d 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -14,13 +14,12 @@ pinnedChats, pageSkip, pageLimit, - scrollPaginationEnabled, - tagView + scrollPaginationEnabled } from '$lib/stores'; import { onMount, getContext, tick } from 'svelte'; const i18n = getContext('i18n'); - import { disablePagination } from '$lib/utils'; + import { disablePagination, enablePagination } from '$lib/utils'; import { updateUserSettings } from '$lib/apis/users'; import { @@ -40,6 +39,7 @@ 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; @@ -55,9 +55,10 @@ let showDropdown = false; let filteredChatList = []; - let paginationScrollThreashold = 0.6; - let nextPageLoading = false; - let chatPagniationComplete = false; + + // Pagination variables + let chatListLoading = false; + let allChatsLoaded = false; pageLimit.set(20); @@ -81,6 +82,18 @@ } }); + const loadMoreChats = async () => { + chatListLoading = true; + pageSkip.set($pageSkip + 1); + const newChatList = await getChatList(localStorage.token, $pageSkip * $pageLimit, $pageLimit); + + // 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) { @@ -151,48 +164,6 @@ window.addEventListener('focus', onFocus); window.addEventListener('blur', onBlur); - // Infinite scroll - const loader = document.getElementById('loader'); - - const observer = new IntersectionObserver( - (entries, observer) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - loadMoreContent(); - observer.unobserve(loader); // Stop observing until content is loaded - } - }); - }, - { - root: null, // viewport - rootMargin: '0px', - threshold: 1.0 // When 100% of the loader is visible - } - ); - - observer.observe(loader); - const loadMoreContent = async () => { - if (!$scrollPaginationEnabled) return; - if ($tagView) return; - if (nextPageLoading) return; - if (chatPagniationComplete) return; - - nextPageLoading = true; - pageSkip.set($pageSkip + 1); - // extend existing chats - const nextPageChats = await getChatList( - localStorage.token, - $pageSkip * $pageLimit, - $pageLimit - ); - // once the bottom of the list has been reached (no results) there is no need to continue querying - chatPagniationComplete = nextPageChats.length === 0; - await chats.set([...$chats, ...nextPageChats]); - nextPageLoading = false; - - observer.observe(loader); // Start observing again after content is loaded - }; - return () => { window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp); @@ -466,8 +437,8 @@ placeholder={$i18n.t('Search')} bind:value={search} on:focus={async () => { - disablePagination(); // TODO: migrate backend for more scalable search mechanism + disablePagination(); await chats.set(await getChatList(localStorage.token)); // when searching, load all chats enrichChatsWithContent($chats); }} @@ -480,8 +451,8 @@
diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index b33046caa..870cc55ff 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -45,7 +45,6 @@ export const showCallOverlay = writable(false); export const scrollPaginationEnabled = writable(true); export const pageSkip = writable(0); export const pageLimit = writable(-1); -export const tagView = writable(false); export type Model = OpenAIModel | OllamaModel; diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 471ecf9d4..55e1a6e95 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -1,7 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; import sha256 from 'js-sha256'; import { WEBUI_BASE_URL } from '$lib/constants'; -import { scrollPaginationEnabled, pageLimit, pageSkip } from '$lib/stores'; +import { scrollPaginationEnabled, pageLimit, pageSkip, chats } from '$lib/stores'; ////////////////////////// // Helper functions @@ -781,6 +781,13 @@ export const bestMatchingLanguage = (supportedLanguages, preferredLanguages, def return match || defaultLocale; }; +export const enablePagination = () => { + chats.set([]); + scrollPaginationEnabled.set(true); + pageLimit.set(20); + pageSkip.set(0); +}; + export const disablePagination = () => { scrollPaginationEnabled.set(false); pageLimit.set(-1); From a084938d9cabcf3a93492d2d48032b7d83cef9f1 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 4 Aug 2024 16:58:08 +0200 Subject: [PATCH 15/19] refac: chatlist skip, limit -> page --- backend/apps/webui/routers/chats.py | 10 ++++- src/lib/apis/chats/index.ts | 9 +++- src/lib/components/chat/Chat.svelte | 42 +++++++++---------- src/lib/components/chat/Messages.svelte | 5 ++- src/lib/components/chat/Settings/Chats.svelte | 2 +- src/lib/components/chat/Tags.svelte | 7 ++-- src/lib/components/layout/Sidebar.svelte | 33 +++++++-------- .../components/layout/Sidebar/ChatItem.svelte | 26 +++++------- src/lib/stores/index.ts | 3 +- src/lib/utils/index.ts | 9 ++-- 10 files changed, 72 insertions(+), 74 deletions(-) diff --git a/backend/apps/webui/routers/chats.py b/backend/apps/webui/routers/chats.py index 47c7c4a87..0b327d0a0 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 = -1 + 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: + limit = 20 + 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 8ff12f4b6..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 = '', skip: number = 0, limit: number = -1) => { +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/?skip=${skip}&limit=${limit}`, { + 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 49cecb696..c55605f82 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -26,8 +26,7 @@ socket, showCallOverlay, tools, - pageSkip, - pageLimit + currentChatPage } from '$lib/stores'; import { convertMessagesToHistory, @@ -423,9 +422,9 @@ params: params, files: chatFiles }); - await chats.set( - await getChatList(localStorage.token, 0, $pageSkip * $pageLimit || $pageLimit) - ); + + currentChatPage.set(0); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); } } }; @@ -471,9 +470,9 @@ params: params, files: chatFiles }); - await chats.set( - await getChatList(localStorage.token, 0, $pageSkip * $pageLimit || $pageLimit) - ); + + currentChatPage.set(0); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); } } }; @@ -633,9 +632,9 @@ tags: [], timestamp: Date.now() }); - await chats.set( - await getChatList(localStorage.token, 0, $pageSkip * $pageLimit || $pageLimit) - ); + + currentChatPage.set(0); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); await chatId.set(chat.id); } else { await chatId.set('local'); @@ -711,7 +710,8 @@ }) ); - await chats.set(await getChatList(localStorage.token, 0, $pageSkip * $pageLimit || $pageLimit)); + currentChatPage.set(0); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); return _responses; }; @@ -958,9 +958,9 @@ params: params, files: chatFiles }); - await chats.set( - await getChatList(localStorage.token, 0, $pageSkip * $pageLimit || $pageLimit) - ); + + currentChatPage.set(0); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); } } } else { @@ -1227,9 +1227,9 @@ params: params, files: chatFiles }); - await chats.set( - await getChatList(localStorage.token, 0, $pageSkip * $pageLimit || $pageLimit) - ); + + currentChatPage.set(0); + await chats.set(await getChatList(localStorage.token, $currentChatPage)); } } } else { @@ -1394,9 +1394,9 @@ if ($settings.saveChatHistory ?? true) { chat = await updateChatById(localStorage.token, _chatId, { title: _title }); - await chats.set( - await getChatList(localStorage.token, 0, $pageSkip * $pageLimit || $pageLimit) - ); + + currentChatPage.set(0); + 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 8e2946c19..5ba9fefaa 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -1,6 +1,6 @@