Procházet zdrojové kódy

enh: emoji folder icon

Timothy Jaeryang Baek před 1 měsícem
rodič
revize
b70c0f36c0

+ 15 - 2
backend/open_webui/models/folders.py

@@ -58,6 +58,14 @@ class FolderModel(BaseModel):
 class FolderForm(BaseModel):
     name: str
     data: Optional[dict] = None
+    meta: Optional[dict] = None
+    model_config = ConfigDict(extra="allow")
+
+
+class FolderUpdateForm(BaseModel):
+    name: Optional[str] = None
+    data: Optional[dict] = None
+    meta: Optional[dict] = None
     model_config = ConfigDict(extra="allow")
 
 
@@ -191,7 +199,7 @@ class FolderTable:
             return
 
     def update_folder_by_id_and_user_id(
-        self, id: str, user_id: str, form_data: FolderForm
+        self, id: str, user_id: str, form_data: FolderUpdateForm
     ) -> Optional[FolderModel]:
         try:
             with get_db() as db:
@@ -222,8 +230,13 @@ class FolderTable:
                         **form_data["data"],
                     }
 
-                folder.updated_at = int(time.time())
+                if "meta" in form_data:
+                    folder.meta = {
+                        **(folder.meta or {}),
+                        **form_data["meta"],
+                    }
 
+                folder.updated_at = int(time.time())
                 db.commit()
 
                 return FolderModel.model_validate(folder)

+ 12 - 9
backend/open_webui/routers/folders.py

@@ -10,6 +10,7 @@ import mimetypes
 
 from open_webui.models.folders import (
     FolderForm,
+    FolderUpdateForm,
     FolderModel,
     Folders,
 )
@@ -113,22 +114,24 @@ async def get_folder_by_id(id: str, user=Depends(get_verified_user)):
 
 @router.post("/{id}/update")
 async def update_folder_name_by_id(
-    id: str, form_data: FolderForm, user=Depends(get_verified_user)
+    id: str, form_data: FolderUpdateForm, user=Depends(get_verified_user)
 ):
     folder = Folders.get_folder_by_id_and_user_id(id, user.id)
     if folder:
-        existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
-            folder.parent_id, user.id, form_data.name
-        )
-        if existing_folder and existing_folder.id != id:
-            raise HTTPException(
-                status_code=status.HTTP_400_BAD_REQUEST,
-                detail=ERROR_MESSAGES.DEFAULT("Folder already exists"),
+
+        if form_data.name is not None:
+            # Check if folder with same name exists
+            existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
+                folder.parent_id, user.id, form_data.name
             )
+            if existing_folder and existing_folder.id != id:
+                raise HTTPException(
+                    status_code=status.HTTP_400_BAD_REQUEST,
+                    detail=ERROR_MESSAGES.DEFAULT("Folder already exists"),
+                )
 
         try:
             folder = Folders.update_folder_by_id_and_user_id(id, user.id, form_data)
-
             return folder
         except Exception as e:
             log.exception(e)

+ 2 - 1
src/lib/apis/folders/index.ts

