wip: frontend

This commit is contained in:
Timothy Jaeryang Baek
2024-11-14 02:20:34 -08:00
parent aaba41339e
commit 0e8c0b452e
12 changed files with 870 additions and 495 deletions

View File

@@ -0,0 +1,148 @@
<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 Textarea from '$lib/components/common/Textarea.svelte';
export let onSubmit: Function = () => {};
export let show = false;
let name = '';
let description = '';
let userIds = [];
let loading = false;
const submitHandler = async () => {
loading = true;
const group = {
name,
description,
user_ids: userIds
};
await onSubmit(group);
loading = false;
show = false;
name = '';
description = '';
userIds = [];
};
onMount(() => {
console.log('mounted');
});
</script>
<Modal size="sm" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
<div class=" text-lg font-medium self-center font-primary">
{$i18n.t('Add User Group')}
</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="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('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={2}
bind:value={description}
placeholder={$i18n.t('Group Description')}
/>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
<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('Create')}
{#if loading}
<div class="ml-2 self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{/if}
</button>
</div>
</form>
</div>
</div>
</div>
</Modal>

View File

@@ -19,8 +19,14 @@
export let show = false;
export let edit = false;
export let users = [];
export let group = null;
export let custom = true;
export let tabs = ['display', 'permissions', 'users'];
let selectedTab = 'display';
let name = '';
@@ -28,6 +34,7 @@
let permissions = {};
let userIds = [];
let adminIds = [];
let loading = false;
@@ -65,18 +72,24 @@
}
onMount(() => {
console.log(tabs);
selectedTab = tabs[0];
init();
});
</script>
<Modal size="sm" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1">
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
<div class=" text-lg font-medium self-center font-primary">
{#if edit}
{$i18n.t('Edit User Group')}
{#if custom}
{#if edit}
{$i18n.t('Edit User Group')}
{:else}
{$i18n.t('Add User Group')}
{/if}
{:else}
{$i18n.t('Add User Group')}
{$i18n.t('Edit Default Permissions')}
{/if}
</div>
<button
@@ -108,54 +121,60 @@
}}
>
<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"
class=" tabs flex flex-row overflow-x-auto gap-2.5 text-sm font-medium 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>
{#if tabs.includes('display')}
<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>
{/if}
<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>
{#if tabs.includes('permissions')}
<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>
{/if}
<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>
{#if tabs.includes('users')}
<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>
{/if}
</div>
<div class="px-1 h-96 lg:max-h-96 overflow-y-auto scrollbar-hidden">
<div class="px-1 h-96 lg:max-h-96 overflow-y-auto scrollbar-hidden mt-2.5">
{#if selectedTab == 'display'}
<Display bind:name bind:description />
{:else if selectedTab == 'permissions'}
<Permissions bind:permissions />
<Permissions bind:permissions {custom} />
{:else if selectedTab == 'users'}
<Users bind:userIds />
<Users bind:userIds bind:adminIds {users} />
{/if}
</div>

View File

@@ -0,0 +1,45 @@
<script>
import Pencil from '$lib/components/icons/Pencil.svelte';
import User from '$lib/components/icons/User.svelte';
import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
import GroupModal from './EditGroupModal.svelte';
export let users = [];
export let group = {
name: 'Admins',
user_ids: [1, 2, 3]
};
let showEdit = false;
</script>
<GroupModal bind:show={showEdit} edit {group} {users} />
<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 hover:bg-gray-100 dark:hover:bg-gray-850 transition"
on:click={() => {
showEdit = true;
}}
>
<Pencil />
</button>
</div>
</div>

View File

@@ -8,6 +8,7 @@
import Plus from '$lib/components/icons/Plus.svelte';
export let permissions = {};
export let custom = true;
let defaultModelId = '';
@@ -135,17 +136,34 @@
<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>
<div class=" self-center text-xs font-medium">
{#if custom}
{$i18n.t('Admins')}:
{/if}
{$i18n.t('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>
<div class=" self-center text-xs font-medium">
{#if custom}
{$i18n.t('Admins')}:
{/if}
{$i18n.t('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>
<div class=" self-center text-xs font-medium">
{#if custom}
{$i18n.t('Admins')}:
{/if}
{$i18n.t('Prompts Access')}
</div>
<Switch bind:state={workspacePromptsAccess} />
</div>
@@ -166,19 +184,34 @@
<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>
<div class=" self-center text-xs font-medium">
{#if custom}
{$i18n.t('Members')}:
{/if}
{$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>
<div class=" self-center text-xs font-medium">
{#if custom}
{$i18n.t('Members')}:
{/if}
{$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>
<div class=" self-center text-xs font-medium">
{#if custom}
{$i18n.t('Members')}:
{/if}
{$i18n.t('Allow Temporary Chat')}
</div>
<Switch bind:state={chatTemporary} />
</div>

View File

@@ -1,7 +1,140 @@
<script lang="ts">
import { getContext } from 'svelte';
const i18n = getContext('i18n');
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
import Checkbox from '$lib/components/common/Checkbox.svelte';
import Badge from '$lib/components/common/Badge.svelte';
export let users = [];
export let userIds = [];
export let adminIds = [];
let filteredUsers = [];
$: filteredUsers = users
.filter((user) => {
if (user?.role === 'admin') {
return false;
}
if (query === '') {
return true;
}
return (
user.name.toLowerCase().includes(query.toLowerCase()) ||
user.email.toLowerCase().includes(query.toLowerCase())
);
})
.sort((a, b) => {
const aIsAdmin = adminIds.includes(a.id);
const bIsAdmin = adminIds.includes(b.id);
const aUserIndex = userIds.indexOf(a.id);
const bUserIndex = userIds.indexOf(b.id);
// Admin users should come first
if (aIsAdmin && !bIsAdmin) return -1; // Place 'a' first if it's admin
if (!aIsAdmin && bIsAdmin) return 1; // Place 'b' first if it's admin
// Neither are admin, compare based on userIds or fall back to alphabetical order
if (aUserIndex !== -1 && bUserIndex === -1) return -1; // 'a' has valid userId -> prioritize
if (bUserIndex !== -1 && aUserIndex === -1) return 1; // 'b' has valid userId -> prioritize
// Both a and b are either in the userIds array or not, so we'll sort them by their indices
if (aUserIndex !== -1 && bUserIndex !== -1) return aUserIndex - bUserIndex;
// If both are not in the userIds, fallback to alphabetical sorting by name
return a.name.localeCompare(b.name);
});
let query = '';
</script>
<div>
{JSON.stringify(userIds)}
<div class="flex w-full">
<div class="flex flex-1">
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
clip-rule="evenodd"
/>
</svg>
</div>
<input
class=" w-full text-sm pr-4 rounded-r-xl outline-none bg-transparent"
bind:value={query}
placeholder={$i18n.t('Search')}
/>
</div>
</div>
<div class="mt-3 max-h-[22rem] overflow-y-auto scrollbar-hidden">
<div class="flex flex-col gap-2.5">
{#if filteredUsers.length > 0}
{#each filteredUsers as user, userIdx (user.id)}
<div class="flex flex-row items-center gap-3 w-full text-sm">
<div class="flex items-center">
<Checkbox
state={userIds.includes(user.id) ? 'checked' : 'unchecked'}
on:change={(e) => {
if (e.detail === 'checked') {
userIds = [...userIds, user.id];
} else {
userIds = userIds.filter((id) => id !== user.id);
}
}}
/>
</div>
<div class="flex w-full items-center justify-between">
<div class="flex">
<img
class=" rounded-full w-6 h-6 object-cover mr-2.5"
src={user.profile_image_url.startsWith(WEBUI_BASE_URL) ||
user.profile_image_url.startsWith('https://www.gravatar.com/avatar/') ||
user.profile_image_url.startsWith('data:')
? user.profile_image_url
: `/user.png`}
alt="user"
/>
<div class=" font-medium self-center">{user.name}</div>
</div>
{#if userIds.includes(user.id)}
<button
on:click={() => {
adminIds = adminIds.includes(user.id)
? adminIds.filter((id) => id !== user.id)
: [...adminIds, user.id];
}}
type="button"
>
{#if adminIds.includes(user.id)}
<Badge type="info" content="admin" />
{:else}
<Badge type="success" content="member" />
{/if}
</button>
{/if}
</div>
</div>
{/each}
{:else}
<div class="text-gray-500 text-xs text-center py-2 px-10">
{$i18n.t('No users were found.')}
</div>
{/if}
</div>
</div>
</div>