diff --git a/backend/open_webui/apps/webui/models/models.py b/backend/open_webui/apps/webui/models/models.py
index f6643aea3..386f56a47 100644
--- a/backend/open_webui/apps/webui/models/models.py
+++ b/backend/open_webui/apps/webui/models/models.py
@@ -238,7 +238,7 @@ class ModelsTable:
result = (
db.query(Model)
.filter_by(id=id)
- .update(model.model_dump(exclude={"id"}, exclude_none=True))
+ .update(model.model_dump(exclude={"id"}))
)
db.commit()
diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py
index a77639561..a0311a6cb 100644
--- a/backend/open_webui/main.py
+++ b/backend/open_webui/main.py
@@ -948,7 +948,7 @@ async def get_all_models():
models = await get_all_base_models()
# If there are no models, return an empty list
- if len([model for model in models if model["owned_by"] != "arena"]) == 0:
+ if len([model for model in models if not model.get("arena", False)]) == 0:
return []
global_action_ids = [
@@ -975,7 +975,7 @@ async def get_all_models():
action_ids.extend(model["info"]["meta"].get("actionIds", []))
model["action_ids"] = action_ids
- else:
+ elif custom_model.id not in [model["id"] for model in models]:
owned_by = "openai"
pipe = None
action_ids = []
@@ -997,7 +997,7 @@ async def get_all_models():
models.append(
{
- "id": f"open-webui.{custom_model.id}",
+ "id": f"{custom_model.id}",
"name": custom_model.name,
"object": "model",
"created": custom_model.created_at,
@@ -1164,10 +1164,6 @@ async def generate_chat_completions(
"selected_model_id": selected_model_id,
}
- if model_id.startswith("open-webui."):
- model_id = model_id[len("open-webui.") :]
- form_data["model"] = model_id
-
if model.get("pipe"):
# Below does not require bypass_filter because this is the only route the uses this function and it is already bypassing the filter
return await generate_function_chat_completion(form_data, user=user)
diff --git a/src/lib/components/admin/Settings.svelte b/src/lib/components/admin/Settings.svelte
index 530f605ed..2cb72f2f7 100644
--- a/src/lib/components/admin/Settings.svelte
+++ b/src/lib/components/admin/Settings.svelte
@@ -327,7 +327,7 @@
</button>
</div>
- <div class="flex-1 mt-3 lg:mt-0 overflow-y-scroll">
+ <div class="flex-1 mt-3 lg:mt-0 overflow-y-scroll pr-1 scrollbar-hidden">
{#if selectedTab === 'general'}
<General
saveHandler={async () => {
diff --git a/src/lib/components/admin/Settings/Models.svelte b/src/lib/components/admin/Settings/Models.svelte
index 704276044..ccc59838f 100644
--- a/src/lib/components/admin/Settings/Models.svelte
+++ b/src/lib/components/admin/Settings/Models.svelte
@@ -20,12 +20,20 @@
import Switch from '$lib/components/common/Switch.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
+ import ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
+
let importFiles;
let modelsImportInputElement: HTMLInputElement;
let models = null;
+
+ let workspaceModels = null;
+ let baseModels = null;
+
let filteredModels = [];
+ let selectedModelId = null;
+
$: if (models) {
filteredModels = models.filter(
(m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase())
@@ -42,23 +50,59 @@
};
const init = async () => {
- const workspaceModels = await getBaseModels(localStorage.token);
- const allModels = await getModels(localStorage.token, true);
+ workspaceModels = await getBaseModels(localStorage.token);
+ baseModels = await getModels(localStorage.token, true);
- models = allModels
- .filter((m) => !(m?.preset ?? false))
- .map((m) => {
- const workspaceModel = workspaceModels.find((wm) => wm.id === m.id);
+ models = baseModels.map((m) => {
+ const workspaceModel = workspaceModels.find((wm) => wm.id === m.id);
- if (workspaceModel) {
- return workspaceModel;
- } else {
- return {
- ...m,
- is_active: true
- };
- }
+ if (workspaceModel) {
+ return workspaceModel;
+ } else {
+ return {
+ id: m.id,
+ name: m.name,
+ is_active: true
+ };
+ }
+ });
+ };
+
+ const upsertModelHandler = async (model) => {
+ model.base_model_id = null;
+
+ if (models.find((m) => m.id === model.id)) {
+ await updateModelById(localStorage.token, model.id, model).catch((error) => {
+ return null;
});
+ } else {
+ await createNewModel(localStorage.token, model).catch((error) => {
+ return null;
+ });
+ }
+
+ await init();
+ };
+
+ const toggleModelHandler = async (model) => {
+ if (!Object.keys(model).includes('base_model_id')) {
+ await createNewModel(localStorage.token, {
+ id: model.id,
+ name: model.name,
+ base_model_id: null,
+ meta: {},
+ params: {},
+ is_active: model.is_active
+ }).catch((error) => {
+ return null;
+ });
+
+ await init();
+ } else {
+ await toggleModelById(localStorage.token, model.id);
+ }
+
+ _models.set(await getModels(localStorage.token));
};
onMount(async () => {
@@ -67,201 +111,229 @@
</script>
{#if models !== null}
- <div class="flex flex-col gap-1 mt-1.5 mb-2">
- <div class="flex justify-between items-center">
- <div class="flex items-center md:self-center text-xl font-medium px-0.5">
- {$i18n.t('Models')}
- <div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
- <span class="text-lg font-medium text-gray-500 dark:text-gray-300"
- >{filteredModels.length}</span
- >
- </div>
- </div>
-
- <div class=" flex flex-1 items-center w-full space-x-2">
- <div class="flex flex-1 items-center">
- <div class=" self-center ml-1 mr-3">
- <Search className="size-3.5" />
+ {#if selectedModelId === null}
+ <div class="flex flex-col gap-1 mt-1.5 mb-2">
+ <div class="flex justify-between items-center">
+ <div class="flex items-center md:self-center text-xl font-medium px-0.5">
+ {$i18n.t('Models')}
+ <div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
+ <span class="text-lg font-medium text-gray-500 dark:text-gray-300"
+ >{filteredModels.length}</span
+ >
+ </div>
+ </div>
+
+ <div class=" flex flex-1 items-center w-full space-x-2">
+ <div class="flex flex-1 items-center">
+ <div class=" self-center ml-1 mr-3">
+ <Search className="size-3.5" />
+ </div>
+ <input
+ class=" w-full text-sm py-1 rounded-r-xl outline-none bg-transparent"
+ bind:value={searchValue}
+ placeholder={$i18n.t('Search Models')}
+ />
</div>
- <input
- class=" w-full text-sm py-1 rounded-r-xl outline-none bg-transparent"
- bind:value={searchValue}
- placeholder={$i18n.t('Search Models')}
- />
</div>
</div>
- </div>
- <div class=" my-2 mb-5" id="model-list">
- {#each filteredModels as model (model.id)}
- <div
- class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-lg transition"
- id="model-item-{model.id}"
- >
- <a
- class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
- href={`/?models=open-webui.${encodeURIComponent(model.id)}`}
- >
- <div class=" self-center w-8">
- <div
- class=" rounded-full object-cover {(model?.is_active ?? true)
- ? ''
- : 'opacity-50 dark:opacity-50'} "
- >
- <img
- src={model?.meta?.profile_image_url ?? '/static/favicon.png'}
- alt="modelfile profile"
- class=" rounded-full w-full h-auto object-cover"
- />
- </div>
- </div>
-
- <div class=" flex-1 self-center {(model?.is_active ?? true) ? '' : 'text-gray-500'}">
- <Tooltip
- content={marked.parse(model?.meta?.description ?? model.id)}
- className=" w-fit"
- placement="top-start"
- >
- <div class=" font-semibold line-clamp-1">{model.name}</div>
- </Tooltip>
- <div class=" text-xs overflow-hidden text-ellipsis line-clamp-1 text-gray-500">
- {model?.meta?.description ?? model.id}
- </div>
- </div>
- </a>
- <div class="flex flex-row gap-0.5 items-center self-center">
- {#if $user?.role === 'admin' || model.user_id === $user?.id}
- <a
- class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+ <div class=" my-2 mb-5" id="model-list">
+ {#if models.length > 0}
+ {#each filteredModels as model (model.id)}
+ <div
+ class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-lg transition"
+ id="model-item-{model.id}"
+ >
+ <button
+ class=" flex flex-1 text-left space-x-3.5 cursor-pointer w-full"
type="button"
- href={`/workspace/models/edit?id=${encodeURIComponent(model.id)}`}
+ on:click={() => {
+ selectedModelId = model.id;
+ }}
>
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- stroke-width="1.5"
- stroke="currentColor"
- class="w-4 h-4"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
- />
- </svg>
- </a>
- {/if}
+ <div class=" self-center w-8">
+ <div
+ class=" rounded-full object-cover {(model?.is_active ?? true)
+ ? ''
+ : 'opacity-50 dark:opacity-50'} "
+ >
+ <img
+ src={model?.meta?.profile_image_url ?? '/static/favicon.png'}
+ alt="modelfile profile"
+ class=" rounded-full w-full h-auto object-cover"
+ />
+ </div>
+ </div>
- <div class="ml-1">
- <Tooltip
- content={(model?.is_active ?? true) ? $i18n.t('Enabled') : $i18n.t('Disabled')}
- >
- <Switch
- bind:state={model.is_active}
- on:change={async (e) => {
- toggleModelById(localStorage.token, model.id);
- _models.set(await getModels(localStorage.token));
+ <div class=" flex-1 self-center {(model?.is_active ?? true) ? '' : 'text-gray-500'}">
+ <Tooltip
+ content={marked.parse(model?.meta?.description ?? model.id)}
+ className=" w-fit"
+ placement="top-start"
+ >
+ <div class=" font-semibold line-clamp-1">{model.name}</div>
+ </Tooltip>
+ <div class=" text-xs overflow-hidden text-ellipsis line-clamp-1 text-gray-500">
+ {model?.meta?.description ?? model.id}
+ </div>
+ </div>
+ </button>
+ <div class="flex flex-row gap-0.5 items-center self-center">
+ <button
+ class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+ type="button"
+ on:click={() => {
+ selectedModelId = model.id;
}}
- />
- </Tooltip>
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-4 h-4"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
+ />
+ </svg>
+ </button>
+
+ <div class="ml-1">
+ <Tooltip
+ content={(model?.is_active ?? true) ? $i18n.t('Enabled') : $i18n.t('Disabled')}
+ >
+ <Switch
+ bind:state={model.is_active}
+ on:change={async () => {
+ toggleModelHandler(model);
+ }}
+ />
+ </Tooltip>
+ </div>
+ </div>
+ </div>
+ {/each}
+ {:else}
+ <div class="flex flex-col items-center justify-center w-full h-20">
+ <div class="text-gray-500 dark:text-gray-400 text-xs">
+ {$i18n.t('No models found')}
</div>
</div>
- </div>
- {/each}
- </div>
+ {/if}
+ </div>
- {#if $user?.role === 'admin'}
- <div class=" flex justify-end w-full mb-3">
- <div class="flex space-x-1">
- <input
- id="models-import-input"
- bind:this={modelsImportInputElement}
- bind:files={importFiles}
- type="file"
- accept=".json"
- hidden
- on:change={() => {
- console.log(importFiles);
+ {#if $user?.role === 'admin'}
+ <div class=" flex justify-end w-full mb-3">
+ <div class="flex space-x-1">
+ <input
+ id="models-import-input"
+ bind:this={modelsImportInputElement}
+ bind:files={importFiles}
+ type="file"
+ accept=".json"
+ hidden
+ on:change={() => {
+ console.log(importFiles);
- let reader = new FileReader();
- reader.onload = async (event) => {
- let savedModels = JSON.parse(event.target.result);
- console.log(savedModels);
+ let reader = new FileReader();
+ reader.onload = async (event) => {
+ let savedModels = JSON.parse(event.target.result);
+ console.log(savedModels);
- for (const model of savedModels) {
- if (model?.info ?? false) {
- if ($_models.find((m) => m.id === model.id)) {
- await updateModelById(localStorage.token, model.id, model.info).catch(
- (error) => {
- return null;
- }
- );
+ for (const model of savedModels) {
+ if (Object.keys(model).includes('base_model_id')) {
+ if (model.base_model_id === null) {
+ upsertModelHandler(model);
+ }
} else {
- await createNewModel(localStorage.token, model.info).catch((error) => {
- return null;
- });
+ if (model?.info ?? false) {
+ if (model.info.base_model_id === null) {
+ upsertModelHandler(model.info);
+ }
+ }
}
}
- }
- await _models.set(await getModels(localStorage.token));
- init();
- };
+ await _models.set(await getModels(localStorage.token));
+ init();
+ };
- reader.readAsText(importFiles[0]);
- }}
- />
+ reader.readAsText(importFiles[0]);
+ }}
+ />
- <button
- class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
- on:click={() => {
- modelsImportInputElement.click();
- }}
- >
- <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Presets')}</div>
+ <button
+ class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
+ on:click={() => {
+ modelsImportInputElement.click();
+ }}
+ >
+ <div class=" self-center mr-2 font-medium line-clamp-1">
+ {$i18n.t('Import Presets')}
+ </div>
- <div class=" self-center">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 16 16"
- fill="currentColor"
- class="w-3.5 h-3.5"
- >
- <path
- fill-rule="evenodd"
- d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
- clip-rule="evenodd"
- />
- </svg>
- </div>
- </button>
+ <div class=" self-center">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 16 16"
+ fill="currentColor"
+ class="w-3.5 h-3.5"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </div>
+ </button>
- <button
- class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
- on:click={async () => {
- downloadModels($_models);
- }}
- >
- <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Presets')}</div>
+ <button
+ class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
+ on:click={async () => {
+ downloadModels(models);
+ }}
+ >
+ <div class=" self-center mr-2 font-medium line-clamp-1">
+ {$i18n.t('Export Presets')}
+ </div>
- <div class=" self-center">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 16 16"
- fill="currentColor"
- class="w-3.5 h-3.5"
- >
- <path
- fill-rule="evenodd"
- d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
- clip-rule="evenodd"
- />
- </svg>
- </div>
- </button>
+ <div class=" self-center">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 16 16"
+ fill="currentColor"
+ class="w-3.5 h-3.5"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </div>
+ </button>
+ </div>
</div>
- </div>
+ {/if}
+ {:else}
+ <ModelEditor
+ edit
+ model={models.find((m) => m.id === selectedModelId)}
+ preset={false}
+ onSubmit={(model) => {
+ console.log(model);
+ upsertModelHandler(model);
+ selectedModelId = null;
+ }}
+ onBack={() => {
+ selectedModelId = null;
+ }}
+ />
{/if}
{:else}
<div class=" h-full w-full flex justify-center items-center">
diff --git a/src/lib/components/workspace/Knowledge.svelte b/src/lib/components/workspace/Knowledge.svelte
index 7945aee65..7b0d50658 100644
--- a/src/lib/components/workspace/Knowledge.svelte
+++ b/src/lib/components/workspace/Knowledge.svelte
@@ -10,15 +10,10 @@
const i18n = getContext('i18n');
import { WEBUI_NAME, knowledge } from '$lib/stores';
-
import { getKnowledgeItems, deleteKnowledgeById } from '$lib/apis/knowledge';
- import { blobToFile, transformFileName } from '$lib/utils';
-
import { goto } from '$app/navigation';
- import Tooltip from '../common/Tooltip.svelte';
- import GarbageBin from '../icons/GarbageBin.svelte';
- import Pencil from '../icons/Pencil.svelte';
+
import DeleteConfirmDialog from '../common/ConfirmDialog.svelte';
import ItemMenu from './Knowledge/ItemMenu.svelte';
import Badge from '../common/Badge.svelte';
diff --git a/src/lib/components/workspace/Models/ModelEditor.svelte b/src/lib/components/workspace/Models/ModelEditor.svelte
index baff78e40..a7039691a 100644
--- a/src/lib/components/workspace/Models/ModelEditor.svelte
+++ b/src/lib/components/workspace/Models/ModelEditor.svelte
@@ -1,8 +1,4 @@
<script lang="ts">
- import { v4 as uuidv4 } from 'uuid';
- import { toast } from 'svelte-sonner';
- import { goto } from '$app/navigation';
-
import { onMount, getContext, tick } from 'svelte';
import { models, tools, functions, knowledge as knowledgeCollections } from '$lib/stores';
@@ -22,8 +18,11 @@
const i18n = getContext('i18n');
export let onSubmit: Function;
+ export let onBack: null | Function = null;
+
export let model = null;
export let edit = false;
+
export let preset = true;
let loading = false;
@@ -234,6 +233,31 @@
</script>
{#if loaded}
+ {#if onBack}
+ <button
+ class="flex space-x-1"
+ on:click={() => {
+ onBack();
+ }}
+ >
+ <div class=" self-center">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ class="h-4 w-4"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </div>
+ <div class=" self-center text-sm font-medium">{'Back'}</div>
+ </button>
+ {/if}
+
<div class="w-full max-h-full flex justify-center">
<input
bind:this={filesInputElement}
@@ -302,7 +326,7 @@
}}
/>
- {#if !edit || model}
+ {#if !edit || (edit && model)}
<form
class="flex flex-col md:flex-row w-full gap-3 md:gap-6"
on:submit|preventDefault={() => {
diff --git a/src/lib/components/workspace/common/AccessControl.svelte b/src/lib/components/workspace/common/AccessControl.svelte
index 4f323dbe2..88ee49f3a 100644
--- a/src/lib/components/workspace/common/AccessControl.svelte
+++ b/src/lib/components/workspace/common/AccessControl.svelte
@@ -1,4 +1,6 @@
<script lang="ts">
+ import Tooltip from '$lib/components/common/Tooltip.svelte';
+ import Plus from '$lib/components/icons/Plus.svelte';
import { getContext, onMount } from 'svelte';
const i18n = getContext('i18n');
@@ -104,6 +106,16 @@
bind:value={query}
placeholder={$i18n.t('Add user or groups')}
/>
+ <div>
+ <Tooltip content={$i18n.t('Add User/Group')}>
+ <button
+ class=" p-2 rounded-xl hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition font-medium text-sm flex items-center space-x-1"
+ on:click={() => {}}
+ >
+ <Plus className="size-3.5" />
+ </button>
+ </Tooltip>
+ </div>
</div>
</div>
</div>