@@ -1,8 +1,9 @@
 import { WEBUI_API_BASE_URL } from '$lib/constants';
 
 type FolderForm = {
-	name: string;
+	name?: string;
 	data?: Record<string, any>;
+	meta?: Record<string, any>;
 };
 
 export const createNewFolder = async (token: string, folderForm: FolderForm) => {

+ 7 - 19
src/lib/components/channel/Messages/Message.svelte

@@ -30,9 +30,10 @@
 	import ProfilePreview from './Message/ProfilePreview.svelte';
 	import ChatBubbleOvalEllipsis from '$lib/components/icons/ChatBubble.svelte';
 	import FaceSmile from '$lib/components/icons/FaceSmile.svelte';
-	import ReactionPicker from './Message/ReactionPicker.svelte';
+	import EmojiPicker from '$lib/components/common/EmojiPicker.svelte';
 	import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
 	import { formatDate } from '$lib/utils';
+	import Emoji from '$lib/components/common/Emoji.svelte';
 
 	export let message;
 	export let showUserProfile = true;
@@ -74,7 +75,7 @@
 				<div
 					class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100 dark:border-gray-850"
 				>
-					<ReactionPicker
+					<EmojiPicker
 						onClose={() => (showButtons = false)}
 						onSubmit={(name) => {
 							showButtons = false;
@@ -91,7 +92,7 @@
 								<FaceSmile />
 							</button>
 						</Tooltip>
-					</ReactionPicker>
+					</EmojiPicker>
 
 					{#if !thread}
 						<Tooltip content={$i18n.t('Reply in Thread')}>
@@ -275,20 +276,7 @@
 												onReaction(reaction.name);
 											}}
 										>
-											{#if $shortCodesToEmojis[reaction.name]}
-												<img
-													src="{WEBUI_BASE_URL}/assets/emojis/{$shortCodesToEmojis[
-														reaction.name
-													].toLowerCase()}.svg"
-													alt={reaction.name}
-													class=" size-4"
-													loading="lazy"
-												/>
-											{:else}
-												<div>
-													{reaction.name}
-												</div>
-											{/if}
+											<Emoji shortCode={reaction.name} />
 
 											{#if reaction.user_ids.length > 0}
 												<div class="text-xs font-medium text-gray-500 dark:text-gray-400">
@@ -299,7 +287,7 @@
 									</Tooltip>
 								{/each}
 
-								<ReactionPicker
+								<EmojiPicker
 									onSubmit={(name) => {
 										onReaction(name);
 									}}
@@ -311,7 +299,7 @@
 											<FaceSmile />
 										</div>
 									</Tooltip>
-								</ReactionPicker>
+								</EmojiPicker>
 							</div>
 						</div>
 					{/if}

+ 38 - 3
src/lib/components/chat/Placeholder/FolderTitle.svelte

@@ -21,6 +21,8 @@
 	import FolderMenu from '$lib/components/layout/Sidebar/Folders/FolderMenu.svelte';
 	import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
 	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
+	import Emoji from '$lib/components/common/Emoji.svelte';
+	import EmojiPicker from '$lib/components/common/EmojiPicker.svelte';
 
 	export let folder = null;
 
@@ -63,6 +65,25 @@
 		}
 	};
 
