From c80bb3196883e692374abdf4b405ad3da68f139b Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 26 Sep 2025 20:48:17 -0500 Subject: [PATCH] refac/enh: folder optimization --- backend/open_webui/models/chats.py | 7 +- backend/open_webui/models/folders.py | 14 ++++ backend/open_webui/routers/chats.py | 22 ++++++ backend/open_webui/routers/folders.py | 11 +-- src/lib/apis/chats/index.ts | 39 +++++++++++ .../components/layout/Sidebar/ChatItem.svelte | 10 ++- .../components/layout/Sidebar/Folders.svelte | 12 ++++ .../layout/Sidebar/Folders/FolderModal.svelte | 2 - .../layout/Sidebar/RecursiveFolder.svelte | 68 ++++++++++++++----- 9 files changed, 153 insertions(+), 32 deletions(-) diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 97fd9b6256..083d044053 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -810,7 +810,7 @@ class ChatTable: return [ChatModel.model_validate(chat) for chat in all_chats] def get_chats_by_folder_id_and_user_id( - self, folder_id: str, user_id: str + self, folder_id: str, user_id: str, skip: int = 0, limit: int = 60 ) -> list[ChatModel]: with get_db() as db: query = db.query(Chat).filter_by(folder_id=folder_id, user_id=user_id) @@ -819,6 +819,11 @@ class ChatTable: query = query.order_by(Chat.updated_at.desc()) + if skip: + query = query.offset(skip) + if limit: + query = query.limit(limit) + all_chats = query.all() return [ChatModel.model_validate(chat) for chat in all_chats] diff --git a/backend/open_webui/models/folders.py b/backend/open_webui/models/folders.py index c876645750..45f8247080 100644 --- a/backend/open_webui/models/folders.py +++ b/backend/open_webui/models/folders.py @@ -50,6 +50,20 @@ class FolderModel(BaseModel): model_config = ConfigDict(from_attributes=True) +class FolderMetadataResponse(BaseModel): + icon: Optional[str] = None + + +class FolderNameIdResponse(BaseModel): + id: str + name: str + meta: Optional[FolderMetadataResponse] = None + parent_id: Optional[str] = None + is_expanded: bool = False + created_at: int + updated_at: int + + #################### # Forms #################### diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 788e355f2b..65f912ff53 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -218,6 +218,28 @@ async def get_chats_by_folder_id(folder_id: str, user=Depends(get_verified_user) ] +@router.get("/folder/{folder_id}/list") +async def get_chat_list_by_folder_id( + folder_id: str, page: Optional[int] = 1, user=Depends(get_verified_user) +): + try: + limit = 60 + skip = (page - 1) * limit + + return [ + {"title": chat.title, "id": chat.id, "updated_at": chat.updated_at} + for chat in Chats.get_chats_by_folder_id_and_user_id( + folder_id, user.id, skip=skip, limit=limit + ) + ] + + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() + ) + + ############################ # GetPinnedChats ############################ diff --git a/backend/open_webui/routers/folders.py b/backend/open_webui/routers/folders.py index ddee71ea4d..51c1eba5f4 100644 --- a/backend/open_webui/routers/folders.py +++ b/backend/open_webui/routers/folders.py @@ -12,6 +12,7 @@ from open_webui.models.folders import ( FolderForm, FolderUpdateForm, FolderModel, + FolderNameIdResponse, Folders, ) from open_webui.models.chats import Chats @@ -44,7 +45,7 @@ router = APIRouter() ############################ -@router.get("/", response_model=list[FolderModel]) +@router.get("/", response_model=list[FolderNameIdResponse]) async def get_folders(user=Depends(get_verified_user)): folders = Folders.get_folders_by_user_id(user.id) @@ -76,14 +77,6 @@ async def get_folders(user=Depends(get_verified_user)): return [ { **folder.model_dump(), - "items": { - "chats": [ - {"title": chat.title, "id": chat.id, "updated_at": chat.updated_at} - for chat in Chats.get_chats_by_folder_id_and_user_id( - folder.id, user.id - ) - ] - }, } for folder in folders ] diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts index 59d8600771..a19220278d 100644 --- a/src/lib/apis/chats/index.ts +++ b/src/lib/apis/chats/index.ts @@ -327,6 +327,45 @@ export const getChatsByFolderId = async (token: string, folderId: string) => { return res; }; +export const getChatListByFolderId = async (token: string, folderId: string, page: number = 1) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (page !== null) { + searchParams.append('page', `${page}`); + } + + const res = await fetch( + `${WEBUI_API_BASE_URL}/chats/folder/${folderId}/list?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const getAllArchivedChats = async (token: string) => { let error = null; diff --git a/src/lib/components/layout/Sidebar/ChatItem.svelte b/src/lib/components/layout/Sidebar/ChatItem.svelte index 2098100457..726455cd6f 100644 --- a/src/lib/components/layout/Sidebar/ChatItem.svelte +++ b/src/lib/components/layout/Sidebar/ChatItem.svelte @@ -51,6 +51,8 @@ export let selected = false; export let shiftKey = false; + export let onDragEnd = () => {}; + let chat = null; let mouseOver = false; @@ -201,11 +203,13 @@ y = event.clientY; }; - const onDragEnd = (event) => { + const onDragEndHandler = (event) => { event.stopPropagation(); itemElement.style.opacity = '1'; // Reset visual cue after drag dragged = false; + + onDragEnd(event); }; const onClickOutside = (event) => { @@ -225,7 +229,7 @@ // Event listener for when dragging occurs (optional) itemElement.addEventListener('drag', onDrag); // Event listener for when dragging ends - itemElement.addEventListener('dragend', onDragEnd); + itemElement.addEventListener('dragend', onDragEndHandler); } }); @@ -235,7 +239,7 @@ itemElement.removeEventListener('dragstart', onDragStart); itemElement.removeEventListener('drag', onDrag); - itemElement.removeEventListener('dragend', onDragEnd); + itemElement.removeEventListener('dragend', onDragEndHandler); } }); diff --git a/src/lib/components/layout/Sidebar/Folders.svelte b/src/lib/components/layout/Sidebar/Folders.svelte index e4661008a5..e9ef68fc39 100644 --- a/src/lib/components/layout/Sidebar/Folders.svelte +++ b/src/lib/components/layout/Sidebar/Folders.svelte @@ -18,15 +18,27 @@ sensitivity: 'base' }) ); + + let folderRegistry = {}; + + const onItemMove = (e) => { + console.log(`onItemMove`, e, folderRegistry); + + if (e.originFolderId) { + folderRegistry[e.originFolderId]?.setFolderItems(); + } + }; {#each folderList as folderId (folderId)} { dispatch('import', e.detail); }} diff --git a/src/lib/components/layout/Sidebar/Folders/FolderModal.svelte b/src/lib/components/layout/Sidebar/Folders/FolderModal.svelte index 5f6af17615..a6ec351caa 100644 --- a/src/lib/components/layout/Sidebar/Folders/FolderModal.svelte +++ b/src/lib/components/layout/Sidebar/Folders/FolderModal.svelte @@ -59,8 +59,6 @@ system_prompt: '', files: [] }; - - console.log(folder); }; const focusInput = async () => { diff --git a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte index 7b236e0ed9..497eedd7a1 100644 --- a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte +++ b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte @@ -8,6 +8,7 @@ import fileSaver from 'file-saver'; const { saveAs } = fileSaver; + import { goto } from '$app/navigation'; import { toast } from 'svelte-sonner'; import { chatId, mobile, selectedFolder, showSidebar } from '$lib/stores'; @@ -21,6 +22,7 @@ import { getChatById, getChatsByFolderId, + getChatListByFolderId, importChat, updateChatFolderIdById } from '$lib/apis/chats'; @@ -37,9 +39,10 @@ import FolderMenu from './Folders/FolderMenu.svelte'; import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import FolderModal from './Folders/FolderModal.svelte'; - import { goto } from '$app/navigation'; import Emoji from '$lib/components/common/Emoji.svelte'; + import Spinner from '$lib/components/common/Spinner.svelte'; + export let folderRegistry = {}; export let open = false; export let folders; @@ -51,6 +54,7 @@ export let parentDragged = false; export let onDelete = (e) => {}; + export let onItemMove = (e) => {}; let folderElement; @@ -171,6 +175,12 @@ return null; }); + onItemMove({ + originFolderId: chat.folder_id, + targetFolderId: folderId, + e + }); + if (res) { dispatch('update'); } @@ -182,6 +192,7 @@ } } + setFolderItems(); draggedOver = false; } }; @@ -234,6 +245,10 @@ }; onMount(async () => { + folderRegistry[folderId] = { + setFolderItems: () => setFolderItems() + }; + open = folders[folderId].is_expanded; if (folderElement) { folderElement.addEventListener('dragover', onDragOver); @@ -250,7 +265,6 @@ if (folders[folderId]?.new) { delete folders[folderId].new; - await tick(); renameHandler(); } @@ -339,6 +353,21 @@ }, 500); }; + let chats = null; + export const setFolderItems = async () => { + await tick(); + if (open) { + chats = await getChatListByFolderId(localStorage.token, folderId).catch((error) => { + toast.error(`${error}`); + return []; + }); + } else { + chats = null; + } + }; + + $: setFolderItems(open); + const renameHandler = async () => { console.log('Edit'); await tick(); @@ -419,8 +448,6 @@ bind:open className="w-full" buttonClassName="w-full" - hide={(folders[folderId]?.childrenIds ?? []).length === 0 && - (folders[folderId].items?.chats ?? []).length === 0} onChange={(state) => { dispatch('open', state); }} @@ -466,6 +493,7 @@ class="text-gray-500 dark:text-gray-500 transition-all p-1 hover:bg-gray-200 dark:hover:bg-gray-850 rounded-lg" on:click={(e) => { e.stopPropagation(); + e.stopImmediatePropagation(); open = !open; isExpandedUpdateDebounceHandler(); }} @@ -548,7 +576,7 @@
- {#if (folders[folderId]?.childrenIds ?? []).length > 0 || (folders[folderId].items?.chats ?? []).length > 0} + {#if (folders[folderId]?.childrenIds ?? []).length > 0 || (chats ?? []).length > 0}
@@ -564,10 +592,12 @@ {#each children as childFolder (`${folderId}-${childFolder.id}`)} { dispatch('import', e.detail); @@ -582,18 +612,22 @@ {/each} {/if} - {#if folders[folderId].items?.chats} - {#each folders[folderId].items.chats as chat (chat.id)} - { - dispatch('change', e.detail); - }} - /> - {/each} - {/if} + {#each chats ?? [] as chat (chat.id)} + { + dispatch('change', e.detail); + }} + /> + {/each} +
+ {/if} + + {#if chats === null} +
+
{/if}