mirror of
https://github.com/open-webui/open-webui
synced 2025-01-18 00:30:51 +00:00
wip: user groups frontend
This commit is contained in:
parent
6caf838964
commit
baea26d9ca
@ -13,9 +13,7 @@ import requests
|
||||
from open_webui.apps.webui.models.models import Models
|
||||
from open_webui.config import (
|
||||
CORS_ALLOW_ORIGIN,
|
||||
ENABLE_MODEL_FILTER,
|
||||
ENABLE_OLLAMA_API,
|
||||
MODEL_FILTER_LIST,
|
||||
OLLAMA_BASE_URLS,
|
||||
OLLAMA_API_CONFIGS,
|
||||
UPLOAD_DIR,
|
||||
@ -66,9 +64,6 @@ app.add_middleware(
|
||||
|
||||
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.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
|
||||
app.state.config.OLLAMA_API_CONFIGS = OLLAMA_API_CONFIGS
|
||||
@ -339,16 +334,18 @@ async def get_ollama_tags(
|
||||
if url_idx is None:
|
||||
models = await get_all_models()
|
||||
|
||||
if app.state.config.ENABLE_MODEL_FILTER:
|
||||
if user.role == "user":
|
||||
models["models"] = list(
|
||||
filter(
|
||||
lambda model: model["name"]
|
||||
in app.state.config.MODEL_FILTER_LIST,
|
||||
models["models"],
|
||||
)
|
||||
)
|
||||
return models
|
||||
# TODO: Check User Group and Filter Models
|
||||
# if app.state.config.ENABLE_MODEL_FILTER:
|
||||
# if user.role == "user":
|
||||
# models["models"] = list(
|
||||
# filter(
|
||||
# lambda model: model["name"]
|
||||
# in app.state.config.MODEL_FILTER_LIST,
|
||||
# models["models"],
|
||||
# )
|
||||
# )
|
||||
# return models
|
||||
|
||||
return models
|
||||
else:
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
@ -922,12 +919,14 @@ async def generate_chat_completion(
|
||||
|
||||
model_id = form_data.model
|
||||
|
||||
if not bypass_filter and 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",
|
||||
)
|
||||
# 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)
|
||||
|
||||
@ -1008,12 +1007,13 @@ async def generate_openai_chat_completion(
|
||||
|
||||
model_id = completion_form.model
|
||||
|
||||
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",
|
||||
)
|
||||
# TODO: Check User Group and Filter Models
|
||||
# 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)
|
||||
|
||||
@ -1054,15 +1054,16 @@ async def get_openai_models(
|
||||
if url_idx is None:
|
||||
models = await get_all_models()
|
||||
|
||||
if app.state.config.ENABLE_MODEL_FILTER:
|
||||
if user.role == "user":
|
||||
models["models"] = list(
|
||||
filter(
|
||||
lambda model: model["name"]
|
||||
in app.state.config.MODEL_FILTER_LIST,
|
||||
models["models"],
|
||||
)
|
||||
)
|
||||
# TODO: Check User Group and Filter Models
|
||||
# if app.state.config.ENABLE_MODEL_FILTER:
|
||||
# if user.role == "user":
|
||||
# models["models"] = list(
|
||||
# filter(
|
||||
# lambda model: model["name"]
|
||||
# in app.state.config.MODEL_FILTER_LIST,
|
||||
# models["models"],
|
||||
# )
|
||||
# )
|
||||
|
||||
return {
|
||||
"data": [
|
||||
|
@ -11,9 +11,7 @@ from open_webui.apps.webui.models.models import Models
|
||||
from open_webui.config import (
|
||||
CACHE_DIR,
|
||||
CORS_ALLOW_ORIGIN,
|
||||
ENABLE_MODEL_FILTER,
|
||||
ENABLE_OPENAI_API,
|
||||
MODEL_FILTER_LIST,
|
||||
OPENAI_API_BASE_URLS,
|
||||
OPENAI_API_KEYS,
|
||||
OPENAI_API_CONFIGS,
|
||||
@ -61,9 +59,6 @@ app.add_middleware(
|
||||
|
||||
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.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
|
||||
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)):
|
||||
if url_idx is None:
|
||||
models = await get_all_models()
|
||||
if app.state.config.ENABLE_MODEL_FILTER:
|
||||
if user.role == "user":
|
||||
models["data"] = list(
|
||||
filter(
|
||||
lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
|
||||
models["data"],
|
||||
)
|
||||
)
|
||||
return models
|
||||
|
||||
# TODO: Check User Group and Filter Models
|
||||
# if app.state.config.ENABLE_MODEL_FILTER:
|
||||
# if user.role == "user":
|
||||
# models["data"] = list(
|
||||
# filter(
|
||||
# lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
|
||||
# models["data"],
|
||||
# )
|
||||
# )
|
||||
# return models
|
||||
|
||||
return models
|
||||
else:
|
||||
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/{url_idx}")
|
||||
async def generate_chat_completion(
|
||||
form_data: dict,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
bypass_filter: Optional[bool] = False,
|
||||
):
|
||||
idx = 0
|
||||
payload = {**form_data}
|
||||
@ -505,6 +502,16 @@ async def generate_chat_completion(
|
||||
del payload["metadata"]
|
||||
|
||||
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)
|
||||
|
||||
if model_info:
|
||||
|
@ -183,7 +183,10 @@ async def lifespan(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()
|
||||
@ -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 app.state.config.ENABLE_MODEL_FILTER:
|
||||
if user.role == "user":
|
||||
models = list(
|
||||
filter(
|
||||
lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
|
||||
models,
|
||||
)
|
||||
)
|
||||
return {"data": models}
|
||||
# TODO: Check User Group and Filter Models
|
||||
# if app.state.config.ENABLE_MODEL_FILTER:
|
||||
# if user.role == "user":
|
||||
# models = list(
|
||||
# filter(
|
||||
# lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
|
||||
# models,
|
||||
# )
|
||||
# )
|
||||
# return {"data": models}
|
||||
|
||||
return {"data": models}
|
||||
|
||||
@ -1106,12 +1110,14 @@ async def generate_chat_completions(
|
||||
detail="Model not found",
|
||||
)
|
||||
|
||||
if not bypass_filter and 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=status.HTTP_403_FORBIDDEN,
|
||||
detail="Model not found",
|
||||
)
|
||||
# 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=status.HTTP_403_FORBIDDEN,
|
||||
# detail="Model not found",
|
||||
# )
|
||||
|
||||
model = app.state.MODELS[model_id]
|
||||
|
||||
@ -1161,14 +1167,16 @@ async def generate_chat_completions(
|
||||
),
|
||||
"selected_model_id": selected_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)
|
||||
if model["owned_by"] == "ollama":
|
||||
# Using /ollama/api/chat endpoint
|
||||
form_data = convert_payload_openai_to_ollama(form_data)
|
||||
form_data = GenerateChatCompletionForm(**form_data)
|
||||
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:
|
||||
response.headers["content-type"] = "text/event-stream"
|
||||
@ -1179,7 +1187,9 @@ async def generate_chat_completions(
|
||||
else:
|
||||
return convert_response_ollama_to_openai(response)
|
||||
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")
|
||||
@ -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
|
||||
|
||||
|
||||
|
@ -12,6 +12,12 @@
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.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');
|
||||
|
||||
@ -38,7 +44,16 @@
|
||||
if ($user?.role !== 'admin') {
|
||||
await goto('/');
|
||||
} 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;
|
||||
});
|
||||
@ -117,7 +132,64 @@
|
||||
</div>
|
||||
</div>
|
||||
{: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}
|
||||
|
||||
<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>
|
||||
{/if}
|
||||
|
191
src/lib/components/admin/Users/Groups/GroupModal.svelte
Normal file
191
src/lib/components/admin/Users/Groups/GroupModal.svelte
Normal 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>
|
11
src/lib/components/icons/UserCircleSolid.svelte
Normal file
11
src/lib/components/icons/UserCircleSolid.svelte
Normal 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>
|
9
src/lib/components/icons/UsersSolid.svelte
Normal file
9
src/lib/components/icons/UsersSolid.svelte
Normal 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>
|
@ -411,7 +411,7 @@
|
||||
</div>
|
||||
</a>
|
||||
<div class="flex flex-row gap-0.5 self-center">
|
||||
{#if shiftKey}
|
||||
{#if $user?.role === 'admin' && shiftKey}
|
||||
<Tooltip
|
||||
content={(model?.info?.meta?.hidden ?? false)
|
||||
? $i18n.t('Show Model')
|
||||
@ -475,28 +475,31 @@
|
||||
</button>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<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"
|
||||
type="button"
|
||||
href={`/workspace/models/edit?id=${encodeURIComponent(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"
|
||||
{#if $user?.role === 'admin'}
|
||||
<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"
|
||||
type="button"
|
||||
href={`/workspace/models/edit?id=${encodeURIComponent(model.id)}`}
|
||||
>
|
||||
<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>
|
||||
<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}
|
||||
|
||||
<ModelMenu
|
||||
user={$user}
|
||||
{model}
|
||||
shareHandler={() => {
|
||||
shareModelHandler(model);
|
||||
@ -532,131 +535,133 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<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;
|
||||
});
|
||||
} else {
|
||||
await addNewModel(localStorage.token, model.info).catch((error) => {
|
||||
return null;
|
||||
});
|
||||
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;
|
||||
});
|
||||
} else {
|
||||
await addNewModel(localStorage.token, model.info).catch((error) => {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await models.set(await getModels(localStorage.token));
|
||||
_models = $models;
|
||||
};
|
||||
await models.set(await getModels(localStorage.token));
|
||||
_models = $models;
|
||||
};
|
||||
|
||||
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 Models')}</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 Models')}</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 Models')}</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 Models')}</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>
|
||||
|
||||
{#if localModelfiles.length > 0}
|
||||
<div class="flex">
|
||||
<div class=" self-center text-sm font-medium mr-4">
|
||||
{localModelfiles.length} Local Modelfiles Detected
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1">
|
||||
<button
|
||||
class="self-center w-fit text-sm p-1.5 border dark:border-gray-600 rounded-xl flex"
|
||||
on:click={async () => {
|
||||
downloadModels(localModelfiles);
|
||||
|
||||
localStorage.removeItem('modelfiles');
|
||||
localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
|
||||
}}
|
||||
>
|
||||
<div class=" self-center">
|
||||
<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="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</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>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if localModelfiles.length > 0}
|
||||
<div class="flex">
|
||||
<div class=" self-center text-sm font-medium mr-4">
|
||||
{localModelfiles.length} Local Modelfiles Detected
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1">
|
||||
<button
|
||||
class="self-center w-fit text-sm p-1.5 border dark:border-gray-600 rounded-xl flex"
|
||||
on:click={async () => {
|
||||
downloadModels(localModelfiles);
|
||||
|
||||
localStorage.removeItem('modelfiles');
|
||||
localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
|
||||
}}
|
||||
>
|
||||
<div class=" self-center">
|
||||
<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="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $config?.features.enable_community_sharing}
|
||||
<div class=" my-16">
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let user;
|
||||
export let model;
|
||||
|
||||
export let shareHandler: Function;
|
||||
@ -82,68 +83,70 @@
|
||||
<div class="flex items-center">{$i18n.t('Export')}</div>
|
||||
</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"
|
||||
on:click={() => {
|
||||
moveToTopHandler();
|
||||
}}
|
||||
>
|
||||
<ArrowUpCircle />
|
||||
{#if user?.role === 'admin'}
|
||||
<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"
|
||||
on:click={() => {
|
||||
moveToTopHandler();
|
||||
}}
|
||||
>
|
||||
<ArrowUpCircle />
|
||||
|
||||
<div class="flex items-center">{$i18n.t('Move to Top')}</div>
|
||||
</DropdownMenu.Item>
|
||||
<div class="flex items-center">{$i18n.t('Move to Top')}</div>
|
||||
</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"
|
||||
on:click={() => {
|
||||
hideHandler();
|
||||
}}
|
||||
>
|
||||
{#if model?.info?.meta?.hidden ?? false}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center">
|
||||
<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"
|
||||
on:click={() => {
|
||||
hideHandler();
|
||||
}}
|
||||
>
|
||||
{#if model?.info?.meta?.hidden ?? false}
|
||||
{$i18n.t('Show Model')}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
{$i18n.t('Hide Model')}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<div class="flex items-center">
|
||||
{#if model?.info?.meta?.hidden ?? false}
|
||||
{$i18n.t('Show Model')}
|
||||
{:else}
|
||||
{$i18n.t('Hide Model')}
|
||||
{/if}
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
{/if}
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800 my-1" />
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user