refac
This commit is contained in:
1 parent
56f2cb5302
commit
e5e2cd7876
12 files changed
+1219
-686
No files matched your search
@@ -149,6 +149,7 @@ class KnowledgeDirectoryForm(BaseModel):
|
|||||||
####################
|
####################
|
||||||
class KnowledgeUserModel(KnowledgeModel):
|
class KnowledgeUserModel(KnowledgeModel):
|
||||||
user: Optional[UserResponse] = None
|
user: Optional[UserResponse] = None
|
||||||
|
file_count: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class KnowledgeResponse(KnowledgeModel):
|
class KnowledgeResponse(KnowledgeModel):
|
||||||
@@ -322,6 +323,14 @@ class KnowledgeTable:
|
|||||||
|
|
||||||
knowledge_ids = [kb.id for kb, _ in items]
|
knowledge_ids = [kb.id for kb, _ in items]
|
||||||
grants_map = await AccessGrants.get_grants_by_resources('knowledge', knowledge_ids, db=db)
|
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 = []
|
knowledge_bases = []
|
||||||
for knowledge_base, user in items:
|
for knowledge_base, user in items:
|
||||||
@@ -336,6 +345,7 @@ class KnowledgeTable:
|
|||||||
)
|
)
|
||||||
).model_dump(),
|
).model_dump(),
|
||||||
'user': (UserModel.model_validate(user).model_dump() if user else None),
|
'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">
|
<script lang="ts">
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import fileSaver from 'file-saver';
|
import fileSaver from 'file-saver';
|
||||||
const { saveAs } = fileSaver;
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
import { WEBUI_NAME, config, functions as _functions, models, settings, user } from '$lib/stores';
|
import { WEBUI_NAME, config, functions as _functions, models, settings, user } from '$lib/stores';
|
||||||
import { onMount, getContext, tick, onDestroy } from 'svelte';
|
import { onMount, getContext, tick, onDestroy } from 'svelte';
|
||||||
|
|
||||||
@@ -39,6 +43,8 @@
|
|||||||
import { capitalizeFirstLetter } from '$lib/utils';
|
import { capitalizeFirstLetter } from '$lib/utils';
|
||||||
import Spinner from '../common/Spinner.svelte';
|
import Spinner from '../common/Spinner.svelte';
|
||||||
import SplitCreateButton from '../common/SplitCreateButton.svelte';
|
import SplitCreateButton from '../common/SplitCreateButton.svelte';
|
||||||
|
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||||
|
import ChevronUp from '../icons/ChevronUp.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -49,6 +55,9 @@
|
|||||||
|
|
||||||
let tagsContainerElement: HTMLDivElement;
|
let tagsContainerElement: HTMLDivElement;
|
||||||
let viewOption = '';
|
let viewOption = '';
|
||||||
|
let sortKey = 'updated_at';
|
||||||
|
let sortDirection = 'desc';
|
||||||
|
let openFunctionMenuId: string | null = null;
|
||||||
|
|
||||||
let query = '';
|
let query = '';
|
||||||
let searchDebounceTimer: ReturnType<typeof setTimeout>;
|
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();
|
setFilteredItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +124,32 @@
|
|||||||
(viewOption === 'created' && f.user_id === $user?.id) ||
|
(viewOption === 'created' && f.user_id === $user?.id) ||
|
||||||
(viewOption === 'shared' && 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 shareHandler = async (func) => {
|
||||||
const item = await getFunctionById(localStorage.token, func.id).catch((error) => {
|
const item = await getFunctionById(localStorage.token, func.id).catch((error) => {
|
||||||
@@ -407,173 +447,275 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if (filteredItems ?? []).length !== 0}
|
{#if (filteredItems ?? []).length !== 0}
|
||||||
<div class="my-1 gap-x-2 gap-y-0.5 grid lg:grid-cols-2">
|
<div class="my-1">
|
||||||
{#each filteredItems as func (func.id)}
|
<div
|
||||||
<div
|
class="flex w-full items-center gap-2 px-1.5 pb-0.5 text-xs text-gray-400 dark:text-gray-600"
|
||||||
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"
|
>
|
||||||
|
<button
|
||||||
|
class="flex min-w-0 flex-1 items-center gap-1 py-0.5 text-left"
|
||||||
|
type="button"
|
||||||
|
on:click={() => setSortKey('name')}
|
||||||
>
|
>
|
||||||
<a
|
{$i18n.t('Title')}
|
||||||
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
|
{#if sortKey === 'name'}
|
||||||
href={`/admin/functions/edit?id=${encodeURIComponent(func.id)}`}
|
{#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 min-w-0 flex-1 items-center gap-1 overflow-hidden">
|
||||||
<div class=" flex-1 self-center pl-1">
|
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||||
<Tooltip content={func.id} placement="top-start">
|
<div class="flex min-w-0 items-center gap-2 overflow-hidden">
|
||||||
<div class=" flex items-center gap-1.5">
|
<div class="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
|
||||||
<div
|
<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}
|
{func.type}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="line-clamp-1 text-sm">
|
<Tooltip content={func.id} className="min-w-0" placement="top-start">
|
||||||
{func.name}
|
<div
|
||||||
</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}
|
{#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 ?? ''}
|
v{func?.meta?.manifest?.version ?? ''}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<div class="flex gap-1.5 px-1">
|
<Tooltip content={dayjs(func.updated_at * 1000).format('LLLL')}>
|
||||||
<div class="text-xs text-gray-500 shrink-0">
|
<div
|
||||||
<Tooltip
|
class="shrink-0 truncate text-[11px] leading-5 text-gray-400 dark:text-gray-600"
|
||||||
content={func?.user?.email ?? $i18n.t('Deleted User')}
|
>
|
||||||
className="flex shrink-0"
|
{dayjs(func.updated_at * 1000).fromNow()}
|
||||||
placement="top-start"
|
</div>
|
||||||
>
|
|
||||||
{$i18n.t('By {{name}}', {
|
|
||||||
name: capitalizeFirstLetter(
|
|
||||||
func?.user?.name ?? func?.user?.email ?? $i18n.t('Deleted User')
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
|
|
||||||
{func.meta.description}
|
|
||||||
</div>
|
|
||||||
</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>
|
||||||
</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')}>
|
<div
|
||||||
<button
|
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"
|
||||||
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"
|
<Tooltip
|
||||||
on:click={() => {
|
content={func?.user?.email ?? $i18n.t('Deleted User')}
|
||||||
selectedFunction = func;
|
className="min-w-0"
|
||||||
showValvesModal = true;
|
placement="top-start"
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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={() => {}}
|
|
||||||
>
|
>
|
||||||
<button
|
<div class="truncate">
|
||||||
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"
|
{capitalizeFirstLetter(
|
||||||
type="button"
|
func?.user?.name ?? func?.user?.email ?? $i18n.t('Deleted User')
|
||||||
>
|
)}
|
||||||
<EllipsisHorizontal className="size-5" />
|
</div>
|
||||||
</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
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" w-full h-full flex flex-col justify-center items-center my-16 mb-24">
|
<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;
|
export let onClose: Function;
|
||||||
|
|
||||||
let show = false;
|
export let show = false;
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
show = false;
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dropdown
|
<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"
|
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={() => {
|
on:click={() => {
|
||||||
editHandler();
|
editHandler();
|
||||||
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<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"
|
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={() => {
|
on:click={() => {
|
||||||
shareHandler();
|
shareHandler();
|
||||||
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Share />
|
<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"
|
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={() => {
|
on:click={() => {
|
||||||
cloneHandler();
|
cloneHandler();
|
||||||
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DocumentDuplicate />
|
<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"
|
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={() => {
|
on:click={() => {
|
||||||
exportHandler();
|
exportHandler();
|
||||||
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Download />
|
<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"
|
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={() => {
|
on:click={() => {
|
||||||
deleteHandler();
|
deleteHandler();
|
||||||
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GarbageBin strokeWidth="2" />
|
<GarbageBin strokeWidth="2" />
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||||
|
|
||||||
import { WEBUI_NAME, knowledge, user, workspaceActions } from '$lib/stores';
|
import { WEBUI_NAME, user, workspaceActions } from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
deleteKnowledgeById,
|
deleteKnowledgeById,
|
||||||
searchKnowledgeBases,
|
searchKnowledgeBases,
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
|
file_count?: number;
|
||||||
write_access?: boolean;
|
write_access?: boolean;
|
||||||
meta?: any;
|
meta?: any;
|
||||||
user?: {
|
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 () => {
|
onMount(async () => {
|
||||||
viewOption = localStorage?.workspaceViewOption || '';
|
viewOption = localStorage?.workspaceViewOption || '';
|
||||||
sourceOption = localStorage?.workspaceKnowledgeSourceOption || '';
|
sourceOption = localStorage?.workspaceKnowledgeSourceOption || '';
|
||||||
@@ -297,102 +351,124 @@
|
|||||||
|
|
||||||
{#if items !== null && total !== null}
|
{#if items !== null && total !== null}
|
||||||
{#if (items ?? []).length !== 0}
|
{#if (items ?? []).length !== 0}
|
||||||
<div class="my-1 grid grid-cols-1 gap-x-2 gap-y-0.5 lg:grid-cols-2">
|
<div class="my-1">
|
||||||
{#each items as item}
|
<div
|
||||||
<button
|
class="flex w-full items-center gap-2 px-1.5 pb-0.5 text-xs text-gray-400 dark:text-gray-600"
|
||||||
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={() => {
|
<div class="flex min-w-0 flex-1 items-center gap-1 py-0.5 text-left">
|
||||||
if (item?.meta?.document) {
|
{$i18n.t('Title')}
|
||||||
toast.error(
|
</div>
|
||||||
$i18n.t(
|
|
||||||
'Only collections can be edited, create a new knowledge base to edit/add documents.'
|
<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">
|
||||||
} else {
|
{$i18n.t('Updated at')}
|
||||||
goto(`/workspace/knowledge/${item.id}`);
|
</div>
|
||||||
}
|
</div>
|
||||||
}}
|
|
||||||
>
|
<div class="grid gap-y-0.5">
|
||||||
<div class="w-full min-w-0">
|
{#each items as item}
|
||||||
<div class="flex-1 self-center justify-between">
|
{@const metaPreview = getKnowledgeMetaPreview(item)}
|
||||||
<div class="flex h-7 items-center justify-between">
|
<div
|
||||||
<div class=" flex gap-2 items-center justify-between w-full">
|
class="group flex min-h-8 w-full cursor-pointer items-center gap-2 overflow-hidden rounded-xl px-2 py-1 text-left"
|
||||||
{#if item?.meta?.source === 'external'}
|
role="button"
|
||||||
<div>
|
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
|
<Badge
|
||||||
type="muted"
|
type="muted"
|
||||||
content={item?.meta?.external?.provider ?? $i18n.t('Connected')}
|
content={item?.meta?.external?.provider ?? $i18n.t('Connected')}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Badge type="muted" content={$i18n.t('Read Only')} />
|
<Badge type="muted" content={$i18n.t('Read Only')} />
|
||||||
</div>
|
{/if}
|
||||||
{:else}
|
|
||||||
<div>
|
|
||||||
<Badge type="success" content={$i18n.t('Collection')} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if !item?.write_access && item?.meta?.source !== 'external'}
|
{#if !item?.write_access && item?.meta?.source !== 'external'}
|
||||||
<div>
|
|
||||||
<Badge type="muted" content={$i18n.t('Read Only')} />
|
<Badge type="muted" content={$i18n.t('Read Only')} />
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if item?.write_access || $user?.role === 'admin'}
|
<Tooltip content={dayjs(item.updated_at * 1000).format('LLLL')}>
|
||||||
<div class="flex items-center gap-2">
|
<div
|
||||||
<div class=" flex self-center">
|
class="shrink-0 truncate text-[11px] leading-5 text-gray-400 dark:text-gray-600"
|
||||||
<ItemMenu
|
>
|
||||||
onExport={$user?.role === 'admin'
|
{dayjs(item.updated_at * 1000).fromNow()}
|
||||||
? () => {
|
</div>
|
||||||
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>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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>
|
</div>
|
||||||
</button>
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !allItemsLoaded}
|
{#if !allItemsLoaded}
|
||||||
|
|||||||
@@ -15,6 +15,11 @@
|
|||||||
export let onClose: Function = () => {};
|
export let onClose: Function = () => {};
|
||||||
|
|
||||||
let show = false;
|
let show = false;
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
show = false;
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -29,12 +34,13 @@
|
|||||||
<Tooltip content={$i18n.t('More')}>
|
<Tooltip content={$i18n.t('More')}>
|
||||||
<slot
|
<slot
|
||||||
><button
|
><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"
|
type="button"
|
||||||
aria-label={$i18n.t('More Options')}
|
aria-label={$i18n.t('More Options')}
|
||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
show = true;
|
show = !show;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EllipsisHorizontal className="size-4" />
|
<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"
|
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={() => {
|
on:click={() => {
|
||||||
onExport();
|
onExport();
|
||||||
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Download className="size-3.5" />
|
<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"
|
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={() => {
|
on:click={() => {
|
||||||
dispatch('delete');
|
dispatch('delete');
|
||||||
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GarbageBin className="size-3.5" />
|
<GarbageBin className="size-3.5" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { marked } from 'marked';
|
import dayjs from 'dayjs';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
import { onMount, getContext, tick } from 'svelte';
|
import { onMount, getContext, tick } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
import {
|
import {
|
||||||
WEBUI_NAME,
|
WEBUI_NAME,
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
user,
|
user,
|
||||||
workspaceActions
|
workspaceActions
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
import {
|
import {
|
||||||
createNewModel,
|
createNewModel,
|
||||||
deleteModelById,
|
deleteModelById,
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
import EyeSlash from '../icons/EyeSlash.svelte';
|
import EyeSlash from '../icons/EyeSlash.svelte';
|
||||||
import Eye from '../icons/Eye.svelte';
|
import Eye from '../icons/Eye.svelte';
|
||||||
import ChevronDown from '../icons/ChevronDown.svelte';
|
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||||
|
import ChevronUp from '../icons/ChevronUp.svelte';
|
||||||
|
|
||||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||||
import DropdownMenu from '$lib/components/common/DropdownMenu.svelte';
|
import DropdownMenu from '$lib/components/common/DropdownMenu.svelte';
|
||||||
@@ -78,6 +80,8 @@
|
|||||||
|
|
||||||
let query = '';
|
let query = '';
|
||||||
let viewOption = '';
|
let viewOption = '';
|
||||||
|
let sortKey = 'updated_at';
|
||||||
|
let sortDirection = 'desc';
|
||||||
|
|
||||||
let page = 1;
|
let page = 1;
|
||||||
let models = null;
|
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();
|
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 () => {
|
const getModelList = async () => {
|
||||||
if (!loaded) return;
|
if (!loaded) return;
|
||||||
|
|
||||||
@@ -122,8 +152,8 @@
|
|||||||
query,
|
query,
|
||||||
viewOption,
|
viewOption,
|
||||||
selectedTag,
|
selectedTag,
|
||||||
null,
|
sortKey,
|
||||||
null,
|
sortDirection,
|
||||||
page
|
page
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
toast.error(`${error}`);
|
toast.error(`${error}`);
|
||||||
@@ -601,216 +631,254 @@
|
|||||||
|
|
||||||
{#if models !== null}
|
{#if models !== null}
|
||||||
{#if (models ?? []).length !== 0}
|
{#if (models ?? []).length !== 0}
|
||||||
<div class="my-1 grid gap-x-2 gap-y-0.5 lg:grid-cols-2" id="model-list">
|
<div class="my-1" id="model-list">
|
||||||
{#each models as model (model.id)}
|
<div
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
class="flex w-full items-center gap-2 px-1.5 pb-0.5 text-xs text-gray-400 dark:text-gray-600"
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
>
|
||||||
<div
|
<button
|
||||||
class="flex w-full rounded-xl px-2 py-1 transition {model.write_access
|
class="flex min-w-0 flex-1 items-center gap-1 py-0.5 text-left"
|
||||||
? 'cursor-pointer hover:bg-gray-50/60 dark:hover:bg-gray-850/40'
|
type="button"
|
||||||
: ''}"
|
on:click={() => setSortKey('name')}
|
||||||
id="model-item-{model.id}"
|
|
||||||
on:click={() => {
|
|
||||||
if (model.write_access) {
|
|
||||||
goto(`/workspace/models/edit?id=${encodeURIComponent(model.id)}`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div class="flex group/item min-w-0 gap-2.5 w-full">
|
{$i18n.t('Title')}
|
||||||
<div class="self-center">
|
{#if sortKey === 'name'}
|
||||||
<div class="flex">
|
{#if sortDirection === 'asc'}
|
||||||
<div
|
<ChevronUp className="size-2" />
|
||||||
class="{model.is_active
|
{:else}
|
||||||
? ''
|
<ChevronDown className="size-2" />
|
||||||
: 'opacity-50 dark:opacity-50'} bg-transparent rounded-xl"
|
{/if}
|
||||||
>
|
{/if}
|
||||||
<img
|
</button>
|
||||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
|
|
||||||
alt="modelfile profile"
|
<div class="hidden w-44 shrink-0 md:block"></div>
|
||||||
class="size-8 rounded-xl object-cover"
|
|
||||||
loading="lazy"
|
<button
|
||||||
decoding="async"
|
class="flex w-36 shrink-0 items-center justify-end gap-1 py-0.5 text-right"
|
||||||
on:error={(e) => {
|
type="button"
|
||||||
e.target.src = '/favicon.png';
|
on:click={() => setSortKey('updated_at')}
|
||||||
}}
|
>
|
||||||
/>
|
{$i18n.t('Updated at')}
|
||||||
</div>
|
{#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>
|
</div>
|
||||||
|
|
||||||
<div class="flex min-w-0 w-full flex-1 self-center pr-1">
|
<div class="flex min-w-0 flex-1 items-center gap-1 overflow-hidden">
|
||||||
<div class="flex h-full w-full flex-1 flex-col justify-start self-center group">
|
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||||
<div class="min-w-0 flex-1 w-full">
|
<div class="flex min-w-0 items-center gap-2 overflow-hidden">
|
||||||
<div class="flex min-w-0 items-center justify-between w-full gap-2">
|
<div class="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
|
||||||
<Tooltip
|
<Tooltip content={model.name} className="min-w-0" placement="top-start">
|
||||||
content={model.name}
|
<div
|
||||||
className="min-w-0 flex-1"
|
class="truncate text-[13px] leading-5 text-gray-800 group-hover:underline dark:text-gray-200"
|
||||||
placement="top-start"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="line-clamp-1 text-sm font-normal capitalize hover:underline"
|
|
||||||
href={`/?models=${encodeURIComponent(model.id)}`}
|
|
||||||
>
|
>
|
||||||
{model.name}
|
{model.name}
|
||||||
</a>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<div class="flex shrink-0 items-center gap-1">
|
<div
|
||||||
{#if !model.write_access}
|
class="min-w-0 max-w-[40%] shrink-0 truncate text-[11px] leading-5 text-gray-500"
|
||||||
<div>
|
>
|
||||||
<Badge type="muted" content={$i18n.t('Read Only')} />
|
{model.id}
|
||||||
</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>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex min-w-0 items-center gap-1 pr-2 text-xs text-gray-500">
|
<Tooltip content={dayjs(model.updated_at * 1000).format('LLLL')}>
|
||||||
<Tooltip
|
<div
|
||||||
content={model?.user?.email ?? $i18n.t('Deleted User')}
|
class="shrink-0 truncate text-[11px] leading-5 text-gray-400 dark:text-gray-600"
|
||||||
className="flex shrink-0"
|
>
|
||||||
placement="top-start"
|
{dayjs(model.updated_at * 1000).fromNow()}
|
||||||
>
|
|
||||||
<div class="shrink-0">
|
|
||||||
{$i18n.t('By {{name}}', {
|
|
||||||
name: capitalizeFirstLetter(
|
|
||||||
model?.user?.name ?? model?.user?.email ?? $i18n.t('Deleted User')
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<div class="shrink-0">·</div>
|
{#if !model.write_access}
|
||||||
|
<Badge type="muted" content={$i18n.t('Read Only')} />
|
||||||
<Tooltip
|
{/if}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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>
|
||||||
</div>
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if total > 30}
|
{#if total > 30}
|
||||||
|
|||||||
@@ -504,7 +504,7 @@
|
|||||||
{:else if (prompts ?? []).length !== 0}
|
{:else if (prompts ?? []).length !== 0}
|
||||||
<div class="my-1">
|
<div class="my-1">
|
||||||
<div
|
<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
|
<button
|
||||||
class="flex min-w-0 flex-1 items-center gap-1 py-0.5 text-left"
|
class="flex min-w-0 flex-1 items-center gap-1 py-0.5 text-left"
|
||||||
@@ -575,39 +575,21 @@
|
|||||||
/{prompt.command}
|
/{prompt.command}
|
||||||
</div>
|
</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
|
<Tooltip
|
||||||
content={dayjs((prompt.updated_at ?? prompt.created_at) * 1000).format(
|
content={dayjs((prompt.updated_at ?? prompt.created_at) * 1000).format(
|
||||||
'LLLL'
|
'LLLL'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<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()}
|
{dayjs((prompt.updated_at ?? prompt.created_at) * 1000).fromNow()}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{#if !prompt.write_access}
|
||||||
|
<Badge type="muted" content={$i18n.t('Read Only')} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -622,6 +604,23 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="ml-2 flex shrink-0 flex-row items-center self-center">
|
||||||
{#if shiftKey}
|
{#if shiftKey}
|
||||||
<Tooltip content={$i18n.t('Delete')}>
|
<Tooltip content={$i18n.t('Delete')}>
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import fileSaver from 'file-saver';
|
import fileSaver from 'file-saver';
|
||||||
const { saveAs } = fileSaver;
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
import { onMount, getContext, tick, onDestroy } from 'svelte';
|
import { onMount, getContext, tick, onDestroy } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -33,6 +37,8 @@
|
|||||||
import Switch from '../common/Switch.svelte';
|
import Switch from '../common/Switch.svelte';
|
||||||
import SkillMenu from './Skills/SkillMenu.svelte';
|
import SkillMenu from './Skills/SkillMenu.svelte';
|
||||||
import Pagination from '../common/Pagination.svelte';
|
import Pagination from '../common/Pagination.svelte';
|
||||||
|
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||||
|
import ChevronUp from '../icons/ChevronUp.svelte';
|
||||||
|
|
||||||
let shiftKey = false;
|
let shiftKey = false;
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
@@ -52,6 +58,9 @@
|
|||||||
|
|
||||||
let tagsContainerElement: HTMLDivElement;
|
let tagsContainerElement: HTMLDivElement;
|
||||||
let viewOption = '';
|
let viewOption = '';
|
||||||
|
let sortKey = 'updated_at';
|
||||||
|
let sortDirection = 'desc';
|
||||||
|
let openSkillMenuId: string | null = null;
|
||||||
let page = 1;
|
let page = 1;
|
||||||
|
|
||||||
$: if (loaded) {
|
$: if (loaded) {
|
||||||
@@ -93,12 +102,17 @@
|
|||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await getSkillItems(localStorage.token, query, viewOption, page).catch(
|
const res = await getSkillItems(
|
||||||
(error) => {
|
localStorage.token,
|
||||||
toast.error(`${error}`);
|
query,
|
||||||
return null;
|
viewOption,
|
||||||
}
|
page,
|
||||||
);
|
sortKey,
|
||||||
|
sortDirection
|
||||||
|
).catch((error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
filteredItems = res.items;
|
filteredItems = res.items;
|
||||||
@@ -124,10 +138,33 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Immediate response to page/filter changes
|
// Immediate response to page/filter changes
|
||||||
$: if (loaded && page && viewOption !== undefined) {
|
$: if (
|
||||||
|
loaded &&
|
||||||
|
page &&
|
||||||
|
viewOption !== undefined &&
|
||||||
|
sortKey !== undefined &&
|
||||||
|
sortDirection !== undefined
|
||||||
|
) {
|
||||||
loadSkillItems();
|
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 cloneHandler = async (skill) => {
|
||||||
const _skill = await getSkillById(localStorage.token, skill.id).catch((error) => {
|
const _skill = await getSkillById(localStorage.token, skill.id).catch((error) => {
|
||||||
toast.error(`${error}`);
|
toast.error(`${error}`);
|
||||||
@@ -343,141 +380,207 @@
|
|||||||
<Spinner className="size-5" />
|
<Spinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
{:else if (filteredItems ?? []).length !== 0}
|
{:else if (filteredItems ?? []).length !== 0}
|
||||||
<div class="my-1 grid gap-x-2 gap-y-0.5 lg:grid-cols-2">
|
<div class="my-1">
|
||||||
{#each filteredItems as skill}
|
<div
|
||||||
<Tooltip content={skill?.description ?? skill?.id}>
|
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
|
<div
|
||||||
class="flex w-full rounded-xl px-2 py-1 text-left transition {skill.write_access
|
class="group flex min-h-8 w-full cursor-pointer items-center gap-2 overflow-hidden rounded-xl px-2 py-1 text-left"
|
||||||
? 'cursor-pointer hover:bg-gray-50/60 dark:hover:bg-gray-850/40'
|
role="button"
|
||||||
: 'cursor-not-allowed opacity-60'}"
|
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}
|
<div class="flex min-w-0 flex-1 items-center gap-1 overflow-hidden">
|
||||||
<a
|
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||||
class="flex w-full min-w-0 flex-1 cursor-pointer"
|
<div class="flex min-w-0 items-center gap-2 overflow-hidden">
|
||||||
href={`/workspace/skills/edit?id=${encodeURIComponent(skill.id)}`}
|
<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="flex min-w-0 items-center text-left">
|
<div
|
||||||
<div class="min-w-0 flex-1 self-center">
|
class="truncate text-[13px] leading-5 text-gray-800 group-hover:underline dark:text-gray-200"
|
||||||
<Tooltip content={skill.id} placement="top-start">
|
>
|
||||||
<div class="flex min-w-0 items-center gap-2">
|
{skill.name}
|
||||||
<div class="line-clamp-1 text-sm">
|
|
||||||
{skill.name}
|
|
||||||
</div>
|
|
||||||
{#if !skill.is_active}
|
|
||||||
<Badge type="muted" content={$i18n.t('Inactive')} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div class="px-0.5">
|
|
||||||
<div class="shrink-0 text-xs text-gray-500">
|
<div
|
||||||
<Tooltip
|
class="min-w-0 max-w-[40%] shrink-0 truncate text-[11px] leading-5 text-gray-500"
|
||||||
content={skill?.user?.email ?? $i18n.t('Deleted User')}
|
>
|
||||||
className="flex shrink-0"
|
/{skill.id}
|
||||||
placement="top-start"
|
|
||||||
>
|
|
||||||
{$i18n.t('By {{name}}', {
|
|
||||||
name: capitalizeFirstLetter(
|
|
||||||
skill?.user?.name ?? skill?.user?.email ?? $i18n.t('Deleted User')
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<Tooltip
|
||||||
</a>
|
content={dayjs((skill.updated_at ?? skill.created_at) * 1000).format(
|
||||||
{:else}
|
'LLLL'
|
||||||
<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
|
||||||
<div class="flex items-center justify-between w-full gap-2">
|
class="shrink-0 truncate text-[11px] leading-5 text-gray-400 dark:text-gray-600"
|
||||||
<Tooltip content={skill.id} placement="top-start">
|
>
|
||||||
<div class="flex min-w-0 items-center gap-2">
|
{dayjs((skill.updated_at ?? skill.created_at) * 1000).fromNow()}
|
||||||
<div class="line-clamp-1 text-sm">
|
</div>
|
||||||
{skill.name}
|
</Tooltip>
|
||||||
</div>
|
|
||||||
{#if !skill.is_active}
|
{#if !skill.is_active}
|
||||||
<Badge type="muted" content={$i18n.t('Inactive')} />
|
<Badge type="muted" content={$i18n.t('Inactive')} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
</Tooltip>
|
{#if !skill.write_access}
|
||||||
<Badge type="muted" content={$i18n.t('Read Only')} />
|
<Badge type="muted" content={$i18n.t('Read Only')} />
|
||||||
</div>
|
{/if}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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}
|
{#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}
|
{#if shiftKey}
|
||||||
<Tooltip content={$i18n.t('Delete')}>
|
<Tooltip content={$i18n.t('Delete')}>
|
||||||
<button
|
<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"
|
type="button"
|
||||||
aria-label={$i18n.t('Delete')}
|
aria-label={$i18n.t('Delete')}
|
||||||
on:click={() => {
|
on:click={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
deleteHandler(skill);
|
deleteHandler(skill);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GarbageBin />
|
<GarbageBin className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{:else}
|
{:else}
|
||||||
<SkillMenu
|
<div class="flex shrink-0 flex-row items-center gap-1.5 self-center">
|
||||||
editHandler={() => {
|
<SkillMenu
|
||||||
goto(`/workspace/skills/edit?id=${encodeURIComponent(skill.id)}`);
|
show={openSkillMenuId === skill.id}
|
||||||
}}
|
editHandler={() => {
|
||||||
cloneHandler={() => {
|
goto(`/workspace/skills/edit?id=${encodeURIComponent(skill.id)}`);
|
||||||
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);
|
|
||||||
}}
|
}}
|
||||||
/>
|
cloneHandler={() => {
|
||||||
</Tooltip>
|
cloneHandler(skill);
|
||||||
</button>
|
}}
|
||||||
|
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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if total > 30}
|
{#if total > 30}
|
||||||
|
|||||||
@@ -17,7 +17,12 @@
|
|||||||
export let deleteHandler: Function;
|
export let deleteHandler: Function;
|
||||||
export let onClose: Function;
|
export let onClose: Function;
|
||||||
|
|
||||||
let show = false;
|
export let show = false;
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
show = false;
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dropdown
|
<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"
|
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={() => {
|
on:click={() => {
|
||||||
editHandler();
|
editHandler();
|
||||||
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<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"
|
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={() => {
|
on:click={() => {
|
||||||
cloneHandler();
|
cloneHandler();
|
||||||
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DocumentDuplicate className="size-3.5" />
|
<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"
|
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={() => {
|
on:click={() => {
|
||||||
exportHandler();
|
exportHandler();
|
||||||
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Download className="size-3.5" />
|
<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"
|
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={() => {
|
on:click={() => {
|
||||||
deleteHandler();
|
deleteHandler();
|
||||||
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GarbageBin className="size-3.5" />
|
<GarbageBin className="size-3.5" />
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import fileSaver from 'file-saver';
|
import fileSaver from 'file-saver';
|
||||||
const { saveAs } = fileSaver;
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
import { onMount, getContext, tick, onDestroy } from 'svelte';
|
import { onMount, getContext, tick, onDestroy } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -36,6 +40,8 @@
|
|||||||
import ViewSelector from './common/ViewSelector.svelte';
|
import ViewSelector from './common/ViewSelector.svelte';
|
||||||
import CommunityDiscover from './common/CommunityDiscover.svelte';
|
import CommunityDiscover from './common/CommunityDiscover.svelte';
|
||||||
import Badge from '$lib/components/common/Badge.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 shiftKey = false;
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
@@ -58,6 +64,9 @@
|
|||||||
|
|
||||||
let tagsContainerElement: HTMLDivElement;
|
let tagsContainerElement: HTMLDivElement;
|
||||||
let viewOption = '';
|
let viewOption = '';
|
||||||
|
let sortKey = 'updated_at';
|
||||||
|
let sortDirection = 'desc';
|
||||||
|
let openToolMenuId: string | null = null;
|
||||||
|
|
||||||
let showImportModal = false;
|
let showImportModal = false;
|
||||||
|
|
||||||
@@ -110,12 +119,17 @@
|
|||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
$: if (tools && viewOption !== undefined) {
|
$: if (
|
||||||
|
tools &&
|
||||||
|
viewOption !== undefined &&
|
||||||
|
sortKey !== undefined &&
|
||||||
|
sortDirection !== undefined
|
||||||
|
) {
|
||||||
setFilteredItems();
|
setFilteredItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
const setFilteredItems = () => {
|
const setFilteredItems = () => {
|
||||||
filteredItems = tools.filter((t) => {
|
const filtered = tools.filter((t) => {
|
||||||
if (query === '' && viewOption === '') return true;
|
if (query === '' && viewOption === '') return true;
|
||||||
const lowerQuery = query.toLowerCase();
|
const lowerQuery = query.toLowerCase();
|
||||||
return (
|
return (
|
||||||
@@ -128,6 +142,33 @@
|
|||||||
(viewOption === 'shared' && t.user_id !== $user?.id))
|
(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) => {
|
const shareHandler = async (tool) => {
|
||||||
@@ -329,127 +370,173 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if (filteredItems ?? []).length !== 0}
|
{#if (filteredItems ?? []).length !== 0}
|
||||||
<div class="my-1 grid gap-x-2 gap-y-0.5 lg:grid-cols-2">
|
<div class="my-1">
|
||||||
{#each filteredItems as tool}
|
<div
|
||||||
<Tooltip content={tool?.meta?.description ?? tool?.id}>
|
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
|
<div
|
||||||
class="flex w-full rounded-xl px-2 py-1 text-left transition {tool.write_access
|
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 hover:bg-gray-50/60 dark:hover:bg-gray-850/40'
|
? 'cursor-pointer'
|
||||||
: 'cursor-not-allowed opacity-60'}"
|
: '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}
|
<div class="flex min-w-0 flex-1 items-center gap-1 overflow-hidden">
|
||||||
<a
|
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||||
class="flex w-full min-w-0 flex-1 cursor-pointer"
|
<div class="flex min-w-0 items-center gap-2 overflow-hidden">
|
||||||
href={`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`}
|
<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="flex min-w-0 items-center text-left">
|
<div
|
||||||
<div class="min-w-0 flex-1 self-center">
|
class="truncate text-[13px] leading-5 text-gray-800 group-hover:underline dark:text-gray-200"
|
||||||
<Tooltip content={tool.id} placement="top-start">
|
>
|
||||||
<div class="flex min-w-0 items-center gap-2">
|
{tool.name}
|
||||||
<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>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div class="px-0.5">
|
|
||||||
<div class="shrink-0 text-xs text-gray-500">
|
{#if tool?.meta?.manifest?.version}
|
||||||
<Tooltip
|
<div
|
||||||
content={tool?.user?.email ?? $i18n.t('Deleted User')}
|
class="min-w-0 max-w-[40%] shrink-0 truncate text-[11px] leading-5 text-gray-500"
|
||||||
className="flex shrink-0"
|
>
|
||||||
placement="top-start"
|
v{tool?.meta?.manifest?.version ?? ''}
|
||||||
>
|
|
||||||
{$i18n.t('By {{name}}', {
|
|
||||||
name: capitalizeFirstLetter(
|
|
||||||
tool?.user?.name ?? tool?.user?.email ?? $i18n.t('Deleted User')
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
|
||||||
</div>
|
<Tooltip content={dayjs(tool.updated_at * 1000).format('LLLL')}>
|
||||||
</a>
|
<div
|
||||||
{:else}
|
class="shrink-0 truncate text-[11px] leading-5 text-gray-400 dark:text-gray-600"
|
||||||
<div class="flex w-full min-w-0 flex-1">
|
>
|
||||||
<div class="flex w-full min-w-0 items-center text-left">
|
{dayjs(tool.updated_at * 1000).fromNow()}
|
||||||
<div class="w-full min-w-0 flex-1 self-center">
|
</div>
|
||||||
<div class="flex items-center justify-between w-full gap-2">
|
</Tooltip>
|
||||||
<Tooltip content={tool.id} placement="top-start">
|
|
||||||
<div class="flex min-w-0 items-center gap-2">
|
{#if !tool.write_access}
|
||||||
<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>
|
|
||||||
<Badge type="muted" content={$i18n.t('Read Only')} />
|
<Badge type="muted" content={$i18n.t('Read Only')} />
|
||||||
</div>
|
{/if}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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}
|
{#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}
|
{#if shiftKey}
|
||||||
<Tooltip content={$i18n.t('Delete')}>
|
<Tooltip content={$i18n.t('Delete')}>
|
||||||
<button
|
<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"
|
type="button"
|
||||||
aria-label={$i18n.t('Delete')}
|
aria-label={$i18n.t('Delete')}
|
||||||
on:click={() => {
|
on:click={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
deleteHandler(tool);
|
deleteHandler(tool);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GarbageBin />
|
<GarbageBin className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{:else}
|
{:else}
|
||||||
{#if tool?.meta?.manifest?.funding_url ?? false}
|
{#if tool?.meta?.manifest?.funding_url ?? false}
|
||||||
<Tooltip content="Support">
|
<Tooltip content="Support">
|
||||||
<button
|
<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"
|
type="button"
|
||||||
aria-label={$i18n.t('Support')}
|
aria-label={$i18n.t('Support')}
|
||||||
on:click={() => {
|
on:click={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
selectedTool = tool;
|
selectedTool = tool;
|
||||||
showManifestModal = true;
|
showManifestModal = true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Heart />
|
<Heart className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Tooltip content={$i18n.t('Valves')}>
|
<Tooltip content={$i18n.t('Valves')}>
|
||||||
<button
|
<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"
|
type="button"
|
||||||
aria-label={$i18n.t('Valves')}
|
aria-label={$i18n.t('Valves')}
|
||||||
on:click={() => {
|
on:click={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
selectedTool = tool;
|
selectedTool = tool;
|
||||||
showValvesModal = true;
|
showValvesModal = true;
|
||||||
}}
|
}}
|
||||||
@@ -476,38 +563,49 @@
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<ToolMenu
|
<div class="ml-0.5 flex shrink-0 flex-row items-center self-center">
|
||||||
editHandler={() => {
|
<ToolMenu
|
||||||
goto(`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`);
|
show={openToolMenuId === tool.id}
|
||||||
}}
|
editHandler={() => {
|
||||||
shareHandler={() => {
|
goto(`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`);
|
||||||
shareHandler(tool);
|
}}
|
||||||
}}
|
shareHandler={() => {
|
||||||
cloneHandler={() => {
|
shareHandler(tool);
|
||||||
cloneHandler(tool);
|
}}
|
||||||
}}
|
cloneHandler={() => {
|
||||||
exportHandler={() => {
|
cloneHandler(tool);
|
||||||
exportHandler(tool);
|
}}
|
||||||
}}
|
exportHandler={() => {
|
||||||
deleteHandler={async () => {
|
exportHandler(tool);
|
||||||
selectedTool = tool;
|
}}
|
||||||
showDeleteConfirm = true;
|
deleteHandler={async () => {
|
||||||
}}
|
selectedTool = tool;
|
||||||
onClose={() => {}}
|
showDeleteConfirm = true;
|
||||||
>
|
}}
|
||||||
<button
|
onClose={() => {
|
||||||
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"
|
openToolMenuId = null;
|
||||||
type="button"
|
}}
|
||||||
>
|
>
|
||||||
<EllipsisHorizontal className="size-4" />
|
<button
|
||||||
</button>
|
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition dark:text-gray-500"
|
||||||
</ToolMenu>
|
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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" w-full h-full flex flex-col justify-center items-center my-16 mb-24">
|
<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 deleteHandler: Function;
|
||||||
export let onClose: Function;
|
export let onClose: Function;
|
||||||
|
|
||||||
let show = false;
|
export let show = false;
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
show = false;
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -42,6 +47,7 @@
|
|||||||
draggable="false"
|
draggable="false"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
editHandler();
|
editHandler();
|
||||||
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -68,6 +74,7 @@
|
|||||||
draggable="false"
|
draggable="false"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
shareHandler();
|
shareHandler();
|
||||||
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Share className="size-3.5" />
|
<Share className="size-3.5" />
|
||||||
@@ -80,6 +87,7 @@
|
|||||||
draggable="false"
|
draggable="false"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
cloneHandler();
|
cloneHandler();
|
||||||
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DocumentDuplicate className="size-3.5" />
|
<DocumentDuplicate className="size-3.5" />
|
||||||
@@ -93,6 +101,7 @@
|
|||||||
draggable="false"
|
draggable="false"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
exportHandler();
|
exportHandler();
|
||||||
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Download className="size-3.5" />
|
<Download className="size-3.5" />
|
||||||
@@ -108,6 +117,7 @@
|
|||||||
draggable="false"
|
draggable="false"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
deleteHandler();
|
deleteHandler();
|
||||||
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GarbageBin className="size-3.5" />
|
<GarbageBin className="size-3.5" />
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
export let description = '';
|
export let description = '';
|
||||||
</script>
|
</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">
|
<div class="mb-0.5 text-[11px] font-normal text-gray-400 dark:text-gray-600">
|
||||||
{$i18n.t('Made by Open WebUI Community')}
|
{$i18n.t('Made by Open WebUI Community')}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user