feat: create model

This commit is contained in:
Timothy J. Baek
2024-05-24 22:21:57 -07:00
parent ca3108a54d
commit dac9634242
13 changed files with 286 additions and 951 deletions

View File

@@ -194,7 +194,7 @@
await settings.set({
..._settings,
system: chatContent.system ?? _settings.system,
options: chatContent.options ?? _settings.options
params: chatContent.options ?? _settings.params
});
autoScroll = true;
await tick();
@@ -283,7 +283,7 @@
models: selectedModels,
system: $settings.system ?? undefined,
options: {
...($settings.options ?? {})
...($settings.params ?? {})
},
messages: messages,
history: history,
@@ -431,7 +431,7 @@
// Prepare the base message object
const baseMessage = {
role: message.role,
content: arr.length - 2 !== idx ? message.content : message?.raContent ?? message.content
content: message.content
};
// Extract and format image URLs if any exist
@@ -443,7 +443,6 @@
if (imageUrls && imageUrls.length > 0 && message.role === 'user') {
baseMessage.images = imageUrls;
}
return baseMessage;
});
@@ -474,10 +473,10 @@
model: model,
messages: messagesBody,
options: {
...($settings.options ?? {}),
...($settings.params ?? {}),
stop:
$settings?.options?.stop ?? undefined
? $settings.options.stop.map((str) =>
$settings?.params?.stop ?? undefined
? $settings.params.stop.map((str) =>
decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
)
: undefined
@@ -718,18 +717,18 @@
: message?.raContent ?? message.content
})
})),
seed: $settings?.options?.seed ?? undefined,
seed: $settings?.params?.seed ?? undefined,
stop:
$settings?.options?.stop ?? undefined
? $settings.options.stop.map((str) =>
$settings?.params?.stop ?? undefined
? $settings.params.stop.map((str) =>
decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
)
: undefined,
temperature: $settings?.options?.temperature ?? undefined,
top_p: $settings?.options?.top_p ?? undefined,
num_ctx: $settings?.options?.num_ctx ?? undefined,
frequency_penalty: $settings?.options?.repeat_penalty ?? undefined,
max_tokens: $settings?.options?.num_predict ?? undefined,
temperature: $settings?.params?.temperature ?? undefined,
top_p: $settings?.params?.top_p ?? undefined,
num_ctx: $settings?.params?.num_ctx ?? undefined,
frequency_penalty: $settings?.params?.repeat_penalty ?? undefined,
max_tokens: $settings?.params?.num_predict ?? undefined,
docs: docs.length > 0 ? docs : undefined,
citations: docs.length > 0
},
@@ -1045,7 +1044,7 @@
bind:files
bind:prompt
bind:autoScroll
bind:selectedModel={atSelectedModel}
bind:atSelectedModel
{selectedModels}
{messages}
{submitPrompt}

View File

