|
|
|
@@ -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}
|
|
|
|
|