ソースを参照

refac/enh: folder optimization

Timothy Jaeryang Baek 1 週間 前
コミット
c80bb31968

+ 6 - 1
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]
 

+ 14 - 0
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
 ####################

+ 22 - 0
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
 ############################

+ 2 - 9
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
     ]

+ 39 - 0
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;
 

+ 7 - 3
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);
 		}
 	});
 

+ 12 - 0
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();
+		}
+	};
 </script>
 
 {#each folderList as folderId (folderId)}
 	<RecursiveFolder
 		className=""
+		bind:folderRegistry
 		{folders}
 		{folderId}
 		{shiftKey}
 		{onDelete}
+		{onItemMove}
 		on:import={(e) => {
 			dispatch('import', e.detail);
 		}}

+ 0 - 2
src/lib/components/layout/Sidebar/Folders/FolderModal.svelte

@@ -59,8 +59,6 @@
 			system_prompt: '',
 			files: []
 		};
-
-		console.log(folder);
 	};
 
 	const focusInput = async () => {

+ 51 - 17
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 @@
 		</div>
 
 		<div slot="content" class="w-full">
-			{#if (folders[folderId]?.childrenIds ?? []).length > 0 || (folders[folderId].items?.chats ?? []).length > 0}
+			{#if (folders[folderId]?.childrenIds ?? []).length > 0 || (chats ?? []).length > 0}
 				<div
 					class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s border-gray-100 dark:border-gray-900"
 				>
@@ -564,10 +592,12 @@
 
 						{#each children as childFolder (`${folderId}-${childFolder.id}`)}
 							<svelte:self
+								bind:folderRegistry
 								{folders}
 								folderId={childFolder.id}
 								{shiftKey}
 								parentDragged={dragged}
+								{onItemMove}
 								{onDelete}
 								on:import={(e) => {
 									dispatch('import', e.detail);
@@ -582,18 +612,22 @@
 						{/each}
 					{/if}
 
-					{#if folders[folderId].items?.chats}
-						{#each folders[folderId].items.chats as chat (chat.id)}
-							<ChatItem
-								id={chat.id}
-								title={chat.title}
-								{shiftKey}
-								on:change={(e) => {
-									dispatch('change', e.detail);
-								}}
-							/>
-						{/each}
-					{/if}
+					{#each chats ?? [] as chat (chat.id)}
+						<ChatItem
+							id={chat.id}
+							title={chat.title}
+							{shiftKey}
+							on:change={(e) => {
+								dispatch('change', e.detail);
+							}}
+						/>
+					{/each}
+				</div>
+			{/if}
+
+			{#if chats === null}
+				<div class="flex justify-center items-center p-2">
+					<Spinner className="size-4 text-gray-500" />
 				</div>
 			{/if}
 		</div>