Browse Source

feat: create model

Timothy J. Baek 11 months ago
parent
commit
dac9634242

+ 11 - 2
backend/apps/ollama/main.py

@@ -39,6 +39,8 @@ from utils.utils import (
     get_admin_user,
 )
 
+from utils.models import get_model_id_from_custom_model_id
+
 
 from config import (
     SRC_LOG_LEVELS,
@@ -873,10 +875,10 @@ async def generate_chat_completion(
     url_idx: Optional[int] = None,
     user=Depends(get_verified_user),
 ):
+    model_id = get_model_id_from_custom_model_id(form_data.model)
+    model = model_id
 
     if url_idx == None:
-        model = form_data.model
-
         if ":" not in model:
             model = f"{model}:latest"
 
@@ -893,6 +895,13 @@ async def generate_chat_completion(
 
     r = None
 
+    # payload = {
+    #     **form_data.model_dump_json(exclude_none=True).encode(),
+    #     "model": model,
+    #     "messages": form_data.messages,
+
+    # }
+
     log.debug(
         "form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(
             form_data.model_dump_json(exclude_none=True).encode()

+ 3 - 1
backend/apps/web/models/models.py

@@ -166,7 +166,9 @@ class ModelsTable:
 
             model = Model.get(Model.id == id)
             return ModelModel(**model_to_dict(model))
-        except:
+        except Exception as e:
+            print(e)
+
             return None
 
     def delete_model_by_id(self, id: str) -> bool:

+ 15 - 7
backend/apps/web/routers/models.py

@@ -28,16 +28,24 @@ async def get_models(user=Depends(get_verified_user)):
 
 
 @router.post("/add", response_model=Optional[ModelModel])
-async def add_new_model(form_data: ModelForm, user=Depends(get_admin_user)):
-    model = Models.insert_new_model(form_data, user.id)
-
-    if model:
-        return model
-    else:
+async def add_new_model(
+    request: Request, form_data: ModelForm, user=Depends(get_admin_user)
+):
+    if form_data.id in request.app.state.MODELS:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
-            detail=ERROR_MESSAGES.DEFAULT(),
+            detail=ERROR_MESSAGES.MODEL_ID_TAKEN,
         )
+    else:
+        model = Models.insert_new_model(form_data, user.id)
+
+        if model:
+            return model
+        else:
+            raise HTTPException(
+                status_code=status.HTTP_401_UNAUTHORIZED,
+                detail=ERROR_MESSAGES.DEFAULT(),
+            )
 
 
 ############################

+ 2 - 0
backend/constants.py

@@ -32,6 +32,8 @@ class ERROR_MESSAGES(str, Enum):
     COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string."
     FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file."
 
+    MODEL_ID_TAKEN = "Uh-oh! This model id is already registered. Please choose another model id string."
+
     NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string."
     INVALID_TOKEN = (
         "Your session has expired or the token is invalid. Please sign in again."

+ 10 - 0
backend/utils/models.py

@@ -0,0 +1,10 @@
+from apps.web.models.models import Models, ModelModel, ModelForm, ModelResponse
+
+
+def get_model_id_from_custom_model_id(id: str):
+    model = Models.get_model_by_id(id)
+
+    if model:
+        return model.id
+    else:
+        return id

+ 15 - 16
src/lib/components/chat/Chat.svelte

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

+ 51 - 53
src/lib/components/chat/MessageInput.svelte

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

+ 0 - 1
src/lib/components/chat/Messages/Placeholder.svelte

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

+ 7 - 1
src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte

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

+ 4 - 478
src/lib/components/chat/Settings/Models.svelte

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

+ 1 - 1
src/lib/components/workspace/Models.svelte

@@ -139,7 +139,7 @@
 				</div>
 
 				<div class=" flex-1 self-center">
-					<div class=" font-bold capitalize line-clamp-1">{model.name}</div>
+					<div class=" font-bold line-clamp-1">{model.name}</div>
 					<div class=" text-sm overflow-hidden text-ellipsis line-clamp-1">
 						{model?.info?.meta?.description ?? model.id}
 					</div>

+ 142 - 366
src/routes/(app)/workspace/models/create/+page.svelte

@@ -4,209 +4,88 @@
 	import { goto } from '$app/navigation';
 	import { settings, user, config, modelfiles, models } from '$lib/stores';
 
-	import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
-	import { splitStream } from '$lib/utils';
 	import { onMount, tick, getContext } from 'svelte';
-	import { createModel } from '$lib/apis/ollama';
 	import { addNewModel, getModelById, getModelInfos } from '$lib/apis/models';
+	import { getModels } from '$lib/apis';
 
-	const i18n = getContext('i18n');
+	import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
 
-	let loading = false;
+	const i18n = getContext('i18n');
 
 	let filesInputElement;
 	let inputFiles;
-	let imageUrl = null;
-	let digest = '';
-	let pullProgress = null;
+
+	let showAdvanced = false;
+	let showPreview = false;
+
+	let loading = false;
 	let success = false;
 
 	// ///////////
-	// Modelfile
+	// Model
 	// ///////////
 
-	let title = '';
-	let tagName = '';
-	let desc = '';
-
-	let raw = true;
-	let advanced = false;
-
-	// Raw Mode
-	let content = '';
-
-	// Builder Mode
-	let model = '';
-	let system = '';
-	let template = '';
-	let params = {
-		// Advanced
-		seed: 0,
-		stop: '',
-		temperature: '',
-		repeat_penalty: '',
-		repeat_last_n: '',
-		mirostat: '',
-		mirostat_eta: '',
-		mirostat_tau: '',
-		top_k: '',
-		top_p: '',
-		tfs_z: '',
-		num_ctx: '',
-		num_predict: ''
-	};
-
-	let modelfileCreator = null;
-
-	$: tagName = title !== '' ? `${title.replace(/\s+/g, '-').toLowerCase()}:latest` : '';
-
-	$: if (!raw) {
-		content = `FROM ${model}
-${template !== '' ? `TEMPLATE """${template}"""` : ''}
-${params.seed !== 0 ? `PARAMETER seed ${params.seed}` : ''}
-${params.stop !== '' ? `PARAMETER stop ${params.stop}` : ''}
-${params.temperature !== '' ? `PARAMETER temperature ${params.temperature}` : ''}
-${params.repeat_penalty !== '' ? `PARAMETER repeat_penalty ${params.repeat_penalty}` : ''}
-${params.repeat_last_n !== '' ? `PARAMETER repeat_last_n ${params.repeat_last_n}` : ''}
-${params.mirostat !== '' ? `PARAMETER mirostat ${params.mirostat}` : ''}
-${params.mirostat_eta !== '' ? `PARAMETER mirostat_eta ${params.mirostat_eta}` : ''}
-${params.mirostat_tau !== '' ? `PARAMETER mirostat_tau ${params.mirostat_tau}` : ''}
-${params.top_k !== '' ? `PARAMETER top_k ${params.top_k}` : ''}
-${params.top_p !== '' ? `PARAMETER top_p ${params.top_p}` : ''}
-${params.tfs_z !== '' ? `PARAMETER tfs_z ${params.tfs_z}` : ''}
-${params.num_ctx !== '' ? `PARAMETER num_ctx ${params.num_ctx}` : ''}
-${params.num_predict !== '' ? `PARAMETER num_predict ${params.num_predict}` : ''}
-SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
-	}
-
-	let suggestions = [
-		{
-			content: ''
+	let id = '';
+	let name = '';
+
+	let info = {
+		id: '',
+		base_model_id: null,
+		name: '',
+		meta: {
+			profile_image_url: null,
+			description: '',
+			suggestion_prompts: [
+				{
+					content: ''
+				}
+			]
+		},
+		params: {
+			system: ''
 		}
-	];
-
-	let categories = {
-		character: false,
-		assistant: false,
-		writing: false,
-		productivity: false,
-		programming: false,
-		'data analysis': false,
-		lifestyle: false,
-		education: false,
-		business: false
 	};
 
-	const saveModelfile = async (modelfile) => {
-		await addNewModel(localStorage.token, modelfile);
-		await modelfiles.set(await getModelInfos(localStorage.token));
-	};
+	let params = {};
+
+	$: if (name) {
+		id = name.replace(/\s+/g, '-').toLowerCase();
+	}
 
 	const submitHandler = async () => {
 		loading = true;
 
-		if (Object.keys(categories).filter((category) => categories[category]).length == 0) {
-			toast.error(
-				'Uh-oh! It looks like you missed selecting a category. Please choose one to complete your modelfile.'
-			);
-			loading = false;
-			success = false;
-			return success;
-		}
+		info.id = id;
+		info.name = name;
 
-		if (
-			$models.map((model) => model.name).includes(tagName) ||
-			(await getModelById(localStorage.token, tagName).catch(() => false))
-		) {
+		if ($models.find((m) => m.id === info.id)) {
 			toast.error(
-				`Uh-oh! It looks like you already have a model named '${tagName}'. Please choose a different name to complete your modelfile.`
+				`Error: A model with the ID '${info.id}' already exists. Please select a different ID to proceed.`
 			);
 			loading = false;
 			success = false;
 			return success;
 		}
 
-		if (
-			title !== '' &&
-			desc !== '' &&
-			content !== '' &&
-			Object.keys(categories).filter((category) => categories[category]).length > 0 &&
-			!$models.includes(tagName)
-		) {
-			const res = await createModel(localStorage.token, tagName, content);
+		if (info) {
+			// TODO: if profile image url === null, set it to default image '/favicon.png'
+			const res = await addNewModel(localStorage.token, {
+				...info,
+				meta: {
+					...info.meta,
+					profile_image_url: info.meta.profile_image_url ?? '/favicon.png',
+					suggestion_prompts: info.meta.suggestion_prompts.filter((prompt) => prompt.content !== '')
+				},
+				params: { ...info.params, ...params }
+			});
 
 			if (res) {
-				const reader = res.body
-					.pipeThrough(new TextDecoderStream())
-					.pipeThrough(splitStream('\n'))
-					.getReader();
-
-				while (true) {
-					const { value, done } = await reader.read();
-					if (done) break;
-
-					try {
-						let lines = value.split('\n');
-
-						for (const line of lines) {
-							if (line !== '') {
-								console.log(line);
-								let data = JSON.parse(line);
-								console.log(data);
-
-								if (data.error) {
-									throw data.error;
-								}
-								if (data.detail) {
-									throw data.detail;
-								}
-
-								if (data.status) {
-									if (
-										!data.digest &&
-										!data.status.includes('writing') &&
-										!data.status.includes('sha256')
-									) {
-										toast.success(data.status);
-
-										if (data.status === 'success') {
-											success = true;
-										}
-									} else {
-										if (data.digest) {
-											digest = data.digest;
-
-											if (data.completed) {
-												pullProgress = Math.round((data.completed / data.total) * 1000) / 10;
-											} else {
-												pullProgress = 100;
-											}
-										}
-									}
-								}
-							}
-						}
-					} catch (error) {
-						console.log(error);
-						toast.error(error);
-					}
-				}
-			}
-
-			if (success) {
-				await saveModelfile({
-					tagName: tagName,
-					imageUrl: imageUrl,
-					title: title,
-					desc: desc,
-					content: content,
-					suggestionPrompts: suggestions.filter((prompt) => prompt.content !== ''),
-					categories: Object.keys(categories).filter((category) => categories[category]),
-					user: modelfileCreator !== null ? modelfileCreator : undefined
-				});
-				await goto('/workspace/modelfiles');
+				toast.success('Model created successfully!');
+				await goto('/workspace/models');
+				await models.set(await getModels(localStorage.token));
 			}
 		}
+
 		loading = false;
 		success = false;
 	};
@@ -223,62 +102,18 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
 				].includes(event.origin)
 			)
 				return;
-			const modelfile = JSON.parse(event.data);
-			console.log(modelfile);
-
-			imageUrl = modelfile.imageUrl;
-			title = modelfile.title;
-			await tick();
-			tagName = `${modelfile.user.username === 'hub' ? '' : `hub/`}${modelfile.user.username}/${
-				modelfile.tagName
-			}`;
-			desc = modelfile.desc;
-			content = modelfile.content;
-			suggestions =
-				modelfile.suggestionPrompts.length != 0
-					? modelfile.suggestionPrompts
-					: [
-							{
-								content: ''
-							}
-					  ];
-
-			modelfileCreator = {
-				username: modelfile.user.username,
-				name: modelfile.user.name
-			};
-			for (const category of modelfile.categories) {
-				categories[category.toLowerCase()] = true;
-			}
+			const model = JSON.parse(event.data);
+			console.log(model);
 		});
 
 		if (window.opener ?? false) {
 			window.opener.postMessage('loaded', '*');
 		}
 
-		if (sessionStorage.modelfile) {
-			const modelfile = JSON.parse(sessionStorage.modelfile);
-			console.log(modelfile);
-			imageUrl = modelfile.imageUrl;
-			title = modelfile.title;
-			await tick();
-			tagName = modelfile.tagName;
-			desc = modelfile.desc;
-			content = modelfile.content;
-			suggestions =
-				modelfile.suggestionPrompts.length != 0
-					? modelfile.suggestionPrompts
-					: [
-							{
-								content: ''
-							}
-					  ];
-
-			for (const category of modelfile.categories) {
-				categories[category.toLowerCase()] = true;
-			}
-
-			sessionStorage.removeItem('modelfile');
+		if (sessionStorage.model) {
+			const model = JSON.parse(sessionStorage.model);
+			console.log(model);
+			sessionStorage.removeItem('model');
 		}
 	});
 </script>
@@ -330,7 +165,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
 					const compressedSrc = canvas.toDataURL('image/jpeg');
 
 					// Display the compressed image
-					imageUrl = compressedSrc;
+					info.meta.profile_image_url = compressedSrc;
 
 					inputFiles = null;
 				};
@@ -382,7 +217,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
 		<div class="flex justify-center my-4">
 			<div class="self-center">
 				<button
-					class=" {imageUrl
+					class=" {info.meta.profile_image_url
 						? ''
 						: 'p-6'} rounded-full dark:bg-gray-700 border border-dashed border-gray-200"
 					type="button"
@@ -390,9 +225,9 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
 						filesInputElement.click();
 					}}
 				>
-					{#if imageUrl}
+					{#if info.meta.profile_image_url}
 						<img
-							src={imageUrl}
+							src={info.meta.profile_image_url}
 							alt="modelfile profile"
 							class=" rounded-full w-20 h-20 object-cover"
 						/>
@@ -401,7 +236,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
 							xmlns="http://www.w3.org/2000/svg"
 							viewBox="0 0 24 24"
 							fill="currentColor"
-							class="w-8"
+							class="size-8"
 						>
 							<path
 								fill-rule="evenodd"
@@ -421,35 +256,55 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
 				<div>
 					<input
 						class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
-						placeholder={$i18n.t('Name your modelfile')}
-						bind:value={title}
+						placeholder={$i18n.t('Name your model')}
+						bind:value={name}
 						required
 					/>
 				</div>
 			</div>
 
 			<div class="flex-1">
-				<div class=" text-sm font-semibold mb-2">{$i18n.t('Model Tag Name')}*</div>
+				<div class=" text-sm font-semibold mb-2">{$i18n.t('Model ID')}*</div>
 
 				<div>
 					<input
 						class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
-						placeholder={$i18n.t('Add a model tag name')}
-						bind:value={tagName}
+						placeholder={$i18n.t('Add a model id')}
+						bind:value={id}
 						required
 					/>
 				</div>
 			</div>
 		</div>
 
+		<div class="my-2">
+			<div class=" text-sm font-semibold mb-2">{$i18n.t('Base Model (From)')}</div>
+
+			<div>
+				<select
+					class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
+					placeholder="Select a base model (e.g. llama3, gpt-4o)"
+					bind:value={info.base_model_id}
+					required
+				>
+					<option value={null} class=" placeholder:text-gray-500"
+						>{$i18n.t('Select a base model')}</option
+					>
+					{#each $models as model}
+						<option value={model.id}>{model.name}</option>
+					{/each}
+				</select>
+			</div>
+		</div>
+
 		<div class="my-2">
 			<div class=" text-sm font-semibold mb-2">{$i18n.t('Description')}*</div>
 
 			<div>
 				<input
 					class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
-					placeholder={$i18n.t('Add a short description about what this modelfile does')}
-					bind:value={desc}
+					placeholder={$i18n.t('Add a short description about what this model does')}
+					bind:value={info.meta.description}
 					required
 				/>
 			</div>
@@ -457,137 +312,53 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
 
 		<div class="my-2">
 			<div class="flex w-full justify-between">
-				<div class=" self-center text-sm font-semibold">{$i18n.t('Modelfile')}</div>
-
-				<button
-					class="p-1 px-3 text-xs flex rounded transition"
-					type="button"
-					on:click={() => {
-						raw = !raw;
-					}}
-				>
-					{#if raw}
-						<span class="ml-2 self-center"> {$i18n.t('Raw Format')} </span>
-					{:else}
-						<span class="ml-2 self-center"> {$i18n.t('Builder Mode')} </span>
-					{/if}
-				</button>
+				<div class=" self-center text-sm font-semibold">{$i18n.t('Model Params')}</div>
 			</div>
 
-			<!-- <div class=" text-sm font-semibold mb-2"></div> -->
-
-			{#if raw}
-				<div class="mt-2">
-					<div class=" text-xs font-semibold mb-2">{$i18n.t('Content')}*</div>
-
-					<div>
-						<textarea
-							class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
-							placeholder={`FROM llama2\nPARAMETER temperature 1\nSYSTEM """\nYou are Mario from Super Mario Bros, acting as an assistant.\n"""`}
-							rows="6"
-							bind:value={content}
-							required
-						/>
-					</div>
-
-					<div class="text-xs text-gray-400 dark:text-gray-500">
-						{$i18n.t('Not sure what to write? Switch to')}
-						<button
-							class="text-gray-500 dark:text-gray-300 font-medium cursor-pointer"
-							type="button"
-							on:click={() => {
-								raw = !raw;
-							}}>{$i18n.t('Builder Mode')}</button
-						>
-						or
-						<a
-							class=" text-gray-500 dark:text-gray-300 font-medium"
-							href="https://openwebui.com"
-							target="_blank"
-						>
-							{$i18n.t('Click here to check other modelfiles.')}
-						</a>
-					</div>
-				</div>
-			{:else}
-				<div class="my-2">
-					<div class=" text-xs font-semibold mb-2">{$i18n.t('From (Base Model)')}*</div>
-
-					<div>
-						<input
-							class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
-							placeholder="Write a modelfile base model name (e.g. llama2, mistral)"
-							bind:value={model}
-							required
-						/>
-					</div>
-
-					<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
-						{$i18n.t('To access the available model names for downloading,')}
-						<a
-							class=" text-gray-500 dark:text-gray-300 font-medium"
-							href="https://ollama.com/library"
-							target="_blank">{$i18n.t('click here.')}</a
-						>
-					</div>
-				</div>
-
+			<div class="mt-2">
 				<div class="my-1">
 					<div class=" text-xs font-semibold mb-2">{$i18n.t('System Prompt')}</div>
-
 					<div>
 						<textarea
 							class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1"
-							placeholder={`Write your modelfile system prompt content here\ne.g.) You are Mario from Super Mario Bros, acting as an assistant.`}
+							placeholder={`Write your model system prompt content here\ne.g.) You are Mario from Super Mario Bros, acting as an assistant.`}
 							rows="4"
-							bind:value={system}
+							bind:value={info.params.system}
 						/>
 					</div>
 				</div>
 
 				<div class="flex w-full justify-between">
 					<div class=" self-center text-sm font-semibold">
-						{$i18n.t('Modelfile Advanced Settings')}
+						{$i18n.t('Advanced Params')}
 					</div>
 
 					<button
 						class="p-1 px-3 text-xs flex rounded transition"
 						type="button"
 						on:click={() => {
-							advanced = !advanced;
+							showAdvanced = !showAdvanced;
 						}}
 					>
-						{#if advanced}
-							<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
+						{#if showAdvanced}
+							<span class="ml-2 self-center">{$i18n.t('Hide')}</span>
 						{:else}
-							<span class="ml-2 self-center">{$i18n.t('Default')}</span>
+							<span class="ml-2 self-center">{$i18n.t('Show')}</span>
 						{/if}
 					</button>
 				</div>
 
-				{#if advanced}
-					<div class="my-2">
-						<div class=" text-xs font-semibold mb-2">{$i18n.t('Template')}</div>
-
-						<div>
-							<textarea
-								class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1"
-								placeholder="Write your modelfile template content here"
-								rows="4"
-								bind:value={template}
-							/>
-						</div>
-					</div>
-
+				{#if showAdvanced}
 					<div class="my-2">
-						<div class=" text-xs font-semibold mb-2">{$i18n.t('Parameters')}</div>
-
-						<div>
-							<AdvancedParams bind:params />
-						</div>
+						<AdvancedParams
+							bind:params
+							on:change={(e) => {
+								info.params = { ...info.params, ...params };
+							}}
+						/>
 					</div>
 				{/if}
-			{/if}
+			</div>
 		</div>
 
 		<div class="my-2">
@@ -598,8 +369,11 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
 					class="p-1 px-3 text-xs flex rounded transition"
 					type="button"
 					on:click={() => {
-						if (suggestions.length === 0 || suggestions.at(-1).content !== '') {
-							suggestions = [...suggestions, { content: '' }];
+						if (
+							info.meta.suggestion_prompts.length === 0 ||
+							info.meta.suggestion_prompts.at(-1).content !== ''
+						) {
+							info.meta.suggestion_prompts = [...info.meta.suggestion_prompts, { content: '' }];
 						}
 					}}
 				>
@@ -616,7 +390,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
 				</button>
 			</div>
 			<div class="flex flex-col space-y-1">
-				{#each suggestions as prompt, promptIdx}
+				{#each info.meta.suggestion_prompts as prompt, promptIdx}
 					<div class=" flex border dark:border-gray-600 rounded-lg">
 						<input
 							class="px-3 py-1.5 text-sm w-full bg-transparent outline-none border-r dark:border-gray-600"
@@ -628,8 +402,8 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
 							class="px-2"
 							type="button"
 							on:click={() => {
-								suggestions.splice(promptIdx, 1);
-								suggestions = suggestions;
+								info.meta.suggestion_prompts.splice(promptIdx, 1);
+								info.meta.suggestion_prompts = info.meta.suggestion_prompts;
 							}}
 						>
 							<svg
@@ -648,37 +422,39 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
 			</div>
 		</div>
 
-		<div class="my-2">
-			<div class=" text-sm font-semibold mb-2">{$i18n.t('Categories')}</div>
+		<div class="my-2 text-gray-500">
+			<div class="flex w-full justify-between mb-2">
+				<div class=" self-center text-sm font-semibold">{$i18n.t('JSON Preview')}</div>
 
-			<div class="grid grid-cols-4">
-				{#each Object.keys(categories) as category}
-					<div class="flex space-x-2 text-sm">
-						<input type="checkbox" bind:checked={categories[category]} />
-						<div class="capitalize">{category}</div>
-					</div>
-				{/each}
+				<button
+					class="p-1 px-3 text-xs flex rounded 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>
-		</div>
 
-		{#if pullProgress !== null}
-			<div class="my-2">
-				<div class=" text-sm font-semibold mb-2">{$i18n.t('Pull Progress')}</div>
-				<div class="w-full rounded-full dark:bg-gray-800">
-					<div
-						class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
-						style="width: {Math.max(15, pullProgress ?? 0)}%"
-					>
-						{pullProgress ?? 0}%
-					</div>
-				</div>
-				<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
-					{digest}
+			{#if showPreview}
+				<div>
+					<textarea
+						class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
+						rows="10"
+						value={JSON.stringify(info, null, 2)}
+						disabled
+						readonly
+					/>
 				</div>
-			</div>
-		{/if}
+			{/if}
+		</div>
 
-		<div class="my-2 flex justify-end">
+		<div class="my-2 flex justify-end mb-20">
 			<button
 				class=" text-sm px-3 py-2 transition rounded-xl {loading
 					? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800'

+ 24 - 24
src/routes/(app)/workspace/models/edit/+page.svelte

@@ -32,6 +32,10 @@
 	// ///////////
 
 	let model = null;
+
+	let id = '';
+	let name = '';
+
 	let info = {
 		id: '',
 		base_model_id: null,
@@ -51,9 +55,14 @@
 
 	const updateHandler = async () => {
 		loading = true;
+
+		info.id = id;
+		info.name = name;
+
 		const res = await updateModelById(localStorage.token, info.id, info);
 
 		if (res) {
+			toast.success('Model updated successfully');
 			await goto('/workspace/models');
 			await models.set(await getModels(localStorage.token));
 		}
@@ -63,11 +72,14 @@
 	};
 
 	onMount(() => {
-		const id = $page.url.searchParams.get('id');
+		const _id = $page.url.searchParams.get('id');
 
-		if (id) {
-			model = $models.find((m) => m.id === id);
+		if (_id) {
+			model = $models.find((m) => m.id === _id);
 			if (model) {
+				id = model.id;
+				name = model.name;
+
 				info = {
 					...info,
 					...JSON.parse(
@@ -235,7 +247,7 @@
 						<input
 							class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
 							placeholder={$i18n.t('Name your model')}
-							bind:value={info.name}
+							bind:value={name}
 							required
 						/>
 					</div>
@@ -248,7 +260,7 @@
 						<input
 							class="px-3 py-1.5 text-sm w-full bg-transparent disabled:text-gray-500 border dark:border-gray-600 outline-none rounded-lg"
 							placeholder={$i18n.t('Add a model id')}
-							value={info.id}
+							value={id}
 							disabled
 							required
 						/>
@@ -333,7 +345,12 @@
 
 					{#if showAdvanced}
 						<div class="my-2">
-							<AdvancedParams bind:params />
+							<AdvancedParams
+								bind:params
+								on:change={(e) => {
+									info.params = { ...info.params, ...params };
+								}}
+							/>
 						</div>
 					{/if}
 				</div>
@@ -432,24 +449,7 @@
 				{/if}
 			</div>
 
-			{#if pullProgress !== null}
-				<div class="my-2">
-					<div class=" text-sm font-semibold mb-2">{$i18n.t('Pull Progress')}</div>
-					<div class="w-full rounded-full dark:bg-gray-800">
-						<div
-							class="dark:bg-gray-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full"
-							style="width: {Math.max(15, pullProgress ?? 0)}%"
-						>
-							{pullProgress ?? 0}%
-						</div>
-					</div>
-					<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
-						{digest}
-					</div>
-				</div>
-			{/if}
-
-			<div class="my-2 flex justify-end">
+			<div class="my-2 flex justify-end mb-20">
 				<button
 					class=" text-sm px-3 py-2 transition rounded-xl {loading
 						? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800'