mirror of
https://git.mirrors.martin98.com/https://github.com/open-webui/open-webui
synced 2025-08-17 22:35:56 +08: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.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": [
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
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>
|
</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,28 +475,31 @@
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{:else}
|
{:else}
|
||||||
<a
|
{#if $user?.role === 'admin'}
|
||||||
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"
|
<a
|
||||||
type="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"
|
||||||
href={`/workspace/models/edit?id=${encodeURIComponent(model.id)}`}
|
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"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
stroke-linecap="round"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke-linejoin="round"
|
fill="none"
|
||||||
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"
|
viewBox="0 0 24 24"
|
||||||
/>
|
stroke-width="1.5"
|
||||||
</svg>
|
stroke="currentColor"
|
||||||
</a>
|
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
|
<ModelMenu
|
||||||
|
user={$user}
|
||||||
{model}
|
{model}
|
||||||
shareHandler={() => {
|
shareHandler={() => {
|
||||||
shareModelHandler(model);
|
shareModelHandler(model);
|
||||||
@ -532,131 +535,133 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex justify-end w-full mb-3">
|
{#if $user?.role === 'admin'}
|
||||||
<div class="flex space-x-1">
|
<div class=" flex justify-end w-full mb-3">
|
||||||
<input
|
<div class="flex space-x-1">
|
||||||
id="models-import-input"
|
<input
|
||||||
bind:this={modelsImportInputElement}
|
id="models-import-input"
|
||||||
bind:files={importFiles}
|
bind:this={modelsImportInputElement}
|
||||||
type="file"
|
bind:files={importFiles}
|
||||||
accept=".json"
|
type="file"
|
||||||
hidden
|
accept=".json"
|
||||||
on:change={() => {
|
hidden
|
||||||
console.log(importFiles);
|
on:change={() => {
|
||||||
|
console.log(importFiles);
|
||||||
|
|
||||||
let reader = new FileReader();
|
let reader = new FileReader();
|
||||||
reader.onload = async (event) => {
|
reader.onload = async (event) => {
|
||||||
let savedModels = JSON.parse(event.target.result);
|
let savedModels = JSON.parse(event.target.result);
|
||||||
console.log(savedModels);
|
console.log(savedModels);
|
||||||
|
|
||||||
for (const model of savedModels) {
|
for (const model of savedModels) {
|
||||||
if (model?.info ?? false) {
|
if (model?.info ?? false) {
|
||||||
if ($models.find((m) => m.id === model.id)) {
|
if ($models.find((m) => m.id === model.id)) {
|
||||||
await updateModelById(localStorage.token, model.id, model.info).catch((error) => {
|
await updateModelById(localStorage.token, model.id, model.info).catch((error) => {
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await addNewModel(localStorage.token, model.info).catch((error) => {
|
await addNewModel(localStorage.token, model.info).catch((error) => {
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await models.set(await getModels(localStorage.token));
|
await models.set(await getModels(localStorage.token));
|
||||||
_models = $models;
|
_models = $models;
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsText(importFiles[0]);
|
reader.readAsText(importFiles[0]);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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"
|
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={() => {
|
on:click={() => {
|
||||||
modelsImportInputElement.click();
|
modelsImportInputElement.click();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Models')}</div>
|
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Models')}</div>
|
||||||
|
|
||||||
<div class=" self-center">
|
<div class=" self-center">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
class="w-3.5 h-3.5"
|
class="w-3.5 h-3.5"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
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"
|
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"
|
clip-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<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"
|
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 () => {
|
on:click={async () => {
|
||||||
downloadModels($models);
|
downloadModels($models);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Models')}</div>
|
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Models')}</div>
|
||||||
|
|
||||||
<div class=" self-center">
|
<div class=" self-center">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
class="w-3.5 h-3.5"
|
class="w-3.5 h-3.5"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
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"
|
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"
|
clip-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</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>
|
</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}
|
{#if $config?.features.enable_community_sharing}
|
||||||
<div class=" my-16">
|
<div class=" my-16">
|
||||||
|
@ -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,68 +83,70 @@
|
|||||||
<div class="flex items-center">{$i18n.t('Export')}</div>
|
<div class="flex items-center">{$i18n.t('Export')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
<DropdownMenu.Item
|
{#if user?.role === 'admin'}
|
||||||
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"
|
<DropdownMenu.Item
|
||||||
on:click={() => {
|
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"
|
||||||
moveToTopHandler();
|
on:click={() => {
|
||||||
}}
|
moveToTopHandler();
|
||||||
>
|
}}
|
||||||
<ArrowUpCircle />
|
>
|
||||||
|
<ArrowUpCircle />
|
||||||
|
|
||||||
<div class="flex items-center">{$i18n.t('Move to Top')}</div>
|
<div class="flex items-center">{$i18n.t('Move to Top')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
<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={() => {
|
||||||
hideHandler();
|
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">
|
|
||||||
{#if model?.info?.meta?.hidden ?? false}
|
{#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}
|
{: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}
|
{/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" />
|
<hr class="border-gray-100 dark:border-gray-800 my-1" />
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user