refac: model workspace view

This commit is contained in:
Timothy Jaeryang Baek 2025-10-04 21:44:51 -05:00
parent 2c59a28860
commit 6050c86ab6
3 changed files with 260 additions and 50 deletions

View file

@ -24,6 +24,8 @@
import { getModels } from '$lib/apis';
import { getGroups } from '$lib/apis/groups';
import { capitalizeFirstLetter, copyToClipboard } from '$lib/utils';
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
import ModelMenu from './Models/ModelMenu.svelte';
import ModelDeleteConfirmDialog from '../common/ConfirmDialog.svelte';
@ -34,10 +36,11 @@
import ChevronRight from '../icons/ChevronRight.svelte';
import Switch from '../common/Switch.svelte';
import Spinner from '../common/Spinner.svelte';
import { capitalizeFirstLetter, copyToClipboard } from '$lib/utils';
import XMark from '../icons/XMark.svelte';
import EyeSlash from '../icons/EyeSlash.svelte';
import Eye from '../icons/Eye.svelte';
import ViewSelector from './common/ViewSelector.svelte';
import TagSelector from './common/TagSelector.svelte';
let shiftKey = false;
@ -49,6 +52,8 @@
let models = [];
let tags = [];
let viewOption = '';
let selectedTag = '';
let filteredModels = [];
@ -58,22 +63,28 @@
let group_ids = [];
$: if (models) {
$: if (models && query !== undefined && selectedTag !== undefined && viewOption !== undefined) {
setFilteredModels();
}
const setFilteredModels = async () => {
filteredModels = models.filter((m) => {
if (query === '' && selectedTag === '') return true;
if (query === '' && selectedTag === '' && viewOption === '') return true;
const lowerQuery = query.toLowerCase();
return (
((m.name || '').toLowerCase().includes(lowerQuery) ||
(m.user?.name || '').toLowerCase().includes(lowerQuery) || // Search by user name
(m.user?.email || '').toLowerCase().includes(lowerQuery)) && // Search by user email
(selectedTag === '' ||
m?.meta?.tags?.some((tag) => tag.name.toLowerCase() === selectedTag.toLowerCase()))
m?.meta?.tags?.some((tag) => tag.name.toLowerCase() === selectedTag.toLowerCase())) &&
(viewOption === '' ||
(viewOption === 'created' && m.user_id === $user?.id) ||
(viewOption === 'shared' && m.user_id !== $user?.id))
);
});
}
};
let query = '';
const deleteModelHandler = async (model) => {
const res = await deleteModelById(localStorage.token, model.id).catch((e) => {
toast.error(`${e}`);
@ -173,11 +184,7 @@
saveAs(blob, `${model.id}-${Date.now()}.json`);
};
onMount(async () => {
models = await getWorkspaceModels(localStorage.token);
let groups = await getGroups(localStorage.token);
group_ids = groups.map((group) => group.id);
const setTags = () => {
if (models) {
tags = models
.filter((model) => !(model?.meta?.hidden ?? false))
@ -187,7 +194,16 @@
// Remove duplicates and sort
tags = Array.from(new Set(tags)).sort((a, b) => a.localeCompare(b));
}
};
onMount(async () => {
viewOption = localStorage.workspaceViewOption ?? '';
models = await getWorkspaceModels(localStorage.token);
let groups = await getGroups(localStorage.token);
group_ids = groups.map((group) => group.id);
setTags();
loaded = true;
const onKeyDown = (event) => {
@ -232,7 +248,7 @@
}}
/>
<div class="flex flex-col gap-1 mt-1.5">
<div class="flex flex-col gap-1 mt-1.5 my-1">
<input
id="models-import-input"
bind:this={modelsImportInputElement}
@ -355,48 +371,40 @@
</div>
</div>
{#if tags.length > 0}
<div
class=" flex w-full bg-transparent overflow-x-auto scrollbar-none -mx-1"
on:wheel={(e) => {
if (e.deltaY !== 0) {
e.preventDefault();
e.currentTarget.scrollLeft += e.deltaY;
}
}}
>
<div
class=" flex w-full bg-transparent overflow-x-auto scrollbar-none -mx-2"
on:wheel={(e) => {
if (e.deltaY !== 0) {
e.preventDefault();
e.currentTarget.scrollLeft += e.deltaY;
}
}}
class="flex gap-1 w-fit text-center text-sm rounded-full bg-transparent px-1.5 whitespace-nowrap"
bind:this={tagsContainerElement}
>
<div
class="flex gap-1 w-fit text-center text-sm rounded-full bg-transparent px-1.5 whitespace-nowrap"
bind:this={tagsContainerElement}
>
<button
class="min-w-fit outline-none p-1.5 {selectedTag === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => {
selectedTag = '';
}}
>
{$i18n.t('All')}
</button>
<ViewSelector
bind:value={viewOption}
onChange={async (value) => {
localStorage.workspaceViewOption = value;
{#each tags as tag}
<Tooltip content={tag}>
<button
class="min-w-fit outline-none p-1.5 {selectedTag === tag
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => {
selectedTag = tag;
}}
>
{tag.length > 32 ? `${tag.slice(0, 32)}...` : tag}
</button>
</Tooltip>
{/each}
</div>
await tick();
setTags();
}}
/>
{#if (tags ?? []).length > 0}
<TagSelector
bind:value={selectedTag}
items={tags.map((tag) => {
return { value: tag, label: tag };
})}
/>
{/if}
</div>
{/if}
</div>
<div class=" my-2 mb-5 gap-2 grid lg:grid-cols-2 xl:grid-cols-3" id="model-list">
{#each filteredModels as model (model.id)}
<div

View file

@ -0,0 +1,106 @@
<script lang="ts">
import { Select } from 'bits-ui';
import { getContext } from 'svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import Check from '$lib/components/icons/Check.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
const i18n = getContext('i18n');
export let value = '';
export let placeholder = $i18n.t('Tag');
export let onChange: (value: string) => void = () => {};
export let items = [];
</script>
<Select.Root
selected={value ? items.find((item) => item.value === value) : null}
{items}
onSelectedChange={(selectedItem) => {
value = selectedItem.value;
onChange(value);
}}
>
<Select.Trigger
class="relative w-full flex items-center gap-0.5 px-2.5 py-1.5 rounded-xl "
aria-label={placeholder}
>
<Select.Value
class="inline-flex h-input px-0.5 w-full outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden capitalize"
{placeholder}
/>
{#if value}
<button
class="outline-none"
on:click={() => {
value = '';
onChange(value);
}}
>
<XMark className="size-3.5" />
</button>
{:else}
<ChevronDown className=" size-3.5" strokeWidth="2.5" />
{/if}
</Select.Trigger>
<Select.Content
class="rounded-2xl min-w-[170px] p-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
sameWidth={false}
align="start"
>
<slot>
{#each items as item}
<Select.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl capitalize"
value={item.value}
label={item.label}
>
{item.label.length > 32 ? `${item.label.slice(0, 32)}...` : item.label}
{#if value === item.value}
<div class="ml-auto">
<Check />
</div>
{/if}
</Select.Item>
{/each}
</slot>
</Select.Content>
</Select.Root>
<!-- <button
class="min-w-fit outline-none p-1.5 {selectedTag === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => {
selectedTag = '';
}}
>
{$i18n.t('All')}
</button>
<button
class="min-w-fit outline-none p-1.5 {selectedTag === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => {
selectedTag = '';
}}
>
{$i18n.t('Created by you')}
</button>
<button
class="min-w-fit outline-none p-1.5 {selectedTag === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => {
selectedTag = '';
}}
>
{$i18n.t('Shared with you')}
</button> -->

View file

@ -0,0 +1,96 @@
<script lang="ts">
import { Select } from 'bits-ui';
import { getContext } from 'svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import Check from '$lib/components/icons/Check.svelte';
const i18n = getContext('i18n');
export let value = '';
export let placeholder = $i18n.t('Select view');
export let onChange: (value: string) => void = () => {};
const items = [
{ value: '', label: $i18n.t('All') },
{ value: 'created', label: $i18n.t('Created by you') },
{ value: 'shared', label: $i18n.t('Shared with you') }
];
</script>
<Select.Root
selected={items.find((item) => item.value === value)}
{items}
onSelectedChange={(selectedItem) => {
value = selectedItem.value;
onChange(value);
}}
>
<Select.Trigger
class="relative w-full flex items-center gap-0.5 px-2.5 py-1.5 bg-gray-50 dark:bg-gray-850 rounded-xl "
aria-label={placeholder}
>
<Select.Value
class="inline-flex h-input px-0.5 w-full outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden"
{placeholder}
/>
<ChevronDown className=" size-3.5" strokeWidth="2.5" />
</Select.Trigger>
<Select.Content
class="rounded-2xl min-w-[170px] p-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
sameWidth={false}
align="start"
>
<slot>
{#each items as item}
<Select.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
value={item.value}
label={item.label}
>
{item.label}
{#if value === item.value}
<div class="ml-auto">
<Check />
</div>
{/if}
</Select.Item>
{/each}
</slot>
</Select.Content>
</Select.Root>
<!-- <button
class="min-w-fit outline-none p-1.5 {selectedTag === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => {
selectedTag = '';
}}
>
{$i18n.t('All')}
</button>
<button
class="min-w-fit outline-none p-1.5 {selectedTag === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => {
selectedTag = '';
}}
>
{$i18n.t('Created by you')}
</button>
<button
class="min-w-fit outline-none p-1.5 {selectedTag === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => {
selectedTag = '';
}}
>
{$i18n.t('Shared with you')}
</button> -->