refac
This commit is contained in:
1 parent
421da67446
commit
d0bea60581
7 files changed
+558
-618
No files matched your search
@@ -7,7 +7,7 @@
|
|||||||
import { extractFrontmatter, formatSkillName, nameToId } from '$lib/utils';
|
import { extractFrontmatter, formatSkillName, nameToId } from '$lib/utils';
|
||||||
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
||||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
import Badge from '$lib/components/common/Badge.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
let loading = false;
|
let loading = false;
|
||||||
let showConfirm = false;
|
let showConfirm = false;
|
||||||
|
|
||||||
export let onSave = () => {};
|
export let onSave = /** @param {any} _value */ async (_value) => {};
|
||||||
|
|
||||||
export let edit = false;
|
export let edit = false;
|
||||||
export let clone = false;
|
export let clone = false;
|
||||||
@@ -305,12 +305,16 @@ class Pipe:
|
|||||||
|
|
||||||
const saveHandler = async () => {
|
const saveHandler = async () => {
|
||||||
loading = true;
|
loading = true;
|
||||||
onSave({
|
try {
|
||||||
id,
|
await onSave({
|
||||||
name,
|
id,
|
||||||
meta,
|
name,
|
||||||
content
|
meta,
|
||||||
});
|
content
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitHandler = async () => {
|
const submitHandler = async () => {
|
||||||
@@ -333,146 +337,148 @@ class Pipe:
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class=" flex flex-col justify-between w-full min-w-0 overflow-y-auto overflow-x-hidden h-full">
|
<div class="flex h-full w-full min-w-0 flex-col overflow-hidden">
|
||||||
<div class="mx-auto w-full min-w-0 md:px-0 h-full">
|
<form
|
||||||
<form
|
bind:this={formElement}
|
||||||
bind:this={formElement}
|
class="flex h-full min-h-0 min-w-0 flex-col"
|
||||||
class=" flex flex-col min-w-0 max-h-[100dvh] h-full"
|
on:submit|preventDefault={() => {
|
||||||
on:submit|preventDefault={() => {
|
if (edit) {
|
||||||
if (edit) {
|
submitHandler();
|
||||||
submitHandler();
|
} else {
|
||||||
} else {
|
showConfirm = true;
|
||||||
showConfirm = true;
|
}
|
||||||
}
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="mb-1 flex h-6 w-fit items-center gap-1 rounded-md px-0.5 text-xs text-gray-400 transition-colors duration-75 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
goto('/admin/functions');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col flex-1 min-w-0 overflow-auto h-0 rounded-lg">
|
<ChevronLeft className="size-3" strokeWidth="2" />
|
||||||
<div class="w-full mb-2 flex flex-col gap-0.5">
|
<span>{$i18n.t('Back')}</span>
|
||||||
<div class="flex w-full min-w-0 items-center">
|
</button>
|
||||||
<div class=" shrink-0 mr-2">
|
|
||||||
<Tooltip content={$i18n.t('Back')}>
|
<div class="flex shrink-0 items-start gap-2 pb-2">
|
||||||
<button
|
<div class="min-w-0 flex-1">
|
||||||
class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
|
<Tooltip content={$i18n.t('e.g. My Filter')} placement="top-start">
|
||||||
on:click={() => {
|
<input
|
||||||
goto('/admin/functions');
|
class="w-full bg-transparent text-sm outline-hidden"
|
||||||
}}
|
type="text"
|
||||||
type="button"
|
placeholder={$i18n.t('Function Name')}
|
||||||
>
|
aria-label={$i18n.t('Function Name')}
|
||||||
<ChevronLeft strokeWidth="2.5" />
|
bind:value={name}
|
||||||
</button>
|
required
|
||||||
</Tooltip>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<div class="mt-0.5 flex min-w-0 items-center gap-2 text-xs text-gray-500">
|
||||||
|
{#if edit}
|
||||||
|
<div class="shrink-0 truncate font-mono" title={id}>
|
||||||
|
{id}
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<Tooltip content={$i18n.t('e.g. My Filter')} placement="top-start">
|
|
||||||
<input
|
|
||||||
class="w-full text-2xl font-normal bg-transparent outline-hidden"
|
|
||||||
type="text"
|
|
||||||
placeholder={$i18n.t('Function Name')}
|
|
||||||
bind:value={name}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex shrink-0 items-center gap-2">
|
|
||||||
{#if !edit}
|
|
||||||
<select
|
|
||||||
class="text-xs bg-transparent border border-gray-100 dark:border-gray-800 rounded-lg px-2 py-1 outline-hidden"
|
|
||||||
bind:value={starterType}
|
|
||||||
on:change={(event) => selectStarterType(event.currentTarget.value)}
|
|
||||||
aria-label={$i18n.t('Function starter')}
|
|
||||||
>
|
|
||||||
<option value="filter">{$i18n.t('Filter')}</option>
|
|
||||||
<option value="event">{$i18n.t('Event')}</option>
|
|
||||||
</select>
|
|
||||||
{/if}
|
|
||||||
<Badge type="muted" content={$i18n.t('Function')} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" flex min-w-0 gap-2 px-1 items-center">
|
|
||||||
{#if edit}
|
|
||||||
<div class="text-sm text-gray-500 shrink-0">
|
|
||||||
{id}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<Tooltip className="w-full" content={$i18n.t('e.g. my_filter')} placement="top-start">
|
|
||||||
<input
|
|
||||||
class="w-full text-sm disabled:text-gray-500 bg-transparent outline-hidden"
|
|
||||||
type="text"
|
|
||||||
placeholder={$i18n.t('Function ID')}
|
|
||||||
bind:value={id}
|
|
||||||
required
|
|
||||||
disabled={edit}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className="w-full self-center items-center flex"
|
className="min-w-[8rem] flex-1"
|
||||||
content={$i18n.t('e.g. A filter to remove profanity from text')}
|
content={$i18n.t('e.g. my_filter')}
|
||||||
placement="top-start"
|
placement="top-start"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="w-full text-sm bg-transparent outline-hidden"
|
class="w-full bg-transparent font-mono outline-hidden disabled:text-gray-500"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Function Description')}
|
placeholder={$i18n.t('Function ID')}
|
||||||
bind:value={meta.description}
|
aria-label={$i18n.t('Function ID')}
|
||||||
|
bind:value={id}
|
||||||
required
|
required
|
||||||
|
disabled={edit}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-2 flex-1 min-w-0 overflow-auto h-0 rounded-lg">
|
<Tooltip
|
||||||
<CodeEditor
|
className="flex min-w-0 flex-1 items-center"
|
||||||
bind:this={codeEditor}
|
content={$i18n.t('e.g. A filter to remove profanity from text')}
|
||||||
value={content}
|
placement="top-start"
|
||||||
lang="python"
|
|
||||||
{boilerplate}
|
|
||||||
onChange={(e) => {
|
|
||||||
_content = e;
|
|
||||||
if (!edit) {
|
|
||||||
const fm = extractFrontmatter(e);
|
|
||||||
if (fm.title && !name) {
|
|
||||||
name = formatSkillName(fm.title);
|
|
||||||
id = nameToId(fm.title);
|
|
||||||
}
|
|
||||||
if (fm.description && !meta.description) {
|
|
||||||
meta = { ...meta, description: fm.description };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onSave={async () => {
|
|
||||||
if (formElement) {
|
|
||||||
formElement.requestSubmit();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pb-3 flex justify-between">
|
|
||||||
<div class="flex-1 pr-3">
|
|
||||||
<div class="text-xs text-gray-500 line-clamp-2">
|
|
||||||
<span class=" font-normal dark:text-gray-200">{$i18n.t('Warning:')}</span>
|
|
||||||
{$i18n.t('Functions allow arbitrary code execution.')} <br />—
|
|
||||||
<span class=" font-normal dark:text-gray-400"
|
|
||||||
>{$i18n.t(`don't install random functions from sources you don't trust.`)}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
|
||||||
type="submit"
|
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
<input
|
||||||
</button>
|
class="w-full bg-transparent outline-hidden"
|
||||||
|
type="text"
|
||||||
|
placeholder={$i18n.t('Function Description')}
|
||||||
|
aria-label={$i18n.t('Function Description')}
|
||||||
|
bind:value={meta.description}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
<div class="flex shrink-0 items-center gap-1">
|
||||||
|
{#if !edit}
|
||||||
|
<select
|
||||||
|
class="h-7 rounded-lg border border-gray-100 bg-transparent px-2 text-xs outline-hidden dark:border-gray-800"
|
||||||
|
bind:value={starterType}
|
||||||
|
on:change={(event) => selectStarterType(event.currentTarget.value)}
|
||||||
|
aria-label={$i18n.t('Function starter')}
|
||||||
|
>
|
||||||
|
<option value="filter">{$i18n.t('Filter')}</option>
|
||||||
|
<option value="event">{$i18n.t('Event')}</option>
|
||||||
|
</select>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-h-0 flex-1 overflow-hidden rounded-lg">
|
||||||
|
<CodeEditor
|
||||||
|
bind:this={codeEditor}
|
||||||
|
value={content}
|
||||||
|
lang="python"
|
||||||
|
{boilerplate}
|
||||||
|
className="text-[11px]"
|
||||||
|
onChange={(e) => {
|
||||||
|
_content = e;
|
||||||
|
if (!edit) {
|
||||||
|
const fm = extractFrontmatter(e);
|
||||||
|
if (fm.title && !name) {
|
||||||
|
name = formatSkillName(fm.title);
|
||||||
|
id = nameToId(fm.title);
|
||||||
|
}
|
||||||
|
if (fm.description && !meta.description) {
|
||||||
|
meta = { ...meta, description: fm.description };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSave={async () => {
|
||||||
|
if (formElement) {
|
||||||
|
formElement.requestSubmit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="shrink-0 py-2 text-xs text-gray-500">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<span class="font-normal dark:text-gray-200">{$i18n.t('Warning:')}</span>
|
||||||
|
{$i18n.t('Functions can execute arbitrary code.')}
|
||||||
|
<span class="font-normal dark:text-gray-400">
|
||||||
|
{$i18n.t('Only install functions from sources you trust.')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="flex h-7 shrink-0 items-center gap-1.5 rounded-lg bg-gray-900 px-2.5 text-xs text-white transition hover:bg-black disabled:opacity-60 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white"
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{$i18n.t(edit ? 'Save' : 'Save & Create')}
|
||||||
|
{#if loading}
|
||||||
|
<Spinner className="size-3" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
import Sortable from 'sortablejs';
|
||||||
import fileSaver from 'file-saver';
|
import fileSaver from 'file-saver';
|
||||||
const { saveAs } = fileSaver;
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
import { onMount, getContext, tick } from 'svelte';
|
import { onMount, onDestroy, getContext, tick } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
import { config, models as _models, settings, user } from '$lib/stores';
|
import { config, models as _models, settings, user } from '$lib/stores';
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
import { updateUserSettings } from '$lib/apis/users';
|
import { updateUserSettings } from '$lib/apis/users';
|
||||||
|
|
||||||
import { getModels } from '$lib/apis';
|
import { getModels } from '$lib/apis';
|
||||||
|
import { getModelsConfig, setModelsConfig } from '$lib/apis/configs';
|
||||||
import Search from '$lib/components/icons/Search.svelte';
|
import Search from '$lib/components/icons/Search.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
@@ -38,19 +40,20 @@
|
|||||||
import Eye from '$lib/components/icons/Eye.svelte';
|
import Eye from '$lib/components/icons/Eye.svelte';
|
||||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||||
import CheckCircle from '$lib/components/icons/CheckCircle.svelte';
|
import CheckCircle from '$lib/components/icons/CheckCircle.svelte';
|
||||||
|
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
||||||
import Minus from '$lib/components/icons/Minus.svelte';
|
import Minus from '$lib/components/icons/Minus.svelte';
|
||||||
import DocumentArrowUp from '$lib/components/icons/DocumentArrowUp.svelte';
|
import DocumentArrowUp from '$lib/components/icons/DocumentArrowUp.svelte';
|
||||||
import Download from '$lib/components/icons/Download.svelte';
|
import Download from '$lib/components/icons/Download.svelte';
|
||||||
|
import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
|
||||||
import Wrench from '$lib/components/icons/Wrench.svelte';
|
import Wrench from '$lib/components/icons/Wrench.svelte';
|
||||||
import SettingsIcon from '$lib/components/icons/Settings.svelte';
|
import SettingsIcon from '$lib/components/icons/Settings.svelte';
|
||||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
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';
|
||||||
import AdminViewSelector from './Models/AdminViewSelector.svelte';
|
import AdminViewSelector from './Models/AdminViewSelector.svelte';
|
||||||
import TagSelector from '$lib/components/workspace/common/TagSelector.svelte';
|
import TagSelector from '$lib/components/workspace/common/TagSelector.svelte';
|
||||||
import Pagination from '$lib/components/common/Pagination.svelte';
|
|
||||||
|
|
||||||
type ModelListItem = { id: string; name?: string };
|
type ModelListItem = { id: string; name?: string };
|
||||||
|
|
||||||
@@ -62,8 +65,12 @@
|
|||||||
let importFiles;
|
let importFiles;
|
||||||
let modelsImportInputElement: HTMLInputElement;
|
let modelsImportInputElement: HTMLInputElement;
|
||||||
let tagsContainerElement: HTMLDivElement;
|
let tagsContainerElement: HTMLDivElement;
|
||||||
|
let modelListElement: HTMLDivElement;
|
||||||
|
let sortable = null;
|
||||||
|
|
||||||
let models = null;
|
let models = null;
|
||||||
|
let modelsConfig = null;
|
||||||
|
let modelOrderList: string[] = [];
|
||||||
|
|
||||||
let workspaceModels: ModelListItem[] = [];
|
let workspaceModels: ModelListItem[] = [];
|
||||||
let baseModels: ModelListItem[] = [];
|
let baseModels: ModelListItem[] = [];
|
||||||
@@ -73,6 +80,9 @@
|
|||||||
|
|
||||||
let showConfigModal = false;
|
let showConfigModal = false;
|
||||||
let showManageModal = false;
|
let showManageModal = false;
|
||||||
|
let showResetModal = false;
|
||||||
|
let savingModelOrder = false;
|
||||||
|
let modelOrderDirty = false;
|
||||||
|
|
||||||
let viewOption = ''; // '' = All, 'enabled', 'disabled', 'visible', 'hidden'
|
let viewOption = ''; // '' = All, 'enabled', 'disabled', 'visible', 'hidden'
|
||||||
let tags: string[] = [];
|
let tags: string[] = [];
|
||||||
@@ -83,9 +93,6 @@
|
|||||||
tabState = null;
|
tabState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const perPage = 30;
|
|
||||||
let currentPage = 1;
|
|
||||||
|
|
||||||
const isPublicModel = (model) => {
|
const isPublicModel = (model) => {
|
||||||
return (model?.access_grants ?? []).some(
|
return (model?.access_grants ?? []).some(
|
||||||
(g) => g.principal_type === 'user' && g.principal_id === '*' && g.permission === 'read'
|
(g) => g.principal_type === 'user' && g.principal_id === '*' && g.permission === 'read'
|
||||||
@@ -115,6 +122,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
$: if (models) {
|
$: if (models) {
|
||||||
|
const modelOrder = new Map(modelOrderList.map((id, idx) => [id, idx]));
|
||||||
|
|
||||||
filteredModels = models
|
filteredModels = models
|
||||||
.filter((m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase()))
|
.filter((m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase()))
|
||||||
.filter((m) => {
|
.filter((m) => {
|
||||||
@@ -127,15 +136,21 @@
|
|||||||
return true; // All
|
return true; // All
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
|
const orderA = modelOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER;
|
||||||
|
const orderB = modelOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
|
if (orderA !== orderB) {
|
||||||
|
return orderA - orderB;
|
||||||
|
}
|
||||||
|
|
||||||
return (a?.name ?? a?.id ?? '').localeCompare(b?.name ?? b?.id ?? '');
|
return (a?.name ?? a?.id ?? '').localeCompare(b?.name ?? b?.id ?? '');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let searchValue = '';
|
let searchValue = '';
|
||||||
|
let canReorderModels = false;
|
||||||
|
|
||||||
$: if (searchValue || viewOption !== undefined) {
|
$: canReorderModels = searchValue === '' && viewOption === '' && selectedTag === '';
|
||||||
currentPage = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const enableAllHandler = async () => {
|
const enableAllHandler = async () => {
|
||||||
const modelsToEnable = filteredModels.filter((m) => !(m.is_active ?? true));
|
const modelsToEnable = filteredModels.filter((m) => !(m.is_active ?? true));
|
||||||
@@ -213,6 +228,9 @@
|
|||||||
const init = async () => {
|
const init = async () => {
|
||||||
models = null;
|
models = null;
|
||||||
|
|
||||||
|
modelsConfig = await getModelsConfig(localStorage.token);
|
||||||
|
modelOrderList = modelsConfig?.MODEL_ORDER_LIST ?? [];
|
||||||
|
|
||||||
tags = await getBaseModelTags(localStorage.token);
|
tags = await getBaseModelTags(localStorage.token);
|
||||||
if (selectedTag && !tags.includes(selectedTag)) {
|
if (selectedTag && !tags.includes(selectedTag)) {
|
||||||
selectedTag = '';
|
selectedTag = '';
|
||||||
@@ -243,6 +261,15 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelOrderList = [
|
||||||
|
...modelOrderList.filter((id) => models.some((model) => model.id === id)),
|
||||||
|
...models
|
||||||
|
.map((model) => model.id)
|
||||||
|
.filter((id) => !modelOrderList.includes(id))
|
||||||
|
.sort((a, b) => a.localeCompare(b))
|
||||||
|
];
|
||||||
|
modelOrderDirty = false;
|
||||||
|
|
||||||
_models.set(
|
_models.set(
|
||||||
await getModels(
|
await getModels(
|
||||||
localStorage.token,
|
localStorage.token,
|
||||||
@@ -251,6 +278,79 @@
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveModelOrder = async (orderedModelIds: string[]) => {
|
||||||
|
savingModelOrder = true;
|
||||||
|
|
||||||
|
const res = await setModelsConfig(localStorage.token, {
|
||||||
|
DEFAULT_MODELS: modelsConfig?.DEFAULT_MODELS ?? null,
|
||||||
|
DEFAULT_PINNED_MODELS: modelsConfig?.DEFAULT_PINNED_MODELS ?? null,
|
||||||
|
MODEL_ORDER_LIST: orderedModelIds,
|
||||||
|
DEFAULT_MODEL_METADATA: modelsConfig?.DEFAULT_MODEL_METADATA ?? null,
|
||||||
|
DEFAULT_MODEL_PARAMS: modelsConfig?.DEFAULT_MODEL_PARAMS ?? null
|
||||||
|
}).catch((error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
modelsConfig = res;
|
||||||
|
modelOrderDirty = false;
|
||||||
|
toast.success($i18n.t('Model order saved successfully'));
|
||||||
|
_models.set(
|
||||||
|
await getModels(
|
||||||
|
localStorage.token,
|
||||||
|
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
savingModelOrder = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const positionChangeHandler = async (event) => {
|
||||||
|
const { oldIndex, newIndex, item } = event;
|
||||||
|
|
||||||
|
if (oldIndex === undefined || newIndex === undefined || oldIndex === newIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = item.parentNode;
|
||||||
|
const target = parent.children[oldIndex < newIndex ? oldIndex : oldIndex + 1];
|
||||||
|
parent.insertBefore(item, target);
|
||||||
|
|
||||||
|
const updatedModels = [...filteredModels];
|
||||||
|
const [movedModel] = updatedModels.splice(oldIndex, 1);
|
||||||
|
updatedModels.splice(newIndex, 0, movedModel);
|
||||||
|
|
||||||
|
const orderedIds = updatedModels.map((model) => model.id);
|
||||||
|
const orderedSet = new Set(orderedIds);
|
||||||
|
|
||||||
|
models = [...updatedModels, ...models.filter((model) => !orderedSet.has(model.id))];
|
||||||
|
modelOrderList = models.map((model) => model.id);
|
||||||
|
modelOrderDirty = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initSortable = () => {
|
||||||
|
if (sortable) {
|
||||||
|
sortable.destroy();
|
||||||
|
sortable = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelListElement && filteredModels.length > 0 && canReorderModels) {
|
||||||
|
sortable = new Sortable(modelListElement, {
|
||||||
|
animation: 150,
|
||||||
|
handle: '.model-item-handle',
|
||||||
|
onUpdate: positionChangeHandler
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$: if (modelListElement && filteredModels) {
|
||||||
|
tick().then(() => {
|
||||||
|
initSortable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const upsertModelHandler = async (model, overrides = {}, showToast = true) => {
|
const upsertModelHandler = async (model, overrides = {}, showToast = true) => {
|
||||||
model = { ...model, base_model_id: null, ...overrides };
|
model = { ...model, base_model_id: null, ...overrides };
|
||||||
|
|
||||||
@@ -436,8 +536,27 @@
|
|||||||
window.removeEventListener('blur', onBlur);
|
window.removeEventListener('blur', onBlur);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (sortable) {
|
||||||
|
sortable.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
title={$i18n.t('Reset All Models')}
|
||||||
|
message={$i18n.t('This will delete all models including custom models and cannot be undone.')}
|
||||||
|
bind:show={showResetModal}
|
||||||
|
onConfirm={async () => {
|
||||||
|
const res = await deleteAllModels(localStorage.token);
|
||||||
|
if (res) {
|
||||||
|
toast.success($i18n.t('All models deleted successfully'));
|
||||||
|
await init();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<ModelSettingsModal bind:show={showConfigModal} initHandler={init} />
|
<ModelSettingsModal bind:show={showConfigModal} initHandler={init} />
|
||||||
<ManageModelsModal bind:show={showManageModal} />
|
<ManageModelsModal bind:show={showManageModal} />
|
||||||
|
|
||||||
@@ -552,7 +671,6 @@
|
|||||||
return { value: tag, label: tag };
|
return { value: tag, label: tag };
|
||||||
})}
|
})}
|
||||||
onChange={async () => {
|
onChange={async () => {
|
||||||
currentPage = 1;
|
|
||||||
await init();
|
await init();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -608,6 +726,17 @@
|
|||||||
<div class="flex items-center">{$i18n.t('Manage')}</div>
|
<div class="flex items-center">{$i18n.t('Manage')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] hover:text-gray-900 dark:hover:text-gray-100"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
showResetModal = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GarbageBin className="size-3.5" />
|
||||||
|
<div class="flex items-center">{$i18n.t('Reset')}</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
<hr class="mx-1 my-0.5 border-gray-100 dark:border-gray-800" />
|
<hr class="mx-1 my-0.5 border-gray-100 dark:border-gray-800" />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -666,16 +795,31 @@
|
|||||||
? 'overflow-y-auto scrollbar-hover pr-1.5'
|
? 'overflow-y-auto scrollbar-hover pr-1.5'
|
||||||
: 'overflow-hidden'}"
|
: 'overflow-hidden'}"
|
||||||
id="model-list"
|
id="model-list"
|
||||||
|
bind:this={modelListElement}
|
||||||
>
|
>
|
||||||
{#if filteredModels.length > 0}
|
{#if filteredModels.length > 0}
|
||||||
{#each filteredModels.slice((currentPage - 1) * perPage, currentPage * perPage) as model, modelIdx (`${model.id}-${modelIdx}`)}
|
{#each filteredModels as model, modelIdx (`${model.id}-${modelIdx}`)}
|
||||||
<div
|
<div
|
||||||
class="flex cursor-pointer transition w-full px-2 py-1 rounded-xl hover:bg-gray-50/70 dark:hover:bg-gray-850/50 {model
|
class="flex cursor-pointer transition w-full px-2 py-1 rounded-xl hover:bg-gray-50/70 dark:hover:bg-gray-850/50 {model
|
||||||
?.meta?.hidden
|
?.meta?.hidden
|
||||||
? 'opacity-50 dark:opacity-50'
|
? 'opacity-50 dark:opacity-50'
|
||||||
: ''}"
|
: ''}"
|
||||||
id="model-item-{model.id}"
|
id="model-item-{model.id}"
|
||||||
>
|
>
|
||||||
|
<div class="self-center pr-1 text-gray-400 dark:text-gray-600">
|
||||||
|
<Tooltip
|
||||||
|
content={canReorderModels
|
||||||
|
? $i18n.t('Drag to reorder')
|
||||||
|
: $i18n.t('Clear filters to reorder')}
|
||||||
|
>
|
||||||
|
<EllipsisVertical
|
||||||
|
className="size-4 {canReorderModels
|
||||||
|
? 'cursor-move model-item-handle'
|
||||||
|
: 'opacity-40'}"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="flex group/item gap-2.5 w-full min-w-0 flex-1 text-left cursor-pointer"
|
class="flex group/item gap-2.5 w-full min-w-0 flex-1 text-left cursor-pointer"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -841,9 +985,23 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if filteredModels.length > perPage}
|
<div class="flex justify-end pt-6 text-sm font-normal">
|
||||||
<Pagination bind:page={currentPage} count={filteredModels.length} {perPage} />
|
<button
|
||||||
{/if}
|
class="flex items-center gap-2 px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
type="button"
|
||||||
|
disabled={!modelOrderDirty || savingModelOrder}
|
||||||
|
on:click={async () => {
|
||||||
|
await saveModelOrder(modelOrderList);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$i18n.t('Save')}
|
||||||
|
{#if savingModelOrder}
|
||||||
|
<span class="shrink-0">
|
||||||
|
<Spinner />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Sortable from 'sortablejs';
|
|
||||||
|
|
||||||
import { createEventDispatcher, getContext, onMount, onDestroy, tick } from 'svelte';
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
import { models } from '$lib/stores';
|
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
|
||||||
import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
|
|
||||||
|
|
||||||
export let modelIds = [];
|
|
||||||
|
|
||||||
let sortable = null;
|
|
||||||
let modelListElement = null;
|
|
||||||
|
|
||||||
const positionChangeHandler = (event) => {
|
|
||||||
const { oldIndex, newIndex, item } = event;
|
|
||||||
|
|
||||||
// Revert SortableJS's DOM manipulation so Svelte doesn't get out of sync.
|
|
||||||
// Svelte expects the DOM to match its virtual DOM before it applies state updates.
|
|
||||||
const parent = item.parentNode;
|
|
||||||
const target = parent.children[oldIndex < newIndex ? oldIndex : oldIndex + 1];
|
|
||||||
parent.insertBefore(item, target);
|
|
||||||
|
|
||||||
// Now apply the logical state update, letting Svelte handle the real DOM move.
|
|
||||||
const updatedIds = [...modelIds];
|
|
||||||
const [movedId] = updatedIds.splice(oldIndex, 1);
|
|
||||||
updatedIds.splice(newIndex, 0, movedId);
|
|
||||||
modelIds = updatedIds;
|
|
||||||
};
|
|
||||||
|
|
||||||
const initSortable = () => {
|
|
||||||
if (sortable) {
|
|
||||||
sortable.destroy();
|
|
||||||
sortable = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modelListElement) {
|
|
||||||
sortable = new Sortable(modelListElement, {
|
|
||||||
animation: 150,
|
|
||||||
handle: '.model-item-handle',
|
|
||||||
onUpdate: (event) => {
|
|
||||||
positionChangeHandler(event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$: if (modelIds && modelListElement) {
|
|
||||||
tick().then(() => {
|
|
||||||
initSortable();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
tick().then(() => {
|
|
||||||
initSortable();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
if (sortable) {
|
|
||||||
sortable.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if modelIds.length > 0}
|
|
||||||
<div class="flex flex-col -translate-x-1" bind:this={modelListElement}>
|
|
||||||
{#each modelIds as modelId (modelId)}
|
|
||||||
<div class=" flex gap-2 w-full justify-between items-center" id="model-item-{modelId}">
|
|
||||||
<Tooltip content={modelId} placement="top-start">
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<EllipsisVertical className="size-4 cursor-move model-item-handle" />
|
|
||||||
|
|
||||||
<div class=" text-sm flex-1 py-1 rounded-lg line-clamp-1">
|
|
||||||
{#if $models.find((model) => model.id === modelId)}
|
|
||||||
{$models.find((model) => model.id === modelId).name}
|
|
||||||
{:else}
|
|
||||||
{modelId}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="text-gray-500 text-xs text-center py-2">
|
|
||||||
{$i18n.t('No models found')}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
@@ -1,28 +1,20 @@
|
|||||||
<script>
|
<script>
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
import { createEventDispatcher, getContext, onMount } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
|
|
||||||
import { models, config as _config } from '$lib/stores';
|
import { models, config as _config } from '$lib/stores';
|
||||||
import { DEFAULT_CAPABILITIES } from '$lib/constants';
|
import { DEFAULT_CAPABILITIES } from '$lib/constants';
|
||||||
import { deleteAllModels } from '$lib/apis/models';
|
|
||||||
import { getModelsConfig, setModelsConfig, setDefaultPromptSuggestions } from '$lib/apis/configs';
|
import { getModelsConfig, setModelsConfig, setDefaultPromptSuggestions } from '$lib/apis/configs';
|
||||||
import { getBackendConfig } from '$lib/apis';
|
import { getBackendConfig } from '$lib/apis';
|
||||||
|
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
|
||||||
import ModelList from './ModelList.svelte';
|
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Minus from '$lib/components/icons/Minus.svelte';
|
|
||||||
import Plus from '$lib/components/icons/Plus.svelte';
|
|
||||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||||
import XMark from '$lib/components/icons/XMark.svelte';
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
import ModelSelector from './ModelSelector.svelte';
|
import ModelSelector from './ModelSelector.svelte';
|
||||||
import Model from '../Evaluations/Model.svelte';
|
|
||||||
import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
|
import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
|
||||||
|
|
||||||
import Capabilities from '$lib/components/workspace/Models/Capabilities.svelte';
|
import Capabilities from '$lib/components/workspace/Models/Capabilities.svelte';
|
||||||
@@ -31,28 +23,19 @@
|
|||||||
import PromptSuggestions from '$lib/components/workspace/Models/PromptSuggestions.svelte';
|
import PromptSuggestions from '$lib/components/workspace/Models/PromptSuggestions.svelte';
|
||||||
|
|
||||||
import AdjustmentsHorizontal from '$lib/components/icons/AdjustmentsHorizontal.svelte';
|
import AdjustmentsHorizontal from '$lib/components/icons/AdjustmentsHorizontal.svelte';
|
||||||
import Eye from '$lib/components/icons/Eye.svelte';
|
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let initHandler = () => {};
|
export let initHandler = () => {};
|
||||||
|
|
||||||
let config = null;
|
let config = null;
|
||||||
|
|
||||||
let selectedTab = 'defaults';
|
|
||||||
|
|
||||||
let selectedModelId = '';
|
|
||||||
let defaultModelIds = [];
|
let defaultModelIds = [];
|
||||||
|
|
||||||
let selectedPinnedModelId = '';
|
|
||||||
let defaultPinnedModelIds = [];
|
let defaultPinnedModelIds = [];
|
||||||
|
|
||||||
let modelIds = [];
|
let modelIds = [];
|
||||||
|
|
||||||
let sortKey = '';
|
|
||||||
let sortOrder = '';
|
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let showResetModal = false;
|
|
||||||
let showDefaultCapabilities = false;
|
let showDefaultCapabilities = false;
|
||||||
let showDefaultParams = false;
|
let showDefaultParams = false;
|
||||||
let showDefaultPromptSuggestions = false;
|
let showDefaultPromptSuggestions = false;
|
||||||
@@ -94,9 +77,6 @@
|
|||||||
...allModelIds.filter((id) => !orderedSet.has(id)).sort((a, b) => a.localeCompare(b))
|
...allModelIds.filter((id) => !orderedSet.has(id)).sort((a, b) => a.localeCompare(b))
|
||||||
];
|
];
|
||||||
|
|
||||||
sortKey = '';
|
|
||||||
sortOrder = '';
|
|
||||||
|
|
||||||
const savedMeta = config?.DEFAULT_MODEL_METADATA;
|
const savedMeta = config?.DEFAULT_MODEL_METADATA;
|
||||||
if (savedMeta && Object.keys(savedMeta).length > 0) {
|
if (savedMeta && Object.keys(savedMeta).length > 0) {
|
||||||
defaultCapabilities = savedMeta.capabilities ?? { ...DEFAULT_CAPABILITIES };
|
defaultCapabilities = savedMeta.capabilities ?? { ...DEFAULT_CAPABILITIES };
|
||||||
@@ -150,19 +130,6 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
title={$i18n.t('Reset All Models')}
|
|
||||||
message={$i18n.t('This will delete all models including custom models and cannot be undone.')}
|
|
||||||
bind:show={showResetModal}
|
|
||||||
onConfirm={async () => {
|
|
||||||
const res = deleteAllModels(localStorage.token);
|
|
||||||
if (res) {
|
|
||||||
toast.success($i18n.t('All models deleted successfully'));
|
|
||||||
initHandler();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Modal size="lg" bind:show className="bg-white dark:bg-gray-900 rounded-4xl">
|
<Modal size="lg" bind:show className="bg-white dark:bg-gray-900 rounded-4xl">
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-100 px-4 pt-3 pb-1">
|
<div class=" flex justify-between dark:text-gray-100 px-4 pt-3 pb-1">
|
||||||
@@ -194,13 +161,7 @@
|
|||||||
class="tabs flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-40 dark:text-gray-200 text-sm font-normal text-left scrollbar-none"
|
class="tabs flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-40 dark:text-gray-200 text-sm font-normal text-left scrollbar-none"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="px-0.5 py-1 max-w-fit w-fit rounded-lg flex-1 lg:flex-none flex text-right transition {selectedTab ===
|
class="px-0.5 py-1 max-w-fit w-fit rounded-lg flex-1 lg:flex-none flex text-right transition"
|
||||||
'defaults'
|
|
||||||
? ''
|
|
||||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
|
||||||
on:click={() => {
|
|
||||||
selectedTab = 'defaults';
|
|
||||||
}}
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-2">
|
<div class=" self-center mr-2">
|
||||||
@@ -208,35 +169,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Defaults')}</div>
|
<div class=" self-center">{$i18n.t('Defaults')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
class="px-0.5 py-1 max-w-fit w-fit rounded-lg flex-1 lg:flex-none flex text-right transition {selectedTab ===
|
|
||||||
'display'
|
|
||||||
? ''
|
|
||||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
|
||||||
on:click={() => {
|
|
||||||
selectedTab = 'display';
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<div class=" self-center mr-2">
|
|
||||||
<Eye />
|
|
||||||
</div>
|
|
||||||
<div class=" self-center">{$i18n.t('Display')}</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 mt-1 lg:mt-1 lg:h-[30rem] lg:max-h-[30rem] flex flex-col min-w-0">
|
<div class="flex-1 mt-1 lg:mt-1 lg:h-[30rem] lg:max-h-[30rem] flex flex-col min-w-0">
|
||||||
<div class="w-full h-full overflow-y-auto overflow-x-hidden scrollbar-hidden">
|
<div class="w-full h-full overflow-y-auto overflow-x-hidden scrollbar-hidden">
|
||||||
{#if selectedTab === 'defaults'}
|
<ModelSelector
|
||||||
<ModelSelector
|
title={$i18n.t('Selected Models')}
|
||||||
title={$i18n.t('Selected Models')}
|
tooltip={$i18n.t(
|
||||||
tooltip={$i18n.t(
|
'Set the default models that are automatically selected for all users when a new chat is created.'
|
||||||
'Set the default models that are automatically selected for all users when a new chat is created.'
|
)}
|
||||||
)}
|
models={$models.filter((model) => !(model?.info?.meta?.hidden ?? false))}
|
||||||
models={$models.filter((model) => !(model?.info?.meta?.hidden ?? false))}
|
bind:modelIds={defaultModelIds}
|
||||||
bind:modelIds={defaultModelIds}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-800/10 my-2.5 w-full" />
|
<hr class=" border-gray-50 dark:border-gray-800/10 my-2.5 w-full" />
|
||||||
|
|
||||||
@@ -370,71 +314,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if selectedTab === 'display'}
|
|
||||||
<div>
|
|
||||||
<div class="flex flex-col w-full">
|
|
||||||
<button
|
|
||||||
class="mb-1 flex gap-2"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
sortKey = 'model';
|
|
||||||
|
|
||||||
if (sortOrder === 'asc') {
|
|
||||||
sortOrder = 'desc';
|
|
||||||
} else {
|
|
||||||
sortOrder = 'asc';
|
|
||||||
}
|
|
||||||
|
|
||||||
modelIds = modelIds
|
|
||||||
.filter((id) => id !== '')
|
|
||||||
.sort((a, b) => {
|
|
||||||
const nameA = $models.find((model) => model.id === a)?.name || a;
|
|
||||||
const nameB = $models.find((model) => model.id === b)?.name || b;
|
|
||||||
return sortOrder === 'desc'
|
|
||||||
? nameA.localeCompare(nameB)
|
|
||||||
: nameB.localeCompare(nameA);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="text-xs text-gray-500">{$i18n.t('Reorder Models')}</div>
|
|
||||||
|
|
||||||
{#if sortKey === 'model'}
|
|
||||||
<span class="font-normal self-center">
|
|
||||||
{#if sortOrder === 'asc'}
|
|
||||||
<ChevronUp className="size-3" />
|
|
||||||
{:else}
|
|
||||||
<ChevronDown className="size-3" />
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
{:else}
|
|
||||||
<span class="invisible">
|
|
||||||
<ChevronUp className="size-3" />
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<ModelList bind:modelIds />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between items-center pt-3 text-sm font-normal gap-1.5">
|
<div class="flex justify-end items-center pt-3 text-sm font-normal gap-1.5">
|
||||||
<div>
|
|
||||||
<Tooltip
|
|
||||||
content={$i18n.t('This will delete all models including custom models')}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="text-sm font-normal text-gray-500 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300 transition hover:underline"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
showResetModal = true;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{$i18n.t('Reset')}
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex items-center gap-2 whitespace-nowrap {loading
|
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex items-center gap-2 whitespace-nowrap {loading
|
||||||
? ' cursor-not-allowed'
|
? ' cursor-not-allowed'
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
export let boilerplate = '';
|
export let boilerplate = '';
|
||||||
export let value = '';
|
export let value = '';
|
||||||
|
export let className = 'text-sm';
|
||||||
|
|
||||||
export let onSave = () => {};
|
export let onSave = () => {};
|
||||||
export let onChange = () => {};
|
export let onChange = () => {};
|
||||||
@@ -318,4 +319,4 @@ print("${endTag}")
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="code-textarea-{id}" class="h-full w-full min-w-0 overflow-hidden text-sm" />
|
<div id="code-textarea-{id}" class="{className} h-full w-full min-w-0 overflow-hidden" />
|
||||||
@@ -97,133 +97,114 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
<div class="flex h-full w-full min-w-0 flex-col overflow-hidden">
|
||||||
<div class="mx-auto w-full md:px-0 h-full">
|
<form class="flex h-full min-h-0 min-w-0 flex-col" on:submit|preventDefault={submitHandler}>
|
||||||
<form class=" flex flex-col max-h-[100dvh] h-full" on:submit|preventDefault={submitHandler}>
|
<button
|
||||||
<div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg">
|
class="mb-1 flex h-6 w-fit items-center gap-1 rounded-md px-0.5 text-xs text-gray-400 transition-colors duration-75 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
|
||||||
<div class="w-full mb-2 flex flex-col gap-0.5">
|
type="button"
|
||||||
<div class="flex w-full items-center">
|
on:click={() => {
|
||||||
<div class=" shrink-0 mr-2">
|
goto('/workspace/skills');
|
||||||
<Tooltip content={$i18n.t('Back')}>
|
}}
|
||||||
<button
|
>
|
||||||
class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
|
<ChevronLeft className="size-3" strokeWidth="2" />
|
||||||
aria-label={$i18n.t('Back')}
|
<span>{$i18n.t('Back')}</span>
|
||||||
on:click={() => {
|
</button>
|
||||||
goto('/workspace/skills');
|
|
||||||
}}
|
<div class="flex shrink-0 items-start gap-2 pb-2">
|
||||||
type="button"
|
<div class="min-w-0 flex-1">
|
||||||
>
|
<Tooltip content={$i18n.t('e.g. Code Review Guidelines')} placement="top-start">
|
||||||
<ChevronLeft strokeWidth="2.5" />
|
<input
|
||||||
</button>
|
class="w-full bg-transparent text-sm outline-hidden"
|
||||||
</Tooltip>
|
type="text"
|
||||||
|
placeholder={$i18n.t('Skill Name')}
|
||||||
|
aria-label={$i18n.t('Skill Name')}
|
||||||
|
bind:value={name}
|
||||||
|
required
|
||||||
|
{disabled}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<div class="mt-0.5 flex min-w-0 items-center gap-2 text-xs text-gray-500">
|
||||||
|
{#if edit}
|
||||||
|
<div class="shrink-0 truncate font-mono" title={id}>
|
||||||
|
{id}
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
<div class="flex-1">
|
|
||||||
<Tooltip content={$i18n.t('e.g. Code Review Guidelines')} placement="top-start">
|
|
||||||
<input
|
|
||||||
class="w-full text-2xl bg-transparent outline-hidden"
|
|
||||||
type="text"
|
|
||||||
placeholder={$i18n.t('Skill Name')}
|
|
||||||
aria-label={$i18n.t('Skill Name')}
|
|
||||||
bind:value={name}
|
|
||||||
required
|
|
||||||
{disabled}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="self-center shrink-0">
|
|
||||||
{#if !disabled}
|
|
||||||
<AccessButton on:click={() => (showAccessControlModal = true)} />
|
|
||||||
{:else}
|
|
||||||
<span
|
|
||||||
class="text-xs text-gray-500 bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded-full"
|
|
||||||
>{$i18n.t('Read Only')}</span
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" flex gap-2 px-1 items-center">
|
|
||||||
{#if edit}
|
|
||||||
<div class="text-sm text-gray-500 shrink-0">
|
|
||||||
{id}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<Tooltip
|
|
||||||
className="w-full"
|
|
||||||
content={$i18n.t('e.g. code-review-guidelines')}
|
|
||||||
placement="top-start"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
class="w-full text-sm disabled:text-gray-500 bg-transparent outline-hidden"
|
|
||||||
type="text"
|
|
||||||
placeholder={$i18n.t('Skill ID')}
|
|
||||||
aria-label={$i18n.t('Skill ID')}
|
|
||||||
bind:value={id}
|
|
||||||
required
|
|
||||||
disabled={edit}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className="w-full self-center items-center flex"
|
className="min-w-[8rem] flex-1"
|
||||||
content={$i18n.t('e.g. Step-by-step instructions for code reviews')}
|
content={$i18n.t('e.g. code-review-guidelines')}
|
||||||
placement="top-start"
|
placement="top-start"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="w-full text-sm bg-transparent outline-hidden"
|
class="w-full bg-transparent font-mono outline-hidden disabled:text-gray-500"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Skill Description')}
|
placeholder={$i18n.t('Skill ID')}
|
||||||
aria-label={$i18n.t('Skill Description')}
|
aria-label={$i18n.t('Skill ID')}
|
||||||
bind:value={description}
|
bind:value={id}
|
||||||
{disabled}
|
required
|
||||||
|
disabled={edit}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
|
|
||||||
<div class="h-full flex flex-col">
|
|
||||||
<div
|
|
||||||
class="bg-gray-50 dark:bg-gray-900 rounded-xl border border-gray-100/50 dark:border-gray-850/50 flex-1 min-h-0 overflow-hidden flex flex-col"
|
|
||||||
>
|
|
||||||
{#if disabled}
|
|
||||||
<div class="px-4 py-3 overflow-y-auto flex-1">
|
|
||||||
<pre class="text-xs whitespace-pre-wrap font-mono">{content}</pre>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<textarea
|
|
||||||
class="w-full flex-1 text-xs bg-transparent outline-hidden resize-none font-mono px-4 py-3"
|
|
||||||
bind:value={content}
|
|
||||||
on:input={handleContentInput}
|
|
||||||
placeholder={$i18n.t('Enter skill instructions in markdown...')}
|
|
||||||
aria-label={$i18n.t('Skill Instructions')}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pb-3 flex justify-end">
|
|
||||||
{#if !disabled}
|
|
||||||
<button
|
|
||||||
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex items-center gap-2 whitespace-nowrap"
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{$i18n.t(edit ? 'Save' : 'Save & Create')}
|
|
||||||
{#if loading}
|
|
||||||
<span class="shrink-0">
|
|
||||||
<Spinner />
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
className="flex min-w-0 flex-1 items-center"
|
||||||
|
content={$i18n.t('e.g. Step-by-step instructions for code reviews')}
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="w-full bg-transparent outline-hidden"
|
||||||
|
type="text"
|
||||||
|
placeholder={$i18n.t('Skill Description')}
|
||||||
|
aria-label={$i18n.t('Skill Description')}
|
||||||
|
bind:value={description}
|
||||||
|
{disabled}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
<div class="flex shrink-0 items-center gap-1 pr-0.5">
|
||||||
|
{#if !disabled}
|
||||||
|
<AccessButton on:click={() => (showAccessControlModal = true)} />
|
||||||
|
{:else}
|
||||||
|
<span class="rounded-lg bg-gray-100 px-2 py-1 text-xs text-gray-500 dark:bg-gray-850">
|
||||||
|
{$i18n.t('Read Only')}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-h-0 flex-1 overflow-hidden rounded-lg bg-gray-50/60 dark:bg-white/[0.03]">
|
||||||
|
{#if disabled}
|
||||||
|
<div class="h-full overflow-y-auto px-3 py-2">
|
||||||
|
<pre class="whitespace-pre-wrap font-mono text-[11px] leading-relaxed">{content}</pre>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<textarea
|
||||||
|
class="h-full w-full resize-none bg-transparent px-3 py-2 font-mono text-[11px] leading-relaxed outline-hidden placeholder:text-gray-400 dark:placeholder:text-gray-600"
|
||||||
|
bind:value={content}
|
||||||
|
on:input={handleContentInput}
|
||||||
|
placeholder={$i18n.t('Enter skill instructions in markdown...')}
|
||||||
|
aria-label={$i18n.t('Skill Instructions')}
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !disabled}
|
||||||
|
<div class="flex shrink-0 justify-end py-2">
|
||||||
|
<button
|
||||||
|
class="flex h-7 items-center gap-1.5 rounded-lg bg-gray-900 px-2.5 text-xs text-white transition hover:bg-black disabled:opacity-60 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white"
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{$i18n.t(edit ? 'Save' : 'Save & Create')}
|
||||||
|
{#if loading}
|
||||||
|
<Spinner className="size-3" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import AccessButton from '$lib/components/common/AccessButton.svelte';
|
import AccessButton from '$lib/components/common/AccessButton.svelte';
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import AccessControlModal from '../common/AccessControlModal.svelte';
|
import AccessControlModal from '../common/AccessControlModal.svelte';
|
||||||
|
|
||||||
let formElement = null;
|
let formElement = null;
|
||||||
@@ -25,7 +26,7 @@
|
|||||||
export let edit = false;
|
export let edit = false;
|
||||||
export let clone = false;
|
export let clone = false;
|
||||||
|
|
||||||
export let onSave = () => {};
|
export let onSave = /** @param {any} _value */ async (_value) => {};
|
||||||
|
|
||||||
export let id = '';
|
export let id = '';
|
||||||
export let name = '';
|
export let name = '';
|
||||||
@@ -159,13 +160,17 @@ class Tools:
|
|||||||
|
|
||||||
const saveHandler = async () => {
|
const saveHandler = async () => {
|
||||||
loading = true;
|
loading = true;
|
||||||
onSave({
|
try {
|
||||||
id,
|
await onSave({
|
||||||
name,
|
id,
|
||||||
meta,
|
name,
|
||||||
content,
|
meta,
|
||||||
access_grants: accessGrants
|
content,
|
||||||
});
|
access_grants: accessGrants
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitHandler = async () => {
|
const submitHandler = async () => {
|
||||||
@@ -207,143 +212,142 @@ class Tools:
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class=" flex flex-col justify-between w-full min-w-0 overflow-y-auto overflow-x-hidden h-full">
|
<div class="flex h-full w-full min-w-0 flex-col overflow-hidden">
|
||||||
<div class="mx-auto w-full min-w-0 md:px-0 h-full">
|
<form
|
||||||
<form
|
bind:this={formElement}
|
||||||
bind:this={formElement}
|
class="flex h-full min-h-0 min-w-0 flex-col"
|
||||||
class=" flex flex-col min-w-0 max-h-[100dvh] h-full"
|
on:submit|preventDefault={() => {
|
||||||
on:submit|preventDefault={() => {
|
if (edit) {
|
||||||
if (edit) {
|
submitHandler();
|
||||||
submitHandler();
|
} else {
|
||||||
} else {
|
showConfirm = true;
|
||||||
showConfirm = true;
|
}
|
||||||
}
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="mb-1 flex h-6 w-fit items-center gap-1 rounded-md px-0.5 text-xs text-gray-400 transition-colors duration-75 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
goto('/workspace/tools');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col flex-1 min-w-0 overflow-auto h-0 rounded-lg">
|
<ChevronLeft className="size-3" strokeWidth="2" />
|
||||||
<div class="w-full mb-2 flex flex-col gap-0.5">
|
<span>{$i18n.t('Back')}</span>
|
||||||
<div class="flex w-full min-w-0 items-center">
|
</button>
|
||||||
<div class=" shrink-0 mr-2">
|
|
||||||
<Tooltip content={$i18n.t('Back')}>
|
<div class="flex shrink-0 items-start gap-2 pb-2">
|
||||||
<button
|
<div class="min-w-0 flex-1">
|
||||||
class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
|
<Tooltip content={$i18n.t('e.g. My Tools')} placement="top-start">
|
||||||
aria-label={$i18n.t('Back')}
|
<input
|
||||||
on:click={() => {
|
class="w-full bg-transparent text-sm outline-hidden"
|
||||||
goto('/workspace/tools');
|
type="text"
|
||||||
}}
|
placeholder={$i18n.t('Tool Name')}
|
||||||
type="button"
|
aria-label={$i18n.t('Tool Name')}
|
||||||
>
|
bind:value={name}
|
||||||
<ChevronLeft strokeWidth="2.5" />
|
required
|
||||||
</button>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
<div class="mt-0.5 flex min-w-0 items-center gap-2 text-xs text-gray-500">
|
||||||
|
{#if edit}
|
||||||
|
<div class="shrink-0 truncate font-mono" title={id}>
|
||||||
|
{id}
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<Tooltip content={$i18n.t('e.g. My Tools')} placement="top-start">
|
|
||||||
<input
|
|
||||||
class="w-full text-2xl bg-transparent outline-hidden"
|
|
||||||
type="text"
|
|
||||||
placeholder={$i18n.t('Tool Name')}
|
|
||||||
aria-label={$i18n.t('Tool Name')}
|
|
||||||
bind:value={name}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="self-center shrink-0">
|
|
||||||
<AccessButton
|
|
||||||
on:click={() => {
|
|
||||||
showAccessControlModal = true;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" flex min-w-0 gap-2 px-1 items-center">
|
|
||||||
{#if edit}
|
|
||||||
<div class="text-sm text-gray-500 shrink-0">
|
|
||||||
{id}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<Tooltip className="w-full" content={$i18n.t('e.g. my_tools')} placement="top-start">
|
|
||||||
<input
|
|
||||||
class="w-full text-sm disabled:text-gray-500 bg-transparent outline-hidden"
|
|
||||||
type="text"
|
|
||||||
placeholder={$i18n.t('Tool ID')}
|
|
||||||
aria-label={$i18n.t('Tool ID')}
|
|
||||||
bind:value={id}
|
|
||||||
required
|
|
||||||
disabled={edit}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className="w-full self-center items-center flex"
|
className="min-w-[8rem] flex-1"
|
||||||
content={$i18n.t('e.g. Tools for performing various operations')}
|
content={$i18n.t('e.g. my_tools')}
|
||||||
placement="top-start"
|
placement="top-start"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="w-full text-sm bg-transparent outline-hidden"
|
class="w-full bg-transparent font-mono outline-hidden disabled:text-gray-500"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Tool Description')}
|
placeholder={$i18n.t('Tool ID')}
|
||||||
aria-label={$i18n.t('Tool Description')}
|
aria-label={$i18n.t('Tool ID')}
|
||||||
bind:value={meta.description}
|
bind:value={id}
|
||||||
required
|
required
|
||||||
|
disabled={edit}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-2 flex-1 min-w-0 overflow-auto h-0 rounded-lg">
|
<Tooltip
|
||||||
<CodeEditor
|
className="flex min-w-0 flex-1 items-center"
|
||||||
bind:this={codeEditor}
|
content={$i18n.t('e.g. Tools for performing various operations')}
|
||||||
value={content}
|
placement="top-start"
|
||||||
lang="python"
|
|
||||||
{boilerplate}
|
|
||||||
onChange={(e) => {
|
|
||||||
_content = e;
|
|
||||||
if (!edit) {
|
|
||||||
const fm = extractFrontmatter(e);
|
|
||||||
if (fm.title && !name) {
|
|
||||||
name = formatSkillName(fm.title);
|
|
||||||
id = nameToId(fm.title);
|
|
||||||
}
|
|
||||||
if (fm.description && !meta.description) {
|
|
||||||
meta = { ...meta, description: fm.description };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onSave={async () => {
|
|
||||||
if (formElement) {
|
|
||||||
formElement.requestSubmit();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pb-3 flex justify-between">
|
|
||||||
<div class="flex-1 pr-3">
|
|
||||||
<div class="text-xs text-gray-500 line-clamp-2">
|
|
||||||
<span class=" font-normal dark:text-gray-200">{$i18n.t('Warning:')}</span>
|
|
||||||
{$i18n.t('Tools are a function calling system with arbitrary code execution')} <br />—
|
|
||||||
<span class=" font-normal dark:text-gray-400"
|
|
||||||
>{$i18n.t(`don't install random tools from sources you don't trust.`)}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
|
||||||
type="submit"
|
|
||||||
>
|
>
|
||||||
{$i18n.t('Save')}
|
<input
|
||||||
</button>
|
class="w-full bg-transparent outline-hidden"
|
||||||
|
type="text"
|
||||||
|
placeholder={$i18n.t('Tool Description')}
|
||||||
|
aria-label={$i18n.t('Tool Description')}
|
||||||
|
bind:value={meta.description}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
<div class="flex shrink-0 items-center gap-1 pr-0.5">
|
||||||
|
<AccessButton
|
||||||
|
on:click={() => {
|
||||||
|
showAccessControlModal = true;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-h-0 flex-1 overflow-hidden rounded-lg">
|
||||||
|
<CodeEditor
|
||||||
|
bind:this={codeEditor}
|
||||||
|
value={content}
|
||||||
|
lang="python"
|
||||||
|
{boilerplate}
|
||||||
|
className="text-[11px]"
|
||||||
|
onChange={(e) => {
|
||||||
|
_content = e;
|
||||||
|
if (!edit) {
|
||||||
|
const fm = extractFrontmatter(e);
|
||||||
|
if (fm.title && !name) {
|
||||||
|
name = formatSkillName(fm.title);
|
||||||
|
id = nameToId(fm.title);
|
||||||
|
}
|
||||||
|
if (fm.description && !meta.description) {
|
||||||
|
meta = { ...meta, description: fm.description };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSave={async () => {
|
||||||
|
if (formElement) {
|
||||||
|
formElement.requestSubmit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="shrink-0 py-2 text-xs text-gray-500">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<span class="font-normal dark:text-gray-200">{$i18n.t('Warning:')}</span>
|
||||||
|
{$i18n.t('Tools can execute arbitrary code.')}
|
||||||
|
<span class="font-normal dark:text-gray-400">
|
||||||
|
{$i18n.t('Only install tools from sources you trust.')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="flex h-7 shrink-0 items-center gap-1.5 rounded-lg bg-gray-900 px-2.5 text-xs text-white transition hover:bg-black disabled:opacity-60 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white"
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{$i18n.t(edit ? 'Save' : 'Save & Create')}
|
||||||
|
{#if loading}
|
||||||
|
<Spinner className="size-3" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
|
|||||||
Reference in new issue
Block a user