wip: user groups frontend

This commit is contained in:
Timothy Jaeryang Baek 2024-11-13 03:09:46 -08:00
parent 6caf838964
commit baea26d9ca
9 changed files with 572 additions and 289 deletions

View File

@ -13,9 +13,7 @@ import requests
from open_webui.apps.webui.models.models import Models from open_webui.apps.webui.models.models import Models
from open_webui.config import ( from open_webui.config import (
CORS_ALLOW_ORIGIN, CORS_ALLOW_ORIGIN,
ENABLE_MODEL_FILTER,
ENABLE_OLLAMA_API, ENABLE_OLLAMA_API,
MODEL_FILTER_LIST,
OLLAMA_BASE_URLS, OLLAMA_BASE_URLS,
OLLAMA_API_CONFIGS, OLLAMA_API_CONFIGS,
UPLOAD_DIR, UPLOAD_DIR,
@ -66,9 +64,6 @@ app.add_middleware(
app.state.config = AppConfig() app.state.config = AppConfig()
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
app.state.config.OLLAMA_API_CONFIGS = OLLAMA_API_CONFIGS app.state.config.OLLAMA_API_CONFIGS = OLLAMA_API_CONFIGS
@ -339,16 +334,18 @@ async def get_ollama_tags(
if url_idx is None: if url_idx is None:
models = await get_all_models() models = await get_all_models()
if app.state.config.ENABLE_MODEL_FILTER: # TODO: Check User Group and Filter Models
if user.role == "user": # if app.state.config.ENABLE_MODEL_FILTER:
models["models"] = list( # if user.role == "user":
filter( # models["models"] = list(
lambda model: model["name"] # filter(
in app.state.config.MODEL_FILTER_LIST, # lambda model: model["name"]
models["models"], # in app.state.config.MODEL_FILTER_LIST,
) # models["models"],
) # )
return models # )
# return models
return models return models
else: else:
url = app.state.config.OLLAMA_BASE_URLS[url_idx] url = app.state.config.OLLAMA_BASE_URLS[url_idx]
@ -922,12 +919,14 @@ async def generate_chat_completion(
model_id = form_data.model model_id = form_data.model
if not bypass_filter and app.state.config.ENABLE_MODEL_FILTER: # TODO: Check User Group and Filter Models
if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST: # if not bypass_filter:
raise HTTPException( # if app.state.config.ENABLE_MODEL_FILTER:
status_code=403, # if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
detail="Model not found", # raise HTTPException(
) # status_code=403,
# detail="Model not found",
# )
model_info = Models.get_model_by_id(model_id) model_info = Models.get_model_by_id(model_id)
@ -1008,12 +1007,13 @@ async def generate_openai_chat_completion(
model_id = completion_form.model model_id = completion_form.model
if app.state.config.ENABLE_MODEL_FILTER: # TODO: Check User Group and Filter Models
if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST: # if app.state.config.ENABLE_MODEL_FILTER:
raise HTTPException( # if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
status_code=403, # raise HTTPException(
detail="Model not found", # status_code=403,
) # detail="Model not found",
# )
model_info = Models.get_model_by_id(model_id) model_info = Models.get_model_by_id(model_id)
@ -1054,15 +1054,16 @@ async def get_openai_models(
if url_idx is None: if url_idx is None:
models = await get_all_models() models = await get_all_models()
if app.state.config.ENABLE_MODEL_FILTER: # TODO: Check User Group and Filter Models
if user.role == "user": # if app.state.config.ENABLE_MODEL_FILTER:
models["models"] = list( # if user.role == "user":
filter( # models["models"] = list(
lambda model: model["name"] # filter(
in app.state.config.MODEL_FILTER_LIST, # lambda model: model["name"]
models["models"], # in app.state.config.MODEL_FILTER_LIST,
) # models["models"],
) # )
# )
return { return {
"data": [ "data": [

View File

@ -11,9 +11,7 @@ from open_webui.apps.webui.models.models import Models
from open_webui.config import ( from open_webui.config import (
CACHE_DIR, CACHE_DIR,
CORS_ALLOW_ORIGIN, CORS_ALLOW_ORIGIN,
ENABLE_MODEL_FILTER,
ENABLE_OPENAI_API, ENABLE_OPENAI_API,
MODEL_FILTER_LIST,
OPENAI_API_BASE_URLS, OPENAI_API_BASE_URLS,
OPENAI_API_KEYS, OPENAI_API_KEYS,
OPENAI_API_CONFIGS, OPENAI_API_CONFIGS,
@ -61,9 +59,6 @@ app.add_middleware(
app.state.config = AppConfig() app.state.config = AppConfig()
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
@ -372,15 +367,18 @@ async def get_all_models(raw=False) -> dict[str, list] | list:
async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)): async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)):
if url_idx is None: if url_idx is None:
models = await get_all_models() models = await get_all_models()
if app.state.config.ENABLE_MODEL_FILTER:
if user.role == "user": # TODO: Check User Group and Filter Models
models["data"] = list( # if app.state.config.ENABLE_MODEL_FILTER:
filter( # if user.role == "user":
lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST, # models["data"] = list(
models["data"], # filter(
) # lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
) # models["data"],
return models # )
# )
# return models
return models return models
else: else:
url = app.state.config.OPENAI_API_BASE_URLS[url_idx] url = app.state.config.OPENAI_API_BASE_URLS[url_idx]
@ -492,11 +490,10 @@ async def verify_connection(
@app.post("/chat/completions") @app.post("/chat/completions")
@app.post("/chat/completions/{url_idx}")
async def generate_chat_completion( async def generate_chat_completion(
form_data: dict, form_data: dict,
url_idx: Optional[int] = None,
user=Depends(get_verified_user), user=Depends(get_verified_user),
bypass_filter: Optional[bool] = False,
): ):
idx = 0 idx = 0
payload = {**form_data} payload = {**form_data}
@ -505,6 +502,16 @@ async def generate_chat_completion(
del payload["metadata"] del payload["metadata"]
model_id = form_data.get("model") model_id = form_data.get("model")
# TODO: Check User Group and Filter Models
# if not bypass_filter:
# if app.state.config.ENABLE_MODEL_FILTER:
# if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
# raise HTTPException(
# status_code=403,
# detail="Model not found",
# )
model_info = Models.get_model_by_id(model_id) model_info = Models.get_model_by_id(model_id)
if model_info: if model_info:

View File

@ -183,7 +183,10 @@ async def lifespan(app: FastAPI):
app = FastAPI( app = FastAPI(
docs_url="/docs" if ENV == "dev" else None, openapi_url="/openapi.json" if ENV == "dev" else None, redoc_url=None, lifespan=lifespan docs_url="/docs" if ENV == "dev" else None,
openapi_url="/openapi.json" if ENV == "dev" else None,
redoc_url=None,
lifespan=lifespan,
) )
app.state.config = AppConfig() app.state.config = AppConfig()
@ -1081,15 +1084,16 @@ async def get_models(user=Depends(get_verified_user)):
if "pipeline" not in model or model["pipeline"].get("type", None) != "filter" if "pipeline" not in model or model["pipeline"].get("type", None) != "filter"
] ]
if app.state.config.ENABLE_MODEL_FILTER: # TODO: Check User Group and Filter Models
if user.role == "user": # if app.state.config.ENABLE_MODEL_FILTER:
models = list( # if user.role == "user":
filter( # models = list(
lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST, # filter(
models, # lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
) # models,
) # )
return {"data": models} # )
# return {"data": models}
return {"data": models} return {"data": models}
@ -1106,12 +1110,14 @@ async def generate_chat_completions(
detail="Model not found", detail="Model not found",
) )
if not bypass_filter and app.state.config.ENABLE_MODEL_FILTER: # TODO: Check User Group and Filter Models
if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST: # if not bypass_filter:
raise HTTPException( # if app.state.config.ENABLE_MODEL_FILTER:
status_code=status.HTTP_403_FORBIDDEN, # if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
detail="Model not found", # raise HTTPException(
) # status_code=status.HTTP_403_FORBIDDEN,
# detail="Model not found",
# )
model = app.state.MODELS[model_id] model = app.state.MODELS[model_id]
@ -1161,14 +1167,16 @@ async def generate_chat_completions(
), ),
"selected_model_id": selected_model_id, "selected_model_id": selected_model_id,
} }
if model.get("pipe"): 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) return await generate_function_chat_completion(form_data, user=user)
if model["owned_by"] == "ollama": if model["owned_by"] == "ollama":
# Using /ollama/api/chat endpoint # Using /ollama/api/chat endpoint
form_data = convert_payload_openai_to_ollama(form_data) form_data = convert_payload_openai_to_ollama(form_data)
form_data = GenerateChatCompletionForm(**form_data) form_data = GenerateChatCompletionForm(**form_data)
response = await generate_ollama_chat_completion( response = await generate_ollama_chat_completion(
form_data=form_data, user=user, bypass_filter=True form_data=form_data, user=user, bypass_filter=bypass_filter
) )
if form_data.stream: if form_data.stream:
response.headers["content-type"] = "text/event-stream" response.headers["content-type"] = "text/event-stream"
@ -1179,7 +1187,9 @@ async def generate_chat_completions(
else: else:
return convert_response_ollama_to_openai(response) return convert_response_ollama_to_openai(response)
else: else:
return await generate_openai_chat_completion(form_data, user=user) return await generate_openai_chat_completion(
form_data, user=user, bypass_filter=bypass_filter
)
@app.post("/api/chat/completed") @app.post("/api/chat/completed")
@ -2297,32 +2307,6 @@ async def get_app_config(request: Request):
} }
@app.get("/api/config/model/filter")
async def get_model_filter_config(user=Depends(get_admin_user)):
return {
"enabled": app.state.config.ENABLE_MODEL_FILTER,
"models": app.state.config.MODEL_FILTER_LIST,
}
class ModelFilterConfigForm(BaseModel):
enabled: bool
models: list[str]
@app.post("/api/config/model/filter")
async def update_model_filter_config(
form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
):
app.state.config.ENABLE_MODEL_FILTER = form_data.enabled
app.state.config.MODEL_FILTER_LIST = form_data.models
return {
"enabled": app.state.config.ENABLE_MODEL_FILTER,
"models": app.state.config.MODEL_FILTER_LIST,
}
# TODO: webhook endpoint should be under config endpoints # TODO: webhook endpoint should be under config endpoints

View File

@ -12,6 +12,12 @@
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import Plus from '$lib/components/icons/Plus.svelte'; import Plus from '$lib/components/icons/Plus.svelte';
import Badge from '$lib/components/common/Badge.svelte';
import UsersSolid from '$lib/components/icons/UsersSolid.svelte';
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
import User from '$lib/components/icons/User.svelte';
import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -38,7 +44,16 @@
if ($user?.role !== 'admin') { if ($user?.role !== 'admin') {
await goto('/'); await goto('/');
} else { } else {
groups = []; groups = [
{
name: 'Admins',
description: 'Admins have full access to all features and settings.',
permissions: {
admin: true
},
user_ids: [1, 2, 3]
}
];
} }
loaded = true; loaded = true;
}); });
@ -117,7 +132,64 @@
</div> </div>
</div> </div>
{:else} {:else}
<div></div> <div>
<div class=" flex items-center gap-3 justify-between text-xs uppercase px-1 font-bold">
<div class="w-full">Group</div>
<div class="w-full">Users</div>
<div class="w-full"></div>
</div>
<hr class="mt-1.5 mb-2 border-gray-50 dark:border-gray-850" />
{#each filteredGroups as group}
<div class="flex items-center gap-3 justify-between px-1 text-xs w-full transition">
<div class="flex items-center gap-1.5 w-full font-medium">
<div>
<UserCircleSolid className="size-4" />
</div>
{group.name}
</div>
<div class="flex items-center gap-1.5 w-full font-medium">
{group.user_ids.length}
<div>
<User className="size-3.5" />
</div>
</div>
<div class="w-full flex justify-end">
<button class=" rounded-lg p-1">
<EllipsisHorizontal />
</button>
</div>
</div>
{/each}
</div>
{/if} {/if}
<hr class="my-2 border-gray-50 dark:border-gray-850" />
<button class="flex items-center justify-between rounded-lg w-full transition pt-1">
<div class="flex items-center gap-2.5">
<div class="p-1.5 bg-black/5 dark:bg-white/10 rounded-full">
<UsersSolid className="size-4" />
</div>
<div class="text-left">
<div class=" text-sm font-medium">{$i18n.t('Default permissions')}</div>
<div class="flex text-xs mt-0.5">
{$i18n.t('applies to all users with the "user" role')}
</div>
</div>
</div>
<div>
<ChevronRight strokeWidth="2.5" />
</div>
</button>
</div> </div>
{/if} {/if}

View File

@ -0,0 +1,191 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { getContext, onMount } from 'svelte';
const i18n = getContext('i18n');
import Modal from '$lib/components/common/Modal.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import Minus from '$lib/components/icons/Minus.svelte';
import PencilSolid from '$lib/components/icons/PencilSolid.svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Switch from '$lib/components/common/Switch.svelte';
export let onSubmit: Function = () => {};
export let onDelete: Function = () => {};
export let show = false;
export let edit = false;
export let group = null;
let name = '';
let description = '';
let permissions = {};
let loading = false;
const submitHandler = async () => {
loading = true;
const group = {
name,
description,
permissions
};
await onSubmit(group);
loading = false;
show = false;
name = '';
permissions = {};
};
const init = () => {
if (group) {
name = group.name;
description = group.description;
permissions = group?.permissions ?? {};
}
};
$: if (show) {
init();
}
onMount(() => {
init();
});
</script>
<Modal size="lg" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2">
<div class=" text-lg font-medium self-center font-primary">
{#if edit}
{$i18n.t('Edit User Group')}
{:else}
{$i18n.t('Add User Group')}
{/if}
</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="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
<form
class="flex flex-col w-full"
on:submit={(e) => {
e.preventDefault();
submitHandler();
}}
>
<div class="px-1">
<div class="flex gap-2">
<div class="flex flex-col w-full">
<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('Name')}</div>
<div class="flex-1">
<input
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
type="text"
bind:value={name}
placeholder={$i18n.t('User Group Name')}
autocomplete="off"
required
/>
</div>
</div>
</div>
<div class="flex flex-col w-full mt-2">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Description')}</div>
<div class="flex-1">
<input
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
type="text"
bind:value={description}
placeholder={$i18n.t('Enter description')}
autocomplete="off"
/>
</div>
</div>
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
</div>
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
{#if edit}
<button
class="px-3.5 py-1.5 text-sm font-medium dark:bg-black dark:hover:bg-gray-900 dark:text-white bg-white text-black hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center"
type="button"
on:click={() => {
onDelete();
show = false;
}}
>
{$i18n.t('Delete')}
</button>
{/if}
<button
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center {loading
? ' cursor-not-allowed'
: ''}"
type="submit"
disabled={loading}
>
{$i18n.t('Save')}
{#if loading}
<div class="ml-2 self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{/if}
</button>
</div>
</form>
</div>
</div>
</div>
</Modal>

View File

@ -0,0 +1,11 @@
<script lang="ts">
export let className = 'size-4';
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class={className}>
<path
fill-rule="evenodd"
d="M15 8A7 7 0 1 1 1 8a7 7 0 0 1 14 0Zm-5-2a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM8 9c-1.825 0-3.422.977-4.295 2.437A5.49 5.49 0 0 0 8 13.5a5.49 5.49 0 0 0 4.294-2.063A4.997 4.997 0 0 0 8 9Z"
clip-rule="evenodd"
/>
</svg>

View File

@ -0,0 +1,9 @@
<script lang="ts">
export let className = 'size-4';
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}>
<path
d="M4.5 6.375a4.125 4.125 0 1 1 8.25 0 4.125 4.125 0 0 1-8.25 0ZM14.25 8.625a3.375 3.375 0 1 1 6.75 0 3.375 3.375 0 0 1-6.75 0ZM1.5 19.125a7.125 7.125 0 0 1 14.25 0v.003l-.001.119a.75.75 0 0 1-.363.63 13.067 13.067 0 0 1-6.761 1.873c-2.472 0-4.786-.684-6.76-1.873a.75.75 0 0 1-.364-.63l-.001-.122ZM17.25 19.128l-.001.144a2.25 2.25 0 0 1-.233.96 10.088 10.088 0 0 0 5.06-1.01.75.75 0 0 0 .42-.643 4.875 4.875 0 0 0-6.957-4.611 8.586 8.586 0 0 1 1.71 5.157v.003Z"
/>
</svg>

View File

@ -411,7 +411,7 @@
</div> </div>
</a> </a>
<div class="flex flex-row gap-0.5 self-center"> <div class="flex flex-row gap-0.5 self-center">
{#if shiftKey} {#if $user?.role === 'admin' && shiftKey}
<Tooltip <Tooltip
content={(model?.info?.meta?.hidden ?? false) content={(model?.info?.meta?.hidden ?? false)
? $i18n.t('Show Model') ? $i18n.t('Show Model')
@ -475,6 +475,7 @@
</button> </button>
</Tooltip> </Tooltip>
{:else} {:else}
{#if $user?.role === 'admin'}
<a <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" 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" type="button"
@ -495,8 +496,10 @@
/> />
</svg> </svg>
</a> </a>
{/if}
<ModelMenu <ModelMenu
user={$user}
{model} {model}
shareHandler={() => { shareHandler={() => {
shareModelHandler(model); shareModelHandler(model);
@ -532,7 +535,8 @@
{/each} {/each}
</div> </div>
<div class=" flex justify-end w-full mb-3"> {#if $user?.role === 'admin'}
<div class=" flex justify-end w-full mb-3">
<div class="flex space-x-1"> <div class="flex space-x-1">
<input <input
id="models-import-input" id="models-import-input"
@ -656,7 +660,8 @@
</div> </div>
</div> </div>
{/if} {/if}
</div> </div>
{/if}
{#if $config?.features.enable_community_sharing} {#if $config?.features.enable_community_sharing}
<div class=" my-16"> <div class=" my-16">

View File

@ -16,6 +16,7 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let user;
export let model; export let model;
export let shareHandler: Function; export let shareHandler: Function;
@ -82,6 +83,7 @@
<div class="flex items-center">{$i18n.t('Export')}</div> <div class="flex items-center">{$i18n.t('Export')}</div>
</DropdownMenu.Item> </DropdownMenu.Item>
{#if user?.role === 'admin'}
<DropdownMenu.Item <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => { on:click={() => {
@ -144,6 +146,7 @@
{/if} {/if}
</div> </div>
</DropdownMenu.Item> </DropdownMenu.Item>
{/if}
<hr class="border-gray-100 dark:border-gray-800 my-1" /> <hr class="border-gray-100 dark:border-gray-800 my-1" />