Kaynağa Gözat

enh: drag and drop import to folders

Timothy J. Baek 6 ay önce
ebeveyn
işleme
f821de9470

+ 33 - 0
backend/open_webui/apps/webui/models/chats.py

@@ -64,6 +64,11 @@ class ChatForm(BaseModel):
     chat: dict
 
 
+class ChatImportForm(ChatForm):
+    pinned: Optional[bool] = False
+    folder_id: Optional[str] = None
+
+
 class ChatTitleMessagesForm(BaseModel):
     title: str
     messages: list[dict]
@@ -119,6 +124,34 @@ class ChatTable:
             db.refresh(result)
             return ChatModel.model_validate(result) if result else None
 
+    def import_chat(
+        self, user_id: str, form_data: ChatImportForm
+    ) -> Optional[ChatModel]:
+        with get_db() as db:
+            id = str(uuid.uuid4())
+            chat = ChatModel(
+                **{
+                    "id": id,
+                    "user_id": user_id,
+                    "title": (
+                        form_data.chat["title"]
+                        if "title" in form_data.chat
+                        else "New Chat"
+                    ),
+                    "chat": form_data.chat,
+                    "pinned": form_data.pinned,
+                    "folder_id": form_data.folder_id,
+                    "created_at": int(time.time()),
+                    "updated_at": int(time.time()),
+                }
+            )
+
+            result = Chat(**chat.model_dump())
+            db.add(result)
+            db.commit()
+            db.refresh(result)
+            return ChatModel.model_validate(result) if result else None
+
     def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
         try:
             with get_db() as db:

+ 18 - 0
backend/open_webui/apps/webui/routers/chats.py

@@ -4,6 +4,7 @@ from typing import Optional
 
 from open_webui.apps.webui.models.chats import (
     ChatForm,
+    ChatImportForm,
     ChatResponse,
     Chats,
     ChatTitleIdResponse,
@@ -99,6 +100,23 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_verified_user)):
         )
 
 
+############################
+# ImportChat
+############################
+
+
+@router.post("/import", response_model=Optional[ChatResponse])
+async def import_chat(form_data: ChatImportForm, user=Depends(get_verified_user)):
+    try:
+        chat = Chats.import_chat(user.id, form_data)
+        return ChatResponse(**chat.model_dump())
+    except Exception as e:
+        log.exception(e)
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
+        )
+
+
 ############################
 # GetChats
 ############################

+ 38 - 0
src/lib/apis/chats/index.ts

@@ -32,6 +32,44 @@ export const createNewChat = async (token: string, chat: object) => {
 	return res;
 };
 
