Timothy Jaeryang Baek 5 месяцев назад
Родитель
Сommit
41bad9abcb

+ 23 - 8
backend/open_webui/apps/webui/models/knowledge.py

@@ -85,6 +85,8 @@ class KnowledgeResponse(BaseModel):
     description: str
     data: Optional[dict] = None
     meta: Optional[dict] = None
+
+    access_control: Optional[dict] = None
     created_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
 
@@ -95,12 +97,7 @@ class KnowledgeForm(BaseModel):
     name: str
     description: str
     data: Optional[dict] = None
-
-
-class KnowledgeUpdateForm(BaseModel):
-    name: Optional[str] = None
-    description: Optional[str] = None
-    data: Optional[dict] = None
+    access_control: Optional[dict] = None
 
 
 class KnowledgeTable:
@@ -159,14 +156,32 @@ class KnowledgeTable:
             return None
 
     def update_knowledge_by_id(
-        self, id: str, form_data: KnowledgeUpdateForm, overwrite: bool = False
+        self, id: str, form_data: KnowledgeForm, overwrite: bool = False
+    ) -> Optional[KnowledgeModel]:
+        try:
+            with get_db() as db:
+                knowledge = self.get_knowledge_by_id(id=id)
+                db.query(Knowledge).filter_by(id=id).update(
+                    {
+                        **form_data.model_dump(),
+                        "updated_at": int(time.time()),
+                    }
+                )
+                db.commit()
+                return self.get_knowledge_by_id(id=id)
+        except Exception as e:
+            log.exception(e)
+            return None
+
+    def update_knowledge_data_by_id(
+        self, id: str, data: dict
     ) -> Optional[KnowledgeModel]:
         try:
             with get_db() as db:
                 knowledge = self.get_knowledge_by_id(id=id)
                 db.query(Knowledge).filter_by(id=id).update(
                     {
-                        **form_data.model_dump(exclude_none=True),
+                        "data": data,
                         "updated_at": int(time.time()),
                     }
                 )

+ 2 - 0
backend/open_webui/apps/webui/models/tools.py

@@ -81,6 +81,7 @@ class ToolResponse(BaseModel):
     user_id: str
     name: str
     meta: ToolMeta
+    access_control: Optional[dict] = None
     updated_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
 
@@ -90,6 +91,7 @@ class ToolForm(BaseModel):
     name: str
     content: str
     meta: ToolMeta
+    access_control: Optional[dict] = None
 
 
 class ToolValves(BaseModel):

+ 9 - 15
backend/open_webui/apps/webui/routers/knowledge.py

@@ -6,7 +6,6 @@ import logging
 
 from open_webui.apps.webui.models.knowledge import (
     Knowledges,
-    KnowledgeUpdateForm,
     KnowledgeForm,
     KnowledgeResponse,
 )
@@ -64,8 +63,8 @@ async def get_knowledge(user=Depends(get_verified_user)):
                         file_ids.remove(missing_file)
 
                     data["file_ids"] = file_ids
-                    Knowledges.update_knowledge_by_id(
-                        id=knowledge_base.id, form_data=KnowledgeUpdateForm(data=data)
+                    Knowledges.update_knowledge_data_by_id(
+                        id=knowledge_base.id, data=data
                     )
 
                     files = Files.get_file_metadatas_by_ids(file_ids)
@@ -109,8 +108,8 @@ async def get_knowledge_list(user=Depends(get_verified_user)):
                         file_ids.remove(missing_file)
 
                     data["file_ids"] = file_ids
-                    Knowledges.update_knowledge_by_id(
-                        id=knowledge_base.id, form_data=KnowledgeUpdateForm(data=data)
+                    Knowledges.update_knowledge_data_by_id(
+                        id=knowledge_base.id, data=data
                     )
 
                     files = Files.get_file_metadatas_by_ids(file_ids)
@@ -186,7 +185,7 @@ async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)):
 @router.post("/{id}/update", response_model=Optional[KnowledgeFilesResponse])
 async def update_knowledge_by_id(
     id: str,
-    form_data: KnowledgeUpdateForm,
+    form_data: KnowledgeForm,
     user=Depends(get_verified_user),
 ):
     knowledge = Knowledges.get_knowledge_by_id(id=id)
