refac: tools

This commit is contained in:
Timothy Jaeryang Baek 2025-10-05 02:03:57 -05:00
parent 23c8f6d507
commit a743b16728
3 changed files with 294 additions and 282 deletions

View file

@ -328,8 +328,8 @@
> >
<div class=" flex flex-col flex-1 space-x-4 cursor-pointer w-full pl-1"> <div class=" flex flex-col flex-1 space-x-4 cursor-pointer w-full pl-1">
<div class=" flex-1 flex items-center gap-2 self-start"> <div class=" flex-1 flex items-center gap-2 self-start">
<div class=" font-semibold line-clamp-1 capitalize">{prompt.title}</div> <div class=" font-medium line-clamp-1 capitalize">{prompt.title}</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1"> <div class=" text-xs overflow-hidden text-ellipsis line-clamp-1 text-gray-500">
{prompt.command} {prompt.command}
</div> </div>
</div> </div>

View file

@ -3,9 +3,10 @@
import fileSaver from 'file-saver'; import fileSaver from 'file-saver';
const { saveAs } = fileSaver; const { saveAs } = fileSaver;
import { onMount, getContext } from 'svelte'; import { onMount, getContext, tick } from 'svelte';
const i18n = getContext('i18n');
import { WEBUI_NAME, config, prompts, tools as _tools, user } from '$lib/stores'; import { WEBUI_NAME, config, prompts, tools as _tools, user } from '$lib/stores';
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { import {
@ -17,7 +18,8 @@
getToolList, getToolList,
getTools getTools
} from '$lib/apis/tools'; } from '$lib/apis/tools';
import Download from '../icons/Download.svelte'; import { capitalizeFirstLetter } from '$lib/utils';
import Tooltip from '../common/Tooltip.svelte'; import Tooltip from '../common/Tooltip.svelte';
import ConfirmDialog from '../common/ConfirmDialog.svelte'; import ConfirmDialog from '../common/ConfirmDialog.svelte';
import ToolMenu from './Tools/ToolMenu.svelte'; import ToolMenu from './Tools/ToolMenu.svelte';
@ -31,12 +33,10 @@
import Plus from '../icons/Plus.svelte'; import Plus from '../icons/Plus.svelte';
import ChevronRight from '../icons/ChevronRight.svelte'; import ChevronRight from '../icons/ChevronRight.svelte';
import Spinner from '../common/Spinner.svelte'; import Spinner from '../common/Spinner.svelte';
import { capitalizeFirstLetter } from '$lib/utils';
import XMark from '../icons/XMark.svelte'; import XMark from '../icons/XMark.svelte';
import AddToolMenu from './Tools/AddToolMenu.svelte'; import AddToolMenu from './Tools/AddToolMenu.svelte';
import ImportModal from '../ImportModal.svelte'; import ImportModal from '../ImportModal.svelte';
import ViewSelector from './common/ViewSelector.svelte';
const i18n = getContext('i18n');
let shiftKey = false; let shiftKey = false;
let loaded = false; let loaded = false;
@ -56,18 +56,30 @@
let tools = []; let tools = [];
let filteredItems = []; let filteredItems = [];
let tagsContainerElement: HTMLDivElement;
let viewOption = '';
let showImportModal = false; let showImportModal = false;
$: filteredItems = tools.filter((t) => { $: if (tools && query !== undefined && viewOption !== undefined) {
if (query === '') return true; setFilteredItems();
}
const setFilteredItems = () => {
filteredItems = tools.filter((t) => {
if (query === '' && viewOption === '') return true;
const lowerQuery = query.toLowerCase(); const lowerQuery = query.toLowerCase();
return ( return (
(t.name || '').toLowerCase().includes(lowerQuery) || ((t.name || '').toLowerCase().includes(lowerQuery) ||
(t.id || '').toLowerCase().includes(lowerQuery) || (t.id || '').toLowerCase().includes(lowerQuery) ||
(t.user?.name || '').toLowerCase().includes(lowerQuery) || // Search by user name (t.user?.name || '').toLowerCase().includes(lowerQuery) || // Search by user name
(t.user?.email || '').toLowerCase().includes(lowerQuery) // Search by user email (t.user?.email || '').toLowerCase().includes(lowerQuery)) && // Search by user email
(viewOption === '' ||
(viewOption === 'created' && t.user_id === $user?.id) ||
(viewOption === 'shared' && t.user_id !== $user?.id))
); );
}); });
};
const shareHandler = async (tool) => { const shareHandler = async (tool) => {
const item = await getToolById(localStorage.token, tool.id).catch((error) => { const item = await getToolById(localStorage.token, tool.id).catch((error) => {
@ -193,18 +205,104 @@
/> />
{#if loaded} {#if loaded}
<div class="flex flex-col gap-1 my-1.5"> <div class="flex flex-col gap-1 px-1 mt-1.5 mb-3">
<input
id="documents-import-input"
bind:this={toolsImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
showConfirm = true;
}}
/>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="flex md:self-center text-xl font-medium px-0.5 items-center"> <div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
<div>
{$i18n.t('Tools')} {$i18n.t('Tools')}
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" /> </div>
<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
>{filteredItems.length}</span <div class="text-lg font-medium text-gray-500 dark:text-gray-500">
> {filteredItems.length}
</div> </div>
</div> </div>
<div class=" flex w-full space-x-2"> <div class="flex w-full justify-end gap-1.5">
{#if $user?.role === 'admin'}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
on:click={() => {
toolsImportInputElement.click();
}}
>
<div class=" self-center font-medium line-clamp-1">
{$i18n.t('Import')}
</div>
</button>
{#if tools.length}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
on:click={async () => {
const _tools = await exportTools(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
if (_tools) {
let blob = new Blob([JSON.stringify(_tools)], {
type: 'application/json'
});
saveAs(blob, `tools-export-${Date.now()}.json`);
}
}}
>
<div class=" self-center font-medium line-clamp-1">
{$i18n.t('Export')}
</div>
</button>
{/if}
{/if}
{#if $user?.role === 'admin'}
<AddToolMenu
createHandler={() => {
goto('/workspace/tools/create');
}}
importFromLinkHandler={() => {
showImportModal = true;
}}
>
<div
class=" px-2 py-1.5 rounded-xl bg-black text-white dark:bg-white dark:text-black transition font-medium text-sm flex items-center"
>
<Plus className="size-3" strokeWidth="2.5" />
<div class=" hidden md:block md:ml-1 text-xs">{$i18n.t('New Tool')}</div>
</div>
</AddToolMenu>
{:else}
<a
class=" px-2 py-1.5 rounded-xl bg-black text-white dark:bg-white dark:text-black transition font-medium text-sm flex items-center"
href="/workspace/tools/create"
>
<Plus className="size-3" strokeWidth="2.5" />
<div class=" hidden md:block md:ml-1 text-xs">{$i18n.t('New Tool')}</div></a
>
{/if}
</div>
</div>
</div>
<div
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100 dark:border-gray-850"
>
<!-- The iron remembers its forge. -->
<div class=" flex w-full space-x-2 py-0.5 px-3.5 pb-2">
<div class="flex flex-1"> <div class="flex flex-1">
<div class=" self-center ml-1 mr-3"> <div class=" self-center ml-1 mr-3">
<Search className="size-3.5" /> <Search className="size-3.5" />
@ -227,39 +325,38 @@
</div> </div>
{/if} {/if}
</div> </div>
</div>
<div> <div
{#if $user?.role === 'admin'} class="px-3 flex w-full bg-transparent overflow-x-auto scrollbar-none -mx-1"
<AddToolMenu on:wheel={(e) => {
createHandler={() => { if (e.deltaY !== 0) {
goto('/workspace/tools/create'); e.preventDefault();
}} e.currentTarget.scrollLeft += e.deltaY;
importFromLinkHandler={() => { }
showImportModal = true;
}} }}
> >
<div <div
class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1" class="flex gap-0.5 w-fit text-center text-sm rounded-full bg-transparent px-1.5 whitespace-nowrap"
bind:this={tagsContainerElement}
> >
<Plus className="size-3.5" /> <ViewSelector
</div> bind:value={viewOption}
</AddToolMenu> onChange={async (value) => {
{:else} localStorage.workspaceViewOption = value;
<a
class=" px-2 py-2 rounded-xl hover:bg-gray-700/10 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition font-medium text-sm flex items-center space-x-1" await tick();
href="/workspace/tools/create" }}
> />
<Plus className="size-3.5" />
</a>
{/if}
</div>
</div> </div>
</div> </div>
<div class="mb-5 gap-2 grid lg:grid-cols-2 xl:grid-cols-3"> {#if (filteredItems ?? []).length !== 0}
<div class=" my-2 gap-2 grid px-3 lg:grid-cols-2">
{#each filteredItems as tool} {#each filteredItems as tool}
<Tooltip content={tool?.meta?.description ?? tool?.id}>
<div <div
class=" flex space-x-4 cursor-pointer w-full px-4 py-3 border border-gray-50 dark:border-gray-850 dark:hover:bg-white/5 hover:bg-black/5 rounded-2xl transition" class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2.5 dark:hover:bg-gray-850/50 hover:bg-gray-50 transition rounded-2xl"
> >
<a <a
class=" flex flex-1 space-x-3.5 cursor-pointer w-full" class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
@ -267,37 +364,20 @@
> >
<div class="flex items-center text-left"> <div class="flex items-center text-left">
<div class=" flex-1 self-center"> <div class=" flex-1 self-center">
<Tooltip content={tool?.meta?.description ?? ''} placement="top-start"> <Tooltip content={tool.id} placement="top-start">
<div class=" font-semibold flex items-center gap-1.5"> <div class="flex items-center gap-2">
<div <div class="line-clamp-1 text-sm">
class=" text-xs font-semibold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" {tool.name}
>
TOOL
</div> </div>
{#if tool?.meta?.manifest?.version} {#if tool?.meta?.manifest?.version}
<div <div class=" text-gray-500 text-xs font-medium shrink-0">
class="text-xs font-semibold px-1 rounded-sm line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
v{tool?.meta?.manifest?.version ?? ''} v{tool?.meta?.manifest?.version ?? ''}
</div> </div>
{/if} {/if}
<div class="line-clamp-1">
{tool.name}
<span class=" text-gray-500 text-xs font-medium shrink-0">{tool.id}</span>
</div>
</div> </div>
</Tooltip> </Tooltip>
<div class="px-0.5"> <div class="px-0.5">
<div class="flex gap-1.5 mt-0.5 mb-0.5">
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{tool.meta.description}
</div>
</div>
<div class="text-xs text-gray-500 shrink-0"> <div class="text-xs text-gray-500 shrink-0">
<Tooltip <Tooltip
content={tool?.user?.email ?? $i18n.t('Deleted User')} content={tool?.user?.email ?? $i18n.t('Deleted User')}
@ -404,89 +484,21 @@
{/if} {/if}
</div> </div>
</div> </div>
</Tooltip>
{/each} {/each}
</div> </div>
{:else}
{#if $user?.role === 'admin'} <div class=" w-full h-full flex flex-col justify-center items-center my-16 mb-24">
<div class=" flex justify-end w-full mb-2"> <div class="max-w-md text-center">
<div class="flex space-x-2"> <div class=" text-3xl mb-3">😕</div>
<input <div class=" text-lg font-medium mb-1">{$i18n.t('No tools found')}</div>
id="documents-import-input" <div class=" text-gray-500 text-center text-xs">
bind:this={toolsImportInputElement} {$i18n.t('Try adjusting your search or filter to find what you are looking for.')}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
showConfirm = true;
}}
/>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={() => {
toolsImportInputElement.click();
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Tools')}</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
clip-rule="evenodd"
/>
</svg>
</div> </div>
</button>
{#if tools.length}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => {
const _tools = await exportTools(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
if (_tools) {
let blob = new Blob([JSON.stringify(_tools)], {
type: 'application/json'
});
saveAs(blob, `tools-export-${Date.now()}.json`);
}
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">
{$i18n.t('Export Tools')} ({tools.length})
</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
{/if}
</div> </div>
</div> </div>
{/if} {/if}
</div>
{#if $config?.features.enable_community_sharing} {#if $config?.features.enable_community_sharing}
<div class=" my-16"> <div class=" my-16">

View file

@ -41,27 +41,27 @@
<div slot="content"> <div slot="content">
<DropdownMenu.Content <DropdownMenu.Content
class="w-full max-w-[190px] text-sm rounded-xl px-1 py-1 dark:text-white shadow-lg border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850" class="w-full max-w-[190px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
sideOffset={-2} sideOffset={6}
side="bottom" side="bottom"
align="start" align="start"
transition={flyAndScale} transition={flyAndScale}
> >
<button <button
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition" class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full"
on:click={async () => { on:click={async () => {
createHandler(); createHandler();
show = false; show = false;
}} }}
> >
<div class=" self-center mr-2"> <div class=" self-center mr-2">
<PencilSolid /> <Pencil />
</div> </div>
<div class=" self-center truncate">{$i18n.t('New Tool')}</div> <div class=" self-center truncate">{$i18n.t('New Tool')}</div>
</button> </button>
<button <button
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition" class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full"
on:click={async () => { on:click={async () => {
importFromLinkHandler(); importFromLinkHandler();
show = false; show = false;