mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 04:15:25 +00:00
refac: functions
This commit is contained in:
parent
299e0e2674
commit
f5e1a42f51
5 changed files with 536 additions and 437 deletions
|
|
@ -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]:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
############################
|
############################
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -475,6 +572,18 @@
|
||||||
</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(
|
||||||
|
|
@ -482,87 +591,8 @@
|
||||||
)}
|
)}
|
||||||
</div> -->
|
</div> -->
|
||||||
|
|
||||||
<div class=" flex justify-end w-full mb-2 px-[16px]">
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
<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}
|
{#if $config?.features.enable_community_sharing}
|
||||||
<div class=" my-16 px-[16px]">
|
<div class=" my-16">
|
||||||
<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>
|
||||||
|
|
@ -587,6 +617,7 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<DeleteConfirmDialog
|
<DeleteConfirmDialog
|
||||||
bind:show={showDeleteConfirm}
|
bind:show={showDeleteConfirm}
|
||||||
|
|
@ -670,3 +701,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
{:else}
|
||||||
|
<div class="w-full h-full flex justify-center items-center">
|
||||||
|
<Spinner className="size-5" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue