diff --git a/backend/open_webui/apps/webui/models/chats.py b/backend/open_webui/apps/webui/models/chats.py index 119a40383..4b97b9d43 100644 --- a/backend/open_webui/apps/webui/models/chats.py +++ b/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: diff --git a/backend/open_webui/apps/webui/routers/chats.py b/backend/open_webui/apps/webui/routers/chats.py index 9c404a9a7..2cc94e803 100644 --- a/backend/open_webui/apps/webui/routers/chats.py +++ b/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 ############################ diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts index 13ad8fdba..745672b40 100644 --- a/src/lib/apis/chats/index.ts +++ b/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(); diff --git a/src/lib/components/common/Folder.svelte b/src/lib/components/common/Folder.svelte index ec6040da9..e8a04db91 100644 --- a/src/lib/components/common/Folder.svelte +++ b/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; diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 1e34b32ef..3c9babfc3 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/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} { + 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; diff --git a/src/lib/components/layout/Sidebar/Folders.svelte b/src/lib/components/layout/Sidebar/Folders.svelte index aaac2378c..fb0c955c5 100644 --- a/src/lib/components/layout/Sidebar/Folders.svelte +++ b/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); }} diff --git a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte index 6561142ca..ecb0684e2 100644 --- a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte +++ b/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); + 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!'); - const { type, id } = data; + // 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 (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; + // Start reading the file + reader.readAsText(file); + } else { + console.error('Only JSON file types are supported.'); } - ); - if (res) { - dispatch('update'); - } - } else if (type === 'chat') { - open = true; + 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); - // Move the chat - const res = await updateChatFolderIdById(localStorage.token, id, folderId).catch( - (error) => { - toast.error(error); - return null; + 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'); + } } - ); - - 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); }}