diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py
index c20c1db247..1d959f0bc6 100644
--- a/backend/open_webui/models/chats.py
+++ b/backend/open_webui/models/chats.py
@@ -1722,17 +1722,21 @@ class ChatTable:
folder_id: str,
skip: int = 0,
limit: int = 60,
+ sort_by: str = 'updated_at',
+ sort_dir: str = 'desc',
db: AsyncSession | None = None,
) -> list[dict]:
"""Get chats in a folder across ALL users. Returns dicts with user_id."""
async with get_async_db_context(db) as session:
+ sort_column = Chat.title if sort_by == 'title' else Chat.updated_at
+ order_clause = sort_column.asc() if sort_dir == 'asc' else sort_column.desc()
stmt = (
select(Chat.id, Chat.title, Chat.user_id, Chat.updated_at, Chat.created_at, Chat.last_read_at)
.filter_by(folder_id=folder_id)
.filter(or_(Chat.pinned == False, Chat.pinned == None))
.filter_by(archived=False)
.where(Chat.meta['internal'].as_boolean().is_not(True))
- .order_by(Chat.updated_at.desc(), Chat.id)
+ .order_by(order_clause, Chat.id)
)
if skip:
@@ -1754,6 +1758,22 @@ class ChatTable:
for chat in all_chats
]
+ async def count_all_chats_by_folder_id(
+ self,
+ folder_id: str,
+ db: AsyncSession | None = None,
+ ) -> int:
+ async with get_async_db_context(db) as session:
+ stmt = (
+ select(func.count(Chat.id))
+ .filter_by(folder_id=folder_id)
+ .filter(or_(Chat.pinned == False, Chat.pinned == None))
+ .filter_by(archived=False)
+ .where(Chat.meta['internal'].as_boolean().is_not(True))
+ )
+ result = await session.execute(stmt)
+ return result.scalar_one()
+
async def get_chats_by_folder_ids_and_user_id(
self, folder_ids: list[str], user_id: str, db: AsyncSession | None = None
) -> list[ChatModel]:
diff --git a/backend/open_webui/routers/folders.py b/backend/open_webui/routers/folders.py
index a4ea4b8961..e3ebbfd96f 100644
--- a/backend/open_webui/routers/folders.py
+++ b/backend/open_webui/routers/folders.py
@@ -6,7 +6,7 @@ import uuid
from pathlib import Path
from typing import Optional
-from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status
+from fastapi import APIRouter, Depends, File, HTTPException, Query, Request, UploadFile, status
from fastapi.responses import FileResponse, StreamingResponse
from open_webui.config import UPLOAD_DIR
from open_webui.constants import ERROR_MESSAGES
@@ -482,6 +482,9 @@ async def update_folder_access_by_id(
async def get_shared_folder_chats(
request: Request,
id: str,
+ page: int | None = Query(None, ge=1),
+ sort_by: str = Query('updated_at'),
+ sort_dir: str = Query('desc'),
user=Depends(get_verified_user),
db: AsyncSession = Depends(get_async_session),
):
@@ -505,7 +508,17 @@ async def get_shared_folder_chats(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
- chats = await Chats.get_all_chats_by_folder_id(id, db=db)
+ limit = 10
+ skip = (page - 1) * limit if page is not None else 0
+ chats = await Chats.get_all_chats_by_folder_id(
+ id,
+ skip=skip,
+ limit=limit if page is not None else 60,
+ sort_by=sort_by,
+ sort_dir=sort_dir,
+ db=db,
+ )
+ total = await Chats.count_all_chats_by_folder_id(id, db=db) if page is not None else len(chats)
# Resolve owner names for display (avatar URLs are constructed client-side)
owner_cache: dict[str, str] = {}
@@ -516,10 +529,13 @@ async def get_shared_folder_chats(
owner_cache[uid] = u.name if u else 'Unknown'
chat['owner_name'] = owner_cache[uid]
- return {
+ response = {
'chats': [{**chat, 'readonly': chat['user_id'] != user.id} for chat in chats],
'folder_permission': 'write' if has_write else 'read',
}
+ if page is not None:
+ response.update({'total': total, 'has_more': skip + limit < total})
+ return response
############################
diff --git a/src/lib/apis/folders/index.ts b/src/lib/apis/folders/index.ts
index b79e588947..d82f591289 100644
--- a/src/lib/apis/folders/index.ts
+++ b/src/lib/apis/folders/index.ts
@@ -290,17 +290,40 @@ export const getSharedFolders = async (token: string) => {
return res;
};
-export const getSharedFolderChats = async (token: string, folderId: string) => {
+export const getSharedFolderChats = async (
+ token: string,
+ folderId: string,
+ params: {
+ page?: number | null;
+ sortBy?: 'title' | 'updated_at';
+ sortDir?: 'asc' | 'desc';
+ } = {}
+) => {
let error = null;
- const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${folderId}/shared/chats`, {
- method: 'GET',
- headers: {
- Accept: 'application/json',
- 'Content-Type': 'application/json',
- authorization: `Bearer ${token}`
+ const searchParams = new URLSearchParams();
+ if (params.page !== undefined && params.page !== null) {
+ searchParams.append('page', `${params.page}`);
+ }
+ if (params.sortBy) {
+ searchParams.append('sort_by', params.sortBy);
+ }
+ if (params.sortDir) {
+ searchParams.append('sort_dir', params.sortDir);
+ }
+ const query = searchParams.toString();
+
+ const res = await fetch(
+ `${WEBUI_API_BASE_URL}/folders/${folderId}/shared/chats${query ? `?${query}` : ''}`,
+ {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ authorization: `Bearer ${token}`
+ }
}
- })
+ )
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte
index e77e296129..08bde0bc9f 100644
--- a/src/lib/components/chat/Chat.svelte
+++ b/src/lib/components/chat/Chat.svelte
@@ -1177,7 +1177,7 @@
}
const pageSubscribe = page.subscribe(async (p) => {
- if (p.url.pathname === '/') {
+ if (p.url.pathname === '/' || p.url.pathname.startsWith('/folders/')) {
await tick();
initNewChat();
}
diff --git a/src/lib/components/chat/Placeholder/ChatList.svelte b/src/lib/components/chat/Placeholder/ChatList.svelte
index 64a9ed215d..77bd5ae4ab 100644
--- a/src/lib/components/chat/Placeholder/ChatList.svelte
+++ b/src/lib/components/chat/Placeholder/ChatList.svelte
@@ -1,27 +1,35 @@
{#if chatList}
@@ -153,7 +170,6 @@
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"
draggable="false"
href={`/c/${chat.id}`}
- on:click={() => (show = false)}
>
{chat?.title}
@@ -177,19 +193,48 @@
{/each}
- {#if !allChatsLoaded && loadHandler}
-
{
- if (!chatListLoading) {
- loadHandler();
- }
- }}
- >
-
-
+ {#if totalPages > 1}
+
+
+
+ {#each pages as item, index (item === 'ellipsis' ? `ellipsis-${index}` : item)}
+ {#if item === 'ellipsis'}
+
+ ...
+
+ {:else}
+
+ {/if}
+ {/each}
+
+
+
{/if}
{/if}
diff --git a/src/lib/components/chat/Placeholder/FolderPlaceholder.svelte b/src/lib/components/chat/Placeholder/FolderPlaceholder.svelte
index d7cbc6b400..c943f57ee0 100644
--- a/src/lib/components/chat/Placeholder/FolderPlaceholder.svelte
+++ b/src/lib/components/chat/Placeholder/FolderPlaceholder.svelte
@@ -11,18 +11,21 @@
import ChatList from './ChatList.svelte';
import FolderKnowledge from './FolderKnowledge.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
- import { getChatListByFolderId } from '$lib/apis/chats';
import { getSharedFolderChats } from '$lib/apis/folders';
export let folder: any = null;
let selectedTab = 'chats';
+ const CHATS_PAGE_SIZE = 10;
let page = 1;
+ let totalChats = 0;
+ let orderBy: 'title' | 'updated_at' = 'updated_at';
+ let direction: 'asc' | 'desc' = 'desc';
+ let currentFolderId: string | null = null;
let chats: any[] | null = null;
let chatListLoading = false;
- let allChatsLoaded = false;
$: showOwnerInfo = Boolean(
folder?.shared ||
@@ -30,42 +33,68 @@
(folder?.access_grants?.length ?? 0) > 0
);
- const loadChats = async () => {
- // getSharedFolderChats returns all users' chats in one shot; no pagination
- allChatsLoaded = true;
+ const setSortKey = (key: 'title' | 'updated_at') => {
+ if (orderBy === key) {
+ direction = direction === 'asc' ? 'desc' : 'asc';
+ } else {
+ orderBy = key;
+ direction = key === 'title' ? 'asc' : 'desc';
+ }
+ page = 1;
+ setChatList();
+ };
+
+ const setPage = (nextPage: number) => {
+ if (nextPage === page || chatListLoading) {
+ return;
+ }
+
+ page = nextPage;
+ setChatList();
};
const setChatList = async () => {
+ const folderId = folder?.id;
chats = null;
- page = 1;
- allChatsLoaded = false;
- chatListLoading = false;
- if (folder && folder.id) {
+ if (folderId) {
// Always use the shared folder endpoint so owners also see
// chats created by users who have write access to this folder.
- const res = await getSharedFolderChats(localStorage.token, folder.id).catch((error) => {
+ chatListLoading = true;
+ const res = await getSharedFolderChats(localStorage.token, folderId, {
+ page,
+ sortBy: orderBy,
+ sortDir: direction
+ }).catch((error) => {
console.error(error);
return null;
});
+ chatListLoading = false;
+
if (res && res.chats) {
chats = res.chats;
- allChatsLoaded = true;
+ totalChats = res.total ?? res.chats.length;
} else {
- // Fallback to regular API (e.g. if user has no shared access)
- const fallback = await getChatListByFolderId(localStorage.token, folder.id, page).catch(
- () => []
- );
- chats = fallback || [];
+ chats = [];
+ totalChats = 0;
}
} else {
chats = [];
+ totalChats = 0;
}
};
- $: if (folder) {
+ $: if (folder?.id && folder.id !== currentFolderId) {
+ currentFolderId = folder.id;
+ page = 1;
setChatList();
}
+
+ $: if (!folder?.id && currentFolderId !== null) {
+ currentFolderId = null;
+ chats = [];
+ totalChats = 0;
+ }
@@ -105,9 +134,14 @@
{:else}
diff --git a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte
index c13058200c..f971a93a9c 100644
--- a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte
+++ b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte
@@ -44,7 +44,6 @@
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import FolderModal from './Folders/FolderModal.svelte';
import Emoji from '$lib/components/common/Emoji.svelte';
- import Spinner from '$lib/components/common/Spinner.svelte';
export let folderRegistry = {};
export let open = false;
@@ -385,24 +384,46 @@
}, 500);
};
+ const SIDEBAR_CHATS_PAGE_SIZE = 10;
+ /** @type {any[] | null} */
let chats = null;
- export const setFolderItems = async () => {
+ let chatsPage = 1;
+ let hasMoreChats = false;
+ let chatsLoading = false;
+
+ export const setFolderItems = async (append = false) => {
await tick();
- if (open) {
+ if (open && !chatsLoading) {
// Always use getSharedFolderChats so owners also see chats
// created by users who have write access to this folder.
+ const nextPage = append ? chatsPage + 1 : 1;
+ chatsLoading = true;
try {
- const res = await getSharedFolderChats(localStorage.token, folderId);
- chats = res?.chats ?? [];
+ const res = await getSharedFolderChats(localStorage.token, folderId, {
+ page: nextPage
+ });
+ const nextChats = res?.chats ?? [];
+ chats = append ? [...(chats ?? []), ...nextChats] : nextChats;
+ chatsPage = nextPage;
+ hasMoreChats = res?.has_more ?? nextChats.length === SIDEBAR_CHATS_PAGE_SIZE;
} catch (error) {
// Fallback to regular API
- chats = await getChatListByFolderId(localStorage.token, folderId).catch((error) => {
- toast.error(`${error}`);
- return [];
- });
+ const fallback = await getChatListByFolderId(localStorage.token, folderId, nextPage).catch(
+ (error) => {
+ toast.error(`${error}`);
+ return [];
+ }
+ );
+ chats = append ? [...(chats ?? []), ...(fallback ?? [])] : (fallback ?? []);
+ chatsPage = nextPage;
+ hasMoreChats = (fallback?.length ?? 0) === SIDEBAR_CHATS_PAGE_SIZE;
+ } finally {
+ chatsLoading = false;
}
} else {
chats = null;
+ chatsPage = 1;
+ hasMoreChats = false;
}
};
@@ -564,7 +585,7 @@
await selectedFolder.set({ ...folders[folderId], ...folder });
}
- await goto('/');
+ await goto(`/folders/${folderId}`);
if ($mobile) {
showSidebar.set(!$showSidebar);
@@ -678,7 +699,7 @@
- {#if (folders[folderId]?.childrenIds ?? []).length > 0 || (chats ?? []).length > 0}
+ {#if (folders[folderId]?.childrenIds ?? []).length > 0 || (chats ?? []).length > 0 || hasMoreChats}
{/if}
- {#if chats === null}
-
-
+ {#if chats === null && chatsLoading}
+
+
+
+
{/if}
diff --git a/src/lib/components/layout/Sidebar/SharedFolderItem.svelte b/src/lib/components/layout/Sidebar/SharedFolderItem.svelte
index ba74ffbba4..98a92f9042 100644
--- a/src/lib/components/layout/Sidebar/SharedFolderItem.svelte
+++ b/src/lib/components/layout/Sidebar/SharedFolderItem.svelte
@@ -1,6 +1,8 @@
+
+{#if ready}
+
+{:else}
+
+
+
+{/if}