refac: functions

This commit is contained in:
Timothy Jaeryang Baek 2025-10-07 14:20:07 -05:00
parent 299e0e2674
commit f5e1a42f51
5 changed files with 536 additions and 437 deletions

View file

@ -3,7 +3,7 @@ import time
from typing import Optional from typing import Optional
from open_webui.internal.db import Base, JSONField, get_db from open_webui.internal.db import Base, JSONField, get_db
from open_webui.models.users import Users from open_webui.models.users import Users, UserModel
from open_webui.env import SRC_LOG_LEVELS from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Boolean, Column, String, Text, Index from sqlalchemy import BigInteger, Boolean, Column, String, Text, Index
@ -76,6 +76,10 @@ class FunctionWithValvesModel(BaseModel):
#################### ####################
class FunctionUserResponse(FunctionModel):
user: Optional[UserModel] = None
class FunctionResponse(BaseModel): class FunctionResponse(BaseModel):
id: str id: str
user_id: str user_id: str
@ -203,6 +207,28 @@ class FunctionsTable:
FunctionModel.model_validate(function) for function in functions FunctionModel.model_validate(function) for function in functions
] ]
def get_function_list(self) -> list[FunctionUserResponse]:
with get_db() as db:
functions = db.query(Function).order_by(Function.updated_at.desc()).all()
user_ids = list(set(func.user_id for func in functions))
users = Users.get_users_by_user_ids(user_ids) if user_ids else []
users_dict = {user.id: user for user in users}
return [
FunctionUserResponse.model_validate(
{
**FunctionModel.model_validate(func).model_dump(),
"user": (
users_dict.get(func.user_id).model_dump()
if func.user_id in users_dict
else None
),
}
)
for func in functions
]
def get_functions_by_type( def get_functions_by_type(
self, type: str, active_only=False self, type: str, active_only=False
) -> list[FunctionModel]: ) -> list[FunctionModel]:

View file

@ -10,6 +10,7 @@ from open_webui.models.functions import (
FunctionForm, FunctionForm,
FunctionModel, FunctionModel,
FunctionResponse, FunctionResponse,
FunctionUserResponse,
FunctionWithValvesModel, FunctionWithValvesModel,
Functions, Functions,
) )
@ -42,6 +43,11 @@ async def get_functions(user=Depends(get_verified_user)):
return Functions.get_functions() return Functions.get_functions()
@router.get("/list", response_model=list[FunctionUserResponse])
async def get_function_list(user=Depends(get_admin_user)):
return Functions.get_function_list()
############################ ############################
# ExportFunctions # ExportFunctions
############################ ############################

View file

