enh: drag and drop import to folders

This commit is contained in:
Timothy J. Baek 2024-10-17 20:13:28 -07:00
parent 590dc0895f
commit f821de9470
7 changed files with 219 additions and 51 deletions

View File

@ -64,6 +64,11 @@ class ChatForm(BaseModel):
chat: dict chat: dict
class ChatImportForm(ChatForm):
pinned: Optional[bool] = False
folder_id: Optional[str] = None
class ChatTitleMessagesForm(BaseModel): class ChatTitleMessagesForm(BaseModel):
title: str title: str
messages: list[dict] messages: list[dict]
@ -119,6 +124,34 @@ class ChatTable:
db.refresh(result) db.refresh(result)
return ChatModel.model_validate(result) if result else None 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]: def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
try: try:
with get_db() as db: with get_db() as db:

View File

@ -4,6 +4,7 @@ from typing import Optional
from open_webui.apps.webui.models.chats import ( from open_webui.apps.webui.models.chats import (
ChatForm, ChatForm,
ChatImportForm,
ChatResponse, ChatResponse,
Chats, Chats,
ChatTitleIdResponse, 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 # GetChats
############################ ############################

View File

@ -32,6 +32,44 @@ export const createNewChat = async (token: string, chat: object) => {
return res; 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) => { export const getChatList = async (token: string = '', page: number | null = null) => {
let error = null; let error = null;
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();

View File

@ -33,14 +33,42 @@
if (folderElement.contains(e.target)) { if (folderElement.contains(e.target)) {
console.log('Dropped on the Button'); console.log('Dropped on the Button');
try { if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
// get data from the drag event // Iterate over all items in the DataTransferItemList use functional programming
const dataTransfer = e.dataTransfer.getData('text/plain'); for (const item of Array.from(e.dataTransfer.items)) {
const data = JSON.parse(dataTransfer); // If dropped items aren't files, reject them
console.log(data); if (item.kind === 'file') {
dispatch('drop', data); const file = item.getAsFile();
} catch (error) { if (file && file.type === 'application/json') {
console.error(error); 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; draggedOver = false;

View File

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

View File

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

View File

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