mirror of
https://github.com/open-webui/open-webui
synced 2024-11-06 16:59:42 +00:00
enh: drag and drop import to folders
This commit is contained in:
parent
590dc0895f
commit
f821de9470
@ -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:
|
||||||
|
@ -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
|
||||||
############################
|
############################
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}}
|
}}
|
||||||
|
@ -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);
|
||||||
}}
|
}}
|
||||||
|
Loading…
Reference in New Issue
Block a user