Ver Fonte

feat: model update

Timothy J. Baek há 11 meses atrás
pai
commit
708d755eda

+ 3 - 0
backend/apps/web/main.py

@@ -40,6 +40,9 @@ app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
 app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
 app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
 app.state.config.WEBHOOK_URL = WEBHOOK_URL
+
+
+app.state.MODELS = {}
 app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
 
 

+ 22 - 11
backend/apps/web/models/models.py

@@ -33,6 +33,8 @@ class ModelParams(BaseModel):
 # ModelMeta is a model for the data stored in the meta field of the Model table
 # It isn't currently used in the backend, but it's here as a reference
 class ModelMeta(BaseModel):
+    profile_image_url: Optional[str] = "/favicon.png"
+
     description: Optional[str] = None
     """
         User-facing description of the model.
@@ -84,6 +86,7 @@ class Model(pw.Model):
 
 class ModelModel(BaseModel):
     id: str
+    user_id: str
     base_model_id: Optional[str] = None
 
     name: str
@@ -123,18 +126,26 @@ class ModelsTable:
         self.db = db
         self.db.create_tables([Model])
 
-    def insert_new_model(self, model: ModelForm, user_id: str) -> Optional[ModelModel]:
+    def insert_new_model(
+        self, form_data: ModelForm, user_id: str
+    ) -> Optional[ModelModel]:
+        model = ModelModel(
+            **{
+                **form_data.model_dump(),
+                "user_id": user_id,
+                "created_at": int(time.time()),
+                "updated_at": int(time.time()),
+            }
+        )
         try:
-            model = Model.create(
-                **{
-                    **model.model_dump(),
-                    "user_id": user_id,
-                    "created_at": int(time.time()),
-                    "updated_at": int(time.time()),
-                }
-            )
-            return ModelModel(**model_to_dict(model))
-        except:
+            result = Model.create(**model.model_dump())
+
+            if result:
+                return model
+            else:
+                return None
+        except Exception as e:
+            print(e)
             return None
 
     def get_all_models(self) -> List[ModelModel]:

+ 17 - 6
backend/apps/web/routers/models.py

@@ -1,4 +1,4 @@
-from fastapi import Depends, FastAPI, HTTPException, status
+from fastapi import Depends, FastAPI, HTTPException, status, Request
 from datetime import datetime, timedelta
 from typing import List, Union, Optional
 
@@ -65,17 +65,28 @@ async def get_model_by_id(id: str, user=Depends(get_verified_user)):
 
 @router.post("/{id}/update", response_model=Optional[ModelModel])
 async def update_model_by_id(
-    id: str, form_data: ModelForm, user=Depends(get_admin_user)
+    request: Request, id: str, form_data: ModelForm, user=Depends(get_admin_user)
 ):
     model = Models.get_model_by_id(id)
     if model:
         model = Models.update_model_by_id(id, form_data)
         return model
     else:
-        raise HTTPException(
-            status_code=status.HTTP_401_UNAUTHORIZED,
-            detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
-        )
+        if form_data.id in request.app.state.MODELS:
+            model = Models.insert_new_model(form_data, user.id)
+            print(model)
+            if model:
+                return model
+            else:
+                raise HTTPException(
+                    status_code=status.HTTP_401_UNAUTHORIZED,
+                    detail=ERROR_MESSAGES.DEFAULT(),
+                )
+        else:
+            raise HTTPException(
+                status_code=status.HTTP_401_UNAUTHORIZED,
+                detail=ERROR_MESSAGES.DEFAULT(),
+            )
 
 
 ############################

+ 19 - 7
backend/main.py

@@ -122,6 +122,9 @@ app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
 
 app.state.config.WEBHOOK_URL = WEBHOOK_URL
 
+
+app.state.MODELS = {}
+
 origins = ["*"]
 
 
@@ -238,6 +241,11 @@ app.add_middleware(
 
 @app.middleware("http")
 async def check_url(request: Request, call_next):
+    if len(app.state.MODELS) == 0:
+        await get_all_models()
+    else:
+        pass
+
     start_time = int(time.time())
     response = await call_next(request)
     process_time = int(time.time()) - start_time
@@ -269,8 +277,7 @@ app.mount("/api/v1", webui_app)
 webui_app.state.EMBEDDING_FUNCTION = rag_app.state.EMBEDDING_FUNCTION
 
 
-@app.get("/api/models")
-async def get_models(user=Depends(get_verified_user)):
+async def get_all_models():
     openai_models = []
     ollama_models = []
 
@@ -282,8 +289,6 @@ async def get_models(user=Depends(get_verified_user)):
     if app.state.config.ENABLE_OLLAMA_API:
         ollama_models = await get_ollama_models()
 
-        print(ollama_models)
-
         ollama_models = [
             {
                 "id": model["model"],
@@ -296,9 +301,6 @@ async def get_models(user=Depends(get_verified_user)):
             for model in ollama_models["models"]
         ]
 
-    print("openai", openai_models)
-    print("ollama", ollama_models)
-
     models = openai_models + ollama_models
     custom_models = Models.get_all_models()
 
@@ -330,6 +332,16 @@ async def get_models(user=Depends(get_verified_user)):
                 }
             )
 
+    app.state.MODELS = {model["id"]: model for model in models}
+
+    webui_app.state.MODELS = app.state.MODELS
+
+    return models
+
+
+@app.get("/api/models")
+async def get_models(user=Depends(get_verified_user)):
+    models = await get_all_models()
     if app.state.config.ENABLE_MODEL_FILTER:
         if user.role == "user":
             models = list(

+ 41 - 11
src/lib/components/workspace/Models.svelte

@@ -7,6 +7,8 @@
 
 	import { WEBUI_NAME, modelfiles, models, settings, user } from '$lib/stores';
 	import { addNewModel, deleteModelById, getModelInfos } from '$lib/apis/models';
+
+	import { deleteModel } from '$lib/apis/ollama';
 	import { goto } from '$app/navigation';
 
 	import { getModels } from '$lib/apis';
@@ -17,13 +19,42 @@
 	let importFiles;
 	let modelfilesImportInputElement: HTMLInputElement;
 
-	const deleteModelHandler = async (id) => {
-		const res = await deleteModelById(localStorage.token, id);
+	const deleteModelHandler = async (model) => {
+		if (model?.info?.base_model_id) {
+			const res = await deleteModelById(localStorage.token, model.id);
+
+			if (res) {
+				toast.success($i18n.t(`Deleted {{name}}`, { name: model.id }));
+			}
+			await models.set(await getModels(localStorage.token));
+		} else if (model?.owned_by === 'ollama') {
+			const res = await deleteModel(localStorage.token, model.id);
+
+			if (res) {
+				toast.success($i18n.t(`Deleted {{name}}`, { name: model.id }));
+			}
+			await models.set(await getModels(localStorage.token));
+		} else {
+			toast.error(
+				$i18n.t('{{ owner }}: You cannot delete this model', {
+					owner: model.owned_by.toUpperCase()
+				})
+			);
+		}
+	};
 
-		if (res) {
-			toast.success($i18n.t(`Deleted {{tagName}}`, { id }));
+	const cloneModelHandler = async (model) => {
+		if ((model?.info?.base_model_id ?? null) === null) {
+			toast.error($i18n.t('You cannot clone a base model'));
+			return;
+		} else {
+			sessionStorage.model = JSON.stringify({
+				...model,
+				id: `${model.id}-clone`,
+				name: `${model.name} (Clone)`
+			});
+			goto('/workspace/models/create');
 		}
-		await models.set(await getModels(localStorage.token));
 	};
 
 	const shareModelHandler = async (model) => {
@@ -104,7 +135,7 @@
 				<div class=" self-center w-10">
 					<div class=" rounded-full bg-stone-700">
 						<img
-							src={model?.meta?.profile_image_url ?? '/favicon.png'}
+							src={model?.info?.meta?.profile_image_url ?? '/favicon.png'}
 							alt="modelfile profile"
 							class=" rounded-full w-full h-auto object-cover"
 						/>
@@ -114,7 +145,7 @@
 				<div class=" flex-1 self-center">
 					<div class=" font-bold capitalize">{model.name}</div>
 					<div class=" text-sm overflow-hidden text-ellipsis line-clamp-1">
-						{model?.meta?.description ?? 'No description'}
+						{model?.info?.meta?.description ?? model.id}
 					</div>
 				</div>
 			</a>
@@ -122,7 +153,7 @@
 				<a
 					class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
 					type="button"
-					href={`/workspace/models/edit?tag=${encodeURIComponent(model.id)}`}
+					href={`/workspace/models/edit?id=${encodeURIComponent(model.id)}`}
 				>
 					<svg
 						xmlns="http://www.w3.org/2000/svg"
@@ -144,8 +175,7 @@
 					class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
 					type="button"
 					on:click={() => {
-						sessionStorage.model = JSON.stringify(model);
-						goto('/workspace/models/create');
+						cloneModelHandler(model);
 					}}
 				>
 					<svg
@@ -191,7 +221,7 @@
 					class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
 					type="button"
 					on:click={() => {
-						deleteModelHandler(model.id);
+						deleteModelHandler(model);
 					}}
 				>
 					<svg

+ 1 - 1
src/lib/stores/index.ts

@@ -48,7 +48,7 @@ export type Model = OpenAIModel | OllamaModel;
 type BaseModel = {
 	id: string;
 	name: string;
-	custom_info?: ModelConfig;
+	info?: ModelConfig;
 };
 
 export interface OpenAIModel extends BaseModel {

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

@@ -5,181 +5,83 @@
 
 	import { onMount, getContext } from 'svelte';
 	import { page } from '$app/stores';
-
-	import { settings, user, config, modelfiles } from '$lib/stores';
+	import { settings, user, config, models } from '$lib/stores';
 	import { splitStream } from '$lib/utils';
 
-	import { createModel } from '$lib/apis/ollama';
 	import { getModelInfos, updateModelById } from '$lib/apis/models';
 
 	import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
+	import { getModels } from '$lib/apis';
 
 	const i18n = getContext('i18n');
 
 	let loading = false;
+	let success = false;
 
 	let filesInputElement;
 	let inputFiles;
-	let imageUrl = null;
+
 	let digest = '';
 	let pullProgress = null;
-	let success = false;
 
-	let modelfile = null;
 	// ///////////
-	// Modelfile
+	// model
 	// ///////////
 
-	let title = '';
-	let tagName = '';
-	let desc = '';
-
-	// Raw Mode
-	let content = '';
-
-	let suggestions = [
-		{
-			content: ''
-		}
-	];
-
-	let categories = {
-		character: false,
-		assistant: false,
-		writing: false,
-		productivity: false,
-		programming: false,
-		'data analysis': false,
-		lifestyle: false,
-		education: false,
-		business: false
-	};
-
-	onMount(() => {
-		tagName = $page.url.searchParams.get('tag');
-
-		if (tagName) {
-			modelfile = $modelfiles.filter((modelfile) => modelfile.tagName === tagName)[0];
-
-			console.log(modelfile);
-
-			imageUrl = modelfile.imageUrl;
-			title = modelfile.title;
-			desc = modelfile.desc;
-			content = modelfile.content;
-			suggestions =
-				modelfile.suggestionPrompts.length != 0
-					? modelfile.suggestionPrompts
-					: [
-							{
-								content: ''
-							}
-					  ];
-
-			for (const category of modelfile.categories) {
-				categories[category.toLowerCase()] = true;
-			}
-		} else {
-			goto('/workspace/modelfiles');
-		}
-	});
-
-	const updateModelfile = async (modelfile) => {
-		await updateModelById(localStorage.token, modelfile.tagName, modelfile);
-		await modelfiles.set(await getModelInfos(localStorage.token));
+	let model = null;
+	let info = {
+		id: '',
+		base_model_id: null,
+		name: '',
+		meta: {
+			profile_image_url: '/favicon.png',
+			description: '',
+			content: '',
+			suggestion_prompts: []
+		},
+		params: {}
 	};
 
 	const updateHandler = async () => {
 		loading = true;
+		const res = await updateModelById(localStorage.token, info.id, info);
 
-		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.'
-			);
+		if (res) {
+			await goto('/workspace/models');
+			await models.set(await getModels(localStorage.token));
 		}
 
-		if (
-			title !== '' &&
-			desc !== '' &&
-			content !== '' &&
-			Object.keys(categories).filter((category) => categories[category]).length > 0
-		) {
-			const res = await createModel(localStorage.token, tagName, content);
-
-			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 updateModelfile({
-					tagName: tagName,
-					imageUrl: imageUrl,
-					title: title,
-					desc: desc,
-					content: content,
-					suggestionPrompts: suggestions.filter((prompt) => prompt.content !== ''),
-					categories: Object.keys(categories).filter((category) => categories[category])
-				});
-				await goto('/workspace/modelfiles');
-			}
-		}
 		loading = false;
 		success = false;
 	};
+
+	onMount(() => {
+		const id = $page.url.searchParams.get('id');
+
+		if (id) {
+			model = $models.find((m) => m.id === id);
+			if (model) {
+				info = {
+					...info,
+					...JSON.parse(
+						JSON.stringify(
+							model?.info
+								? model?.info
+								: {
+										id: model.id,
+										name: model.name
+								  }
+						)
+					)
+				};
+				console.log(model);
+			} else {
+				goto('/workspace/models');
+			}
+		} else {
+			goto('/workspace/models');
+		}
+	});
 </script>
 
 <div class="w-full max-h-full">
@@ -229,7 +131,7 @@
 					const compressedSrc = canvas.toDataURL('image/jpeg');
 
 					// Display the compressed image
-					imageUrl = compressedSrc;
+					info.meta.profile_image_url = compressedSrc;
 
 					inputFiles = null;
 				};
@@ -270,238 +172,230 @@
 		</div>
 		<div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
 	</button>
-	<form
-		class="flex flex-col max-w-2xl mx-auto mt-4 mb-10"
-		on:submit|preventDefault={() => {
-			updateHandler();
-		}}
-	>
-		<div class="flex justify-center my-4">
-			<div class="self-center">
-				<button
-					class=" {imageUrl
-						? ''
-						: 'p-6'} rounded-full dark:bg-gray-700 border border-dashed border-gray-200"
-					type="button"
-					on:click={() => {
-						filesInputElement.click();
-					}}
-				>
-					{#if imageUrl}
-						<img
-							src={imageUrl}
-							alt="modelfile profile"
-							class=" rounded-full w-20 h-20 object-cover"
-						/>
-					{:else}
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							viewBox="0 0 24 24"
-							fill="currentColor"
-							class="w-8"
-						>
-							<path
-								fill-rule="evenodd"
-								d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
-								clip-rule="evenodd"
-							/>
-						</svg>
-					{/if}
-				</button>
-			</div>
-		</div>
-
-		<div class="my-2 flex space-x-2">
-			<div class="flex-1">
-				<div class=" text-sm font-semibold mb-2">{$i18n.t('Name')}*</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('Name your modelfile')}
-						bind:value={title}
-						required
-					/>
+	{#if model}
+		<form
+			class="flex flex-col max-w-2xl mx-auto mt-4 mb-10"
+			on:submit|preventDefault={() => {
+				updateHandler();
+			}}
+		>
+			<div class="flex justify-center my-4">
+				<div class="self-center">
+					<button
+						class=" {info?.meta?.profile_image_url
+							? ''
+							: 'p-6'} rounded-full dark:bg-gray-700 border border-dashed border-gray-200"
+						type="button"
+						on:click={() => {
+							filesInputElement.click();
+						}}
+					>
+						{#if info?.meta?.profile_image_url}
+							<img
+								src={info?.meta?.profile_image_url}
+								alt="modelfile profile"
+								class=" rounded-full w-20 h-20 object-cover"
+							/>
+						{:else}
+							<svg
+								xmlns="http://www.w3.org/2000/svg"
+								viewBox="0 0 24 24"
+								fill="currentColor"
+								class="w-8"
+							>
+								<path
+									fill-rule="evenodd"
+									d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
+									clip-rule="evenodd"
+								/>
+							</svg>
+						{/if}
+					</button>
 				</div>
 			</div>
 
-			<div class="flex-1">
-				<div class=" text-sm font-semibold mb-2">{$i18n.t('Model Tag Name')}*</div>
+			<div class="my-2 flex space-x-2">
+				<div class="flex-1">
+					<div class=" text-sm font-semibold mb-2">{$i18n.t('Name')}*</div>
 
-				<div>
-					<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 tag name')}
-						value={tagName}
-						disabled
-						required
-					/>
+					<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 model')}
+							bind:value={info.name}
+							required
+						/>
+					</div>
 				</div>
-			</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}
-					required
-				/>
-			</div>
-		</div>
+				<div class="flex-1">
+					<div class=" text-sm font-semibold mb-2">{$i18n.t('Model ID')}*</div>
 
-		<div class="my-2">
-			<div class="flex w-full justify-between">
-				<div class=" self-center text-sm font-semibold">{$i18n.t('Modelfile')}</div>
+					<div>
+						<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}
+							disabled
+							required
+						/>
+					</div>
+				</div>
 			</div>
 
-			<!-- <div class=" text-sm font-semibold mb-2"></div> -->
-
-			<div class="mt-2">
-				<div class=" text-xs font-semibold mb-2">{$i18n.t('Content')}*</div>
+			<div class="my-2">
+				<div class=" text-sm font-semibold mb-2">{$i18n.t('description')}*</div>
 
 				<div>
-					<textarea
+					<input
 						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}
+						placeholder={$i18n.t('Add a short description about what this model does')}
+						bind:value={info.meta.description}
 						required
 					/>
 				</div>
 			</div>
-		</div>
-
-		<div class="my-2">
-			<div class="flex w-full justify-between mb-2">
-				<div class=" self-center text-sm font-semibold">{$i18n.t('Prompt suggestions')}</div>
-
-				<button
-					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: '' }];
-						}
-					}}
-				>
-					<svg
-						xmlns="http://www.w3.org/2000/svg"
-						viewBox="0 0 20 20"
-						fill="currentColor"
-						class="w-4 h-4"
-					>
-						<path
-							d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
-						/>
-					</svg>
-				</button>
-			</div>
-			<div class="flex flex-col space-y-1">
-				{#each suggestions 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"
-							placeholder={$i18n.t('Write a prompt suggestion (e.g. Who are you?)')}
-							bind:value={prompt.content}
-						/>
 
-						<button
-							class="px-2"
-							type="button"
-							on:click={() => {
-								suggestions.splice(promptIdx, 1);
-								suggestions = suggestions;
-							}}
-						>
-							<svg
-								xmlns="http://www.w3.org/2000/svg"
-								viewBox="0 0 20 20"
-								fill="currentColor"
-								class="w-4 h-4"
-							>
-								<path
-									d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
-								/>
-							</svg>
-						</button>
-					</div>
-				{/each}
-			</div>
-		</div>
+			<div class="my-2">
+				<div class="flex w-full justify-between">
+					<div class=" self-center text-sm font-semibold">{$i18n.t('Model')}</div>
+				</div>
 
-		<div class="my-2">
-			<div class=" text-sm font-semibold mb-2">{$i18n.t('Categories')}</div>
+				<!-- <div class=" text-sm font-semibold mb-2"></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="mt-2">
+					<div class=" text-xs font-semibold mb-2">{$i18n.t('Params')}*</div>
 
-						<div class=" capitalize">{category}</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>
-				{/each}
+				</div>
 			</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 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full"
-						style="width: {Math.max(15, pullProgress ?? 0)}%"
+				<div class="flex w-full justify-between mb-2">
+					<div class=" self-center text-sm font-semibold">{$i18n.t('Prompt suggestions')}</div>
+
+					<button
+						class="p-1 px-3 text-xs flex rounded transition"
+						type="button"
+						on:click={() => {
+							if (
+								info.meta.suggestion_prompts.length === 0 ||
+								info.meta.suggestion_prompts.at(-1).content !== ''
+							) {
+								info.meta.suggestion_prompts = [...info.meta.suggestion_prompts, { content: '' }];
+							}
+						}}
 					>
-						{pullProgress ?? 0}%
-					</div>
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							viewBox="0 0 20 20"
+							fill="currentColor"
+							class="w-4 h-4"
+						>
+							<path
+								d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
+							/>
+						</svg>
+					</button>
 				</div>
-				<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
-					{digest}
+				<div class="flex flex-col space-y-1">
+					{#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"
+								placeholder={$i18n.t('Write a prompt suggestion (e.g. Who are you?)')}
+								bind:value={prompt.content}
+							/>
+
+							<button
+								class="px-2"
+								type="button"
+								on:click={() => {
+									info.meta.suggestion_prompts.splice(promptIdx, 1);
+									info.meta.suggestion_prompts = info.meta.suggestion_prompts;
+								}}
+							>
+								<svg
+									xmlns="http://www.w3.org/2000/svg"
+									viewBox="0 0 20 20"
+									fill="currentColor"
+									class="w-4 h-4"
+								>
+									<path
+										d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
+									/>
+								</svg>
+							</button>
+						</div>
+					{/each}
 				</div>
 			</div>
-		{/if}
-
-		<div class="my-2 flex justify-end">
-			<button
-				class=" text-sm px-3 py-2 transition rounded-xl {loading
-					? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800'
-					: ' bg-gray-50 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-800'} flex"
-				type="submit"
-				disabled={loading}
-			>
-				<div class=" self-center font-medium">{$i18n.t('Save & Update')}</div>
 
-				{#if loading}
-					<div class="ml-1.5 self-center">
-						<svg
-							class=" w-4 h-4"
-							viewBox="0 0 24 24"
-							fill="currentColor"
-							xmlns="http://www.w3.org/2000/svg"
-							><style>
-								.spinner_ajPY {
-									transform-origin: center;
-									animation: spinner_AtaB 0.75s infinite linear;
-								}
-								@keyframes spinner_AtaB {
-									100% {
-										transform: rotate(360deg);
-									}
-								}
-							</style><path
-								d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-								opacity=".25"
-							/><path
-								d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-								class="spinner_ajPY"
-							/></svg
+			{#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>
-				{/if}
-			</button>
-		</div>
-	</form>
+					<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">
+				<button
+					class=" text-sm px-3 py-2 transition rounded-xl {loading
+						? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800'
+						: ' bg-gray-50 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-800'} flex"
+					type="submit"
+					disabled={loading}
+				>
+					<div class=" self-center font-medium">{$i18n.t('Save & Update')}</div>
+
+					{#if loading}
+						<div class="ml-1.5 self-center">
+							<svg
+								class=" w-4 h-4"
+								viewBox="0 0 24 24"
+								fill="currentColor"
+								xmlns="http://www.w3.org/2000/svg"
+								><style>
+									.spinner_ajPY {
+										transform-origin: center;
+										animation: spinner_AtaB 0.75s infinite linear;
+									}
+									@keyframes spinner_AtaB {
+										100% {
+											transform: rotate(360deg);
+										}
+									}
+								</style><path
+									d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
+									opacity=".25"
+								/><path
+									d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
+									class="spinner_ajPY"
+								/></svg
+							>
+						</div>
+					{/if}
+				</button>
+			</div>
+		</form>
+	{/if}
 </div>

Diff do ficheiro suprimidas por serem muito extensas
+ 2 - 0
src/routes/(app)/workspace/models/edit/asdf.json


Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff