mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-25 17:21:00 -05:00
refac
This commit is contained in:
@@ -149,6 +149,7 @@ class KnowledgeDirectoryForm(BaseModel):
|
||||
####################
|
||||
class KnowledgeUserModel(KnowledgeModel):
|
||||
user: Optional[UserResponse] = None
|
||||
file_count: int | None = None
|
||||
|
||||
|
||||
class KnowledgeResponse(KnowledgeModel):
|
||||
@@ -322,6 +323,14 @@ class KnowledgeTable:
|
||||
|
||||
knowledge_ids = [kb.id for kb, _ in items]
|
||||
grants_map = await AccessGrants.get_grants_by_resources('knowledge', knowledge_ids, db=db)
|
||||
file_counts = {}
|
||||
if knowledge_ids:
|
||||
file_count_result = await db.execute(
|
||||
select(KnowledgeFile.knowledge_id, func.count(KnowledgeFile.id))
|
||||
.where(KnowledgeFile.knowledge_id.in_(knowledge_ids))
|
||||
.group_by(KnowledgeFile.knowledge_id)
|
||||
)
|
||||
file_counts = dict(file_count_result.all())
|
||||
|
||||
knowledge_bases = []
|
||||
for knowledge_base, user in items:
|
||||
@@ -336,6 +345,7 @@ class KnowledgeTable:
|
||||
)
|
||||
).model_dump(),
|
||||
'user': (UserModel.model_validate(user).model_dump() if user else None),
|
||||
'file_count': file_counts.get(knowledge_base.id, 0),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<script lang="ts">
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
import { WEBUI_NAME, config, functions as _functions, models, settings, user } from '$lib/stores';
|
||||
import { onMount, getContext, tick, onDestroy } from 'svelte';
|
||||
|
||||
@@ -39,6 +43,8 @@
|
||||
import { capitalizeFirstLetter } from '$lib/utils';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
import SplitCreateButton from '../common/SplitCreateButton.svelte';
|
||||
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||
import ChevronUp from '../icons/ChevronUp.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -49,6 +55,9 @@
|
||||
|
||||
let tagsContainerElement: HTMLDivElement;
|
||||
let viewOption = '';
|
||||
let sortKey = 'updated_at';
|
||||
let sortDirection = 'desc';
|
||||
let openFunctionMenuId: string | null = null;
|
||||
|
||||
let query = '';
|
||||
let searchDebounceTimer: ReturnType<typeof setTimeout>;
|
||||
@@ -90,7 +99,13 @@
|
||||
}
|
||||
};
|
||||
|
||||
$: if (functions && selectedType !== undefined && viewOption !== undefined) {
|
||||
$: if (
|
||||
functions &&
|
||||
selectedType !== undefined &&
|
||||
viewOption !== undefined &&
|
||||
sortKey !== undefined &&
|
||||
sortDirection !== undefined
|
||||
) {
|
||||
setFilteredItems();
|
||||
}
|
||||
|
||||
@@ -109,7 +124,32 @@
|
||||
(viewOption === 'created' && f.user_id === $user?.id) ||
|
||||
(viewOption === 'shared' && f.user_id !== $user?.id))
|
||||
)
|
||||
.sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
|
||||
.sort((a, b) => {
|
||||
const direction = sortDirection === 'asc' ? 1 : -1;
|
||||
|
||||
if (sortKey === 'name') {
|
||||
return direction * (a.name ?? '').localeCompare(b.name ?? '');
|
||||
}
|
||||
|
||||
return direction * ((a.updated_at ?? 0) - (b.updated_at ?? 0));
|
||||
});
|
||||
};
|
||||
|
||||
const setSortKey = (key: string) => {
|
||||
if (sortKey === key) {
|
||||
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortKey = key;
|
||||
sortDirection = key === 'updated_at' ? 'desc' : 'asc';
|
||||
}
|
||||
};
|
||||
|
||||
const openFunction = (func) => {
|
||||
goto(`/admin/functions/edit?id=${encodeURIComponent(func.id)}`);
|
||||
};
|
||||
|
||||
const shouldIgnoreRowClick = (target: EventTarget | null) => {
|
||||
return target instanceof Element && !!target.closest('button, a, input, [role="menu"]');
|
||||
};
|
||||
const shareHandler = async (func) => {
|
||||
const item = await getFunctionById(localStorage.token, func.id).catch((error) => {
|
||||
@@ -407,173 +447,275 @@
|
||||
</div>
|
||||
|
||||
{#if (filteredItems ?? []).length !== 0}
|
||||
<div class="my-1 gap-x-2 gap-y-0.5 grid lg:grid-cols-2">
|
||||
{#each filteredItems as func (func.id)}
|
||||
<div
|
||||
class="flex space-x-4 cursor-pointer w-full px-2.5 py-1.5 rounded-2xl hover:bg-gray-50/70 dark:hover:bg-gray-850/50"
|
||||
<div class="my-1">
|
||||
<div
|
||||
class="flex w-full items-center gap-2 px-1.5 pb-0.5 text-xs text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
<button
|
||||
class="flex min-w-0 flex-1 items-center gap-1 py-0.5 text-left"
|
||||
type="button"
|
||||
on:click={() => setSortKey('name')}
|
||||
>
|
||||
<a
|
||||
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
|
||||
href={`/admin/functions/edit?id=${encodeURIComponent(func.id)}`}
|
||||
{$i18n.t('Title')}
|
||||
{#if sortKey === 'name'}
|
||||
{#if sortDirection === 'asc'}
|
||||
<ChevronUp className="size-2" />
|
||||
{:else}
|
||||
<ChevronDown className="size-2" />
|
||||
{/if}
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<div class="hidden w-44 shrink-0 md:block"></div>
|
||||
|
||||
<button
|
||||
class="flex w-36 shrink-0 items-center justify-end gap-1 py-0.5 text-right"
|
||||
type="button"
|
||||
on:click={() => setSortKey('updated_at')}
|
||||
>
|
||||
{$i18n.t('Updated at')}
|
||||
{#if sortKey === 'updated_at'}
|
||||
{#if sortDirection === 'asc'}
|
||||
<ChevronUp className="size-2" />
|
||||
{:else}
|
||||
<ChevronDown className="size-2" />
|
||||
{/if}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-y-0.5">
|
||||
{#each filteredItems as func (func.id)}
|
||||
<div
|
||||
class="group flex min-h-8 w-full cursor-pointer items-center gap-2 overflow-hidden rounded-xl px-2 py-1 text-left"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click={(e) => {
|
||||
if (shouldIgnoreRowClick(e.target)) return;
|
||||
openFunction(func);
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (e.currentTarget !== e.target) return;
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
openFunction(func);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center text-left">
|
||||
<div class=" flex-1 self-center pl-1">
|
||||
<Tooltip content={func.id} placement="top-start">
|
||||
<div class=" flex items-center gap-1.5">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-1 overflow-hidden">
|
||||
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<div class="flex min-w-0 items-center gap-2 overflow-hidden">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
|
||||
<div
|
||||
class=" text-xs font-normal px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||
class="shrink-0 rounded-sm bg-gray-500/20 px-1 text-[10px] uppercase leading-4 text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
{func.type}
|
||||
</div>
|
||||
|
||||
<div class="line-clamp-1 text-sm">
|
||||
{func.name}
|
||||
</div>
|
||||
<Tooltip content={func.id} className="min-w-0" placement="top-start">
|
||||
<div
|
||||
class="truncate text-[13px] leading-5 text-gray-800 group-hover:underline dark:text-gray-200"
|
||||
>
|
||||
{func.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{#if func?.meta?.manifest?.version}
|
||||
<div class=" text-gray-500 text-xs font-normal shrink-0">
|
||||
<div
|
||||
class="min-w-0 max-w-[40%] shrink-0 truncate text-[11px] leading-5 text-gray-500"
|
||||
>
|
||||
v{func?.meta?.manifest?.version ?? ''}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<div class="flex gap-1.5 px-1">
|
||||
<div class="text-xs text-gray-500 shrink-0">
|
||||
<Tooltip
|
||||
content={func?.user?.email ?? $i18n.t('Deleted User')}
|
||||
className="flex shrink-0"
|
||||
placement="top-start"
|
||||
>
|
||||
{$i18n.t('By {{name}}', {
|
||||
name: capitalizeFirstLetter(
|
||||
func?.user?.name ?? func?.user?.email ?? $i18n.t('Deleted User')
|
||||
)
|
||||
})}
|
||||
<Tooltip content={dayjs(func.updated_at * 1000).format('LLLL')}>
|
||||
<div
|
||||
class="shrink-0 truncate text-[11px] leading-5 text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
{dayjs(func.updated_at * 1000).fromNow()}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
|
||||
{func.meta.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if func?.meta?.description}
|
||||
<Tooltip
|
||||
content={func?.meta?.description}
|
||||
className="min-w-0"
|
||||
placement="top-start"
|
||||
>
|
||||
<div
|
||||
class="mt-0.5 truncate text-[0.6875rem] leading-4 text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
{func?.meta?.description}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="flex flex-row gap-0.5 self-center">
|
||||
{#if shiftKey}
|
||||
<Tooltip content={$i18n.t('Delete')}>
|
||||
<button
|
||||
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"
|
||||
on:click={() => {
|
||||
deleteHandler(func);
|
||||
}}
|
||||
>
|
||||
<GarbageBin />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
{#if func?.meta?.manifest?.funding_url ?? false}
|
||||
<Tooltip content={$i18n.t('Support')}>
|
||||
<button
|
||||
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"
|
||||
on:click={() => {
|
||||
selectedFunction = func;
|
||||
showManifestModal = true;
|
||||
}}
|
||||
>
|
||||
<Heart />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content={$i18n.t('Valves')}>
|
||||
<button
|
||||
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"
|
||||
on:click={() => {
|
||||
selectedFunction = func;
|
||||
showValvesModal = true;
|
||||
}}
|
||||
>
|
||||
<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="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<FunctionMenu
|
||||
{func}
|
||||
editHandler={() => {
|
||||
goto(`/admin/functions/edit?id=${encodeURIComponent(func.id)}`);
|
||||
}}
|
||||
shareHandler={() => {
|
||||
shareHandler(func);
|
||||
}}
|
||||
cloneHandler={() => {
|
||||
cloneHandler(func);
|
||||
}}
|
||||
exportHandler={() => {
|
||||
exportHandler(func);
|
||||
}}
|
||||
deleteHandler={async () => {
|
||||
selectedFunction = func;
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
toggleGlobalHandler={() => {
|
||||
if (['filter', 'action'].includes(func.type)) {
|
||||
toggleGlobalHandler(func);
|
||||
}
|
||||
}}
|
||||
onClose={() => {}}
|
||||
<div
|
||||
class="hidden max-w-44 shrink-0 self-center truncate text-right text-[11px] leading-5 text-gray-500 dark:text-gray-500 md:block"
|
||||
>
|
||||
<Tooltip
|
||||
content={func?.user?.email ?? $i18n.t('Deleted User')}
|
||||
className="min-w-0"
|
||||
placement="top-start"
|
||||
>
|
||||
<button
|
||||
class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
type="button"
|
||||
>
|
||||
<EllipsisHorizontal className="size-5" />
|
||||
</button>
|
||||
</FunctionMenu>
|
||||
{/if}
|
||||
|
||||
<div class=" self-center mx-1">
|
||||
<Tooltip content={func.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
|
||||
<Switch
|
||||
bind:state={func.is_active}
|
||||
on:change={async (e) => {
|
||||
toggleFunctionById(localStorage.token, func.id);
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections &&
|
||||
($settings?.directConnections ?? null),
|
||||
false,
|
||||
true
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div class="truncate">
|
||||
{capitalizeFirstLetter(
|
||||
func?.user?.name ?? func?.user?.email ?? $i18n.t('Deleted User')
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="ml-2 flex shrink-0 flex-row items-center self-center">
|
||||
{#if shiftKey}
|
||||
<Tooltip content={$i18n.t('Delete')}>
|
||||
<button
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition dark:text-gray-500"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Delete')}
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
deleteHandler(func);
|
||||
}}
|
||||
>
|
||||
<GarbageBin className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
{#if func?.meta?.manifest?.funding_url ?? false}
|
||||
<Tooltip content={$i18n.t('Support')}>
|
||||
<button
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition dark:text-gray-500"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Support')}
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
selectedFunction = func;
|
||||
showManifestModal = true;
|
||||
}}
|
||||
>
|
||||
<Heart className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content={$i18n.t('Valves')}>
|
||||
<button
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition dark:text-gray-500"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Valves')}
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
selectedFunction = func;
|
||||
showValvesModal = true;
|
||||
}}
|
||||
>
|
||||
<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="M9.594 3.94c.09-.542.56-.940 1.11-.940h2.593c.55 0 1.02.398 1.11.940l.213 1.281c.063.374.313.686.645.870.074.040.147.083.220.127.325.196.720.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.370.490l1.296 2.247a1.125 1.125 0 0 1-.260 1.431l-1.003.827c-.293.241-.438.613-.430.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.750.430.991l1.004.827c.424.350.534.955.260 1.430l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.750-.072-1.076.124a6.47 6.47 0 0 1-.220.128c-.331.183-.581.495-.644.869l-.213 1.281c-.090.543-.560.940-1.110.940h-2.594c-.550 0-1.019-.398-1.110-.940l-.213-1.281c-.062-.374-.312-.686-.644-.870a6.52 6.52 0 0 1-.220-.127c-.325-.196-.720-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.490l-1.297-2.247a1.125 1.125 0 0 1 .260-1.431l1.004-.827c.292-.240.437-.613.430-.991a6.932 6.932 0 0 1 0-.255c.007-.380-.138-.751-.430-.992l-1.004-.827a1.125 1.125 0 0 1-.260-1.430l1.297-2.247a1.125 1.125 0 0 1 1.370-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.220-.128.332-.183.582-.495.644-.869l.214-1.280Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<div class="ml-0.5 flex shrink-0 flex-row items-center gap-1.5 self-center">
|
||||
<FunctionMenu
|
||||
{func}
|
||||
show={openFunctionMenuId === func.id}
|
||||
editHandler={() => {
|
||||
goto(`/admin/functions/edit?id=${encodeURIComponent(func.id)}`);
|
||||
}}
|
||||
shareHandler={() => {
|
||||
shareHandler(func);
|
||||
}}
|
||||
cloneHandler={() => {
|
||||
cloneHandler(func);
|
||||
}}
|
||||
exportHandler={() => {
|
||||
exportHandler(func);
|
||||
}}
|
||||
deleteHandler={async () => {
|
||||
selectedFunction = func;
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
toggleGlobalHandler={() => {
|
||||
if (['filter', 'action'].includes(func.type)) {
|
||||
toggleGlobalHandler(func);
|
||||
}
|
||||
}}
|
||||
onClose={() => {
|
||||
openFunctionMenuId = null;
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition dark:text-gray-500"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Function Menu')}
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openFunctionMenuId = openFunctionMenuId === func.id ? null : func.id;
|
||||
}}
|
||||
>
|
||||
<EllipsisHorizontal className="size-4" />
|
||||
</button>
|
||||
</FunctionMenu>
|
||||
|
||||
<button
|
||||
class="flex h-6 items-center"
|
||||
type="button"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
content={func.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}
|
||||
>
|
||||
<Switch
|
||||
bind:state={func.is_active}
|
||||
on:change={async () => {
|
||||
toggleFunctionById(localStorage.token, func.id);
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections &&
|
||||
($settings?.directConnections ?? null),
|
||||
false,
|
||||
true
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class=" w-full h-full flex flex-col justify-center items-center my-16 mb-24">
|
||||
|
||||
@@ -24,7 +24,12 @@
|
||||
|
||||
export let onClose: Function;
|
||||
|
||||
let show = false;
|
||||
export let show = false;
|
||||
|
||||
const closeMenu = () => {
|
||||
show = false;
|
||||
onClose();
|
||||
};
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
@@ -62,6 +67,7 @@
|
||||
class="select-none flex gap-2 items-center h-[1.6875rem] px-2 text-[13px] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl w-full"
|
||||
on:click={() => {
|
||||
editHandler();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -86,6 +92,7 @@
|
||||
class="select-none flex gap-2 items-center h-[1.6875rem] px-2 text-[13px] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl w-full"
|
||||
on:click={() => {
|
||||
shareHandler();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<Share />
|
||||
@@ -96,6 +103,7 @@
|
||||
class="select-none flex gap-2 items-center h-[1.6875rem] px-2 text-[13px] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl w-full"
|
||||
on:click={() => {
|
||||
cloneHandler();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<DocumentDuplicate />
|
||||
@@ -106,6 +114,7 @@
|
||||
class="select-none flex gap-2 items-center h-[1.6875rem] px-2 text-[13px] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl w-full"
|
||||
on:click={() => {
|
||||
exportHandler();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<Download />
|
||||
@@ -118,6 +127,7 @@
|
||||
class="select-none flex gap-2 items-center h-[1.6875rem] px-2 text-[13px] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl w-full"
|
||||
on:click={() => {
|
||||
deleteHandler();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<GarbageBin strokeWidth="2" />
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||
|
||||
import { WEBUI_NAME, knowledge, user, workspaceActions } from '$lib/stores';
|
||||
import { WEBUI_NAME, user, workspaceActions } from '$lib/stores';
|
||||
import {
|
||||
deleteKnowledgeById,
|
||||
searchKnowledgeBases,
|
||||
@@ -38,6 +38,7 @@
|
||||
name: string;
|
||||
description?: string;
|
||||
updated_at: number;
|
||||
file_count?: number;
|
||||
write_access?: boolean;
|
||||
meta?: any;
|
||||
user?: {
|
||||
@@ -192,6 +193,59 @@
|
||||
}
|
||||
};
|
||||
|
||||
const openKnowledge = (item: KnowledgeListItem) => {
|
||||
if (item?.meta?.document) {
|
||||
toast.error(
|
||||
$i18n.t(
|
||||
'Only collections can be edited, create a new knowledge base to edit/add documents.'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
goto(`/workspace/knowledge/${item.id}`);
|
||||
};
|
||||
|
||||
const shouldIgnoreRowClick = (target: EventTarget | null) => {
|
||||
return target instanceof Element && !!target.closest('button, a, input, [role="menu"]');
|
||||
};
|
||||
|
||||
const getKnowledgeMetaPreview = (item: KnowledgeListItem) => {
|
||||
const fileCount =
|
||||
item.file_count !== undefined
|
||||
? item.file_count === 1
|
||||
? $i18n.t('1 file')
|
||||
: $i18n.t('{{count}} files', { count: item.file_count })
|
||||
: null;
|
||||
|
||||
if (!item?.meta) return [fileCount, item.description].filter(Boolean).join(' · ');
|
||||
|
||||
if (item.meta.source === 'external') {
|
||||
return [
|
||||
fileCount,
|
||||
item.meta.external?.provider,
|
||||
item.meta.external?.source?.name,
|
||||
item.meta.external?.auth_mode,
|
||||
item.description
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ');
|
||||
}
|
||||
|
||||
const metadata = Object.entries(item.meta)
|
||||
.filter(([, value]) => value !== null && value !== undefined && value !== '')
|
||||
.map(([key, value]) => {
|
||||
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
||||
return `${key}: ${value}`;
|
||||
}
|
||||
|
||||
return key;
|
||||
})
|
||||
.join(' · ');
|
||||
|
||||
return [fileCount, metadata, item.description].filter(Boolean).join(' · ');
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
viewOption = localStorage?.workspaceViewOption || '';
|
||||
sourceOption = localStorage?.workspaceKnowledgeSourceOption || '';
|
||||
@@ -297,102 +351,124 @@
|
||||
|
||||
{#if items !== null && total !== null}
|
||||
{#if (items ?? []).length !== 0}
|
||||
<div class="my-1 grid grid-cols-1 gap-x-2 gap-y-0.5 lg:grid-cols-2">
|
||||
{#each items as item}
|
||||
<button
|
||||
class="flex w-full cursor-pointer rounded-xl px-2 py-1 text-left transition hover:bg-gray-50/60 dark:hover:bg-gray-850/40"
|
||||
on:click={() => {
|
||||
if (item?.meta?.document) {
|
||||
toast.error(
|
||||
$i18n.t(
|
||||
'Only collections can be edited, create a new knowledge base to edit/add documents.'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
goto(`/workspace/knowledge/${item.id}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="w-full min-w-0">
|
||||
<div class="flex-1 self-center justify-between">
|
||||
<div class="flex h-7 items-center justify-between">
|
||||
<div class=" flex gap-2 items-center justify-between w-full">
|
||||
{#if item?.meta?.source === 'external'}
|
||||
<div>
|
||||
<div class="my-1">
|
||||
<div
|
||||
class="flex w-full items-center gap-2 px-1.5 pb-0.5 text-xs text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
<div class="flex min-w-0 flex-1 items-center gap-1 py-0.5 text-left">
|
||||
{$i18n.t('Title')}
|
||||
</div>
|
||||
|
||||
<div class="hidden w-44 shrink-0 md:block"></div>
|
||||
|
||||
<div class="flex w-36 shrink-0 items-center justify-end gap-1 py-0.5 text-right">
|
||||
{$i18n.t('Updated at')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-y-0.5">
|
||||
{#each items as item}
|
||||
{@const metaPreview = getKnowledgeMetaPreview(item)}
|
||||
<div
|
||||
class="group flex min-h-8 w-full cursor-pointer items-center gap-2 overflow-hidden rounded-xl px-2 py-1 text-left"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click={(e) => {
|
||||
if (shouldIgnoreRowClick(e.target)) return;
|
||||
openKnowledge(item);
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (e.currentTarget !== e.target) return;
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
openKnowledge(item);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex min-w-0 flex-1 items-center gap-1 overflow-hidden">
|
||||
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<div class="flex min-w-0 items-center gap-2 overflow-hidden">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
|
||||
<Tooltip
|
||||
content={item?.description ?? item.name}
|
||||
className="min-w-0"
|
||||
placement="top-start"
|
||||
>
|
||||
<div
|
||||
class="truncate text-[13px] leading-5 text-gray-800 group-hover:underline dark:text-gray-200"
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{#if item?.meta?.source === 'external'}
|
||||
<Badge
|
||||
type="muted"
|
||||
content={item?.meta?.external?.provider ?? $i18n.t('Connected')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Badge type="muted" content={$i18n.t('Read Only')} />
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<Badge type="success" content={$i18n.t('Collection')} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if !item?.write_access && item?.meta?.source !== 'external'}
|
||||
<div>
|
||||
{#if !item?.write_access && item?.meta?.source !== 'external'}
|
||||
<Badge type="muted" content={$i18n.t('Read Only')} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if item?.write_access || $user?.role === 'admin'}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class=" flex self-center">
|
||||
<ItemMenu
|
||||
onExport={$user?.role === 'admin'
|
||||
? () => {
|
||||
exportHandler(item);
|
||||
}
|
||||
: null}
|
||||
on:delete={() => {
|
||||
selectedItem = item;
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-2 px-0.5">
|
||||
<Tooltip content={item?.description ?? item.name}>
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<div class="line-clamp-1 text-sm font-normal capitalize">{item.name}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<Tooltip content={dayjs(item.updated_at * 1000).format('LLLL')}>
|
||||
<div class=" text-xs text-gray-500 line-clamp-1 hidden sm:block">
|
||||
{$i18n.t('Updated')}
|
||||
{dayjs(item.updated_at * 1000).fromNow()}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<div class="text-xs text-gray-500 shrink-0">
|
||||
<Tooltip
|
||||
content={item?.user?.email ?? $i18n.t('Deleted User')}
|
||||
className="flex shrink-0"
|
||||
placement="top-start"
|
||||
>
|
||||
{$i18n.t('By {{name}}', {
|
||||
name: capitalizeFirstLetter(
|
||||
item?.user?.name ?? item?.user?.email ?? $i18n.t('Deleted User')
|
||||
)
|
||||
})}
|
||||
<Tooltip content={dayjs(item.updated_at * 1000).format('LLLL')}>
|
||||
<div
|
||||
class="shrink-0 truncate text-[11px] leading-5 text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
{dayjs(item.updated_at * 1000).fromNow()}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if metaPreview}
|
||||
<Tooltip content={metaPreview} className="min-w-0" placement="top-start">
|
||||
<div
|
||||
class="mt-0.5 truncate text-[0.6875rem] leading-4 text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
{metaPreview}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="hidden max-w-44 shrink-0 self-center truncate text-right text-[11px] leading-5 text-gray-500 dark:text-gray-500 md:block"
|
||||
>
|
||||
<Tooltip
|
||||
content={item?.user?.email ?? $i18n.t('Deleted User')}
|
||||
className="min-w-0"
|
||||
placement="top-start"
|
||||
>
|
||||
<div class="truncate">
|
||||
{capitalizeFirstLetter(
|
||||
item?.user?.name ?? item?.user?.email ?? $i18n.t('Deleted User')
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{#if item?.write_access || $user?.role === 'admin'}
|
||||
<div class="ml-2 flex shrink-0 flex-row items-center self-center">
|
||||
<ItemMenu
|
||||
onExport={$user?.role === 'admin'
|
||||
? () => {
|
||||
exportHandler(item);
|
||||
}
|
||||
: null}
|
||||
on:delete={() => {
|
||||
selectedItem = item;
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !allItemsLoaded}
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
export let onClose: Function = () => {};
|
||||
|
||||
let show = false;
|
||||
|
||||
const closeMenu = () => {
|
||||
show = false;
|
||||
onClose();
|
||||
};
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
@@ -29,12 +34,13 @@
|
||||
<Tooltip content={$i18n.t('More')}>
|
||||
<slot
|
||||
><button
|
||||
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition dark:text-gray-500"
|
||||
type="button"
|
||||
aria-label={$i18n.t('More Options')}
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
show = true;
|
||||
show = !show;
|
||||
}}
|
||||
>
|
||||
<EllipsisHorizontal className="size-4" />
|
||||
@@ -49,6 +55,7 @@
|
||||
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] hover:text-gray-900 dark:hover:text-gray-100"
|
||||
on:click={() => {
|
||||
onExport();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<Download className="size-3.5" />
|
||||
@@ -60,6 +67,7 @@
|
||||
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] hover:text-gray-900 dark:hover:text-gray-100"
|
||||
on:click={() => {
|
||||
dispatch('delete');
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<GarbageBin className="size-3.5" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { marked } from 'marked';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { onMount, getContext, tick } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
const i18n = getContext('i18n');
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
import {
|
||||
WEBUI_NAME,
|
||||
@@ -20,7 +21,7 @@
|
||||
user,
|
||||
workspaceActions
|
||||
} from '$lib/stores';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import {
|
||||
createNewModel,
|
||||
deleteModelById,
|
||||
@@ -50,6 +51,7 @@
|
||||
import EyeSlash from '../icons/EyeSlash.svelte';
|
||||
import Eye from '../icons/Eye.svelte';
|
||||
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||
import ChevronUp from '../icons/ChevronUp.svelte';
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import DropdownMenu from '$lib/components/common/DropdownMenu.svelte';
|
||||
@@ -78,6 +80,8 @@
|
||||
|
||||
let query = '';
|
||||
let viewOption = '';
|
||||
let sortKey = 'updated_at';
|
||||
let sortDirection = 'desc';
|
||||
|
||||
let page = 1;
|
||||
let models = null;
|
||||
@@ -109,10 +113,36 @@
|
||||
]);
|
||||
}
|
||||
|
||||
$: if (loaded && page !== undefined && selectedTag !== undefined && viewOption !== undefined) {
|
||||
$: if (
|
||||
loaded &&
|
||||
page !== undefined &&
|
||||
selectedTag !== undefined &&
|
||||
viewOption !== undefined &&
|
||||
sortKey !== undefined &&
|
||||
sortDirection !== undefined
|
||||
) {
|
||||
getModelList();
|
||||
}
|
||||
|
||||
const setSortKey = (key: string) => {
|
||||
if (sortKey === key) {
|
||||
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortKey = key;
|
||||
sortDirection = key === 'updated_at' ? 'desc' : 'asc';
|
||||
}
|
||||
};
|
||||
|
||||
const openModel = (model) => {
|
||||
if (model.write_access) {
|
||||
goto(`/workspace/models/edit?id=${encodeURIComponent(model.id)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const shouldIgnoreRowClick = (target: EventTarget | null) => {
|
||||
return target instanceof Element && !!target.closest('button, a, input, [role="menu"]');
|
||||
};
|
||||
|
||||
const getModelList = async () => {
|
||||
if (!loaded) return;
|
||||
|
||||
@@ -122,8 +152,8 @@
|
||||
query,
|
||||
viewOption,
|
||||
selectedTag,
|
||||
null,
|
||||
null,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
page
|
||||
).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
@@ -601,216 +631,254 @@
|
||||
|
||||
{#if models !== null}
|
||||
{#if (models ?? []).length !== 0}
|
||||
<div class="my-1 grid gap-x-2 gap-y-0.5 lg:grid-cols-2" id="model-list">
|
||||
{#each models as model (model.id)}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="flex w-full rounded-xl px-2 py-1 transition {model.write_access
|
||||
? 'cursor-pointer hover:bg-gray-50/60 dark:hover:bg-gray-850/40'
|
||||
: ''}"
|
||||
id="model-item-{model.id}"
|
||||
on:click={() => {
|
||||
if (model.write_access) {
|
||||
goto(`/workspace/models/edit?id=${encodeURIComponent(model.id)}`);
|
||||
}
|
||||
}}
|
||||
<div class="my-1" id="model-list">
|
||||
<div
|
||||
class="flex w-full items-center gap-2 px-1.5 pb-0.5 text-xs text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
<button
|
||||
class="flex min-w-0 flex-1 items-center gap-1 py-0.5 text-left"
|
||||
type="button"
|
||||
on:click={() => setSortKey('name')}
|
||||
>
|
||||
<div class="flex group/item min-w-0 gap-2.5 w-full">
|
||||
<div class="self-center">
|
||||
<div class="flex">
|
||||
<div
|
||||
class="{model.is_active
|
||||
? ''
|
||||
: 'opacity-50 dark:opacity-50'} bg-transparent rounded-xl"
|
||||
>
|
||||
<img
|
||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
|
||||
alt="modelfile profile"
|
||||
class="size-8 rounded-xl object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
on:error={(e) => {
|
||||
e.target.src = '/favicon.png';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{$i18n.t('Title')}
|
||||
{#if sortKey === 'name'}
|
||||
{#if sortDirection === 'asc'}
|
||||
<ChevronUp className="size-2" />
|
||||
{:else}
|
||||
<ChevronDown className="size-2" />
|
||||
{/if}
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<div class="hidden w-44 shrink-0 md:block"></div>
|
||||
|
||||
<button
|
||||
class="flex w-36 shrink-0 items-center justify-end gap-1 py-0.5 text-right"
|
||||
type="button"
|
||||
on:click={() => setSortKey('updated_at')}
|
||||
>
|
||||
{$i18n.t('Updated at')}
|
||||
{#if sortKey === 'updated_at'}
|
||||
{#if sortDirection === 'asc'}
|
||||
<ChevronUp className="size-2" />
|
||||
{:else}
|
||||
<ChevronDown className="size-2" />
|
||||
{/if}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-y-0.5">
|
||||
{#each models as model (model.id)}
|
||||
<div
|
||||
class="group flex min-h-8 w-full items-center gap-2 overflow-hidden rounded-xl px-2 py-1 text-left {model.write_access
|
||||
? 'cursor-pointer'
|
||||
: ''}"
|
||||
id="model-item-{model.id}"
|
||||
role="button"
|
||||
tabindex={model.write_access ? 0 : -1}
|
||||
on:click={(e) => {
|
||||
if (shouldIgnoreRowClick(e.target)) return;
|
||||
openModel(model);
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (e.currentTarget !== e.target) return;
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
openModel(model);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="mr-1 shrink-0 self-center">
|
||||
<div class="{model.is_active ? '' : 'opacity-50 dark:opacity-50'} bg-transparent">
|
||||
<img
|
||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
|
||||
alt=""
|
||||
class="size-7 rounded-lg object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
on:error={(e) => {
|
||||
e.target.src = '/favicon.png';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex min-w-0 w-full flex-1 self-center pr-1">
|
||||
<div class="flex h-full w-full flex-1 flex-col justify-start self-center group">
|
||||
<div class="min-w-0 flex-1 w-full">
|
||||
<div class="flex min-w-0 items-center justify-between w-full gap-2">
|
||||
<Tooltip
|
||||
content={model.name}
|
||||
className="min-w-0 flex-1"
|
||||
placement="top-start"
|
||||
>
|
||||
<a
|
||||
class="line-clamp-1 text-sm font-normal capitalize hover:underline"
|
||||
href={`/?models=${encodeURIComponent(model.id)}`}
|
||||
<div class="flex min-w-0 flex-1 items-center gap-1 overflow-hidden">
|
||||
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<div class="flex min-w-0 items-center gap-2 overflow-hidden">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
|
||||
<Tooltip content={model.name} className="min-w-0" placement="top-start">
|
||||
<div
|
||||
class="truncate text-[13px] leading-5 text-gray-800 group-hover:underline dark:text-gray-200"
|
||||
>
|
||||
{model.name}
|
||||
</a>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
{#if !model.write_access}
|
||||
<div>
|
||||
<Badge type="muted" content={$i18n.t('Read Only')} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex {model.is_active ? '' : 'text-gray-500'}">
|
||||
<div class="flex items-center gap-0.5">
|
||||
{#if shiftKey && model.write_access}
|
||||
<Tooltip
|
||||
content={model?.meta?.hidden ? $i18n.t('Show') : $i18n.t('Hide')}
|
||||
>
|
||||
<button
|
||||
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-white dark:hover:bg-white/5"
|
||||
type="button"
|
||||
aria-label={model?.meta?.hidden
|
||||
? $i18n.t('Show')
|
||||
: $i18n.t('Hide')}
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
hideModelHandler(model);
|
||||
}}
|
||||
>
|
||||
{#if model?.meta?.hidden}
|
||||
<EyeSlash />
|
||||
{:else}
|
||||
<Eye />
|
||||
{/if}
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={$i18n.t('Delete')}>
|
||||
<button
|
||||
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-white dark:hover:bg-white/5"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Delete')}
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteModelHandler(model);
|
||||
}}
|
||||
>
|
||||
<GarbageBin />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<ModelMenu
|
||||
user={$user}
|
||||
{model}
|
||||
writeAccess={model.write_access}
|
||||
editHandler={() => {
|
||||
goto(
|
||||
`/workspace/models/edit?id=${encodeURIComponent(model.id)}`
|
||||
);
|
||||
}}
|
||||
shareHandler={() => {
|
||||
shareModelHandler(model);
|
||||
}}
|
||||
cloneHandler={() => {
|
||||
cloneModelHandler(model);
|
||||
}}
|
||||
exportHandler={() => {
|
||||
exportModelHandler(model);
|
||||
}}
|
||||
hideHandler={() => {
|
||||
hideModelHandler(model);
|
||||
}}
|
||||
pinModelHandler={() => {
|
||||
pinModelHandler(model.id);
|
||||
}}
|
||||
copyLinkHandler={() => {
|
||||
copyLinkHandler(model);
|
||||
}}
|
||||
deleteHandler={() => {
|
||||
selectedModel = model;
|
||||
showModelDeleteConfirm = true;
|
||||
}}
|
||||
onClose={() => {}}
|
||||
>
|
||||
<div
|
||||
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-white dark:hover:bg-white/5"
|
||||
>
|
||||
<EllipsisHorizontal className="size-4" />
|
||||
</div>
|
||||
</ModelMenu>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if model.write_access}
|
||||
<button
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
content={model.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}
|
||||
>
|
||||
<Switch
|
||||
bind:state={model.is_active}
|
||||
on:change={async () => {
|
||||
toggleModelById(localStorage.token, model.id);
|
||||
_models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections &&
|
||||
($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</button>
|
||||
{/if}
|
||||
<div
|
||||
class="min-w-0 max-w-[40%] shrink-0 truncate text-[11px] leading-5 text-gray-500"
|
||||
>
|
||||
{model.id}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex min-w-0 items-center gap-1 pr-2 text-xs text-gray-500">
|
||||
<Tooltip
|
||||
content={model?.user?.email ?? $i18n.t('Deleted User')}
|
||||
className="flex shrink-0"
|
||||
placement="top-start"
|
||||
>
|
||||
<div class="shrink-0">
|
||||
{$i18n.t('By {{name}}', {
|
||||
name: capitalizeFirstLetter(
|
||||
model?.user?.name ?? model?.user?.email ?? $i18n.t('Deleted User')
|
||||
)
|
||||
})}
|
||||
<Tooltip content={dayjs(model.updated_at * 1000).format('LLLL')}>
|
||||
<div
|
||||
class="shrink-0 truncate text-[11px] leading-5 text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
{dayjs(model.updated_at * 1000).fromNow()}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<div class="shrink-0">·</div>
|
||||
|
||||
<Tooltip
|
||||
content={marked.parse(model?.meta?.description ?? model.id)}
|
||||
className="min-w-0 text-left"
|
||||
placement="top-start"
|
||||
>
|
||||
<div class="flex min-w-0 gap-1 overflow-hidden">
|
||||
<div class="line-clamp-1 min-w-0">
|
||||
{#if (model?.meta?.description ?? '').trim()}
|
||||
{model?.meta?.description}
|
||||
{:else}
|
||||
{model.id}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{#if !model.write_access}
|
||||
<Badge type="muted" content={$i18n.t('Read Only')} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tooltip
|
||||
content={(model?.meta?.description ?? '').trim() ||
|
||||
model.base_model_id ||
|
||||
$i18n.t('No description')}
|
||||
className="min-w-0"
|
||||
placement="top-start"
|
||||
>
|
||||
<div
|
||||
class="truncate text-[0.6875rem] leading-4 text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
{(model?.meta?.description ?? '').trim() ||
|
||||
model.base_model_id ||
|
||||
$i18n.t('No description')}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="hidden max-w-44 shrink-0 self-center truncate text-right text-[11px] leading-5 text-gray-500 dark:text-gray-500 md:block"
|
||||
>
|
||||
<Tooltip
|
||||
content={model?.user?.email ?? $i18n.t('Deleted User')}
|
||||
className="min-w-0"
|
||||
placement="top-start"
|
||||
>
|
||||
<div class="truncate">
|
||||
{capitalizeFirstLetter(
|
||||
model?.user?.name ?? model?.user?.email ?? $i18n.t('Deleted User')
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="ml-2 flex shrink-0 flex-row items-center self-center">
|
||||
{#if shiftKey && model.write_access}
|
||||
<Tooltip content={model?.meta?.hidden ? $i18n.t('Show') : $i18n.t('Hide')}>
|
||||
<button
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition dark:text-gray-500"
|
||||
type="button"
|
||||
aria-label={model?.meta?.hidden ? $i18n.t('Show') : $i18n.t('Hide')}
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
hideModelHandler(model);
|
||||
}}
|
||||
>
|
||||
{#if model?.meta?.hidden}
|
||||
<EyeSlash className="size-4" />
|
||||
{:else}
|
||||
<Eye className="size-4" />
|
||||
{/if}
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={$i18n.t('Delete')}>
|
||||
<button
|
||||
class="ml-0.5 flex size-6 items-center justify-center rounded-lg text-gray-400 transition dark:text-gray-500"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Delete')}
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
deleteModelHandler(model);
|
||||
}}
|
||||
>
|
||||
<GarbageBin className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<div class="flex shrink-0 flex-row items-center gap-1.5 self-center">
|
||||
<ModelMenu
|
||||
user={$user}
|
||||
{model}
|
||||
writeAccess={model.write_access}
|
||||
editHandler={() => {
|
||||
goto(`/workspace/models/edit?id=${encodeURIComponent(model.id)}`);
|
||||
}}
|
||||
shareHandler={() => {
|
||||
shareModelHandler(model);
|
||||
}}
|
||||
cloneHandler={() => {
|
||||
cloneModelHandler(model);
|
||||
}}
|
||||
exportHandler={() => {
|
||||
exportModelHandler(model);
|
||||
}}
|
||||
hideHandler={() => {
|
||||
hideModelHandler(model);
|
||||
}}
|
||||
pinModelHandler={() => {
|
||||
pinModelHandler(model.id);
|
||||
}}
|
||||
copyLinkHandler={() => {
|
||||
copyLinkHandler(model);
|
||||
}}
|
||||
deleteHandler={() => {
|
||||
selectedModel = model;
|
||||
showModelDeleteConfirm = true;
|
||||
}}
|
||||
onClose={() => {}}
|
||||
>
|
||||
<div
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition dark:text-gray-500"
|
||||
>
|
||||
<EllipsisHorizontal className="size-4" />
|
||||
</div>
|
||||
</ModelMenu>
|
||||
|
||||
{#if model.write_access}
|
||||
<button
|
||||
class="flex h-6 items-center"
|
||||
type="button"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
content={model.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}
|
||||
>
|
||||
<Switch
|
||||
bind:state={model.is_active}
|
||||
on:change={async () => {
|
||||
toggleModelById(localStorage.token, model.id);
|
||||
_models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections &&
|
||||
($settings?.directConnections ?? null)
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if total > 30}
|
||||
|
||||
@@ -504,7 +504,7 @@
|
||||
{:else if (prompts ?? []).length !== 0}
|
||||
<div class="my-1">
|
||||
<div
|
||||
class="flex w-full items-center gap-2 px-2 pb-0.5 text-xs text-gray-400 dark:text-gray-600"
|
||||
class="flex w-full items-center gap-2 px-1.5 pb-0.5 text-xs text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
<button
|
||||
class="flex min-w-0 flex-1 items-center gap-1 py-0.5 text-left"
|
||||
@@ -575,39 +575,21 @@
|
||||
/{prompt.command}
|
||||
</div>
|
||||
|
||||
{#if !prompt.write_access}
|
||||
<Badge type="muted" content={$i18n.t('Read Only')} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="hidden max-w-44 shrink-0 truncate text-right text-[11px] leading-5 text-gray-500 dark:text-gray-500 md:block"
|
||||
>
|
||||
<Tooltip
|
||||
content={prompt?.user?.email ?? $i18n.t('Deleted User')}
|
||||
className="min-w-0"
|
||||
placement="top-start"
|
||||
>
|
||||
<div class="truncate">
|
||||
{capitalizeFirstLetter(
|
||||
prompt?.user?.name ?? prompt?.user?.email ?? $i18n.t('Deleted User')
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0">
|
||||
<Tooltip
|
||||
content={dayjs((prompt.updated_at ?? prompt.created_at) * 1000).format(
|
||||
'LLLL'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
class="min-w-0 truncate text-right text-xs text-gray-400 dark:text-gray-600"
|
||||
class="shrink-0 truncate text-[11px] leading-5 text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
{dayjs((prompt.updated_at ?? prompt.created_at) * 1000).fromNow()}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{#if !prompt.write_access}
|
||||
<Badge type="muted" content={$i18n.t('Read Only')} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -622,6 +604,23 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="hidden max-w-44 shrink-0 self-center truncate text-right text-[11px] leading-5 text-gray-500 dark:text-gray-500 md:block"
|
||||
>
|
||||
<Tooltip
|
||||
content={prompt?.user?.email ?? $i18n.t('Deleted User')}
|
||||
className="min-w-0"
|
||||
placement="top-start"
|
||||
>
|
||||
<div class="truncate">
|
||||
{capitalizeFirstLetter(
|
||||
prompt?.user?.name ?? prompt?.user?.email ?? $i18n.t('Deleted User')
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="ml-2 flex shrink-0 flex-row items-center self-center">
|
||||
{#if shiftKey}
|
||||
<Tooltip content={$i18n.t('Delete')}>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<script lang="ts">
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
import { onMount, getContext, tick, onDestroy } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -33,6 +37,8 @@
|
||||
import Switch from '../common/Switch.svelte';
|
||||
import SkillMenu from './Skills/SkillMenu.svelte';
|
||||
import Pagination from '../common/Pagination.svelte';
|
||||
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||
import ChevronUp from '../icons/ChevronUp.svelte';
|
||||
|
||||
let shiftKey = false;
|
||||
let loaded = false;
|
||||
@@ -52,6 +58,9 @@
|
||||
|
||||
let tagsContainerElement: HTMLDivElement;
|
||||
let viewOption = '';
|
||||
let sortKey = 'updated_at';
|
||||
let sortDirection = 'desc';
|
||||
let openSkillMenuId: string | null = null;
|
||||
let page = 1;
|
||||
|
||||
$: if (loaded) {
|
||||
@@ -93,12 +102,17 @@
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
const res = await getSkillItems(localStorage.token, query, viewOption, page).catch(
|
||||
(error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
const res = await getSkillItems(
|
||||
localStorage.token,
|
||||
query,
|
||||
viewOption,
|
||||
page,
|
||||
sortKey,
|
||||
sortDirection
|
||||
).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
filteredItems = res.items;
|
||||
@@ -124,10 +138,33 @@
|
||||
};
|
||||
|
||||
// Immediate response to page/filter changes
|
||||
$: if (loaded && page && viewOption !== undefined) {
|
||||
$: if (
|
||||
loaded &&
|
||||
page &&
|
||||
viewOption !== undefined &&
|
||||
sortKey !== undefined &&
|
||||
sortDirection !== undefined
|
||||
) {
|
||||
loadSkillItems();
|
||||
}
|
||||
|
||||
const setSortKey = (key: string) => {
|
||||
if (sortKey === key) {
|
||||
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortKey = key;
|
||||
sortDirection = key === 'updated_at' ? 'desc' : 'asc';
|
||||
}
|
||||
};
|
||||
|
||||
const openSkill = (skill) => {
|
||||
goto(`/workspace/skills/edit?id=${encodeURIComponent(skill.id)}`);
|
||||
};
|
||||
|
||||
const shouldIgnoreRowClick = (target: EventTarget | null) => {
|
||||
return target instanceof Element && !!target.closest('button, a, input, [role="menu"]');
|
||||
};
|
||||
|
||||
const cloneHandler = async (skill) => {
|
||||
const _skill = await getSkillById(localStorage.token, skill.id).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
@@ -343,141 +380,207 @@
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{:else if (filteredItems ?? []).length !== 0}
|
||||
<div class="my-1 grid gap-x-2 gap-y-0.5 lg:grid-cols-2">
|
||||
{#each filteredItems as skill}
|
||||
<Tooltip content={skill?.description ?? skill?.id}>
|
||||
<div class="my-1">
|
||||
<div
|
||||
class="flex w-full items-center gap-2 px-1.5 pb-0.5 text-xs text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
<button
|
||||
class="flex min-w-0 flex-1 items-center gap-1 py-0.5 text-left"
|
||||
type="button"
|
||||
on:click={() => setSortKey('name')}
|
||||
>
|
||||
{$i18n.t('Title')}
|
||||
{#if sortKey === 'name'}
|
||||
{#if sortDirection === 'asc'}
|
||||
<ChevronUp className="size-2" />
|
||||
{:else}
|
||||
<ChevronDown className="size-2" />
|
||||
{/if}
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<div class="hidden w-44 shrink-0 md:block"></div>
|
||||
|
||||
<button
|
||||
class="flex w-36 shrink-0 items-center justify-end gap-1 py-0.5 text-right"
|
||||
type="button"
|
||||
on:click={() => setSortKey('updated_at')}
|
||||
>
|
||||
{$i18n.t('Updated at')}
|
||||
{#if sortKey === 'updated_at'}
|
||||
{#if sortDirection === 'asc'}
|
||||
<ChevronUp className="size-2" />
|
||||
{:else}
|
||||
<ChevronDown className="size-2" />
|
||||
{/if}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-y-0.5">
|
||||
{#each filteredItems as skill (skill.id)}
|
||||
<div
|
||||
class="flex w-full rounded-xl px-2 py-1 text-left transition {skill.write_access
|
||||
? 'cursor-pointer hover:bg-gray-50/60 dark:hover:bg-gray-850/40'
|
||||
: 'cursor-not-allowed opacity-60'}"
|
||||
class="group flex min-h-8 w-full cursor-pointer items-center gap-2 overflow-hidden rounded-xl px-2 py-1 text-left"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click={(e) => {
|
||||
if (shouldIgnoreRowClick(e.target)) return;
|
||||
openSkill(skill);
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (e.currentTarget !== e.target) return;
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
openSkill(skill);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if skill.write_access}
|
||||
<a
|
||||
class="flex w-full min-w-0 flex-1 cursor-pointer"
|
||||
href={`/workspace/skills/edit?id=${encodeURIComponent(skill.id)}`}
|
||||
>
|
||||
<div class="flex min-w-0 items-center text-left">
|
||||
<div class="min-w-0 flex-1 self-center">
|
||||
<Tooltip content={skill.id} placement="top-start">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<div class="line-clamp-1 text-sm">
|
||||
{skill.name}
|
||||
</div>
|
||||
{#if !skill.is_active}
|
||||
<Badge type="muted" content={$i18n.t('Inactive')} />
|
||||
{/if}
|
||||
<div class="flex min-w-0 flex-1 items-center gap-1 overflow-hidden">
|
||||
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<div class="flex min-w-0 items-center gap-2 overflow-hidden">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
|
||||
<Tooltip content={skill.id} className="min-w-0" placement="top-start">
|
||||
<div
|
||||
class="truncate text-[13px] leading-5 text-gray-800 group-hover:underline dark:text-gray-200"
|
||||
>
|
||||
{skill.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="px-0.5">
|
||||
<div class="shrink-0 text-xs text-gray-500">
|
||||
<Tooltip
|
||||
content={skill?.user?.email ?? $i18n.t('Deleted User')}
|
||||
className="flex shrink-0"
|
||||
placement="top-start"
|
||||
>
|
||||
{$i18n.t('By {{name}}', {
|
||||
name: capitalizeFirstLetter(
|
||||
skill?.user?.name ?? skill?.user?.email ?? $i18n.t('Deleted User')
|
||||
)
|
||||
})}
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="min-w-0 max-w-[40%] shrink-0 truncate text-[11px] leading-5 text-gray-500"
|
||||
>
|
||||
/{skill.id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{:else}
|
||||
<div class="flex w-full min-w-0 flex-1">
|
||||
<div class="flex w-full min-w-0 items-center text-left">
|
||||
<div class="w-full min-w-0 flex-1 self-center">
|
||||
<div class="flex items-center justify-between w-full gap-2">
|
||||
<Tooltip content={skill.id} placement="top-start">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<div class="line-clamp-1 text-sm">
|
||||
{skill.name}
|
||||
</div>
|
||||
{#if !skill.is_active}
|
||||
<Badge type="muted" content={$i18n.t('Inactive')} />
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
content={dayjs((skill.updated_at ?? skill.created_at) * 1000).format(
|
||||
'LLLL'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
class="shrink-0 truncate text-[11px] leading-5 text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
{dayjs((skill.updated_at ?? skill.created_at) * 1000).fromNow()}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{#if !skill.is_active}
|
||||
<Badge type="muted" content={$i18n.t('Inactive')} />
|
||||
{/if}
|
||||
|
||||
{#if !skill.write_access}
|
||||
<Badge type="muted" content={$i18n.t('Read Only')} />
|
||||
</div>
|
||||
<div class="px-0.5">
|
||||
<div class="shrink-0 text-xs text-gray-500">
|
||||
<Tooltip
|
||||
content={skill?.user?.email ?? $i18n.t('Deleted User')}
|
||||
className="flex shrink-0"
|
||||
placement="top-start"
|
||||
>
|
||||
{$i18n.t('By {{name}}', {
|
||||
name: capitalizeFirstLetter(
|
||||
skill?.user?.name ?? skill?.user?.email ?? $i18n.t('Deleted User')
|
||||
)
|
||||
})}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if skill.description}
|
||||
<Tooltip content={skill.description} className="min-w-0" placement="top-start">
|
||||
<div
|
||||
class="mt-0.5 truncate text-[0.6875rem] leading-4 text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
{skill.description}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="hidden max-w-44 shrink-0 self-center truncate text-right text-[11px] leading-5 text-gray-500 dark:text-gray-500 md:block"
|
||||
>
|
||||
<Tooltip
|
||||
content={skill?.user?.email ?? $i18n.t('Deleted User')}
|
||||
className="min-w-0"
|
||||
placement="top-start"
|
||||
>
|
||||
<div class="truncate">
|
||||
{capitalizeFirstLetter(
|
||||
skill?.user?.name ?? skill?.user?.email ?? $i18n.t('Deleted User')
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{#if skill.write_access}
|
||||
<div class="flex flex-row gap-0.5 self-center">
|
||||
<div class="ml-2 flex shrink-0 flex-row items-center self-center">
|
||||
{#if shiftKey}
|
||||
<Tooltip content={$i18n.t('Delete')}>
|
||||
<button
|
||||
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition dark:text-gray-500"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Delete')}
|
||||
on:click={() => {
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
deleteHandler(skill);
|
||||
}}
|
||||
>
|
||||
<GarbageBin />
|
||||
<GarbageBin className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<SkillMenu
|
||||
editHandler={() => {
|
||||
goto(`/workspace/skills/edit?id=${encodeURIComponent(skill.id)}`);
|
||||
}}
|
||||
cloneHandler={() => {
|
||||
cloneHandler(skill);
|
||||
}}
|
||||
exportHandler={() => {
|
||||
exportHandler(skill);
|
||||
}}
|
||||
deleteHandler={async () => {
|
||||
selectedSkill = skill;
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
onClose={() => {}}
|
||||
>
|
||||
<button
|
||||
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
type="button"
|
||||
>
|
||||
<EllipsisHorizontal className="size-4" />
|
||||
</button>
|
||||
</SkillMenu>
|
||||
{/if}
|
||||
|
||||
<button on:click|stopPropagation|preventDefault>
|
||||
<Tooltip content={skill.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
|
||||
<Switch
|
||||
bind:state={skill.is_active}
|
||||
on:change={async () => {
|
||||
toggleSkillById(localStorage.token, skill.id);
|
||||
<div class="flex shrink-0 flex-row items-center gap-1.5 self-center">
|
||||
<SkillMenu
|
||||
show={openSkillMenuId === skill.id}
|
||||
editHandler={() => {
|
||||
goto(`/workspace/skills/edit?id=${encodeURIComponent(skill.id)}`);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</button>
|
||||
cloneHandler={() => {
|
||||
cloneHandler(skill);
|
||||
}}
|
||||
exportHandler={() => {
|
||||
exportHandler(skill);
|
||||
}}
|
||||
deleteHandler={async () => {
|
||||
selectedSkill = skill;
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
onClose={() => {
|
||||
openSkillMenuId = null;
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition dark:text-gray-500"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Skill Menu')}
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openSkillMenuId = openSkillMenuId === skill.id ? null : skill.id;
|
||||
}}
|
||||
>
|
||||
<EllipsisHorizontal className="size-4" />
|
||||
</button>
|
||||
</SkillMenu>
|
||||
|
||||
<button
|
||||
class="flex h-6 items-center"
|
||||
type="button"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
content={skill.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}
|
||||
>
|
||||
<Switch
|
||||
bind:state={skill.is_active}
|
||||
on:change={async () => {
|
||||
toggleSkillById(localStorage.token, skill.id);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if total > 30}
|
||||
|
||||
@@ -17,7 +17,12 @@
|
||||
export let deleteHandler: Function;
|
||||
export let onClose: Function;
|
||||
|
||||
let show = false;
|
||||
export let show = false;
|
||||
|
||||
const closeMenu = () => {
|
||||
show = false;
|
||||
onClose();
|
||||
};
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
@@ -38,6 +43,7 @@
|
||||
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] hover:text-gray-900 dark:hover:text-gray-100"
|
||||
on:click={() => {
|
||||
editHandler();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -62,6 +68,7 @@
|
||||
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] hover:text-gray-900 dark:hover:text-gray-100"
|
||||
on:click={() => {
|
||||
cloneHandler();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<DocumentDuplicate className="size-3.5" />
|
||||
@@ -73,6 +80,7 @@
|
||||
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] hover:text-gray-900 dark:hover:text-gray-100"
|
||||
on:click={() => {
|
||||
exportHandler();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<Download className="size-3.5" />
|
||||
@@ -86,6 +94,7 @@
|
||||
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] hover:text-gray-900 dark:hover:text-gray-100"
|
||||
on:click={() => {
|
||||
deleteHandler();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<GarbageBin className="size-3.5" />
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<script lang="ts">
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
import { onMount, getContext, tick, onDestroy } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -36,6 +40,8 @@
|
||||
import ViewSelector from './common/ViewSelector.svelte';
|
||||
import CommunityDiscover from './common/CommunityDiscover.svelte';
|
||||
import Badge from '$lib/components/common/Badge.svelte';
|
||||
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||
import ChevronUp from '../icons/ChevronUp.svelte';
|
||||
|
||||
let shiftKey = false;
|
||||
let loaded = false;
|
||||
@@ -58,6 +64,9 @@
|
||||
|
||||
let tagsContainerElement: HTMLDivElement;
|
||||
let viewOption = '';
|
||||
let sortKey = 'updated_at';
|
||||
let sortDirection = 'desc';
|
||||
let openToolMenuId: string | null = null;
|
||||
|
||||
let showImportModal = false;
|
||||
|
||||
@@ -110,12 +119,17 @@
|
||||
}, 300);
|
||||
};
|
||||
|
||||
$: if (tools && viewOption !== undefined) {
|
||||
$: if (
|
||||
tools &&
|
||||
viewOption !== undefined &&
|
||||
sortKey !== undefined &&
|
||||
sortDirection !== undefined
|
||||
) {
|
||||
setFilteredItems();
|
||||
}
|
||||
|
||||
const setFilteredItems = () => {
|
||||
filteredItems = tools.filter((t) => {
|
||||
const filtered = tools.filter((t) => {
|
||||
if (query === '' && viewOption === '') return true;
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return (
|
||||
@@ -128,6 +142,33 @@
|
||||
(viewOption === 'shared' && t.user_id !== $user?.id))
|
||||
);
|
||||
});
|
||||
|
||||
filteredItems = [...filtered].sort((a, b) => {
|
||||
const direction = sortDirection === 'asc' ? 1 : -1;
|
||||
|
||||
if (sortKey === 'name') {
|
||||
return direction * (a.name ?? '').localeCompare(b.name ?? '');
|
||||
}
|
||||
|
||||
return direction * ((a.updated_at ?? 0) - (b.updated_at ?? 0));
|
||||
});
|
||||
};
|
||||
|
||||
const setSortKey = (key: string) => {
|
||||
if (sortKey === key) {
|
||||
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortKey = key;
|
||||
sortDirection = key === 'updated_at' ? 'desc' : 'asc';
|
||||
}
|
||||
};
|
||||
|
||||
const openTool = (tool) => {
|
||||
goto(`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`);
|
||||
};
|
||||
|
||||
const shouldIgnoreRowClick = (target: EventTarget | null) => {
|
||||
return target instanceof Element && !!target.closest('button, a, input, [role="menu"]');
|
||||
};
|
||||
|
||||
const shareHandler = async (tool) => {
|
||||
@@ -329,127 +370,173 @@
|
||||
</div>
|
||||
|
||||
{#if (filteredItems ?? []).length !== 0}
|
||||
<div class="my-1 grid gap-x-2 gap-y-0.5 lg:grid-cols-2">
|
||||
{#each filteredItems as tool}
|
||||
<Tooltip content={tool?.meta?.description ?? tool?.id}>
|
||||
<div class="my-1">
|
||||
<div
|
||||
class="flex w-full items-center gap-2 px-1.5 pb-0.5 text-xs text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
<button
|
||||
class="flex min-w-0 flex-1 items-center gap-1 py-0.5 text-left"
|
||||
type="button"
|
||||
on:click={() => setSortKey('name')}
|
||||
>
|
||||
{$i18n.t('Title')}
|
||||
{#if sortKey === 'name'}
|
||||
{#if sortDirection === 'asc'}
|
||||
<ChevronUp className="size-2" />
|
||||
{:else}
|
||||
<ChevronDown className="size-2" />
|
||||
{/if}
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<div class="hidden w-44 shrink-0 md:block"></div>
|
||||
|
||||
<button
|
||||
class="flex w-36 shrink-0 items-center justify-end gap-1 py-0.5 text-right"
|
||||
type="button"
|
||||
on:click={() => setSortKey('updated_at')}
|
||||
>
|
||||
{$i18n.t('Updated at')}
|
||||
{#if sortKey === 'updated_at'}
|
||||
{#if sortDirection === 'asc'}
|
||||
<ChevronUp className="size-2" />
|
||||
{:else}
|
||||
<ChevronDown className="size-2" />
|
||||
{/if}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-y-0.5">
|
||||
{#each filteredItems as tool}
|
||||
<div
|
||||
class="flex w-full rounded-xl px-2 py-1 text-left transition {tool.write_access
|
||||
? 'cursor-pointer hover:bg-gray-50/60 dark:hover:bg-gray-850/40'
|
||||
class="group flex min-h-8 w-full items-center gap-2 overflow-hidden rounded-xl px-2 py-1 text-left {tool.write_access
|
||||
? 'cursor-pointer'
|
||||
: 'cursor-not-allowed opacity-60'}"
|
||||
role="button"
|
||||
tabindex={tool.write_access ? 0 : -1}
|
||||
on:click={(e) => {
|
||||
if (!tool.write_access || shouldIgnoreRowClick(e.target)) return;
|
||||
openTool(tool);
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (!tool.write_access || e.currentTarget !== e.target) return;
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
openTool(tool);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if tool.write_access}
|
||||
<a
|
||||
class="flex w-full min-w-0 flex-1 cursor-pointer"
|
||||
href={`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`}
|
||||
>
|
||||
<div class="flex min-w-0 items-center text-left">
|
||||
<div class="min-w-0 flex-1 self-center">
|
||||
<Tooltip content={tool.id} placement="top-start">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<div class="line-clamp-1 text-sm">
|
||||
{tool.name}
|
||||
</div>
|
||||
{#if tool?.meta?.manifest?.version}
|
||||
<div class=" text-gray-500 text-xs font-normal shrink-0">
|
||||
v{tool?.meta?.manifest?.version ?? ''}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex min-w-0 flex-1 items-center gap-1 overflow-hidden">
|
||||
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<div class="flex min-w-0 items-center gap-2 overflow-hidden">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
|
||||
<Tooltip content={tool.id} className="min-w-0" placement="top-start">
|
||||
<div
|
||||
class="truncate text-[13px] leading-5 text-gray-800 group-hover:underline dark:text-gray-200"
|
||||
>
|
||||
{tool.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="px-0.5">
|
||||
<div class="shrink-0 text-xs text-gray-500">
|
||||
<Tooltip
|
||||
content={tool?.user?.email ?? $i18n.t('Deleted User')}
|
||||
className="flex shrink-0"
|
||||
placement="top-start"
|
||||
>
|
||||
{$i18n.t('By {{name}}', {
|
||||
name: capitalizeFirstLetter(
|
||||
tool?.user?.name ?? tool?.user?.email ?? $i18n.t('Deleted User')
|
||||
)
|
||||
})}
|
||||
</Tooltip>
|
||||
|
||||
{#if tool?.meta?.manifest?.version}
|
||||
<div
|
||||
class="min-w-0 max-w-[40%] shrink-0 truncate text-[11px] leading-5 text-gray-500"
|
||||
>
|
||||
v{tool?.meta?.manifest?.version ?? ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{:else}
|
||||
<div class="flex w-full min-w-0 flex-1">
|
||||
<div class="flex w-full min-w-0 items-center text-left">
|
||||
<div class="w-full min-w-0 flex-1 self-center">
|
||||
<div class="flex items-center justify-between w-full gap-2">
|
||||
<Tooltip content={tool.id} placement="top-start">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<div class="line-clamp-1 text-sm">
|
||||
{tool.name}
|
||||
</div>
|
||||
{#if tool?.meta?.manifest?.version}
|
||||
<div class=" text-gray-500 text-xs font-normal shrink-0">
|
||||
v{tool?.meta?.manifest?.version ?? ''}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content={dayjs(tool.updated_at * 1000).format('LLLL')}>
|
||||
<div
|
||||
class="shrink-0 truncate text-[11px] leading-5 text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
{dayjs(tool.updated_at * 1000).fromNow()}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{#if !tool.write_access}
|
||||
<Badge type="muted" content={$i18n.t('Read Only')} />
|
||||
</div>
|
||||
<div class="px-0.5">
|
||||
<div class="shrink-0 text-xs text-gray-500">
|
||||
<Tooltip
|
||||
content={tool?.user?.email ?? $i18n.t('Deleted User')}
|
||||
className="flex shrink-0"
|
||||
placement="top-start"
|
||||
>
|
||||
{$i18n.t('By {{name}}', {
|
||||
name: capitalizeFirstLetter(
|
||||
tool?.user?.name ?? tool?.user?.email ?? $i18n.t('Deleted User')
|
||||
)
|
||||
})}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if tool?.meta?.description}
|
||||
<Tooltip
|
||||
content={tool?.meta?.description}
|
||||
className="min-w-0"
|
||||
placement="top-start"
|
||||
>
|
||||
<div
|
||||
class="mt-0.5 truncate text-[0.6875rem] leading-4 text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
{tool?.meta?.description}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="hidden max-w-44 shrink-0 self-center truncate text-right text-[11px] leading-5 text-gray-500 dark:text-gray-500 md:block"
|
||||
>
|
||||
<Tooltip
|
||||
content={tool?.user?.email ?? $i18n.t('Deleted User')}
|
||||
className="min-w-0"
|
||||
placement="top-start"
|
||||
>
|
||||
<div class="truncate">
|
||||
{capitalizeFirstLetter(
|
||||
tool?.user?.name ?? tool?.user?.email ?? $i18n.t('Deleted User')
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{#if tool.write_access}
|
||||
<div class="flex flex-row gap-0.5 self-center">
|
||||
<div class="ml-2 flex shrink-0 flex-row items-center self-center">
|
||||
{#if shiftKey}
|
||||
<Tooltip content={$i18n.t('Delete')}>
|
||||
<button
|
||||
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition dark:text-gray-500"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Delete')}
|
||||
on:click={() => {
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
deleteHandler(tool);
|
||||
}}
|
||||
>
|
||||
<GarbageBin />
|
||||
<GarbageBin className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
{#if tool?.meta?.manifest?.funding_url ?? false}
|
||||
<Tooltip content="Support">
|
||||
<button
|
||||
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition dark:text-gray-500"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Support')}
|
||||
on:click={() => {
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
selectedTool = tool;
|
||||
showManifestModal = true;
|
||||
}}
|
||||
>
|
||||
<Heart />
|
||||
<Heart className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content={$i18n.t('Valves')}>
|
||||
<button
|
||||
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition dark:text-gray-500"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Valves')}
|
||||
on:click={() => {
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
selectedTool = tool;
|
||||
showValvesModal = true;
|
||||
}}
|
||||
@@ -476,38 +563,49 @@
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<ToolMenu
|
||||
editHandler={() => {
|
||||
goto(`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`);
|
||||
}}
|
||||
shareHandler={() => {
|
||||
shareHandler(tool);
|
||||
}}
|
||||
cloneHandler={() => {
|
||||
cloneHandler(tool);
|
||||
}}
|
||||
exportHandler={() => {
|
||||
exportHandler(tool);
|
||||
}}
|
||||
deleteHandler={async () => {
|
||||
selectedTool = tool;
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
onClose={() => {}}
|
||||
>
|
||||
<button
|
||||
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
type="button"
|
||||
<div class="ml-0.5 flex shrink-0 flex-row items-center self-center">
|
||||
<ToolMenu
|
||||
show={openToolMenuId === tool.id}
|
||||
editHandler={() => {
|
||||
goto(`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`);
|
||||
}}
|
||||
shareHandler={() => {
|
||||
shareHandler(tool);
|
||||
}}
|
||||
cloneHandler={() => {
|
||||
cloneHandler(tool);
|
||||
}}
|
||||
exportHandler={() => {
|
||||
exportHandler(tool);
|
||||
}}
|
||||
deleteHandler={async () => {
|
||||
selectedTool = tool;
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
onClose={() => {
|
||||
openToolMenuId = null;
|
||||
}}
|
||||
>
|
||||
<EllipsisHorizontal className="size-4" />
|
||||
</button>
|
||||
</ToolMenu>
|
||||
<button
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition dark:text-gray-500"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Tool Menu')}
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openToolMenuId = openToolMenuId === tool.id ? null : tool.id;
|
||||
}}
|
||||
>
|
||||
<EllipsisHorizontal className="size-4" />
|
||||
</button>
|
||||
</ToolMenu>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class=" w-full h-full flex flex-col justify-center items-center my-16 mb-24">
|
||||
|
||||
@@ -20,7 +20,12 @@
|
||||
export let deleteHandler: Function;
|
||||
export let onClose: Function;
|
||||
|
||||
let show = false;
|
||||
export let show = false;
|
||||
|
||||
const closeMenu = () => {
|
||||
show = false;
|
||||
onClose();
|
||||
};
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
@@ -42,6 +47,7 @@
|
||||
draggable="false"
|
||||
on:click={() => {
|
||||
editHandler();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -68,6 +74,7 @@
|
||||
draggable="false"
|
||||
on:click={() => {
|
||||
shareHandler();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<Share className="size-3.5" />
|
||||
@@ -80,6 +87,7 @@
|
||||
draggable="false"
|
||||
on:click={() => {
|
||||
cloneHandler();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<DocumentDuplicate className="size-3.5" />
|
||||
@@ -93,6 +101,7 @@
|
||||
draggable="false"
|
||||
on:click={() => {
|
||||
exportHandler();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<Download className="size-3.5" />
|
||||
@@ -108,6 +117,7 @@
|
||||
draggable="false"
|
||||
on:click={() => {
|
||||
deleteHandler();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<GarbageBin className="size-3.5" />
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
export let description = '';
|
||||
</script>
|
||||
|
||||
<div class="mt-6 px-2">
|
||||
<div class="mt-6 px-2 pb-8">
|
||||
<div class="mb-0.5 text-[11px] font-normal text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Made by Open WebUI Community')}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user