Prechádzať zdrojové kódy

wip: user groups frontend

Timothy Jaeryang Baek 5 mesiacov pred
rodič
commit
baea26d9ca

+ 37 - 36
backend/open_webui/apps/ollama/main.py

@@ -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": [

+ 23 - 16
backend/open_webui/apps/openai/main.py

@@ -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:

+ 28 - 44
backend/open_webui/main.py

@@ -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
 
 

+ 74 - 2
src/lib/components/admin/Users/Groups.svelte

@@ -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 - 0
src/lib/components/admin/Users/Groups/GroupModal.svelte

@@ -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 - 0
src/lib/components/icons/UserCircleSolid.svelte

@@ -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 - 0
src/lib/components/icons/UsersSolid.svelte

@@ -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>

+ 138 - 133
src/lib/components/workspace/Models.svelte

@@ -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);
-
-				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;
-								});
+{#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);
+
+						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>
+				<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>
+		{#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);
+				<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>
+							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}
+	</div>
+{/if}
 
 {#if $config?.features.enable_community_sharing}
 	<div class=" my-16">

+ 62 - 59
src/lib/components/workspace/Models/ModelMenu.svelte

@@ -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 />
-
-				<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">
+			{#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>
+
+				<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" />