@ -62,6 +62,37 @@ export const getFunctions = async (token: string = '') => {
return res; return res;
}; };
export const getFunctionList = async (token: string = '') => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/functions/list`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const loadFunctionByUrl = async (token: string = '', url: string) => { export const loadFunctionByUrl = async (token: string = '', url: string) => {
let error = null; let error = null;

View file

@ -3,7 +3,7 @@
import fileSaver from 'file-saver'; import fileSaver from 'file-saver';
const { saveAs } = fileSaver; const { saveAs } = fileSaver;
import { WEBUI_NAME, config, functions, models, settings } from '$lib/stores'; import { WEBUI_NAME, config, functions as _functions, models, settings, user } from '$lib/stores';
import { onMount, getContext, tick } from 'svelte'; import { onMount, getContext, tick } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
@ -12,6 +12,7 @@
deleteFunctionById, deleteFunctionById,
exportFunctions, exportFunctions,
getFunctionById, getFunctionById,
getFunctionList,
getFunctions, getFunctions,
loadFunctionByUrl, loadFunctionByUrl,
toggleFunctionById, toggleFunctionById,
@ -36,6 +37,10 @@
import XMark from '../icons/XMark.svelte'; import XMark from '../icons/XMark.svelte';
import AddFunctionMenu from './Functions/AddFunctionMenu.svelte'; import AddFunctionMenu from './Functions/AddFunctionMenu.svelte';
import ImportModal from '../ImportModal.svelte'; import ImportModal from '../ImportModal.svelte';
import ViewSelector from '../workspace/common/ViewSelector.svelte';
import TagSelector from '../workspace/common/TagSelector.svelte';
import { capitalizeFirstLetter } from '$lib/utils';
import Spinner from '../common/Spinner.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -44,12 +49,16 @@
let functionsImportInputElement: HTMLInputElement; let functionsImportInputElement: HTMLInputElement;
let importFiles; let importFiles;
let tagsContainerElement: HTMLDivElement;
let viewOption = '';
let query = '';
let selectedTag = '';
let selectedType = '';
let showImportModal = false; let showImportModal = false;
let showConfirm = false; let showConfirm = false;
let query = '';
let selectedType = 'all';
let showManifestModal = false; let showManifestModal = false;
let showValvesModal = false; let showValvesModal = false;
@ -57,17 +66,33 @@
let showDeleteConfirm = false; let showDeleteConfirm = false;
let loaded = false;
let functions = null;
let filteredItems = []; let filteredItems = [];
$: filteredItems = $functions
$: if (
functions &&
query !== undefined &&
selectedType !== undefined &&
viewOption !== undefined
) {
setFilteredItems();
}
const setFilteredItems = () => {
filteredItems = functions
.filter( .filter(
(f) => (f) =>
(selectedType !== 'all' ? f.type === selectedType : true) && (selectedType !== '' ? f.type === selectedType : true) &&
(query === '' || (query === '' ||
f.name.toLowerCase().includes(query.toLowerCase()) || f.name.toLowerCase().includes(query.toLowerCase()) ||
f.id.toLowerCase().includes(query.toLowerCase())) f.id.toLowerCase().includes(query.toLowerCase())) &&
(viewOption === '' ||
(viewOption === 'created' && f.user_id === $user?.id) ||
(viewOption === 'shared' && f.user_id !== $user?.id))
) )
.sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name)); .sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
};
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) => {
toast.error(`${error}`); toast.error(`${error}`);
@ -134,7 +159,7 @@
if (res) { if (res) {
toast.success($i18n.t('Function deleted successfully')); toast.success($i18n.t('Function deleted successfully'));
functions.set(await getFunctions(localStorage.token)); _functions.set(await getFunctions(localStorage.token));
models.set( models.set(
await getModels( await getModels(
localStorage.token, localStorage.token,
@ -162,7 +187,7 @@
: toast.success($i18n.t('Function is now globally disabled')); : toast.success($i18n.t('Function is now globally disabled'));
} }
functions.set(await getFunctions(localStorage.token)); _functions.set(await getFunctions(localStorage.token));
models.set( models.set(
await getModels( await getModels(
localStorage.token, localStorage.token,
@ -174,7 +199,16 @@
} }
}; };
onMount(() => { onMount(async () => {
viewOption = localStorage?.workspaceViewOption || '';
functions = await getFunctionList(localStorage.token).catch((error) => {
toast.error(`${error}`);
return [];
});
await tick();
loaded = true;
const onKeyDown = (event) => { const onKeyDown = (event) => {
if (event.key === 'Shift') { if (event.key === 'Shift') {
shiftKey = true; shiftKey = true;
@ -222,16 +256,95 @@
}} }}
/> />
<div class="flex flex-col mt-1.5 mb-0.5 px-[16px]"> {#if loaded}
<div class="flex justify-between items-center mb-1"> <div class="px-4.5 w-full">
<div class="flex md:self-center text-xl items-center font-medium px-0.5"> <div class="flex flex-col gap-1 px-1 mt-2.5 mb-2">
<div class="flex justify-between items-center mb-1 w-full">
<input
id="documents-import-input"
bind:this={functionsImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
showConfirm = true;
}}
/>
<div class="flex justify-between items-center w-full">
<div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
<div>
{$i18n.t('Functions')} {$i18n.t('Functions')}
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" /> </div>
<span class="text-base font-lg 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={() => {
functionsImportInputElement.click();
}}
>
<div class=" self-center font-medium line-clamp-1">
{$i18n.t('Import')}
</div>
</button>
{#if functions.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 _functions = await exportFunctions(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
if (_functions) {
let blob = new Blob([JSON.stringify(_functions)], {
type: 'application/json'
});
saveAs(blob, `functions-export-${Date.now()}.json`);
}
}}
>
<div class=" self-center font-medium line-clamp-1">
{$i18n.t('Export')}
</div>
</button>
{/if}
{/if}
<AddFunctionMenu
createHandler={() => {
goto('/admin/functions/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 Function')}</div>
</div>
</AddFunctionMenu>
</div>
</div>
</div>
</div>
<div
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100 dark:border-gray-850"
>
<div class="px-3.5 flex flex-1 items-center w-full space-x-2 py-0.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" />
@ -255,69 +368,43 @@
</div> </div>
{/if} {/if}
</div> </div>
</div>
<div> <div
<AddFunctionMenu class="px-3 flex w-full bg-transparent overflow-x-auto scrollbar-none"
createHandler={() => { on:wheel={(e) => {
goto('/admin/functions/create'); if (e.deltaY !== 0) {
}} e.preventDefault();
importFromLinkHandler={() => { e.currentTarget.scrollLeft += e.deltaY;
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-0.5 whitespace-nowrap"
bind:this={tagsContainerElement}
> >
<Plus className="size-3.5" /> <ViewSelector
</div> bind:value={viewOption}
</AddFunctionMenu> onChange={async (value) => {
localStorage.workspaceViewOption = value;
await tick();
}}
/>
<TagSelector
bind:value={selectedType}
items={[
{ value: 'pipe', label: $i18n.t('Pipe') },
{ value: 'filter', label: $i18n.t('Filter') },
{ value: 'action', label: $i18n.t('Action') }
]}
/>
</div> </div>
</div> </div>
<div class=" flex w-full"> {#if (filteredItems ?? []).length !== 0}
<div <div class="px-3 my-2 gap-1 lg:gap-2 grid lg:grid-cols-2">
class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent"
>
<button
class="min-w-fit p-1.5 {selectedType === 'all'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
on:click={() => {
selectedType = 'all';
}}>{$i18n.t('All')}</button
>
<button
class="min-w-fit p-1.5 {selectedType === 'pipe'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
on:click={() => {
selectedType = 'pipe';
}}>{$i18n.t('Pipe')}</button
>
<button
class="min-w-fit p-1.5 {selectedType === 'filter'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
on:click={() => {
selectedType = 'filter';
}}>{$i18n.t('Filter')}</button
>
<button
class="min-w-fit p-1.5 {selectedType === 'action'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
on:click={() => {
selectedType = 'action';
}}>{$i18n.t('Action')}</button
>
</div>
</div>
</div>
<div class="mb-5 px-[16px]">
{#each filteredItems as func (func.id)} {#each filteredItems as func (func.id)}
<div <div
class=" flex space-x-4 cursor-pointer w-full px-2 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl" class=" flex space-x-4 cursor-pointer w-full px-2 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
@ -328,29 +415,39 @@
> >
<div class="flex items-center text-left"> <div class="flex items-center text-left">
<div class=" flex-1 self-center pl-1"> <div class=" flex-1 self-center pl-1">
<div class=" font-semibold flex items-center gap-1.5"> <Tooltip content={func.id} placement="top-start">
<div class=" flex items-center gap-1.5">
<div <div
class=" text-xs font-semibold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" class=" text-xs font-semibold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
> >
{func.type} {func.type}
</div> </div>
<div class="line-clamp-1 text-sm">
{func.name}
</div>
{#if func?.meta?.manifest?.version} {#if func?.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{func?.meta?.manifest?.version ?? ''} v{func?.meta?.manifest?.version ?? ''}
</div> </div>
{/if} {/if}
<div class=" line-clamp-1">
{func.name}
</div>
</div> </div>
</Tooltip>
<div class="flex gap-1.5 px-1"> <div class="flex gap-1.5 px-1">
<div class=" text-gray-500 text-xs font-medium shrink-0">{func.id}</div> <div class="text-xs text-gray-500 shrink-0">
<Tooltip
content={func?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
placement="top-start"
>
{$i18n.t('By {{name}}', {
name: capitalizeFirstLetter(
func?.user?.name ?? func?.user?.email ?? $i18n.t('Deleted User')
)
})}
</Tooltip>
</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1"> <div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{func.meta.description} {func.meta.description}
</div> </div>
@ -474,95 +571,28 @@
</div> </div>
</div> </div>
{/each} {/each}
</div> </div>
{:else}
<div class=" w-full h-full flex flex-col justify-center items-center my-16 mb-24">
<div class="max-w-md text-center">
<div class=" text-3xl mb-3">😕</div>
<div class=" text-lg font-medium mb-1">{$i18n.t('No functions found')}</div>
<div class=" text-gray-500 text-center text-xs">
{$i18n.t('Try adjusting your search or filter to find what you are looking for.')}
</div>
</div>
</div>
{/if}
</div>
<!-- <div class=" text-gray-500 text-xs mt-1 mb-2"> <!-- <div class=" text-gray-500 text-xs mt-1 mb-2">
{$i18n.t( {$i18n.t(
'Admins have access to all tools at all times; users need tools assigned per model in the workspace.' 'Admins have access to all tools at all times; users need tools assigned per model in the workspace.'
)} )}
</div> --> </div> -->
<div class=" flex justify-end w-full mb-2 px-[16px]"> {#if $config?.features.enable_community_sharing}
<div class="flex space-x-2"> <div class=" my-16">
<input
id="documents-import-input"
bind:this={functionsImportInputElement}
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={() => {
functionsImportInputElement.click();
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Functions')}</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>
</button>
{#if $functions.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 _functions = await exportFunctions(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
if (_functions) {
let blob = new Blob([JSON.stringify(_functions)], {
type: 'application/json'
});
saveAs(blob, `functions-export-${Date.now()}.json`);
}
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">
{$i18n.t('Export Functions')} ({$functions.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>
{#if $config?.features.enable_community_sharing}
<div class=" my-16 px-[16px]">
<div class=" text-xl font-medium mb-1 line-clamp-1"> <div class=" text-xl font-medium mb-1 line-clamp-1">
{$i18n.t('Made by Open WebUI Community')} {$i18n.t('Made by Open WebUI Community')}
</div> </div>
@ -586,22 +616,23 @@
</div> </div>
</a> </a>
</div> </div>
{/if} {/if}
</div>
<DeleteConfirmDialog <DeleteConfirmDialog
bind:show={showDeleteConfirm} bind:show={showDeleteConfirm}
title={$i18n.t('Delete function?')} title={$i18n.t('Delete function?')}
on:confirm={() => { on:confirm={() => {
deleteHandler(selectedFunction); deleteHandler(selectedFunction);
}} }}
> >
<div class=" text-sm text-gray-500 truncate"> <div class=" text-sm text-gray-500 truncate">
{$i18n.t('This will delete')} <span class=" font-semibold">{selectedFunction.name}</span>. {$i18n.t('This will delete')} <span class=" font-semibold">{selectedFunction.name}</span>.
</div> </div>
</DeleteConfirmDialog> </DeleteConfirmDialog>
<ManifestModal bind:show={showManifestModal} manifest={selectedFunction?.meta?.manifest ?? {}} /> <ManifestModal bind:show={showManifestModal} manifest={selectedFunction?.meta?.manifest ?? {}} />
<ValvesModal <ValvesModal
bind:show={showValvesModal} bind:show={showValvesModal}
type="function" type="function"
id={selectedFunction?.id ?? null} id={selectedFunction?.id ?? null}
@ -616,9 +647,9 @@
) )
); );
}} }}
/> />
<ConfirmDialog <ConfirmDialog
bind:show={showConfirm} bind:show={showConfirm}
on:confirm={() => { on:confirm={() => {
const reader = new FileReader(); const reader = new FileReader();
@ -652,7 +683,7 @@
reader.readAsText(importFiles[0]); reader.readAsText(importFiles[0]);
}} }}
> >
<div class="text-sm text-gray-500"> <div class="text-sm text-gray-500">
<div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3"> <div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3">
<div>{$i18n.t('Please carefully review the following warnings:')}</div> <div>{$i18n.t('Please carefully review the following warnings:')}</div>
@ -669,4 +700,9 @@
)} )}
</div> </div>
</div> </div>
</ConfirmDialog> </ConfirmDialog>
{:else}
<div class="w-full h-full flex justify-center items-center">
<Spinner className="size-5" />
</div>
{/if}

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 p-1 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg font-primary" 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 Function')}</div> <div class=" self-center truncate">{$i18n.t('New Function')}</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;