@@ -277,9 +276,7 @@ def add_file_to_knowledge_by_id(
             file_ids.append(form_data.file_id)
             data["file_ids"] = file_ids
 
-            knowledge = Knowledges.update_knowledge_by_id(
-                id=id, form_data=KnowledgeUpdateForm(data=data)
-            )
+            knowledge = Knowledges.update_knowledge_data_by_id(id=id.id, data=data)
 
             if knowledge:
                 files = Files.get_files_by_ids(file_ids)
@@ -413,9 +410,7 @@ def remove_file_from_knowledge_by_id(
             file_ids.remove(form_data.file_id)
             data["file_ids"] = file_ids
 
-            knowledge = Knowledges.update_knowledge_by_id(
-                id=id, form_data=KnowledgeUpdateForm(data=data)
-            )
+            knowledge = Knowledges.update_knowledge_data_by_id(id=id.id, data=data)
 
             if knowledge:
                 files = Files.get_files_by_ids(file_ids)
@@ -496,7 +491,6 @@ async def reset_knowledge_by_id(id: str, user=Depends(get_verified_user)):
         log.debug(e)
         pass
 
-    knowledge = Knowledges.update_knowledge_by_id(
-        id=id, form_data=KnowledgeUpdateForm(data={"file_ids": []})
-    )
+    knowledge = Knowledges.update_knowledge_data_by_id(id=id.id, data={"file_ids": []})
+
     return knowledge

+ 6 - 3
src/lib/apis/knowledge/index.ts

@@ -1,6 +1,6 @@
 import { WEBUI_API_BASE_URL } from '$lib/constants';
 
-export const createNewKnowledge = async (token: string, name: string, description: string) => {
+export const createNewKnowledge = async (token: string, name: string, description: string, accessControl: null|object) => {
 	let error = null;
 
 	const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/create`, {
@@ -12,7 +12,8 @@ export const createNewKnowledge = async (token: string, name: string, descriptio
 		},
 		body: JSON.stringify({
 			name: name,
-			description: description
+			description: description,
+			access_control: accessControl
 		})
 	})
 		.then(async (res) => {
@@ -130,6 +131,7 @@ type KnowledgeUpdateForm = {
 	name?: string;
 	description?: string;
 	data?: object;
+	access_control?: null|object;
 };
 
 export const updateKnowledgeById = async (token: string, id: string, form: KnowledgeUpdateForm) => {
@@ -145,7 +147,8 @@ export const updateKnowledgeById = async (token: string, id: string, form: Knowl
 		body: JSON.stringify({
 			name: form?.name ? form.name : undefined,
 			description: form?.description ? form.description : undefined,
-			data: form?.data ? form.data : undefined
+			data: form?.data ? form.data : undefined,
+			access_control: form.access_control
 		})
 	})
 		.then(async (res) => {

+ 19 - 0
src/lib/components/icons/LockClosed.svelte

@@ -0,0 +1,19 @@
+<script lang="ts">
+	export let className = 'size-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+	stroke-width={strokeWidth}
+	stroke="currentColor"
+	class={className}
+>
+	<path
+		stroke-linecap="round"
+		stroke-linejoin="round"
+		d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z"
+	/>
+</svg>

+ 10 - 2
src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte

@@ -12,6 +12,7 @@
 
 	let name = '';
 	let description = '';
+	let accessControl = null;
 
 	const submitHandler = async () => {
 		loading = true;
@@ -24,7 +25,12 @@
 			return;
 		}
 
-		const res = await createNewKnowledge(localStorage.token, name, description).catch((e) => {
+		const res = await createNewKnowledge(
+			localStorage.token,
+			name,
+			description,
+			accessControl
+		).catch((e) => {
 			toast.error(e);
 		});
 
@@ -105,7 +111,9 @@
 		</div>
 
 		<div class="mt-2">
-			<AccessControl />
+			<div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
+				<AccessControl bind:accessControl />
+			</div>
 		</div>
 
 		<div class="flex justify-end mt-2">

+ 61 - 38
src/lib/components/workspace/Knowledge/KnowledgeBase.svelte

@@ -38,8 +38,8 @@
 	import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
 	import Drawer from '$lib/components/common/Drawer.svelte';
 	import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
-	import MenuLines from '$lib/components/icons/MenuLines.svelte';
-	import AccessControl from '../common/AccessControl.svelte';
+	import LockClosed from '$lib/components/icons/LockClosed.svelte';
+	import AccessControlModal from '../common/AccessControlModal.svelte';
 
 	let largeScreen = true;
 
@@ -63,6 +63,7 @@
 
 	let showAddTextContentModal = false;
 	let showSyncConfirmModal = false;
+	let showAccessControlModal = false;
 
 	let inputFiles = null;
 
@@ -421,7 +422,8 @@
 
 			const res = await updateKnowledgeById(localStorage.token, id, {
 				name: knowledge.name,
-				description: knowledge.description
+				description: knowledge.description,
+				access_control: knowledge.access_control
 			}).catch((e) => {
 				toast.error(e);
 			});
@@ -599,6 +601,61 @@
 
 <div class="flex flex-col w-full h-full max-h-[100dvh] translate-y-1" id="collection-container">
 	{#if id && knowledge}
+		<AccessControlModal
+			bind:show={showAccessControlModal}
+			bind:accessControl={knowledge.access_control}
+			onChange={() => {
+				changeDebounceHandler();
+			}}
+		/>
+		<div class="w-full mb-2.5">
+			<div class=" flex w-full">
+				<div class="flex-1">
+					<div class="flex items-center justify-between w-full px-0.5 mb-1">
+						<div class="w-full">
+							<input
+								type="text"
+								class="text-left w-full font-semibold text-2xl font-primary bg-transparent outline-none"
+								bind:value={knowledge.name}
+								placeholder="Knowledge Name"
+								on:input={() => {
+									changeDebounceHandler();
+								}}
+							/>
+						</div>
+
+						<div class="self-center">
+							<button
+								class="bg-gray-50 hover:bg-gray-100 text-black transition px-2 py-1 rounded-full flex gap-1 items-center"
+								type="button"
+								on:click={() => {
+									showAccessControlModal = true;
+								}}
+							>
+								<LockClosed strokeWidth="2.5" />
+
+								<div class="text-sm font-medium flex-shrink-0">
+									{$i18n.t('Share')}
+								</div>
+							</button>
+						</div>
+					</div>
+
+					<div class="flex w-full px-1">
+						<input
+							type="text"
+							class="text-left text-xs w-full text-gray-500 bg-transparent outline-none"
+							bind:value={knowledge.description}
+							placeholder="Knowledge Description"
+							on:input={() => {
+								changeDebounceHandler();
+							}}
+						/>
+					</div>
+				</div>
+			</div>
+		</div>
+
 		<div class="flex flex-row flex-1 h-full max-h-full pb-2.5">
 			<PaneGroup direction="horizontal">
 				<Pane
@@ -764,41 +821,7 @@
 									</div>
 								</div>
 							{:else}
-								<div class="m-auto pb-20">
-									<div>
-										<div class=" flex w-full mt-1 mb-3.5">
-											<div class="flex-1">
-												<div class="flex items-center justify-between w-full px-0.5 mb-1">
-													<div class="w-full">
-														<input
-															type="text"
-															class="text-center w-full font-medium text-3xl font-primary bg-transparent outline-none"
-															bind:value={knowledge.name}
-															on:input={() => {
-																changeDebounceHandler();
-															}}
-														/>
-													</div>
-												</div>
-
-												<div class="flex w-full px-1">
-													<input
-														type="text"
-														class="text-center w-full text-gray-500 bg-transparent outline-none"
-														bind:value={knowledge.description}
-														on:input={() => {
-															changeDebounceHandler();
-														}}
-													/>
-												</div>
-											</div>
-										</div>
-									</div>
-
-									<div class="mt-2">
-										<AccessControl />
-									</div>
-								</div>
+								<div></div>
 							{/if}
 						</div>
 					</Pane>

+ 24 - 3
src/lib/components/workspace/Tools/ToolkitEditor.svelte

@@ -9,12 +9,16 @@
 	import Badge from '$lib/components/common/Badge.svelte';
 	import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import LockClosed from '$lib/components/icons/LockClosed.svelte';
+	import AccessControlModal from '../common/AccessControlModal.svelte';
 
 	const dispatch = createEventDispatcher();
 
 	let formElement = null;
 	let loading = false;
+
 	let showConfirm = false;
+	let showAccessControlModal = false;
 
 	export let edit = false;
 	export let clone = false;
@@ -25,6 +29,8 @@
 		description: ''
 	};
 	export let content = '';
+	export let accessControl = null;
+
 	let _content = '';
 
 	$: if (content) {
@@ -148,7 +154,8 @@ class Tools:
 			id,
 			name,
 			meta,
-			content
+			content,
+			access_control: accessControl
 		});
 	};
 
@@ -172,6 +179,8 @@ class Tools:
 	};
 </script>
 
+<AccessControlModal bind:show={showAccessControlModal} bind:accessControl />
+
 <div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
 	<div class="mx-auto w-full md:px-0 h-full">
 		<form
@@ -205,7 +214,7 @@ class Tools:
 						<div class="flex-1">
 							<Tooltip content={$i18n.t('e.g. My Tools')} placement="top-start">
 								<input
-									class="w-full text-2xl font-medium bg-transparent outline-none"
+									class="w-full text-2xl font-semibold bg-transparent outline-none"
 									type="text"
 									placeholder={$i18n.t('Tool Name')}
 									bind:value={name}
@@ -215,7 +224,19 @@ class Tools:
 						</div>
 
 						<div>
-							<Badge type="muted" content={$i18n.t('Tool')} />
+							<button
+								class="bg-gray-50 hover:bg-gray-100 text-black transition px-2 py-1 rounded-full flex gap-1 items-center"
+								type="button"
+								on:click={() => {
+									showAccessControlModal = true;
+								}}
+							>
+								<LockClosed strokeWidth="2.5" />
+
+								<div class="text-sm font-medium flex-shrink-0">
+									{$i18n.t('Share')}
+								</div>
+							</button>
 						</div>
 					</div>
 

+ 4 - 0
src/lib/components/workspace/common/AccessControl.svelte

@@ -9,6 +9,8 @@
 	import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
 	import XMark from '$lib/components/icons/XMark.svelte';
 
+	export let onChange: Function = () => {};
+
 	export let accessControl = null;
 
 	let selectedGroupId = '';
@@ -17,6 +19,8 @@
 	onMount(async () => {
 		groups = await getGroups(localStorage.token);
 	});
+
+	$: onChange(accessControl);
 </script>
 
 <div class=" rounded-lg flex flex-col gap-2">

+ 43 - 0
src/lib/components/workspace/common/AccessControlModal.svelte

@@ -0,0 +1,43 @@
+<script>
+	import { getContext } from 'svelte';
+	const i18n = getContext('i18n');
+
+	import Modal from '$lib/components/common/Modal.svelte';
+	import AccessControl from './AccessControl.svelte';
+
+	export let show = false;
+	export let accessControl = null;
+
+	export let onChange = () => {};
+</script>
+
+<Modal size="sm" bind:show>
+	<div>
+		<div class=" flex justify-between dark:text-gray-100 px-5 pt-3 pb-1">
+			<div class=" text-lg font-medium self-center font-primary">
+				{$i18n.t('Share')}
+			</div>
+			<button
+				class="self-center"
+				on:click={() => {
+					show = false;
+				}}
+			>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 20 20"
+					fill="currentColor"
+					class="w-5 h-5"
+				>
+					<path
+						d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
+					/>
+				</svg>
+			</button>
+		</div>
+
+		<div class="w-full px-5 pb-4">
+			<AccessControl bind:accessControl {onChange} />
+		</div>
+	</div>
+</Modal>

+ 2 - 2
src/routes/(app)/workspace/+layout.svelte

@@ -36,7 +36,7 @@
 			? 'md:max-w-[calc(100%-260px)]'
 			: ''}"
 	>
-		<div class="   px-2.5 py-1 backdrop-blur-xl">
+		<div class="   px-2.5 pt-1 backdrop-blur-xl">
 			<div class=" flex items-center gap-1">
 				<div class="{$showSidebar ? 'md:hidden' : ''} self-center flex flex-none items-center">
 					<button
@@ -97,7 +97,7 @@
 			</div>
 		</div>
 
-		<div class=" -mt-1 pb-1 px-[18px] flex-1 max-h-full overflow-y-auto" id="workspace-container">
+		<div class="  pb-1 px-[18px] flex-1 max-h-full overflow-y-auto" id="workspace-container">
 			<slot />
 		</div>
 	</div>

+ 3 - 1
src/routes/(app)/workspace/tools/create/+page.svelte

@@ -36,7 +36,8 @@
 			id: data.id,
 			name: data.name,
 			meta: data.meta,
-			content: data.content
+			content: data.content,
+			access_control: data.access_control
 		}).catch((error) => {
 			toast.error(error);
 			return null;
@@ -86,6 +87,7 @@
 			name={tool?.name ?? ''}
 			meta={tool?.meta ?? { description: '' }}
 			content={tool?.content ?? ''}
+			access_control={null}
 			{clone}
 			on:save={(e) => {
 				saveHandler(e.detail);

+ 3 - 1
src/routes/(app)/workspace/tools/edit/+page.svelte

@@ -36,7 +36,8 @@
 			id: data.id,
 			name: data.name,
 			meta: data.meta,
-			content: data.content
+			content: data.content,
+			access_control: data.access_control
 		}).catch((error) => {
 			toast.error(error);
 			return null;
@@ -73,6 +74,7 @@
 		name={tool.name}
 		meta={tool.meta}
 		content={tool.content}
+		accessControl={tool.access_control}
 		on:save={(e) => {
 			saveHandler(e.detail);
 		}}