diff --git a/backend/open_webui/apps/webui/main.py b/backend/open_webui/apps/webui/main.py index 1d12d708e..94e42f4a8 100644 --- a/backend/open_webui/apps/webui/main.py +++ b/backend/open_webui/apps/webui/main.py @@ -9,6 +9,7 @@ from open_webui.apps.webui.models.models import Models from open_webui.apps.webui.routers import ( auths, chats, + folders, configs, files, functions, @@ -110,6 +111,7 @@ app.include_router(configs.router, prefix="/configs", tags=["configs"]) app.include_router(auths.router, prefix="/auths", tags=["auths"]) app.include_router(users.router, prefix="/users", tags=["users"]) app.include_router(chats.router, prefix="/chats", tags=["chats"]) +app.include_router(folders.router, prefix="/folders", tags=["folders"]) app.include_router(models.router, prefix="/models", tags=["models"]) app.include_router(knowledge.router, prefix="/knowledge", tags=["knowledge"]) diff --git a/backend/open_webui/apps/webui/models/chats.py b/backend/open_webui/apps/webui/models/chats.py index 12bdd1c38..2b0d29795 100644 --- a/backend/open_webui/apps/webui/models/chats.py +++ b/backend/open_webui/apps/webui/models/chats.py @@ -33,6 +33,7 @@ class Chat(Base): pinned = Column(Boolean, default=False, nullable=True) meta = Column(JSON, server_default="{}") + folder_id = Column(Text, nullable=True) class ChatModel(BaseModel): @@ -51,6 +52,7 @@ class ChatModel(BaseModel): pinned: Optional[bool] = False meta: dict = {} + folder_id: Optional[str] = None #################### @@ -512,6 +514,29 @@ class ChatTable: # Validate and return chats 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 + ) -> list[ChatModel]: + with get_db() as db: + all_chats = ( + db.query(Chat).filter_by(folder_id=folder_id, user_id=user_id).all() + ) + return [ChatModel.model_validate(chat) for chat in all_chats] + + def update_chat_folder_id_by_id_and_user_id( + self, id: str, user_id: str, folder_id: str + ) -> Optional[ChatModel]: + try: + with get_db() as db: + chat = db.get(Chat, id) + chat.folder_id = folder_id + chat.updated_at = int(time.time()) + db.commit() + db.refresh(chat) + return ChatModel.model_validate(chat) + except Exception: + return None + def get_chat_tags_by_id_and_user_id(self, id: str, user_id: str) -> list[TagModel]: with get_db() as db: chat = db.get(Chat, id) diff --git a/backend/open_webui/apps/webui/models/folders.py b/backend/open_webui/apps/webui/models/folders.py index e8e6ffef1..93a53a590 100644 --- a/backend/open_webui/apps/webui/models/folders.py +++ b/backend/open_webui/apps/webui/models/folders.py @@ -22,7 +22,6 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"]) class FolderItems(BaseModel): chat_ids: Optional[list[str]] = None file_ids: Optional[list[str]] = None - folder_ids: Optional[list[str]] = None model_config = ConfigDict(extra="allow") @@ -52,6 +51,21 @@ class FolderModel(BaseModel): model_config = ConfigDict(from_attributes=True) +#################### +# Forms +#################### + + +class FolderForm(BaseModel): + name: str + model_config = ConfigDict(extra="allow") + + +class FolderItemsUpdateForm(BaseModel): + items: FolderItems + model_config = ConfigDict(extra="allow") + + class FolderTable: def insert_new_folder(self, name: str, user_id: str) -> Optional[FolderModel]: with get_db() as db: @@ -96,7 +110,59 @@ class FolderTable: for folder in db.query(Folder).filter_by(user_id=user_id).all() ] - def update_folder_by_name_and_user_id( + def get_folders_by_parent_id_and_user_id(self, parent_id: str, user_id: str): + with get_db() as db: + return [ + FolderModel.model_validate(folder) + for folder in db.query(Folder) + .filter_by(parent_id=parent_id, user_id=user_id) + .all() + ] + + def update_folder_parent_id_by_id_and_user_id( + self, + id: str, + user_id: str, + parent_id: str, + ) -> Optional[FolderModel]: + try: + with get_db() as db: + folder = db.query(Folder).filter_by(id=id, user_id=user_id).first() + folder.parent_id = parent_id + folder.updated_at = int(time.time()) + + db.commit() + + return FolderModel.model_validate(folder) + except Exception as e: + log.error(f"update_folder: {e}") + return + + def update_folder_name_by_name_and_user_id( + self, name: str, user_id: str, new_name: str + ) -> Optional[FolderModel]: + try: + id = name.lower() + new_id = new_name.lower() + with get_db() as db: + # Check if new folder name already exists + folder = db.query(Folder).filter_by(id=new_id, user_id=user_id).first() + if folder: + return None + + folder = db.query(Folder).filter_by(id=id, user_id=user_id).first() + folder.id = new_id + folder.name = new_name + folder.updated_at = int(time.time()) + + db.commit() + + return FolderModel.model_validate(folder) + except Exception as e: + log.error(f"update_folder: {e}") + return + + def update_folder_items_by_name_and_user_id( self, name: str, user_id: str, items: FolderItems ) -> Optional[FolderModel]: try: diff --git a/backend/open_webui/apps/webui/routers/folders.py b/backend/open_webui/apps/webui/routers/folders.py new file mode 100644 index 000000000..b0c26b595 --- /dev/null +++ b/backend/open_webui/apps/webui/routers/folders.py @@ -0,0 +1,197 @@ +import logging +import os +import shutil +import uuid +from pathlib import Path +from typing import Optional +from pydantic import BaseModel +import mimetypes + + +from open_webui.apps.webui.models.folders import ( + FolderForm, + FolderItemsUpdateForm, + FolderModel, + Folders, +) +from open_webui.apps.webui.models.chats import Chats + +from open_webui.config import UPLOAD_DIR +from open_webui.env import SRC_LOG_LEVELS +from open_webui.constants import ERROR_MESSAGES + + +from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status +from fastapi.responses import FileResponse, StreamingResponse + + +from open_webui.utils.utils import get_admin_user, get_verified_user + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + + +router = APIRouter() + + +############################ +# Get Folders +############################ + + +@router.get("/", response_model=list[FolderModel]) +async def get_folders(user=Depends(get_verified_user)): + folders = Folders.get_folders_by_user_id(user.id) + return folders + + +############################ +# Create Folder +############################ + + +@router.post("/") +def create_folder(form_data: FolderForm, user=Depends(get_verified_user)): + folder = Folders.get_folder_by_name_and_user_id(form_data.name, user.id) + if folder: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Folder already exists"), + ) + + try: + folder = Folders.insert_new_folder(form_data.name, user.id) + return folder + except Exception as e: + log.exception(e) + log.error(f"Error creating folder: {form_data.name}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error creating folder"), + ) + + +############################ +# Get Folders By Id +############################ + + +@router.get("/{id}", response_model=Optional[FolderModel]) +async def get_folder_by_id(id: str, user=Depends(get_verified_user)): + folder = Folders.get_folder_by_name_and_user_id(id, user.id) + if folder: + return folder + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# Update Folder Name By Id +############################ + + +@router.post("/{id}/update") +async def update_folder_name_by_id( + id: str, form_data: FolderForm, user=Depends(get_verified_user) +): + new_id = form_data.name.lower() + folder = Folders.get_folder_by_name_and_user_id(new_id, user.id) + if folder: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Folder already exists"), + ) + + folder = Folders.get_folder_by_name_and_user_id(id, user.id) + if folder: + try: + folder = Folders.update_folder_name_by_name_and_user_id( + id, user.id, form_data.name + ) + + # Update children folders parent_id + children_folders = Folders.get_folders_by_parent_id_and_user_id(id, user.id) + for child in children_folders: + Folders.update_folder_parent_id_by_id_and_user_id( + child.id, user.id, folder.id + ) + + # Update children items parent_id + chats = Chats.get_chats_by_folder_id_and_user_id(id, user.id) + for chat in chats: + Chats.update_chat_folder_id_by_id_and_user_id( + chat.id, user.id, folder.id + ) + + return folder + except Exception as e: + log.exception(e) + log.error(f"Error updating folder: {id}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error updating folder"), + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# Update Folder Items By Id +############################ + + +@router.post("/{id}/update/items") +async def update_folder_items_by_id( + id: str, form_data: FolderItemsUpdateForm, user=Depends(get_verified_user) +): + folder = Folders.get_folder_by_name_and_user_id(id, user.id) + if folder: + try: + folder = Folders.update_folder_by_name_and_user_id( + id, user.id, form_data.items + ) + return folder + except Exception as e: + log.exception(e) + log.error(f"Error updating folder: {id}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error updating folder"), + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# Delete Folder By Id +############################ + + +@router.delete("/{id}") +async def delete_folder_by_id(id: str, user=Depends(get_verified_user)): + folder = Folders.get_folder_by_name_and_user_id(id, user.id) + if folder: + try: + result = Folders.delete_folder_by_name_and_user_id(id, user.id) + return result + except Exception as e: + log.exception(e) + log.error(f"Error deleting folder: {id}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error deleting folder"), + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) diff --git a/backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py b/backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py index 9bbec3a59..0341cad07 100644 --- a/backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py +++ b/backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py @@ -34,9 +34,16 @@ def upgrade(): server_default=sa.func.now(), onupdate=sa.func.now(), ), - sa.PrimaryKeyConstraint("id", "user_id") + sa.PrimaryKeyConstraint("id", "user_id"), + ) + + op.add_column( + "chat", + sa.Column("folder_id", sa.Text(), nullable=True), ) def downgrade(): + op.drop_column("chat", "folder_id") + op.drop_table("folder") diff --git a/src/lib/apis/folders/index.ts b/src/lib/apis/folders/index.ts new file mode 100644 index 000000000..2098a7810 --- /dev/null +++ b/src/lib/apis/folders/index.ts @@ -0,0 +1,198 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const createNewFolder = async (token: string, name: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: name + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFolders = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFolderById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateFolderNameById = async (token: string, id: string, name: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: name + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type FolderItems = { + chat_ids: string[]; + file_ids: string[]; + folder_ids: string[]; +}; + +export const updateFolderItemsById = async (token: string, id: string, items: FolderItems) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update/items`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + items: items + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteFolderById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/components/common/DragGhost.svelte b/src/lib/components/common/DragGhost.svelte index 6d97a4517..7169d72f0 100644 --- a/src/lib/components/common/DragGhost.svelte +++ b/src/lib/components/common/DragGhost.svelte @@ -24,7 +24,7 @@ bind:this={popupElement} class="fixed top-0 left-0 w-screen h-[100dvh] z-50 touch-none pointer-events-none" > -