+	const updateIconHandler = async (iconName) => {
+		const res = await updateFolderById(localStorage.token, folder.id, {
+			meta: {
+				icon: iconName
+			}
+		}).catch((error) => {
+			toast.error(`${error}`);
+			return null;
+		});
+
+		if (res) {
+			folder.meta = { ...folder.meta, icon: iconName };
+
+			toast.success($i18n.t('Folder updated successfully'));
+			selectedFolder.set(folder);
+			onUpdate(folder);
+		}
+	};
+
 	const deleteHandler = async () => {
 		const res = await deleteFolderById(localStorage.token, folder.id).catch((error) => {
 			toast.error(`${error}`);
@@ -116,9 +137,23 @@
 
 	<div class="mb-3 px-6 @md:max-w-3xl justify-between w-full flex relative group items-center">
 		<div class="text-center flex gap-3.5 items-center">
-			<div class=" rounded-full bg-gray-50 dark:bg-gray-800 p-3 w-fit">
-				<Folder className="size-4.5" strokeWidth="2" />
-			</div>
+			<EmojiPicker
+				onClose={() => {}}
+				onSubmit={(name) => {
+					console.log(name);
+					updateIconHandler(name);
+				}}
+			>
+				<button
+					class=" rounded-full bg-gray-50 dark:bg-gray-800 size-11 flex justify-center items-center"
+				>
+					{#if folder?.meta?.icon}
+						<Emoji className="size-6" shortCode={folder.meta.icon} />
+					{:else}
+						<Folder className="size-4.5" strokeWidth="2" />
+					{/if}
+				</button>
+			</EmojiPicker>
 
 			<div class="text-3xl">
 				{folder.name}

+ 20 - 0
src/lib/components/common/Emoji.svelte

@@ -0,0 +1,20 @@
+<script>
+	import { WEBUI_BASE_URL } from '$lib/constants';
+	import { shortCodesToEmojis } from '$lib/stores';
+
+	export let shortCode;
+	export let className = 'size-4';
+</script>
+
+{#if $shortCodesToEmojis[shortCode]}
+	<img
+		src="{WEBUI_BASE_URL}/assets/emojis/{$shortCodesToEmojis[shortCode].toLowerCase()}.svg"
+		alt={shortCode}
+		class={className}
+		loading="lazy"
+	/>
+{:else}
+	<div>
+		{shortCode}
+	</div>
+{/if}

+ 0 - 0
src/lib/components/channel/Messages/Message/ReactionPicker.svelte → src/lib/components/common/EmojiPicker.svelte


+ 3 - 3
src/lib/components/common/Folder.svelte

@@ -142,11 +142,11 @@
 			>
 				<button class="w-full py-1.5 pl-2 flex items-center gap-1.5 text-xs font-medium">
 					{#if chevron}
-						<div class="text-gray-300 dark:text-gray-600">
+						<div class="text-gray-300 dark:text-gray-600 p-[1px]">
 							{#if open}
-								<ChevronDown className=" size-3" strokeWidth="2.5" />
+								<ChevronDown className=" size-3.5" strokeWidth="2.5" />
 							{:else}
-								<ChevronRight className=" size-3" strokeWidth="2.5" />
+								<ChevronRight className=" size-3.5" strokeWidth="2.5" />
 							{/if}
 						</div>
 					{/if}

+ 1 - 1
src/lib/components/layout/Sidebar.svelte

@@ -695,7 +695,7 @@
 			? `ml-[4.5rem] md:ml-0 `
 			: ' transition-all duration-300 '} shrink-0 text-gray-900 dark:text-gray-200 text-sm fixed top-0 left-0 overflow-x-hidden
         "
-		transition:slide={{ duration: 200, axis: 'x' }}
+		transition:slide={{ duration: 250, axis: 'x' }}
 		data-state={$showSidebar}
 	>
 		<div

+ 21 - 4
src/lib/components/layout/Sidebar/RecursiveFolder.svelte

@@ -38,6 +38,7 @@
 	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 	import FolderModal from './Folders/FolderModal.svelte';
 	import { goto } from '$app/navigation';
+	import Emoji from '$lib/components/common/Emoji.svelte';
 
 	export let open = false;
 
@@ -444,15 +445,31 @@
 				}}
 			>
 				<button
-					class="text-gray-300 dark:text-gray-600"
+					class="text-gray-300 dark:text-gray-600 transition-all"
 					on:click={(e) => {
 						e.stopPropagation();
 					}}
 				>
-					{#if open}
-						<ChevronDown className=" size-3" strokeWidth="2.5" />
+					{#if folders[folderId]?.meta?.icon}
+						<div class="flex group-hover:hidden transition-all">
+							<Emoji className="size-4" shortCode={folders[folderId].meta.icon} />
+						</div>
+
+						<div class="hidden group-hover:flex transition-all p-[1px]">
+							{#if open}
+								<ChevronDown className=" size-3.5" strokeWidth="2.5" />
+							{:else}
+								<ChevronRight className=" size-3.5" strokeWidth="2.5" />
+							{/if}
+						</div>
 					{:else}
-						<ChevronRight className=" size-3" strokeWidth="2.5" />
+						<div class="p-[1px]">
+							{#if open}
+								<ChevronDown className=" size-3.5" strokeWidth="2.5" />
+							{:else}
+								<ChevronRight className=" size-3.5" strokeWidth="2.5" />
+							{/if}
+						</div>
 					{/if}
 				</button>