This commit is contained in:
Timothy Jaeryang Baek
2026-07-17 05:48:41 -04:00
parent 437c06c479
commit ea2e3d0afc
5 changed files with 406 additions and 406 deletions
@@ -350,7 +350,7 @@ class Pipe:
}}
>
<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"
class="mb-1 flex h-6 w-fit items-center gap-1 rounded-md 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');
@@ -360,7 +360,7 @@ class Pipe:
<span>{$i18n.t('Back')}</span>
</button>
<div class="flex shrink-0 items-start gap-2 pb-2">
<div class="flex shrink-0 items-start gap-2 pb-2 px-1">
<div class="min-w-0 flex-1">
<Tooltip content={$i18n.t('e.g. My Filter')} placement="top-start">
<input
@@ -425,7 +425,7 @@
<div class="flex h-full min-h-0 w-full flex-col">
{#if onBack}
<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"
class="mb-1 flex h-6 w-fit items-center gap-1 rounded-md 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={() => {
onBack();
@@ -448,200 +448,308 @@
reader.onload = (event) => {
let originalImageUrl = `${event.target?.result}`;
// For animated formats (gif, webp), skip resizing to preserve animation
const fileType = (inputFiles[0] as any)?.['type'];
if (fileType === 'image/gif' || fileType === 'image/webp') {
info.meta.profile_image_url = originalImageUrl;
inputFiles = null;
filesInputElement.value = '';
return;
}
const img = new Image();
img.src = originalImageUrl;
img.onload = function () {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Calculate the aspect ratio of the image
const aspectRatio = img.width / img.height;
// Calculate the new width and height to fit within 100x100
let newWidth, newHeight;
if (aspectRatio > 1) {
newWidth = 250 * aspectRatio;
newHeight = 250;
} else {
newWidth = 250;
newHeight = 250 / aspectRatio;
// For animated formats (gif, webp), skip resizing to preserve animation
const fileType = (inputFiles[0] as any)?.['type'];
if (fileType === 'image/gif' || fileType === 'image/webp') {
info.meta.profile_image_url = originalImageUrl;
inputFiles = null;
filesInputElement.value = '';
return;
}
// Set the canvas size
canvas.width = 250;
canvas.height = 250;
const img = new Image();
img.src = originalImageUrl;
// Calculate the position to center the image
const offsetX = (250 - newWidth) / 2;
const offsetY = (250 - newHeight) / 2;
img.onload = function () {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Draw the image on the canvas
ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight);
// Calculate the aspect ratio of the image
const aspectRatio = img.width / img.height;
// Get the base64 representation of the compressed image
const compressedSrc = canvas.toDataURL('image/webp', 0.8);
// Calculate the new width and height to fit within 100x100
let newWidth, newHeight;
if (aspectRatio > 1) {
newWidth = 250 * aspectRatio;
newHeight = 250;
} else {
newWidth = 250;
newHeight = 250 / aspectRatio;
}
// Display the compressed image
info.meta.profile_image_url = compressedSrc;
// Set the canvas size
canvas.width = 250;
canvas.height = 250;
inputFiles = null;
filesInputElement.value = '';
// Calculate the position to center the image
const offsetX = (250 - newWidth) / 2;
const offsetY = (250 - newHeight) / 2;
// Draw the image on the canvas
ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight);
// Get the base64 representation of the compressed image
const compressedSrc = canvas.toDataURL('image/webp', 0.8);
// Display the compressed image
info.meta.profile_image_url = compressedSrc;
inputFiles = null;
filesInputElement.value = '';
};
};
};
if (
inputFiles &&
inputFiles.length > 0 &&
['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/svg+xml'].includes(
(inputFiles[0] as any)?.['type']
)
) {
reader.readAsDataURL(inputFiles[0]);
} else {
console.log(`Unsupported File Type '${(inputFiles[0] as any)?.['type']}'.`);
inputFiles = null;
}
}}
/>
{#if !edit || (edit && model)}
<form
class="flex w-full flex-col gap-2.5 md:flex-row"
on:submit|preventDefault={() => {
submitHandler();
if (
inputFiles &&
inputFiles.length > 0 &&
['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/svg+xml'].includes(
(inputFiles[0] as any)?.['type']
)
) {
reader.readAsDataURL(inputFiles[0]);
} else {
console.log(`Unsupported File Type '${(inputFiles[0] as any)?.['type']}'.`);
inputFiles = null;
}
}}
>
<div class="w-full px-1">
<div class="flex w-full flex-col gap-3">
<div class="flex w-full min-w-0 items-center gap-3 py-0.5">
<div class="flex min-w-0 flex-1 items-center gap-3">
<button
class="group relative flex size-12 shrink-0 items-center overflow-hidden rounded-xl md:size-14 {info
.meta.profile_image_url !== `${WEBUI_BASE_URL}/static/favicon.png`
? 'bg-transparent'
: 'bg-gray-50 dark:bg-gray-850'} ring-1 ring-gray-200/70 transition hover:ring-gray-300 dark:ring-white/10 dark:hover:ring-white/20"
type="button"
aria-label={$i18n.t('Upload profile image')}
on:click={() => {
filesInputElement.click();
}}
>
{#if info.meta.profile_image_url}
<img
src={info.meta.profile_image_url}
alt="model profile"
class="size-full object-cover"
/>
{:else}
<img
src="{WEBUI_BASE_URL}/static/favicon.png"
alt="model profile"
class="size-full object-cover"
/>
{/if}
/>
<div
class="absolute bottom-0 right-0 z-10 opacity-0 transition group-hover:opacity-100"
{#if !edit || (edit && model)}
<form
class="flex w-full flex-col gap-2.5 md:flex-row"
on:submit|preventDefault={() => {
submitHandler();
}}
>
<div class="w-full px-1">
<div class="flex w-full flex-col gap-3">
<div class="flex w-full min-w-0 items-center gap-3 py-0.5">
<div class="flex min-w-0 flex-1 items-center gap-3">
<button
class="group relative flex size-12 shrink-0 items-center overflow-hidden rounded-xl md:size-14 {info
.meta.profile_image_url !== `${WEBUI_BASE_URL}/static/favicon.png`
? 'bg-transparent'
: 'bg-gray-50 dark:bg-gray-850'} ring-1 ring-gray-200/70 transition hover:ring-gray-300 dark:ring-white/10 dark:hover:ring-white/20"
type="button"
aria-label={$i18n.t('Upload profile image')}
on:click={() => {
filesInputElement.click();
}}
>
<div class="m-1">
<div
class="rounded-full bg-gray-900 p-1 text-white shadow-sm transition dark:bg-white dark:text-black"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-3"
{#if info.meta.profile_image_url}
<img
src={info.meta.profile_image_url}
alt="model profile"
class="size-full object-cover"
/>
{:else}
<img
src="{WEBUI_BASE_URL}/static/favicon.png"
alt="model profile"
class="size-full object-cover"
/>
{/if}
<div
class="absolute bottom-0 right-0 z-10 opacity-0 transition group-hover:opacity-100"
>
<div class="m-1">
<div
class="rounded-full bg-gray-900 p-1 text-white shadow-sm transition dark:bg-white dark:text-black"
>
<path
fill-rule="evenodd"
d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
clip-rule="evenodd"
/>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-3"
>
<path
fill-rule="evenodd"
d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
clip-rule="evenodd"
/>
</svg>
</div>
</div>
</div>
</div>
<div
class="absolute inset-0 bg-white opacity-0 transition group-hover:opacity-20 dark:bg-black"
></div>
</button>
<div
class="absolute inset-0 bg-white opacity-0 transition group-hover:opacity-20 dark:bg-black"
></div>
</button>
<div class="min-w-0 flex-1">
<div class="flex min-w-0 items-center gap-2">
<input
class="min-w-0 flex-1 bg-transparent text-base leading-tight text-gray-900 outline-hidden placeholder:text-gray-300 dark:text-white dark:placeholder:text-gray-700 md:text-lg"
placeholder={$i18n.t('Model Name')}
bind:value={name}
required
/>
<AccessButton
on:click={() => {
showAccessControlModal = true;
}}
/>
</div>
<div class="min-w-0 flex-1">
<div class="flex min-w-0 items-center gap-2">
<input
class="min-w-0 flex-1 bg-transparent text-base leading-tight text-gray-900 outline-hidden placeholder:text-gray-300 dark:text-white dark:placeholder:text-gray-700 md:text-lg"
placeholder={$i18n.t('Model Name')}
bind:value={name}
class="block w-full bg-transparent py-0.5 text-xs text-gray-500 outline-hidden placeholder:text-gray-300 dark:text-gray-500 dark:placeholder:text-gray-700"
placeholder={$i18n.t('Model ID')}
bind:value={id}
disabled={edit}
required
/>
</div>
</div>
</div>
<AccessButton
on:click={() => {
showAccessControlModal = true;
}}
/>
{#if preset}
<div>
<div class="mb-1 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Base Model (From)')}
</div>
<input
class="block w-full bg-transparent py-0.5 text-xs text-gray-500 outline-hidden placeholder:text-gray-300 dark:text-gray-500 dark:placeholder:text-gray-700"
placeholder={$i18n.t('Model ID')}
bind:value={id}
disabled={edit}
required
<ModelSelector
id="workspace-base-model"
placeholder={$i18n.t('Select a base model (e.g. llama3, gpt-4o)')}
searchPlaceholder={$i18n.t('Search a model')}
items={getBaseModelItems($models)}
triggerClassName="text-xs"
selectionOnly
includeHidden={$user?.role === 'admin'}
bind:value={info.base_model_id}
/>
</div>
{/if}
<div>
<div class="mb-1 flex w-full items-center justify-between">
<div class="self-center text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Description')}
</div>
<button
class="text-xs text-gray-500 transition hover:text-gray-700 dark:hover:text-gray-300"
type="button"
aria-pressed={enableDescription ? 'true' : 'false'}
aria-label={enableDescription
? $i18n.t('Custom description enabled')
: $i18n.t('Default description enabled')}
on:click={() => {
enableDescription = !enableDescription;
}}
>
{#if !enableDescription}
<span>{$i18n.t('Default')}</span>
{:else}
<span>{$i18n.t('Custom')}</span>
{/if}
</button>
</div>
{#if enableDescription}
<Textarea
className="w-full resize-none overflow-y-hidden bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
placeholder={$i18n.t('Add a short description about what this model does')}
minSize={32}
bind:value={info.meta.description}
/>
{/if}
</div>
<div class="w-full max-w-full">
<Tags
tags={info?.meta?.tags ?? []}
{suggestionTags}
on:delete={(e) => {
const tagName = e.detail;
info.meta.tags = info.meta.tags.filter((tag) => tag.name !== tagName);
}}
on:add={(e) => {
const tagName = e.detail;
if (!(info?.meta?.tags ?? null)) {
info.meta.tags = [{ name: tagName }];
} else {
info.meta.tags = [...info.meta.tags, { name: tagName }];
}
}}
/>
</div>
</div>
{#if preset}
<div>
<div class="mb-1 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Base Model (From)')}
<section class="mt-2.5">
<div class="mb-2 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Model Params')}
</div>
<div class="space-y-2.5">
<div>
<div class="mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('System Prompt')}
</div>
<div>
<Textarea
className="min-h-12 w-full resize-none overflow-y-hidden bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
placeholder={$i18n.t(
'Write your model system prompt content here\ne.g.) You are Mario from Super Mario Bros, acting as an assistant.'
)}
rows={2}
minSize={48}
bind:value={system}
/>
</div>
</div>
<ModelSelector
id="workspace-base-model"
placeholder={$i18n.t('Select a base model (e.g. llama3, gpt-4o)')}
searchPlaceholder={$i18n.t('Search a model')}
items={getBaseModelItems($models)}
triggerClassName="text-xs"
selectionOnly
includeHidden={$user?.role === 'admin'}
bind:value={info.base_model_id}
/>
</div>
{/if}
<div class="flex h-7 w-full justify-between">
<div class="self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Advanced Params')}
</div>
<div>
<div class="mb-1 flex w-full items-center justify-between">
<button
class="text-xs text-gray-500 transition hover:text-gray-700 dark:hover:text-gray-300"
type="button"
on:click={() => {
showAdvanced = !showAdvanced;
}}
>
{#if showAdvanced}
<span>{$i18n.t('Hide')}</span>
{:else}
<span>{$i18n.t('Show')}</span>
{/if}
</button>
</div>
{#if showAdvanced}
<div class="my-2">
<AdvancedParams admin={true} custom={true} layout="grid" bind:params />
</div>
{/if}
</div>
</section>
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
<section class="my-2.5">
<div class="flex w-full items-center justify-between">
<div class="self-center text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Description')}
{$i18n.t('Prompts')}
</div>
<button
class="text-xs text-gray-500 transition hover:text-gray-700 dark:hover:text-gray-300"
type="button"
aria-pressed={enableDescription ? 'true' : 'false'}
aria-label={enableDescription
? $i18n.t('Custom description enabled')
: $i18n.t('Default description enabled')}
on:click={() => {
enableDescription = !enableDescription;
if ((info?.meta?.suggestion_prompts ?? null) === null) {
info.meta.suggestion_prompts = [{ content: '', title: ['', ''] }];
} else {
info.meta.suggestion_prompts = null;
}
}}
>
{#if !enableDescription}
{#if (info?.meta?.suggestion_prompts ?? null) === null}
<span>{$i18n.t('Default')}</span>
{:else}
<span>{$i18n.t('Custom')}</span>
@@ -649,275 +757,167 @@
</button>
</div>
{#if enableDescription}
<Textarea
className="w-full resize-none overflow-y-hidden bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
placeholder={$i18n.t('Add a short description about what this model does')}
minSize={32}
bind:value={info.meta.description}
/>
{#if info?.meta?.suggestion_prompts}
<PromptSuggestions bind:promptSuggestions={info.meta.suggestion_prompts} />
{/if}
</section>
<div class="my-3">
<Knowledge bind:selectedItems={knowledge} />
</div>
<div class="w-full max-w-full">
<Tags
tags={info?.meta?.tags ?? []}
{suggestionTags}
on:delete={(e) => {
const tagName = e.detail;
info.meta.tags = info.meta.tags.filter((tag) => tag.name !== tagName);
}}
on:add={(e) => {
const tagName = e.detail;
if (!(info?.meta?.tags ?? null)) {
info.meta.tags = [{ name: tagName }];
} else {
info.meta.tags = [...info.meta.tags, { name: tagName }];
}
}}
/>
</div>
</div>
<section class="mt-2.5">
<div class="mb-2 text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Model Params')}
<div class="my-3">
<ToolsSelector bind:selectedToolIds={toolIds} tools={$tools ?? []} />
</div>
<div class="space-y-2.5">
<div>
<div class="mb-1 text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('System Prompt')}
</div>
<div>
<Textarea
className="min-h-12 w-full resize-none overflow-y-hidden bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
placeholder={$i18n.t(
'Write your model system prompt content here\ne.g.) You are Mario from Super Mario Bros, acting as an assistant.'
)}
rows={2}
minSize={48}
bind:value={system}
<div class="my-3">
<SkillsSelector bind:selectedSkillIds={skillIds} skills={skillsList} />
</div>
{#if ($functions ?? []).filter((func) => func.type === 'filter').length > 0 || ($functions ?? []).filter((func) => func.type === 'action').length > 0}
<hr class="my-3 border-gray-100/30 dark:border-gray-850/30" />
{#if ($functions ?? []).filter((func) => func.type === 'filter').length > 0}
<div class="my-3">
<FiltersSelector
bind:selectedFilterIds={filterIds}
filters={($functions ?? []).filter((func) => func.type === 'filter')}
/>
</div>
</div>
<div class="flex h-7 w-full justify-between">
<div class="self-center text-xs text-gray-600 dark:text-gray-400">
{$i18n.t('Advanced Params')}
{@const toggleableFilters = $functions.filter(
(func) =>
func.type === 'filter' &&
(filterIds.includes(func.id) || func?.is_global) &&
func?.meta?.toggle
)}
{#if toggleableFilters.length > 0}
<div class="my-3">
<DefaultFiltersSelector
bind:selectedFilterIds={defaultFilterIds}
filters={toggleableFilters}
/>
</div>
{/if}
{/if}
{#if ($functions ?? []).filter((func) => func.type === 'action').length > 0}
<div class="my-3">
<ActionsSelector
bind:selectedActionIds={actionIds}
actions={($functions ?? []).filter((func) => func.type === 'action')}
/>
</div>
{/if}
{/if}
<hr class="my-3 border-gray-100/30 dark:border-gray-850/30" />
<div class="my-3">
<Capabilities bind:capabilities />
</div>
{#if Object.keys(capabilities).filter((key) => capabilities[key]).length > 0}
{@const availableFeatures = Object.entries(capabilities)
.filter(
([key, value]) =>
value && ['web_search', 'code_interpreter', 'image_generation'].includes(key)
)
.map(([key, value]) => key)}
{#if availableFeatures.length > 0}
<div class="my-3">
<DefaultFeatures {availableFeatures} bind:featureIds={defaultFeatureIds} />
</div>
{/if}
{/if}
{#if capabilities.builtin_tools}
<div class="my-3">
<BuiltinTools bind:builtinTools />
</div>
{/if}
{#if capabilities.terminal}
<div class="my-3">
<TerminalSelector bind:terminalId />
</div>
{/if}
<div class="my-3">
<div class="flex w-full justify-between mb-1">
<div class="self-center text-xs font-normal text-gray-500">
{$i18n.t('TTS Voice')}
</div>
</div>
<TTSVoiceInput
bind:value={tts.voice}
{voices}
placeholder={$i18n.t('e.g. alloy, echo, shimmer')}
/>
</div>
<hr class="my-3 border-gray-100/30 dark:border-gray-850/30" />
<div class="my-2 flex justify-end">
<button
class=" text-sm px-3 py-2 transition rounded-lg {loading
? ' cursor-not-allowed bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'
: 'bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'} flex w-full justify-center"
type="submit"
disabled={loading}
>
<div class=" self-center font-normal">
{#if edit}
{$i18n.t('Save & Update')}
{:else}
{$i18n.t('Save & Create')}
{/if}
</div>
{#if loading}
<div class="ml-1.5 self-center">
<Spinner />
</div>
{/if}
</button>
</div>
<div class="my-2 text-gray-300 dark:text-gray-700 pb-20">
<div class="flex w-full justify-between mb-2">
<div class=" self-center text-sm font-normal">{$i18n.t('JSON Preview')}</div>
<button
class="text-xs text-gray-500 transition hover:text-gray-700 dark:hover:text-gray-300"
class="p-1 px-3 text-xs flex rounded-sm transition"
type="button"
on:click={() => {
showAdvanced = !showAdvanced;
showPreview = !showPreview;
}}
>
{#if showAdvanced}
<span>{$i18n.t('Hide')}</span>
{#if showPreview}
<span class="ml-2 self-center">{$i18n.t('Hide')}</span>
{:else}
<span>{$i18n.t('Show')}</span>
<span class="ml-2 self-center">{$i18n.t('Show')}</span>
{/if}
</button>
</div>
{#if showAdvanced}
<div class="my-2">
<AdvancedParams admin={true} custom={true} layout="grid" bind:params />
</div>
{/if}
</div>
</section>
<hr class=" border-gray-100/30 dark:border-gray-850/30 my-2" />
<section class="my-2.5">
<div class="flex w-full items-center justify-between">
<div class="self-center text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Prompts')}
</div>
<button
class="text-xs text-gray-500 transition hover:text-gray-700 dark:hover:text-gray-300"
type="button"
on:click={() => {
if ((info?.meta?.suggestion_prompts ?? null) === null) {
info.meta.suggestion_prompts = [{ content: '', title: ['', ''] }];
} else {
info.meta.suggestion_prompts = null;
}
}}
>
{#if (info?.meta?.suggestion_prompts ?? null) === null}
<span>{$i18n.t('Default')}</span>
{:else}
<span>{$i18n.t('Custom')}</span>
{/if}
</button>
</div>
{#if info?.meta?.suggestion_prompts}
<PromptSuggestions bind:promptSuggestions={info.meta.suggestion_prompts} />
{/if}
</section>
<div class="my-3">
<Knowledge bind:selectedItems={knowledge} />
</div>
<div class="my-3">
<ToolsSelector bind:selectedToolIds={toolIds} tools={$tools ?? []} />
</div>
<div class="my-3">
<SkillsSelector bind:selectedSkillIds={skillIds} skills={skillsList} />
</div>
{#if ($functions ?? []).filter((func) => func.type === 'filter').length > 0 || ($functions ?? []).filter((func) => func.type === 'action').length > 0}
<hr class="my-3 border-gray-100/30 dark:border-gray-850/30" />
{#if ($functions ?? []).filter((func) => func.type === 'filter').length > 0}
<div class="my-3">
<FiltersSelector
bind:selectedFilterIds={filterIds}
filters={($functions ?? []).filter((func) => func.type === 'filter')}
/>
</div>
{@const toggleableFilters = $functions.filter(
(func) =>
func.type === 'filter' &&
(filterIds.includes(func.id) || func?.is_global) &&
func?.meta?.toggle
)}
{#if toggleableFilters.length > 0}
<div class="my-3">
<DefaultFiltersSelector
bind:selectedFilterIds={defaultFilterIds}
filters={toggleableFilters}
{#if showPreview}
<div>
<textarea
class="text-sm w-full bg-transparent outline-hidden resize-none"
rows="10"
value={JSON.stringify(info, null, 2)}
disabled
readonly
/>
</div>
{/if}
{/if}
{#if ($functions ?? []).filter((func) => func.type === 'action').length > 0}
<div class="my-3">
<ActionsSelector
bind:selectedActionIds={actionIds}
actions={($functions ?? []).filter((func) => func.type === 'action')}
/>
</div>
{/if}
{/if}
<hr class="my-3 border-gray-100/30 dark:border-gray-850/30" />
<div class="my-3">
<Capabilities bind:capabilities />
</div>
{#if Object.keys(capabilities).filter((key) => capabilities[key]).length > 0}
{@const availableFeatures = Object.entries(capabilities)
.filter(
([key, value]) =>
value && ['web_search', 'code_interpreter', 'image_generation'].includes(key)
)
.map(([key, value]) => key)}
{#if availableFeatures.length > 0}
<div class="my-3">
<DefaultFeatures {availableFeatures} bind:featureIds={defaultFeatureIds} />
</div>
{/if}
{/if}
{#if capabilities.builtin_tools}
<div class="my-3">
<BuiltinTools bind:builtinTools />
</div>
{/if}
{#if capabilities.terminal}
<div class="my-3">
<TerminalSelector bind:terminalId />
</div>
{/if}
<div class="my-3">
<div class="flex w-full justify-between mb-1">
<div class="self-center text-xs font-normal text-gray-500">
{$i18n.t('TTS Voice')}
</div>
</div>
<TTSVoiceInput
bind:value={tts.voice}
{voices}
placeholder={$i18n.t('e.g. alloy, echo, shimmer')}
/>
</div>
<hr class="my-3 border-gray-100/30 dark:border-gray-850/30" />
<div class="my-2 flex justify-end">
<button
class=" text-sm px-3 py-2 transition rounded-lg {loading
? ' cursor-not-allowed bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'
: 'bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'} flex w-full justify-center"
type="submit"
disabled={loading}
>
<div class=" self-center font-normal">
{#if edit}
{$i18n.t('Save & Update')}
{:else}
{$i18n.t('Save & Create')}
{/if}
</div>
{#if loading}
<div class="ml-1.5 self-center">
<Spinner />
</div>
{/if}
</button>
</div>
<div class="my-2 text-gray-300 dark:text-gray-700 pb-20">
<div class="flex w-full justify-between mb-2">
<div class=" self-center text-sm font-normal">{$i18n.t('JSON Preview')}</div>
<button
class="p-1 px-3 text-xs flex rounded-sm transition"
type="button"
on:click={() => {
showPreview = !showPreview;
}}
>
{#if showPreview}
<span class="ml-2 self-center">{$i18n.t('Hide')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Show')}</span>
{/if}
</button>
</div>
{#if showPreview}
<div>
<textarea
class="text-sm w-full bg-transparent outline-hidden resize-none"
rows="10"
value={JSON.stringify(info, null, 2)}
disabled
readonly
/>
</div>
{/if}
</div>
</div>
</form>
{/if}
</form>
{/if}
</div>
</div>
{/if}
@@ -385,7 +385,7 @@
<!-- Edit mode: Read-only view with history -->
<div class="flex h-full max-h-[100dvh] w-full flex-col">
<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"
class="mb-1 flex h-6 w-fit items-center gap-1 rounded-md 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/prompts');
@@ -100,7 +100,7 @@
<div class="flex h-full w-full min-w-0 flex-col overflow-hidden">
<form class="flex h-full min-h-0 min-w-0 flex-col" on:submit|preventDefault={submitHandler}>
<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"
class="mb-1 flex h-6 w-fit items-center gap-1 rounded-md 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/skills');
@@ -110,7 +110,7 @@
<span>{$i18n.t('Back')}</span>
</button>
<div class="flex shrink-0 items-start gap-2 pb-2">
<div class="flex shrink-0 items-start gap-2 pb-2 px-1">
<div class="min-w-0 flex-1">
<Tooltip content={$i18n.t('e.g. Code Review Guidelines')} placement="top-start">
<input
@@ -225,7 +225,7 @@ class Tools:
}}
>
<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"
class="mb-1 flex h-6 w-fit items-center gap-1 rounded-md 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');
@@ -235,7 +235,7 @@ class Tools:
<span>{$i18n.t('Back')}</span>
</button>
<div class="flex shrink-0 items-start gap-2 pb-2">
<div class="flex shrink-0 items-start gap-2 pb-2 px-1">
<div class="min-w-0 flex-1">
<Tooltip content={$i18n.t('e.g. My Tools')} placement="top-start">
<input