Timothy Jaeryang Baek před 5 měsíci
rodič
revize
ce9aef900c

+ 1 - 1
src/lib/components/admin/Users.svelte

@@ -25,7 +25,7 @@
 <div class="flex flex-col lg:flex-row w-full h-full -mt-0.5 pb-2 lg:space-x-4">
 	<div
 		id="users-tabs-container"
-		class="tabs flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-40 dark:text-gray-200 text-sm font-medium text-left scrollbar-none"
+		class=" flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-40 dark:text-gray-200 text-sm font-medium text-left scrollbar-none"
 	>
 		<button
 			class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition {selectedTab ===

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

@@ -18,6 +18,7 @@
 	import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
 	import User from '$lib/components/icons/User.svelte';
 	import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
+	import GroupModal from './Groups/GroupModal.svelte';
 
 	const i18n = getContext('i18n');
 
@@ -60,6 +61,7 @@
 </script>
 
 {#if loaded}
+	<GroupModal bind:show={showCreateGroupModal} />
 	<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
 		<div class="flex md:self-center text-lg font-medium px-0.5">
 			{$i18n.t('Groups')}

+ 61 - 0
src/lib/components/admin/Users/Groups/Display.svelte

@@ -0,0 +1,61 @@
+<script lang="ts">
+	import { getContext } from 'svelte';
+	import Textarea from '$lib/components/common/Textarea.svelte';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+
+	const i18n = getContext('i18n');
+
+	export let name = '';
+	export let color = '';
+	export let description = '';
+</script>
+
+<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('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('Color')}</div>
+
+	<div class="flex-1">
+		<Tooltip content={$i18n.t('Hex Color - Leave empty for default color')} placement="top-start">
+			<div class="flex gap-0.5">
+				<div class="text-gray-500">#</div>
+
+				<input
+					class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+					type="text"
+					bind:value={color}
+					placeholder={$i18n.t('Hex Color')}
+					autocomplete="off"
+				/>
+			</div>
+		</Tooltip>
+	</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">
+		<Textarea
+			className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none resize-none"
+			rows={4}
+			bind:value={description}
+			placeholder={$i18n.t('Group Description')}
+		/>
+	</div>
+</div>

+ 63 - 36
src/lib/components/admin/Users/Groups/GroupModal.svelte

@@ -10,6 +10,9 @@
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
+	import Display from './Display.svelte';
+	import Permissions from './Permissions.svelte';
+	import Users from './Users.svelte';
 
 	export let onSubmit: Function = () => {};
 	export let onDelete: Function = () => {};
@@ -18,9 +21,13 @@
 	export let edit = false;
 	export let group = null;
 
+	let selectedTab = 'display';
+
 	let name = '';
 	let description = '';
+
 	let permissions = {};
+	let userIds = [];
 
 	let loading = false;
 
@@ -30,7 +37,8 @@
 		const group = {
 			name,
 			description,
-			permissions
+			permissions,
+			user_ids: userIds
 		};
 
 		await onSubmit(group);
@@ -40,6 +48,7 @@
 
 		name = '';
 		permissions = {};
+		userIds = [];
 	};
 
 	const init = () => {
@@ -47,6 +56,7 @@
 			name = group.name;
 			description = group.description;
 			permissions = group?.permissions ?? {};
+			userIds = group?.user_ids ?? [];
 		}
 	};
 
@@ -59,9 +69,9 @@
 	});
 </script>
 
-<Modal size="lg" bind:show>
+<Modal size="sm" bind:show>
 	<div>