@@ -27,7 +27,8 @@
export let stopResponse: Function;
export let autoScroll = true;
export let selectedAtModel: Model | undefined;
export let atSelectedModel: Model | undefined;
export let selectedModels: [''];
let chatTextAreaElement: HTMLTextAreaElement;
@@ -52,7 +53,6 @@
export let messages = [];
let speechRecognition;
let visionCapableState = 'all';
$: if (prompt) {
@@ -62,19 +62,48 @@
}
}
$: {
if (selectedAtModel || selectedModels) {
visionCapableState = checkModelsAreVisionCapable();
if (visionCapableState === 'none') {
// Remove all image files
const fileCount = files.length;
files = files.filter((file) => file.type != 'image');
if (files.length < fileCount) {
toast.warning($i18n.t('All selected models do not support image input, removed images'));
}
// $: {
// if (atSelectedModel || selectedModels) {
// visionCapableState = checkModelsAreVisionCapable();
// if (visionCapableState === 'none') {
// // Remove all image files
// const fileCount = files.length;
// files = files.filter((file) => file.type != 'image');
// if (files.length < fileCount) {
// toast.warning($i18n.t('All selected models do not support image input, removed images'));
// }
// }
// }
// }
const checkModelsAreVisionCapable = () => {
let modelsToCheck = [];
if (atSelectedModel !== undefined) {
modelsToCheck = [atSelectedModel.id];
} else {
modelsToCheck = selectedModels;
}
if (modelsToCheck.length == 0 || modelsToCheck[0] == '') {
return 'all';
}
let visionCapableCount = 0;
for (const modelName of modelsToCheck) {
const model = $models.find((m) => m.id === modelName);
if (!model) {
continue;
}
if (model.custom_info?.meta.vision_capable ?? true) {
visionCapableCount++;
}
}
}
if (visionCapableCount == modelsToCheck.length) {
return 'all';
} else if (visionCapableCount == 0) {
return 'none';
} else {
return 'some';
}
};
let mediaRecorder;
let audioChunks = [];
@@ -343,35 +372,6 @@
}
};
const checkModelsAreVisionCapable = () => {
let modelsToCheck = [];
if (selectedAtModel !== undefined) {
modelsToCheck = [selectedAtModel.id];
} else {
modelsToCheck = selectedModels;
}
if (modelsToCheck.length == 0 || modelsToCheck[0] == '') {
return 'all';
}
let visionCapableCount = 0;
for (const modelName of modelsToCheck) {
const model = $models.find((m) => m.id === modelName);
if (!model) {
continue;
}
if (model.custom_info?.meta.vision_capable ?? true) {
visionCapableCount++;
}
}
if (visionCapableCount == modelsToCheck.length) {
return 'all';
} else if (visionCapableCount == 0) {
return 'none';
} else {
return 'some';
}
};
onMount(() => {
window.setTimeout(() => chatTextAreaElement?.focus(), 0);
@@ -479,8 +479,8 @@
<div class="fixed bottom-0 {$showSidebar ? 'left-0 md:left-[260px]' : 'left-0'} right-0">
<div class="w-full">
<div class="px-2.5 md:px-16 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
<div class="flex flex-col max-w-5xl w-full">
<div class=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
<div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full">
<div class="relative">
{#if autoScroll === false && messages.length > 0}
<div class=" absolute -top-12 left-0 right-0 flex justify-center z-30">
@@ -544,12 +544,12 @@
bind:chatInputPlaceholder
{messages}
on:select={(e) => {
selectedAtModel = e.detail;
atSelectedModel = e.detail;
chatTextAreaElement?.focus();
}}
/>
{#if selectedAtModel !== undefined}
{#if atSelectedModel !== undefined}
<div
class="px-3 py-2.5 text-left w-full flex justify-between items-center absolute bottom-0 left-0 right-0 bg-gradient-to-t from-50% from-white dark:from-gray-900"
>
@@ -558,23 +558,21 @@
crossorigin="anonymous"
alt="model profile"
class="size-5 max-w-[28px] object-cover rounded-full"
src={$modelfiles.find((modelfile) => modelfile.tagName === selectedAtModel.id)
?.imageUrl ??
src={$models.find((model) => model.id === atSelectedModel.id)?.info?.meta
?.profile_image_url ??
($i18n.language === 'dg-DG'
? `/doge.png`
: `${WEBUI_BASE_URL}/static/favicon.png`)}
/>
<div>
Talking to <span class=" font-medium"
>{selectedAtModel.custom_info?.name ?? selectedAtModel.name}
</span>
Talking to <span class=" font-medium">{atSelectedModel.name}</span>
</div>
</div>
<div>
<button
class="flex items-center"
on:click={() => {
selectedAtModel = undefined;
atSelectedModel = undefined;
}}
>
<XMark />
@@ -966,7 +964,7 @@
if (e.key === 'Escape') {
console.log('Escape');
selectedAtModel = undefined;
atSelectedModel = undefined;
}
}}
rows="1"

View File

@@ -13,7 +13,6 @@
export let models = [];
export let submitPrompt;
export let suggestionPrompts;
let mounted = false;
let selectedModelIdx = 0;

View File

@@ -1,5 +1,7 @@
<script lang="ts">
import { getContext } from 'svelte';
import { getContext, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
@@ -23,6 +25,10 @@
let customFieldName = '';
let customFieldValue = '';
$: if (params) {
dispatch('change', params);
}
</script>
<div class=" space-y-3 text-xs">

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import queue from 'async/queue';
import { toast } from 'svelte-sonner';
import {
@@ -12,33 +11,19 @@
cancelOllamaRequest,
uploadModel
} from '$lib/apis/ollama';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores';
import { splitStream } from '$lib/utils';
import { onMount, getContext } from 'svelte';
import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm';
import { getModelConfig, type GlobalModelConfig, updateModelConfig } from '$lib/apis';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
export let getModels: Function;
let showLiteLLM = false;
let showLiteLLMParams = false;
let modelUploadInputElement: HTMLInputElement;
let liteLLMModelInfo = [];
let liteLLMModel = '';
let liteLLMModelName = '';
let liteLLMAPIBase = '';
let liteLLMAPIKey = '';
let liteLLMRPM = '';
let liteLLMMaxTokens = '';
let deleteLiteLLMModelName = '';
$: liteLLMModelName = liteLLMModel;
// Models
@@ -68,23 +53,6 @@
let deleteModelTag = '';
// Model configuration
let modelConfig: GlobalModelConfig;
let showModelInfo = false;
let selectedModelId = '';
let modelName = '';
let modelDescription = '';
let modelIsVisionCapable = false;
const onModelInfoIdChange = () => {
const model = $models.find((m) => m.id === selectedModelId);
if (model) {
modelName = model.custom_info?.name ?? model.name;
modelDescription = model.custom_info?.meta.description ?? '';
modelIsVisionCapable = model.custom_info?.meta.vision_capable ?? false;
}
};
const updateModelsHandler = async () => {
for (const model of $models.filter(
(m) =>
@@ -457,106 +425,6 @@
}
};
const addLiteLLMModelHandler = async () => {
if (!liteLLMModelInfo.find((info) => info.model_name === liteLLMModelName)) {
const res = await addLiteLLMModel(localStorage.token, {
name: liteLLMModelName,
model: liteLLMModel,
api_base: liteLLMAPIBase,
api_key: liteLLMAPIKey,
rpm: liteLLMRPM,
max_tokens: liteLLMMaxTokens
}).catch((error) => {
toast.error(error);
return null;
});
if (res) {
if (res.message) {
toast.success(res.message);
}
}
} else {
toast.error($i18n.t(`Model {{modelName}} already exists.`, { modelName: liteLLMModelName }));
}
liteLLMModelName = '';
liteLLMModel = '';
liteLLMAPIBase = '';
liteLLMAPIKey = '';
liteLLMRPM = '';
liteLLMMaxTokens = '';
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
models.set(await getModels());
};
const deleteLiteLLMModelHandler = async () => {
const res = await deleteLiteLLMModel(localStorage.token, deleteLiteLLMModelName).catch(
(error) => {
toast.error(error);
return null;
}
);
if (res) {
if (res.message) {
toast.success(res.message);
}
}
deleteLiteLLMModelName = '';
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
models.set(await getModels());
};
const addModelInfoHandler = async () => {
if (!selectedModelId) {
return;
}
let model = $models.find((m) => m.id === selectedModelId);
if (!model) {
return;
}
// Remove any existing config
modelConfig = modelConfig.filter((m) => !(m.id === selectedModelId));
// Add new config
modelConfig.push({
id: selectedModelId,
name: modelName,
params: {},
meta: {
description: modelDescription,
vision_capable: modelIsVisionCapable
}
});
await updateModelConfig(localStorage.token, modelConfig);
toast.success(
$i18n.t('Model info for {{modelName}} added successfully', { modelName: selectedModelId })
);
models.set(await getModels());
};
const deleteModelInfoHandler = async () => {
if (!selectedModelId) {
return;
}
let model = $models.find((m) => m.id === selectedModelId);
if (!model) {
return;
}
modelConfig = modelConfig.filter((m) => !(m.id === selectedModelId));
await updateModelConfig(localStorage.token, modelConfig);
toast.success(
$i18n.t('Model info for {{modelName}} deleted successfully', { modelName: selectedModelId })
);
models.set(await getModels());
};
const toggleIsVisionCapable = () => {
modelIsVisionCapable = !modelIsVisionCapable;
};
onMount(async () => {
await Promise.all([
(async () => {
@@ -569,12 +437,6 @@
selectedOllamaUrlIdx = 0;
}
})(),
(async () => {
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
})(),
(async () => {
modelConfig = await getModelConfig(localStorage.token);
})(),
(async () => {
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
})()
@@ -1015,344 +877,8 @@
{/if}
</div>
</div>
<hr class=" dark:border-gray-700 my-2" />
{:else}
<div>Ollama Not Detected</div>
{/if}
<!--TODO: Hide LiteLLM options when ENABLE_LITELLM=false-->
<div class=" space-y-3">
<div class="mt-2 space-y-3 pr-1.5">
<div>
<div class="mb-2">
<div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">{$i18n.t('Manage LiteLLM Models')}</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
showLiteLLM = !showLiteLLM;
}}>{showLiteLLM ? $i18n.t('Hide') : $i18n.t('Show')}</button
>
</div>
</div>
{#if showLiteLLM}
<div>
<div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">{$i18n.t('Add a model')}</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
showLiteLLMParams = !showLiteLLMParams;
}}
>{showLiteLLMParams
? $i18n.t('Hide Additional Params')
: $i18n.t('Show Additional Params')}</button
>
</div>
</div>
<div class="my-2 space-y-2">
<div class="flex w-full mb-1.5">
<div class="flex-1 mr-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter LiteLLM Model (litellm_params.model)')}
bind:value={liteLLMModel}
autocomplete="off"
/>
</div>
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
addLiteLLMModelHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</button>
</div>
{#if showLiteLLMParams}
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Model Name')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter Model Name (model_name)"
bind:value={liteLLMModelName}
autocomplete="off"
/>
</div>
</div>
</div>
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Base URL')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t(
'Enter LiteLLM API Base URL (litellm_params.api_base)'
)}
bind:value={liteLLMAPIBase}
autocomplete="off"
/>
</div>
</div>
</div>
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Key')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter LiteLLM API Key (litellm_params.api_key)')}
bind:value={liteLLMAPIKey}
autocomplete="off"
/>
</div>
</div>
</div>
<div>
<div class="mb-1.5 text-sm font-medium">{$i18n.t('API RPM')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter LiteLLM API RPM (litellm_params.rpm)')}
bind:value={liteLLMRPM}
autocomplete="off"
/>
</div>
</div>
</div>
<div>
<div class="mb-1.5 text-sm font-medium">{$i18n.t('Max Tokens')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter Max Tokens (litellm_params.max_tokens)')}
bind:value={liteLLMMaxTokens}
type="number"
min="1"
autocomplete="off"
/>
</div>
</div>
</div>
{/if}
</div>
<div class="mb-2 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Not sure what to add?')}
<a
class=" text-gray-300 font-medium underline"
href="https://litellm.vercel.app/docs/proxy/configs#quick-start"
target="_blank"
>
{$i18n.t('Click here for help.')}
</a>
</div>
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Delete a model')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={deleteLiteLLMModelName}
placeholder={$i18n.t('Select a model')}
>
{#if !deleteLiteLLMModelName}
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{/if}
{#each liteLLMModelInfo as model}
<option value={model.model_name} class="bg-gray-100 dark:bg-gray-700"
>{model.model_name}</option
>
{/each}
</select>
</div>
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
deleteLiteLLMModelHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
{/if}
</div>
</div>
<hr class=" dark:border-gray-700 my-2" />
</div>
<div class=" space-y-3">
<div class="mt-2 space-y-3 pr-1.5">
<div>
<div class="mb-2">
<div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">{$i18n.t('Manage Model Information')}</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
showModelInfo = !showModelInfo;
}}>{showModelInfo ? $i18n.t('Hide') : $i18n.t('Show')}</button
>
</div>
</div>
{#if showModelInfo}
<div>
<div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">{$i18n.t('Current Models')}</div>
</div>
<div class="flex gap-2">
<div class="flex-1 pb-1">
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={selectedModelId}
on:change={onModelInfoIdChange}
>
{#if !selectedModelId}
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{/if}
{#each $models as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700"
>{'details' in model
? 'Ollama'
: model.source === 'LiteLLM'
? 'LiteLLM'
: 'OpenAI'}: {model.name}{`${
model.custom_info?.name ? ' - ' + model.custom_info?.name : ''
}`}</option
>
{/each}
</select>
</div>
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
deleteModelInfoHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
{#if selectedModelId}
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Model Display Name')}</div>
<div class="flex w-full mb-1.5">
<div class="flex-1 mr-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter Model Display Name')}
bind:value={modelName}
autocomplete="off"
/>
</div>
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
addModelInfoHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</button>
</div>
</div>
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Model Description')}</div>
<div class="flex w-full">
<div class="flex-1">
<textarea
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1"
rows="2"
bind:value={modelDescription}
/>
</div>
</div>
</div>
<div class="py-0.5 flex w-full justify-between">
<div class=" self-center text-sm font-medium">
{$i18n.t('Is Model Vision Capable')}
</div>
<button
class="p-1 px-3sm flex rounded transition"
on:click={() => {
toggleIsVisionCapable();
}}
type="button"
>
{#if modelIsVisionCapable === true}
<span class="ml-2 self-center">{$i18n.t('Yes')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('No')}</span>
{/if}
</button>
</div>
{/if}
</div>
{/if}
</div>
</div>
</div>
</div>
</div>