+export const importChat = async (
+	token: string,
+	chat: object,
+	pinned?: boolean,
+	folderId?: string | null
+) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/import`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			chat: chat,
+			pinned: pinned,
+			folder_id: folderId
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			error = err;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
 export const getChatList = async (token: string = '', page: number | null = null) => {
 	let error = null;
 	const searchParams = new URLSearchParams();

+ 36 - 8
src/lib/components/common/Folder.svelte

@@ -33,14 +33,42 @@
 		if (folderElement.contains(e.target)) {
 			console.log('Dropped on the Button');
 
-			try {
-				// get data from the drag event
-				const dataTransfer = e.dataTransfer.getData('text/plain');
-				const data = JSON.parse(dataTransfer);
-				console.log(data);
-				dispatch('drop', data);
-			} catch (error) {
-				console.error(error);
+			if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
+				// Iterate over all items in the DataTransferItemList use functional programming
+				for (const item of Array.from(e.dataTransfer.items)) {
+					// If dropped items aren't files, reject them
+					if (item.kind === 'file') {
+						const file = item.getAsFile();
+						if (file && file.type === 'application/json') {
+							console.log('Dropped file is a JSON file!');
+
+							// Read the JSON file with FileReader
+							const reader = new FileReader();
+							reader.onload = async function (event) {
+								try {
+									const fileContent = JSON.parse(event.target.result);
+									console.log('Parsed JSON Content: ', fileContent);
+									dispatch('import', fileContent);
+								} catch (error) {
+									console.error('Error parsing JSON file:', error);
+								}
+							};
+
+							// Start reading the file
+							reader.readAsText(file);
+						} else {
+							console.error('Only JSON file types are supported.');
+						}
+
+						console.log(file);
+					} else {
+						// Handle the drag-and-drop data for folders or chats (same as before)
+						const dataTransfer = e.dataTransfer.getData('text/plain');
+						const data = JSON.parse(dataTransfer);
+						console.log(data);
+						dispatch('drop', data);
+					}
+				}
 			}
 
 			draggedOver = false;

+ 25 - 10
src/lib/components/layout/Sidebar.svelte

@@ -32,7 +32,8 @@
 		toggleChatPinnedStatusById,
 		getChatPinnedStatusById,
 		getChatById,
-		updateChatFolderIdById
+		updateChatFolderIdById,
+		importChat
 	} from '$lib/apis/chats';
 	import { WEBUI_BASE_URL } from '$lib/constants';
 
@@ -208,6 +209,17 @@
 		}
 	};
 
+	const importChatHandler = async (items, pinned = false, folderId = null) => {
+		console.log('importChatHandler', items, pinned, folderId);
+		for (const item of items) {
+			if (item.chat) {
+				await importChat(localStorage.token, item.chat, pinned, folderId);
+			}
+		}
+
+		initChatList();
+	};
+
 	const inputFilesHandler = async (files) => {
 		console.log(files);
 
@@ -217,18 +229,11 @@
 				const content = e.target.result;
 
 				try {
-					const items = JSON.parse(content);
-
-					for (const item of items) {
-						if (item.chat) {
-							await createNewChat(localStorage.token, item.chat);
-						}
-					}
+					const chatItems = JSON.parse(content);
+					importChatHandler(chatItems);
 				} catch {
 					toast.error($i18n.t(`Invalid file format.`));
 				}
-
-				initChatList();
 			};
 
 			reader.readAsText(file);
@@ -564,6 +569,9 @@
 							localStorage.setItem('showPinnedChat', e.detail);
 							console.log(e.detail);
 						}}
+						on:import={(e) => {
+							importChatHandler(e.detail, true);
+						}}
 						on:drop={async (e) => {
 							const { type, id } = e.detail;
 
@@ -633,6 +641,10 @@
 				{#if !search && folders}
 					<Folders
 						{folders}
+						on:import={(e) => {
+							const { folderId, items } = e.detail;
+							importChatHandler(items, false, folderId);
+						}}
 						on:update={async (e) => {
 							initChatList();
 						}}
@@ -646,6 +658,9 @@
 					collapsible={!search}
 					className="px-2"
 					name={$i18n.t('All chats')}
+					on:import={(e) => {
+						importChatHandler(e.detail);
+					}}
 					on:drop={async (e) => {
 						const { type, id } = e.detail;
 

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

@@ -22,6 +22,9 @@
 		className="px-2"
 		{folders}
 		{folderId}
+		on:import={(e) => {
+			dispatch('import', e.detail);
+		}}
 		on:update={(e) => {
 			dispatch('update', e.detail);
 		}}

+ 69 - 36
src/lib/components/layout/Sidebar/RecursiveFolder.svelte

@@ -61,47 +61,77 @@
 		if (folderElement.contains(e.target)) {
 			console.log('Dropped on the Button');
 
-			try {
-				// get data from the drag event
-				const dataTransfer = e.dataTransfer.getData('text/plain');
-				const data = JSON.parse(dataTransfer);
-				console.log(data);
-
-				const { type, id } = data;
-
-				if (type === 'folder') {
-					open = true;
-					if (id === folderId) {
-						return;
-					}
-					// Move the folder
-					const res = await updateFolderParentIdById(localStorage.token, id, folderId).catch(
-						(error) => {
-							toast.error(error);
-							return null;
-						}
-					);
+			if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
+				// Iterate over all items in the DataTransferItemList use functional programming
+				for (const item of Array.from(e.dataTransfer.items)) {
+					// If dropped items aren't files, reject them
+					if (item.kind === 'file') {
+						const file = item.getAsFile();
+						if (file && file.type === 'application/json') {
+							console.log('Dropped file is a JSON file!');
+
+							// Read the JSON file with FileReader
+							const reader = new FileReader();
+							reader.onload = async function (event) {
+								try {
+									const fileContent = JSON.parse(event.target.result);
+									dispatch('import', {
+										folderId: folderId,
+										items: fileContent
+									});
+								} catch (error) {
+									console.error('Error parsing JSON file:', error);
+								}
+							};
 
-					if (res) {
-						dispatch('update');
-					}
-				} else if (type === 'chat') {
-					open = true;
-
-					// Move the chat
-					const res = await updateChatFolderIdById(localStorage.token, id, folderId).catch(
-						(error) => {
-							toast.error(error);
-							return null;
+							// Start reading the file
+							reader.readAsText(file);
+						} else {
+							console.error('Only JSON file types are supported.');
 						}
-					);
 
-					if (res) {
-						dispatch('update');
+						console.log(file);
+					} else {
+						// Handle the drag-and-drop data for folders or chats (same as before)
+						const dataTransfer = e.dataTransfer.getData('text/plain');
+						const data = JSON.parse(dataTransfer);
+						console.log(data);
+
+						const { type, id } = data;
+
+						if (type === 'folder') {
+							open = true;
+							if (id === folderId) {
+								return;
+							}
+							// Move the folder
+							const res = await updateFolderParentIdById(localStorage.token, id, folderId).catch(
+								(error) => {
+									toast.error(error);
+									return null;
+								}
+							);
+
+							if (res) {
+								dispatch('update');
+							}
+						} else if (type === 'chat') {
+							open = true;
+
+							// Move the chat
+							const res = await updateChatFolderIdById(localStorage.token, id, folderId).catch(
+								(error) => {
+									toast.error(error);
+									return null;
+								}
+							);
+
+							if (res) {
+								dispatch('update');
+							}
+						}
 					}
 				}
-			} catch (error) {
-				console.error(error);
 			}
 
 			draggedOver = false;
@@ -398,6 +428,9 @@
 								{folders}
 								folderId={childFolder.id}
 								parentDragged={dragged}
+								on:import={(e) => {
+									dispatch('import', e.detail);
+								}}
 								on:update={(e) => {
 									dispatch('update', e.detail);
 								}}