mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-13 04:45:19 +00:00
refac: model workspace view
This commit is contained in:
parent
2c59a28860
commit
6050c86ab6
3 changed files with 260 additions and 50 deletions
|
|
@ -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
|
||||
|
|
|
|||
106
src/lib/components/workspace/common/TagSelector.svelte
Normal file
106
src/lib/components/workspace/common/TagSelector.svelte
Normal 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> -->
|
||||
96
src/lib/components/workspace/common/ViewSelector.svelte
Normal file
96
src/lib/components/workspace/common/ViewSelector.svelte
Normal 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> -->
|
||||
Loading…
Reference in a new issue