-		<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2">
+		<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1">
 			<div class=" text-lg font-medium self-center font-primary">
 				{#if edit}
 					{$i18n.t('Edit User Group')}
@@ -97,39 +107,56 @@
 						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
+						class=" tabs flex flex-row overflow-x-auto gap-2.5 text-sm font-medium mb-3 border-b border-b-gray-800 scrollbar-hidden"
+					>
+						<button
+							class="px-0.5 pb-1.5 min-w-fit flex text-right transition border-b-2 {selectedTab ===
+							'display'
+								? ' dark:border-white'
+								: 'border-transparent text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
+							on:click={() => {
+								selectedTab = 'display';
+							}}
+							type="button"
+						>
+							{$i18n.t('Display')}
+						</button>
+
+						<button
+							class="px-0.5 pb-1.5 min-w-fit flex text-right transition border-b-2 {selectedTab ===
+							'permissions'
+								? '  dark:border-white'
+								: 'border-transparent text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
+							on:click={() => {
+								selectedTab = 'permissions';
+							}}
+							type="button"
+						>
+							{$i18n.t('Permissions')}
+						</button>
+
+						<button
+							class="px-0.5 pb-1.5 min-w-fit flex text-right transition border-b-2 {selectedTab ===
+							'users'
+								? ' dark:border-white'
+								: ' border-transparent text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
+							on:click={() => {
+								selectedTab = 'users';
+							}}
+							type="button"
+						>
+							{$i18n.t('Users')} ({userIds.length})
+						</button>
+					</div>
+					<div class="px-1 h-96 lg:max-h-96 overflow-y-auto scrollbar-hidden">
+						{#if selectedTab == 'display'}
+							<Display bind:name bind:description />
+						{:else if selectedTab == 'permissions'}
+							<Permissions bind:permissions />
+						{:else if selectedTab == 'users'}
+							<Users bind:userIds />
+						{/if}
 					</div>
 
 					<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">

+ 186 - 0
src/lib/components/admin/Users/Groups/Permissions.svelte

@@ -0,0 +1,186 @@
+<script lang="ts">
+	import { getContext } from 'svelte';
+	const i18n = getContext('i18n');
+
+	import Switch from '$lib/components/common/Switch.svelte';
+	import { models } from '$lib/stores';
+	import Minus from '$lib/components/icons/Minus.svelte';
+	import Plus from '$lib/components/icons/Plus.svelte';
+
+	export let permissions = {};
+
+	let defaultModelId = '';
+
+	let selectedModelId = '';
+
+	let filterEnabled = false;
+	let filterMode = 'include';
+	let filterModelIds = [];
+
+	let workspaceModelsAccess = false;
+	let workspaceKnowledgeAccess = false;
+	let workspacePromptsAccess = false;
+	let workspaceToolsAccess = false;
+	let workspaceFunctionsAccess = false;
+
+	let chatDeletion = true;
+	let chatEdit = true;
+	let chatTemporary = true;
+</script>
+
+<div>
+	<div>
+		<div class=" mb-2 text-sm font-medium">{$i18n.t('Model Permissions')}</div>
+
+		<div class="mb-2">
+			<div class="flex justify-between items-center text-xs pr-2">
+				<div class=" text-xs font-medium">{$i18n.t('Model Filtering')}</div>
+
+				<Switch bind:state={filterEnabled} />
+			</div>
+		</div>
+
+		{#if filterEnabled}
+			<div class="mb-2">
+				<div class=" space-y-1.5">
+					<div class="flex flex-col w-full">
+						<div class="mb-1 flex justify-between">
+							<div class="text-xs text-gray-500">{$i18n.t('Model IDs')}</div>
+						</div>
+
+						{#if filterModelIds.length > 0}
+							<div class="flex flex-col">
+								{#each filterModelIds as modelId, modelIdx}
+									<div class=" flex gap-2 w-full justify-between items-center">
+										<div class=" text-sm flex-1 rounded-lg">
+											{modelId}
+										</div>
+										<div class="flex-shrink-0">
+											<button
+												type="button"
+												on:click={() => {
+													filterModelIds = filterModelIds.filter((_, idx) => idx !== modelIdx);
+												}}
+											>
+												<Minus strokeWidth="2" className="size-3.5" />
+											</button>
+										</div>
+									</div>
+								{/each}
+							</div>
+						{:else}
+							<div class="text-gray-500 text-xs text-center py-2 px-10">
+								{$i18n.t('No model IDs')}
+							</div>
+						{/if}
+					</div>
+				</div>
+				<hr class=" border-gray-100 dark:border-gray-700/10 mt-2.5 mb-1 w-full" />
+
+				<div class="flex items-center">
+					<select
+						class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
+							? ''
+							: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+						bind:value={selectedModelId}
+					>
+						<option value="">{$i18n.t('Select a model')}</option>
+						{#each $models.filter((m) => m?.owned_by !== 'arena') as model}
+							<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
+						{/each}
+					</select>
+
+					<div>
+						<button
+							type="button"
+							on:click={() => {
+								if (selectedModelId && !filterModelIds.includes(selectedModelId)) {
+									filterModelIds = [...filterModelIds, selectedModelId];
+									selectedModelId = '';
+								}
+							}}
+						>
+							<Plus className="size-3.5" strokeWidth="2" />
+						</button>
+					</div>
+				</div>
+			</div>
+		{/if}
+
+		<div class=" space-y-1 mb-3">
+			<div class="">
+				<div class="flex justify-between items-center text-xs">
+					<div class=" text-xs font-medium">{$i18n.t('Default Model')}</div>
+				</div>
+			</div>
+
+			<div class="flex-1 mr-2">
+				<select
+					class="w-full bg-transparent outline-none py-0.5 text-sm"
+					bind:value={defaultModelId}
+					placeholder="Select a model"
+				>
+					<option value="" disabled selected>{$i18n.t('Select a model')}</option>
+					{#each filterEnabled ? $models.filter( (model) => filterModelIds.includes(model.id) ) : $models.filter((model) => model.id) as model}
+						<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
+					{/each}
+				</select>
+			</div>
+		</div>
+	</div>
+
+	<hr class=" border-gray-50 dark:border-gray-850 my-2" />
+
+	<div>
+		<div class=" mb-2 text-sm font-medium">{$i18n.t('Workspace Permissions')}</div>
+
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">{$i18n.t('Allow Models Access')}</div>
+			<Switch bind:state={workspaceModelsAccess} />
+		</div>
+
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">{$i18n.t('Allow Knowledge Access')}</div>
+			<Switch bind:state={workspaceKnowledgeAccess} />
+		</div>
+
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">{$i18n.t('Allow Prompts Access')}</div>
+			<Switch bind:state={workspacePromptsAccess} />
+		</div>
+
+		<!-- <div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">{$i18n.t('Allow Tools Access')}</div>
+			<Switch bind:state={workspaceToolsAccess} />
+		</div>
+
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">{$i18n.t('Allow Functions Access')}</div>
+			<Switch bind:state={workspaceFunctionsAccess} />
+		</div> -->
+	</div>
+
+	<hr class=" border-gray-50 dark:border-gray-850 my-2" />
+
+	<div>
+		<div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
+
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div>
+
+			<Switch bind:state={chatDeletion} />
+		</div>
+
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Editing')}</div>
+
+			<Switch bind:state={chatEdit} />
+		</div>
+
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">{$i18n.t('Allow Temporary Chat')}</div>
+
+			<Switch bind:state={chatTemporary} />
+		</div>
+	</div>
+</div>

+ 7 - 0
src/lib/components/admin/Users/Groups/Users.svelte

@@ -0,0 +1,7 @@
+<script lang="ts">
+	export let userIds = [];
+</script>
+
+<div>
+	{JSON.stringify(userIds)}
+</div>