浏览代码

refac: frontend

Timothy Jaeryang Baek 5 月之前
父节点
当前提交
d9dc04f1a1

+ 22 - 17
src/lib/apis/models/index.ts

@@ -1,23 +1,26 @@
 import { WEBUI_API_BASE_URL } from '$lib/constants';
 
-export const addNewModel = async (token: string, model: object) => {
+
+export const getWorkspaceModels = async (token: string = '') => {
 	let error = null;
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/models/add`, {
-		method: 'POST',
+	const res = await fetch(`${WEBUI_API_BASE_URL}/models`, {
+		method: 'GET',
 		headers: {
 			Accept: 'application/json',
 			'Content-Type': 'application/json',
 			authorization: `Bearer ${token}`
-		},
-		body: JSON.stringify(model)
+		}
 	})
 		.then(async (res) => {
 			if (!res.ok) throw await res.json();
 			return res.json();
 		})
+		.then((json) => {
+			return json;
+		})
 		.catch((err) => {
-			error = err.detail;
+			error = err;
 			console.log(err);
 			return null;
 		});
@@ -29,26 +32,26 @@ export const addNewModel = async (token: string, model: object) => {
 	return res;
 };
 
-export const getModelInfos = async (token: string = '') => {
+
+
+export const createNewModel = async (token: string, model: object) => {
 	let error = null;
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/models`, {
-		method: 'GET',
+	const res = await fetch(`${WEBUI_API_BASE_URL}/models/create`, {
+		method: 'POST',
 		headers: {
 			Accept: 'application/json',
 			'Content-Type': 'application/json',
 			authorization: `Bearer ${token}`
-		}
+		},
+		body: JSON.stringify(model)
 	})
 		.then(async (res) => {
 			if (!res.ok) throw await res.json();
 			return res.json();
 		})
-		.then((json) => {
-			return json;
-		})
 		.catch((err) => {
-			error = err;
+			error = err.detail;
 			console.log(err);
 			return null;
 		});
@@ -60,13 +63,15 @@ export const getModelInfos = async (token: string = '') => {
 	return res;
 };
 
+
+
 export const getModelById = async (token: string, id: string) => {
 	let error = null;
 
 	const searchParams = new URLSearchParams();
 	searchParams.append('id', id);
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/models?${searchParams.toString()}`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/models/id/${id}`, {
 		method: 'GET',
 		headers: {
 			Accept: 'application/json',
@@ -101,7 +106,7 @@ export const updateModelById = async (token: string, id: string, model: object)
 	const searchParams = new URLSearchParams();
 	searchParams.append('id', id);
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/models/update?${searchParams.toString()}`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/models/id/${id}/update`, {
 		method: 'POST',
 		headers: {
 			Accept: 'application/json',
@@ -137,7 +142,7 @@ export const deleteModelById = async (token: string, id: string) => {
 	const searchParams = new URLSearchParams();
 	searchParams.append('id', id);
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/models/delete?${searchParams.toString()}`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/models/id/${id}/delete`, {
 		method: 'DELETE',
 		headers: {
 			Accept: 'application/json',

+ 4 - 2
src/lib/apis/ollama/index.ts

@@ -211,10 +211,12 @@ export const getOllamaVersion = async (token: string, urlIdx?: number) => {
 	return res?.version ?? false;
 };
 
-export const getOllamaModels = async (token: string = '') => {
+export const getOllamaModels = async (token: string = '', urlIdx: null|number = null) => {
 	let error = null;
 
-	const res = await fetch(`${OLLAMA_API_BASE_URL}/api/tags`, {
+	const res = await fetch(`${OLLAMA_API_BASE_URL}/api/tags${
+		urlIdx !== null ? `/${urlIdx}` : ''
+	}`, {
 		method: 'GET',
 		headers: {
 			Accept: 'application/json',

+ 2 - 2
src/lib/apis/users/index.ts

@@ -4,7 +4,7 @@ import { getUserPosition } from '$lib/utils';
 export const getUserPermissions = async (token: string) => {
 	let error = null;
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/users/permissions/user`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/users/permissions`, {
 		method: 'GET',
 		headers: {
 			'Content-Type': 'application/json',
@@ -31,7 +31,7 @@ export const getUserPermissions = async (token: string) => {
 export const updateUserPermissions = async (token: string, permissions: object) => {
 	let error = null;
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/users/permissions/user`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/users/permissions`, {
 		method: 'POST',
 		headers: {
 			'Content-Type': 'application/json',

+ 1 - 0
src/lib/components/admin/Settings/Connections.svelte

@@ -302,6 +302,7 @@
 									<OllamaConnection
 										bind:url
 										bind:config={OLLAMA_API_CONFIGS[url]}
+										{idx}
 										onSubmit={() => {
 											updateOllamaHandler();
 										}}

+ 1019 - 0
src/lib/components/admin/Settings/Connections/ManageOllamaModal.svelte

@@ -0,0 +1,1019 @@
+<script lang="ts">
+	import { toast } from 'svelte-sonner';
+	import { getContext, onMount } from 'svelte';
+	const i18n = getContext('i18n');
+
+	import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores';
+	import { splitStream } from '$lib/utils';
+
+	import {
+		createModel,
+		deleteModel,
+		downloadModel,
+		getOllamaUrls,
+		getOllamaVersion,
+		pullModel,
+		uploadModel,
+		getOllamaConfig,
+		getOllamaModels
+	} from '$lib/apis/ollama';
+	import { getModels } from '$lib/apis';
+
+	import Modal from '$lib/components/common/Modal.svelte';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import ModelDeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
+	import Spinner from '$lib/components/common/Spinner.svelte';
+
+	export let show = false;
+
+	let modelUploadInputElement: HTMLInputElement;
+	let showModelDeleteConfirm = false;
+
+	let loading = true;
+
+	// Models
+	export let urlIdx: number | null = null;
+
+	let ollamaModels = [];
+
+	let updateModelId = null;
+	let updateProgress = null;
+	let showExperimentalOllama = false;
+
+	const MAX_PARALLEL_DOWNLOADS = 3;
+
+	let modelTransferring = false;
+	let modelTag = '';
+
+	let createModelLoading = false;
+	let createModelTag = '';
+	let createModelContent = '';
+	let createModelDigest = '';
+	let createModelPullProgress = null;
+
+	let digest = '';
+	let pullProgress = null;
+
+	let modelUploadMode = 'file';
+	let modelInputFile: File[] | null = null;
+	let modelFileUrl = '';
+	let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`;
+	let modelFileDigest = '';
+
+	let uploadProgress = null;
+	let uploadMessage = '';
+
+	let deleteModelTag = '';
+
+	const updateModelsHandler = async () => {
+		for (const model of ollamaModels) {
+			console.log(model);
+
+			updateModelId = model.id;
+			const [res, controller] = await pullModel(localStorage.token, model.id, urlIdx).catch(
+				(error) => {
+					toast.error(error);
+					return null;
+				}
+			);
+
+			if (res) {
+				const reader = res.body
+					.pipeThrough(new TextDecoderStream())
+					.pipeThrough(splitStream('\n'))
+					.getReader();
+
+				while (true) {
+					try {
+						const { value, done } = await reader.read();
+						if (done) break;
+
+						let lines = value.split('\n');
+
+						for (const line of lines) {
+							if (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) {
+										updateProgress = 0;
+										if (data.completed) {
+											updateProgress = Math.round((data.completed / data.total) * 1000) / 10;
+										} else {
+											updateProgress = 100;
+										}
+									} else {
+										toast.success(data.status);
+									}
+								}
+							}
+						}
+					} catch (error) {
+						console.log(error);
+					}
+				}
+			}
+		}
+
+		updateModelId = null;
+		updateProgress = null;
+	};
+
+	const pullModelHandler = async () => {
+		const sanitizedModelTag = modelTag.trim().replace(/^ollama\s+(run|pull)\s+/, '');
+		console.log($MODEL_DOWNLOAD_POOL);
+		if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]) {
+			toast.error(
+				$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {
+					modelTag: sanitizedModelTag
+				})
+			);
+			return;
+		}
+		if (Object.keys($MODEL_DOWNLOAD_POOL).length === MAX_PARALLEL_DOWNLOADS) {
+			toast.error(
+				$i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.')
+			);
+			return;
+		}
+
+		const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, urlIdx).catch(
+			(error) => {
+				toast.error(error);
+				return null;
+			}
+		);
+
+		if (res) {
+			const reader = res.body
+				.pipeThrough(new TextDecoderStream())
+				.pipeThrough(splitStream('\n'))
+				.getReader();
+
+			MODEL_DOWNLOAD_POOL.set({
+				...$MODEL_DOWNLOAD_POOL,
+				[sanitizedModelTag]: {
+					...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
+					abortController: controller,
+					reader,
+					done: false
+				}
+			});
+
+			while (true) {
+				try {
+					const { value, done } = await reader.read();
+					if (done) break;
+
+					let lines = value.split('\n');
+
+					for (const line of lines) {
+						if (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) {
+									let downloadProgress = 0;
+									if (data.completed) {
+										downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
+									} else {
+										downloadProgress = 100;
+									}
+
+									MODEL_DOWNLOAD_POOL.set({
+										...$MODEL_DOWNLOAD_POOL,
+										[sanitizedModelTag]: {
+											...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
+											pullProgress: downloadProgress,
+											digest: data.digest
+										}
+									});
+								} else {
+									toast.success(data.status);
+
+									MODEL_DOWNLOAD_POOL.set({
+										...$MODEL_DOWNLOAD_POOL,
+										[sanitizedModelTag]: {
+											...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
+											done: data.status === 'success'
+										}
+									});
+								}
+							}
+						}
+					}
+				} catch (error) {
+					console.log(error);
+					if (typeof error !== 'string') {
+						error = error.message;
+					}
+
+					toast.error(error);
+					// opts.callback({ success: false, error, modelName: opts.modelName });
+				}
+			}
+
+			console.log($MODEL_DOWNLOAD_POOL[sanitizedModelTag]);
+
+			if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) {
+				toast.success(
+					$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, {
+						modelName: sanitizedModelTag
+					})
+				);
+
+				models.set(await getModels(localStorage.token));
+			} else {
+				toast.error($i18n.t('Download canceled'));
+			}
+
+			delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag];
+
+			MODEL_DOWNLOAD_POOL.set({
+				...$MODEL_DOWNLOAD_POOL
+			});
+		}
+
+		modelTag = '';
+		modelTransferring = false;
+	};
+
+	const uploadModelHandler = async () => {
+		modelTransferring = true;
+
+		let uploaded = false;
+		let fileResponse = null;
+		let name = '';
+
+		if (modelUploadMode === 'file') {
+			const file = modelInputFile ? modelInputFile[0] : null;
+
+			if (file) {
+				uploadMessage = 'Uploading...';
+
+				fileResponse = await uploadModel(localStorage.token, file, urlIdx).catch((error) => {
+					toast.error(error);
+					return null;
+				});
+			}
+		} else {
+			uploadProgress = 0;
+			fileResponse = await downloadModel(localStorage.token, modelFileUrl, urlIdx).catch(
+				(error) => {
+					toast.error(error);
+					return null;
+				}
+			);
+		}
+
+		if (fileResponse && fileResponse.ok) {
+			const reader = fileResponse.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 !== '') {
+							let data = JSON.parse(line.replace(/^data: /, ''));
+
+							if (data.progress) {
+								if (uploadMessage) {
+									uploadMessage = '';
+								}
+								uploadProgress = data.progress;
+							}
+
+							if (data.error) {
+								throw data.error;
+							}
+
+							if (data.done) {
+								modelFileDigest = data.blob;
+								name = data.name;
+								uploaded = true;
+							}
+						}
+					}
+				} catch (error) {
+					console.log(error);
+				}
+			}
+		} else {
+			const error = await fileResponse?.json();
+			toast.error(error?.detail ?? error);
+		}
+
+		if (uploaded) {
+			const res = await createModel(
+				localStorage.token,
+				`${name}:latest`,
+				`FROM @${modelFileDigest}\n${modelFileContent}`
+			);
+
+			if (res && res.ok) {
+				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);
+									} 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);
+					}
+				}
+			}
+		}
+
+		modelFileUrl = '';
+
+		if (modelUploadInputElement) {
+			modelUploadInputElement.value = '';
+		}
+		modelInputFile = null;
+		modelTransferring = false;
+		uploadProgress = null;
+
+		models.set(await getModels(localStorage.token));
+	};
+
+	const deleteModelHandler = async () => {
+		const res = await deleteModel(localStorage.token, deleteModelTag, urlIdx).catch((error) => {
+			toast.error(error);
+		});
+
+		if (res) {
+			toast.success($i18n.t(`Deleted {{deleteModelTag}}`, { deleteModelTag }));
+		}
+
+		deleteModelTag = '';
+		models.set(await getModels(localStorage.token));
+	};
+
+	const cancelModelPullHandler = async (model: string) => {
+		const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model];
+		if (abortController) {
+			abortController.abort();
+		}
+		if (reader) {
+			await reader.cancel();
+			delete $MODEL_DOWNLOAD_POOL[model];
+			MODEL_DOWNLOAD_POOL.set({
+				...$MODEL_DOWNLOAD_POOL
+			});
+			await deleteModel(localStorage.token, model);
+			toast.success(`${model} download has been canceled`);
+		}
+	};
+
+	const createModelHandler = async () => {
+		createModelLoading = true;
+		const res = await createModel(
+			localStorage.token,
+			createModelTag,
+			createModelContent,
+			urlIdx
+		).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+
+		if (res && res.ok) {
+			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);
+								} else {
+									if (data.digest) {
+										createModelDigest = data.digest;
+
+										if (data.completed) {
+											createModelPullProgress =
+												Math.round((data.completed / data.total) * 1000) / 10;
+										} else {
+											createModelPullProgress = 100;
+										}
+									}
+								}
+							}
+						}
+					}
+				} catch (error) {
+					console.log(error);
+					toast.error(error);
+				}
+			}
+		}
+
+		models.set(await getModels(localStorage.token));
+
+		createModelLoading = false;
+
+		createModelTag = '';
+		createModelContent = '';
+		createModelDigest = '';
+		createModelPullProgress = null;
+	};
+
+	const init = async () => {
+		loading = true;
+		ollamaModels = await getOllamaModels(localStorage.token, urlIdx);
+
+		console.log(ollamaModels);
+		loading = false;
+	};
+
+	$: if (show) {
+		init();
+	}
+</script>
+
+<ModelDeleteConfirmDialog
+	bind:show={showModelDeleteConfirm}
+	on:confirm={() => {
+		deleteModelHandler();
+	}}
+/>
+
+<Modal size="md" bind:show>
+	<div>
+		<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2">
+			<div class=" text-lg font-medium self-center font-primary">
+				{$i18n.t('Manage Ollama')} ({urlIdx})
+			</div>
+			<button
+				class="self-center"
+				on:click={() => {
+					show = false;
+				}}
+			>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 20 20"
+					fill="currentColor"
+					class="w-5 h-5"
+				>
+					<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>
+
+		<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
+			{#if !loading}
+				<div class=" flex flex-col w-full">
+					<div>
+						<div class="space-y-2">
+							<div>
+								<div class=" mb-2 text-sm font-medium">
+									{$i18n.t('Pull a model from Ollama.com')}
+								</div>
+								<div class="flex w-full">
+									<div class="flex-1 mr-2">
+										<input
+											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+											placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
+												modelTag: 'mistral:7b'
+											})}
+											bind:value={modelTag}
+										/>
+									</div>
+									<button
+										class="px-2.5 bg-gray-50 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={() => {
+											pullModelHandler();
+										}}
+										disabled={modelTransferring}
+									>
+										{#if modelTransferring}
+											<div class="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>
+										{:else}
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												viewBox="0 0 16 16"
+												fill="currentColor"
+												class="w-4 h-4"
+											>
+												<path
+													d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
+												/>
+												<path
+													d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
+												/>
+											</svg>
+										{/if}
+									</button>
+								</div>
+
+								<div class="mt-2 mb-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 underline"
+										href="https://ollama.com/library"
+										target="_blank">{$i18n.t('click here.')}</a
+									>
+								</div>
+
+								{#if Object.keys($MODEL_DOWNLOAD_POOL).length > 0}
+									{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
+										{#if 'pullProgress' in $MODEL_DOWNLOAD_POOL[model]}
+											<div class="flex flex-col">
+												<div class="font-medium mb-1">{model}</div>
+												<div class="">
+													<div class="flex flex-row justify-between space-x-4 pr-2">
+														<div class=" flex-1">
+															<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,
+																	$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0
+																)}%"
+															>
+																{$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0}%
+															</div>
+														</div>
+
+														<Tooltip content={$i18n.t('Cancel')}>
+															<button
+																class="text-gray-800 dark:text-gray-100"
+																on:click={() => {
+																	cancelModelPullHandler(model);
+																}}
+															>
+																<svg
+																	class="w-4 h-4 text-gray-800 dark:text-white"
+																	aria-hidden="true"
+																	xmlns="http://www.w3.org/2000/svg"
+																	width="24"
+																	height="24"
+																	fill="currentColor"
+																	viewBox="0 0 24 24"
+																>
+																	<path
+																		stroke="currentColor"
+																		stroke-linecap="round"
+																		stroke-linejoin="round"
+																		stroke-width="2"
+																		d="M6 18 17.94 6M18 18 6.06 6"
+																	/>
+																</svg>
+															</button>
+														</Tooltip>
+													</div>
+													{#if 'digest' in $MODEL_DOWNLOAD_POOL[model]}
+														<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
+															{$MODEL_DOWNLOAD_POOL[model].digest}
+														</div>
+													{/if}
+												</div>
+											</div>
+										{/if}
+									{/each}
+								{/if}
+							</div>
+
+							<div>
+								<div class=" mb-2 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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+											bind:value={deleteModelTag}
+											placeholder={$i18n.t('Select a model')}
+										>
+											{#if !deleteModelTag}
+												<option value="" disabled selected>{$i18n.t('Select a model')}</option>
+											{/if}
+											{#each ollamaModels as model}
+												<option value={model.id} class="bg-gray-50 dark:bg-gray-700"
+													>{model.name +
+														' (' +
+														(model.size / 1024 ** 3).toFixed(1) +
+														' GB)'}</option
+												>
+											{/each}
+										</select>
+									</div>
+									<button
+										class="px-2.5 bg-gray-50 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={() => {
+											showModelDeleteConfirm = true;
+										}}
+									>
+										<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>
+
+							<div>
+								<div class=" mb-2 text-sm font-medium">{$i18n.t('Create a model')}</div>
+								<div class="flex w-full">
+									<div class="flex-1 mr-2 flex flex-col gap-2">
+										<input
+											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+											placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
+												modelTag: 'my-modelfile'
+											})}
+											bind:value={createModelTag}
+											disabled={createModelLoading}
+										/>
+
+										<textarea
+											bind:value={createModelContent}
+											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden"
+											rows="6"
+											placeholder={`TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`}
+											disabled={createModelLoading}
+										/>
+									</div>
+
+									<div class="flex self-start">
+										<button
+											class="px-2.5 py-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed"
+											on:click={() => {
+												createModelHandler();
+											}}
+											disabled={createModelLoading}
+										>
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												viewBox="0 0 16 16"
+												fill="currentColor"
+												class="size-4"
+											>
+												<path
+													d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
+												/>
+												<path
+													d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
+												/>
+											</svg>
+										</button>
+									</div>
+								</div>
+
+								{#if createModelDigest !== ''}
+									<div class="flex flex-col mt-1">
+										<div class="font-medium mb-1">{createModelTag}</div>
+										<div class="">
+											<div class="flex flex-row justify-between space-x-4 pr-2">
+												<div class=" flex-1">
+													<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, createModelPullProgress ?? 0)}%"
+													>
+														{createModelPullProgress ?? 0}%
+													</div>
+												</div>
+											</div>
+											{#if createModelDigest}
+												<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
+													{createModelDigest}
+												</div>
+											{/if}
+										</div>
+									</div>
+								{/if}
+							</div>
+
+							<div class="pt-1">
+								<div class="flex justify-between items-center text-xs">
+									<div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
+									<button
+										class=" text-xs font-medium text-gray-500"
+										type="button"
+										on:click={() => {
+											showExperimentalOllama = !showExperimentalOllama;
+										}}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button
+									>
+								</div>
+							</div>
+
+							{#if showExperimentalOllama}
+								<form
+									on:submit|preventDefault={() => {
+										uploadModelHandler();
+									}}
+								>
+									<div class=" mb-2 flex w-full justify-between">
+										<div class="  text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
+
+										<button
+											class="p-1 px-3 text-xs flex rounded transition"
+											on:click={() => {
+												if (modelUploadMode === 'file') {
+													modelUploadMode = 'url';
+												} else {
+													modelUploadMode = 'file';
+												}
+											}}
+											type="button"
+										>
+											{#if modelUploadMode === 'file'}
+												<span class="ml-2 self-center">{$i18n.t('File Mode')}</span>
+											{:else}
+												<span class="ml-2 self-center">{$i18n.t('URL Mode')}</span>
+											{/if}
+										</button>
+									</div>
+
+									<div class="flex w-full mb-1.5">
+										<div class="flex flex-col w-full">
+											{#if modelUploadMode === 'file'}
+												<div
+													class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}"
+												>
+													<input
+														id="model-upload-input"
+														bind:this={modelUploadInputElement}
+														type="file"
+														bind:files={modelInputFile}
+														on:change={() => {
+															console.log(modelInputFile);
+														}}
+														accept=".gguf,.safetensors"
+														required
+														hidden
+													/>
+
+													<button
+														type="button"
+														class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850"
+														on:click={() => {
+															modelUploadInputElement.click();
+														}}
+													>
+														{#if modelInputFile && modelInputFile.length > 0}
+															{modelInputFile[0].name}
+														{:else}
+															{$i18n.t('Click here to select')}
+														{/if}
+													</button>
+												</div>
+											{:else}
+												<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
+													<input
+														class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
+														''
+															? 'mr-2'
+															: ''}"
+														type="url"
+														required
+														bind:value={modelFileUrl}
+														placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')}
+													/>
+												</div>
+											{/if}
+										</div>
+
+										{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
+											<button
+												class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition"
+												type="submit"
+												disabled={modelTransferring}
+											>
+												{#if modelTransferring}
+													<div class="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>
+												{:else}
+													<svg
+														xmlns="http://www.w3.org/2000/svg"
+														viewBox="0 0 16 16"
+														fill="currentColor"
+														class="w-4 h-4"
+													>
+														<path
+															d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
+														/>
+														<path
+															d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
+														/>
+													</svg>
+												{/if}
+											</button>
+										{/if}
+									</div>
+
+									{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
+										<div>
+											<div>
+												<div class=" my-2.5 text-sm font-medium">
+													{$i18n.t('Modelfile Content')}
+												</div>
+												<textarea
+													bind:value={modelFileContent}
+													class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
+													rows="6"
+												/>
+											</div>
+										</div>
+									{/if}
+									<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
+										{$i18n.t('To access the GGUF models available for downloading,')}
+										<a
+											class=" text-gray-500 dark:text-gray-300 font-medium underline"
+											href="https://huggingface.co/models?search=gguf"
+											target="_blank">{$i18n.t('click here.')}</a
+										>
+									</div>
+
+									{#if uploadMessage}
+										<div class="mt-2">
+											<div class=" mb-2 text-xs">{$i18n.t('Upload 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: 100%"
+												>
+													{uploadMessage}
+												</div>
+											</div>
+											<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
+												{modelFileDigest}
+											</div>
+										</div>
+									{:else if uploadProgress !== null}
+										<div class="mt-2">
+											<div class=" mb-2 text-xs">{$i18n.t('Upload 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, uploadProgress ?? 0)}%"
+												>
+													{uploadProgress ?? 0}%
+												</div>
+											</div>
+											<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
+												{modelFileDigest}
+											</div>
+										</div>
+									{/if}
+								</form>
+							{/if}
+						</div>
+					</div>
+				</div>
+			{:else}
+				<Spinner />
+			{/if}
+		</div>
+	</div>
+</Modal>

+ 20 - 1
src/lib/components/admin/Settings/Connections/OllamaConnection.svelte

@@ -4,15 +4,20 @@
 
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
-	import Cog6 from '$lib/components/icons/Cog6.svelte';
 	import AddConnectionModal from './AddConnectionModal.svelte';
 
+	import Cog6 from '$lib/components/icons/Cog6.svelte';
+	import Wrench from '$lib/components/icons/Wrench.svelte';
+	import ManageOllamaModal from './ManageOllamaModal.svelte';
+
 	export let onDelete = () => {};
 	export let onSubmit = () => {};
 
 	export let url = '';
+	export let idx = 0;
 	export let config = {};
 
+	let showManageModal = false;
 	let showConfigModal = false;
 </script>
 
@@ -33,6 +38,8 @@
 	}}
 />
 
+<ManageOllamaModal bind:show={showManageModal} urlIdx={idx} />
+
 <div class="flex gap-1.5">
 	<Tooltip
 		className="w-full relative"
@@ -55,6 +62,18 @@
 	</Tooltip>
 
 	<div class="flex gap-1">
+		<Tooltip content={$i18n.t('Manage')} className="self-start">
+			<button
+				class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
+				on:click={() => {
+					showManageModal = true;
+				}}
+				type="button"
+			>
+				<Wrench />
+			</button>
+		</Tooltip>
+
 		<Tooltip content={$i18n.t('Configure')} className="self-start">
 			<button
 				class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"

+ 1 - 1075
src/lib/components/admin/Settings/Models.svelte

@@ -1,1082 +1,8 @@
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
 	import { onMount, getContext } from 'svelte';
-
-	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 {
-		createModel,
-		deleteModel,
-		downloadModel,
-		getOllamaUrls,
-		getOllamaVersion,
-		pullModel,
-		uploadModel,
-		getOllamaConfig
-	} from '$lib/apis/ollama';
-	import { getModels as _getModels } from '$lib/apis';
-
-	import Tooltip from '$lib/components/common/Tooltip.svelte';
-	import Spinner from '$lib/components/common/Spinner.svelte';
-	import ModelDeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
-
-	const i18n = getContext('i18n');
-
-	const getModels = async () => {
-		return await _getModels(localStorage.token);
-	};
-
-	let modelUploadInputElement: HTMLInputElement;
-
-	let showModelDeleteConfirm = false;
-
-	// Models
-
-	let ollamaEnabled = null;
-
-	let OLLAMA_BASE_URLS = [];
-	let selectedOllamaUrlIdx: number | null = null;
-
-	let updateModelId = null;
-	let updateProgress = null;
-
-	let showExperimentalOllama = false;
-
-	let ollamaVersion = null;
-	const MAX_PARALLEL_DOWNLOADS = 3;
-
-	let modelTransferring = false;
-	let modelTag = '';
-
-	let createModelLoading = false;
-	let createModelTag = '';
-	let createModelContent = '';
-	let createModelDigest = '';
-	let createModelPullProgress = null;
-
-	let digest = '';
-	let pullProgress = null;
-
-	let modelUploadMode = 'file';
-	let modelInputFile: File[] | null = null;
-	let modelFileUrl = '';
-	let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`;
-	let modelFileDigest = '';
-
-	let uploadProgress = null;
-	let uploadMessage = '';
-
-	let deleteModelTag = '';
-
-	const updateModelsHandler = async () => {
-		for (const model of $models.filter(
-			(m) =>
-				!(m?.preset ?? false) &&
-				m.owned_by === 'ollama' &&
-				(selectedOllamaUrlIdx === null
-					? true
-					: (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))
-		)) {
-			console.log(model);
-
-			updateModelId = model.id;
-			const [res, controller] = await pullModel(
-				localStorage.token,
-				model.id,
-				selectedOllamaUrlIdx
-			).catch((error) => {
-				toast.error(error);
-				return null;
-			});
-
-			if (res) {
-				const reader = res.body
-					.pipeThrough(new TextDecoderStream())
-					.pipeThrough(splitStream('\n'))
-					.getReader();
-
-				while (true) {
-					try {
-						const { value, done } = await reader.read();
-						if (done) break;
-
-						let lines = value.split('\n');
-
-						for (const line of lines) {
-							if (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) {
-										updateProgress = 0;
-										if (data.completed) {
-											updateProgress = Math.round((data.completed / data.total) * 1000) / 10;
-										} else {
-											updateProgress = 100;
-										}
-									} else {
-										toast.success(data.status);
-									}
-								}
-							}
-						}
-					} catch (error) {
-						console.log(error);
-					}
-				}
-			}
-		}
-
-		updateModelId = null;
-		updateProgress = null;
-	};
-
-	const pullModelHandler = async () => {
-		const sanitizedModelTag = modelTag.trim().replace(/^ollama\s+(run|pull)\s+/, '');
-		console.log($MODEL_DOWNLOAD_POOL);
-		if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]) {
-			toast.error(
-				$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {
-					modelTag: sanitizedModelTag
-				})
-			);
-			return;
-		}
-		if (Object.keys($MODEL_DOWNLOAD_POOL).length === MAX_PARALLEL_DOWNLOADS) {
-			toast.error(
-				$i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.')
-			);
-			return;
-		}
-
-		const [res, controller] = await pullModel(
-			localStorage.token,
-			sanitizedModelTag,
-			selectedOllamaUrlIdx
-		).catch((error) => {
-			toast.error(error);
-			return null;
-		});
-
-		if (res) {
-			const reader = res.body
-				.pipeThrough(new TextDecoderStream())
-				.pipeThrough(splitStream('\n'))
-				.getReader();
-
-			MODEL_DOWNLOAD_POOL.set({
-				...$MODEL_DOWNLOAD_POOL,
-				[sanitizedModelTag]: {
-					...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
-					abortController: controller,
-					reader,
-					done: false
-				}
-			});
-
-			while (true) {
-				try {
-					const { value, done } = await reader.read();
-					if (done) break;
-
-					let lines = value.split('\n');
-
-					for (const line of lines) {
-						if (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) {
-									let downloadProgress = 0;
-									if (data.completed) {
-										downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
-									} else {
-										downloadProgress = 100;
-									}
-
-									MODEL_DOWNLOAD_POOL.set({
-										...$MODEL_DOWNLOAD_POOL,
-										[sanitizedModelTag]: {
-											...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
-											pullProgress: downloadProgress,
-											digest: data.digest
-										}
-									});
-								} else {
-									toast.success(data.status);
-
-									MODEL_DOWNLOAD_POOL.set({
-										...$MODEL_DOWNLOAD_POOL,
-										[sanitizedModelTag]: {
-											...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
-											done: data.status === 'success'
-										}
-									});
-								}
-							}
-						}
-					}
-				} catch (error) {
-					console.log(error);
-					if (typeof error !== 'string') {
-						error = error.message;
-					}
-
-					toast.error(error);
-					// opts.callback({ success: false, error, modelName: opts.modelName });
-				}
-			}
-
-			console.log($MODEL_DOWNLOAD_POOL[sanitizedModelTag]);
-
-			if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) {
-				toast.success(
-					$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, {
-						modelName: sanitizedModelTag
-					})
-				);
-
-				models.set(await getModels());
-			} else {
-				toast.error($i18n.t('Download canceled'));
-			}
-
-			delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag];
-
-			MODEL_DOWNLOAD_POOL.set({
-				...$MODEL_DOWNLOAD_POOL
-			});
-		}
-
-		modelTag = '';
-		modelTransferring = false;
-	};
-
-	const uploadModelHandler = async () => {
-		modelTransferring = true;
-
-		let uploaded = false;
-		let fileResponse = null;
-		let name = '';
-
-		if (modelUploadMode === 'file') {
-			const file = modelInputFile ? modelInputFile[0] : null;
-
-			if (file) {
-				uploadMessage = 'Uploading...';
-
-				fileResponse = await uploadModel(localStorage.token, file, selectedOllamaUrlIdx).catch(
-					(error) => {
-						toast.error(error);
-						return null;
-					}
-				);
-			}
-		} else {
-			uploadProgress = 0;
-			fileResponse = await downloadModel(
-				localStorage.token,
-				modelFileUrl,
-				selectedOllamaUrlIdx
-			).catch((error) => {
-				toast.error(error);
-				return null;
-			});
-		}
-
-		if (fileResponse && fileResponse.ok) {
-			const reader = fileResponse.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 !== '') {
-							let data = JSON.parse(line.replace(/^data: /, ''));
-
-							if (data.progress) {
-								if (uploadMessage) {
-									uploadMessage = '';
-								}
-								uploadProgress = data.progress;
-							}
-
-							if (data.error) {
-								throw data.error;
-							}
-
-							if (data.done) {
-								modelFileDigest = data.blob;
-								name = data.name;
-								uploaded = true;
-							}
-						}
-					}
-				} catch (error) {
-					console.log(error);
-				}
-			}
-		} else {
-			const error = await fileResponse?.json();
-			toast.error(error?.detail ?? error);
-		}
-
-		if (uploaded) {
-			const res = await createModel(
-				localStorage.token,
-				`${name}:latest`,
-				`FROM @${modelFileDigest}\n${modelFileContent}`
-			);
-
-			if (res && res.ok) {
-				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);
-									} 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);
-					}
-				}
-			}
-		}
-
-		modelFileUrl = '';
-
-		if (modelUploadInputElement) {
-			modelUploadInputElement.value = '';
-		}
-		modelInputFile = null;
-		modelTransferring = false;
-		uploadProgress = null;
-
-		models.set(await getModels());
-	};
-
-	const deleteModelHandler = async () => {
-		const res = await deleteModel(localStorage.token, deleteModelTag, selectedOllamaUrlIdx).catch(
-			(error) => {
-				toast.error(error);
-			}
-		);
-
-		if (res) {
-			toast.success($i18n.t(`Deleted {{deleteModelTag}}`, { deleteModelTag }));
-		}
-
-		deleteModelTag = '';
-		models.set(await getModels());
-	};
-
-	const cancelModelPullHandler = async (model: string) => {
-		const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model];
-		if (abortController) {
-			abortController.abort();
-		}
-		if (reader) {
-			await reader.cancel();
-			delete $MODEL_DOWNLOAD_POOL[model];
-			MODEL_DOWNLOAD_POOL.set({
-				...$MODEL_DOWNLOAD_POOL
-			});
-			await deleteModel(localStorage.token, model);
-			toast.success(`${model} download has been canceled`);
-		}
-	};
-
-	const createModelHandler = async () => {
-		createModelLoading = true;
-		const res = await createModel(
-			localStorage.token,
-			createModelTag,
-			createModelContent,
-			selectedOllamaUrlIdx
-		).catch((error) => {
-			toast.error(error);
-			return null;
-		});
-
-		if (res && res.ok) {
-			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);
-								} else {
-									if (data.digest) {
-										createModelDigest = data.digest;
-
-										if (data.completed) {
-											createModelPullProgress =
-												Math.round((data.completed / data.total) * 1000) / 10;
-										} else {
-											createModelPullProgress = 100;
-										}
-									}
-								}
-							}
-						}
-					}
-				} catch (error) {
-					console.log(error);
-					toast.error(error);
-				}
-			}
-		}
-
-		models.set(await getModels());
-
-		createModelLoading = false;
-
-		createModelTag = '';
-		createModelContent = '';
-		createModelDigest = '';
-		createModelPullProgress = null;
-	};
-
-	onMount(async () => {
-		const ollamaConfig = await getOllamaConfig(localStorage.token);
-
-		if (ollamaConfig.ENABLE_OLLAMA_API) {
-			ollamaEnabled = true;
-
-			OLLAMA_BASE_URLS = ollamaConfig.OLLAMA_BASE_URLS;
-
-			if (OLLAMA_BASE_URLS.length > 0) {
-				selectedOllamaUrlIdx = 0;
-			}
-
-			ollamaVersion = true;
-		} else {
-			ollamaEnabled = false;
-			toast.error($i18n.t('Ollama API is disabled'));
-		}
-	});
 </script>
 
-<ModelDeleteConfirmDialog
-	bind:show={showModelDeleteConfirm}
-	on:confirm={() => {
-		deleteModelHandler();
-	}}
-/>
-
 <div class="flex flex-col h-full justify-between text-sm">
-	<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
-		{#if ollamaEnabled}
-			{#if ollamaVersion !== null}
-				<div class="space-y-2 pr-1.5">
-					<div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
-
-					{#if OLLAMA_BASE_URLS.length > 0}
-						<div class="flex gap-2">
-							<div class="flex-1 pb-1">
-								<select
-									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-									bind:value={selectedOllamaUrlIdx}
-									placeholder={$i18n.t('Select an Ollama instance')}
-								>
-									{#each OLLAMA_BASE_URLS as url, idx}
-										<option value={idx} class="bg-gray-50 dark:bg-gray-700">{url}</option>
-									{/each}
-								</select>
-							</div>
-
-							<div>
-								<div class="flex w-full justify-end">
-									<Tooltip content="Update All Models" placement="top">
-										<button
-											class="p-2.5 flex gap-2 items-center bg-gray-50 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={() => {
-												updateModelsHandler();
-											}}
-										>
-											<svg
-												xmlns="http://www.w3.org/2000/svg"
-												viewBox="0 0 16 16"
-												fill="currentColor"
-												class="w-4 h-4"
-											>
-												<path
-													d="M7 1a.75.75 0 0 1 .75.75V6h-1.5V1.75A.75.75 0 0 1 7 1ZM6.25 6v2.94L5.03 7.72a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06L7.75 8.94V6H10a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.25Z"
-												/>
-												<path
-													d="M4.268 14A2 2 0 0 0 6 15h6a2 2 0 0 0 2-2v-3a2 2 0 0 0-1-1.732V11a3 3 0 0 1-3 3H4.268Z"
-												/>
-											</svg>
-										</button>
-									</Tooltip>
-								</div>
-							</div>
-						</div>
-
-						{#if updateModelId}
-							Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}
-						{/if}
-					{/if}
-
-					<div class="space-y-2">
-						<div>
-							<div class=" mb-2 text-sm font-medium">{$i18n.t('Pull a model from Ollama.com')}</div>
-							<div class="flex w-full">
-								<div class="flex-1 mr-2">
-									<input
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-										placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
-											modelTag: 'mistral:7b'
-										})}
-										bind:value={modelTag}
-									/>
-								</div>
-								<button
-									class="px-2.5 bg-gray-50 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={() => {
-										pullModelHandler();
-									}}
-									disabled={modelTransferring}
-								>
-									{#if modelTransferring}
-										<div class="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>
-									{:else}
-										<svg
-											xmlns="http://www.w3.org/2000/svg"
-											viewBox="0 0 16 16"
-											fill="currentColor"
-											class="w-4 h-4"
-										>
-											<path
-												d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
-											/>
-											<path
-												d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
-											/>
-										</svg>
-									{/if}
-								</button>
-							</div>
-
-							<div class="mt-2 mb-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 underline"
-									href="https://ollama.com/library"
-									target="_blank">{$i18n.t('click here.')}</a
-								>
-							</div>
-
-							{#if Object.keys($MODEL_DOWNLOAD_POOL).length > 0}
-								{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
-									{#if 'pullProgress' in $MODEL_DOWNLOAD_POOL[model]}
-										<div class="flex flex-col">
-											<div class="font-medium mb-1">{model}</div>
-											<div class="">
-												<div class="flex flex-row justify-between space-x-4 pr-2">
-													<div class=" flex-1">
-														<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,
-																$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0
-															)}%"
-														>
-															{$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0}%
-														</div>
-													</div>
-
-													<Tooltip content={$i18n.t('Cancel')}>
-														<button
-															class="text-gray-800 dark:text-gray-100"
-															on:click={() => {
-																cancelModelPullHandler(model);
-															}}
-														>
-															<svg
-																class="w-4 h-4 text-gray-800 dark:text-white"
-																aria-hidden="true"
-																xmlns="http://www.w3.org/2000/svg"
-																width="24"
-																height="24"
-																fill="currentColor"
-																viewBox="0 0 24 24"
-															>
-																<path
-																	stroke="currentColor"
-																	stroke-linecap="round"
-																	stroke-linejoin="round"
-																	stroke-width="2"
-																	d="M6 18 17.94 6M18 18 6.06 6"
-																/>
-															</svg>
-														</button>
-													</Tooltip>
-												</div>
-												{#if 'digest' in $MODEL_DOWNLOAD_POOL[model]}
-													<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
-														{$MODEL_DOWNLOAD_POOL[model].digest}
-													</div>
-												{/if}
-											</div>
-										</div>
-									{/if}
-								{/each}
-							{/if}
-						</div>
-
-						<div>
-							<div class=" mb-2 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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-										bind:value={deleteModelTag}
-										placeholder={$i18n.t('Select a model')}
-									>
-										{#if !deleteModelTag}
-											<option value="" disabled selected>{$i18n.t('Select a model')}</option>
-										{/if}
-										{#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
-											<option value={model.id} class="bg-gray-50 dark:bg-gray-700"
-												>{model.name +
-													' (' +
-													(model.ollama.size / 1024 ** 3).toFixed(1) +
-													' GB)'}</option
-											>
-										{/each}
-									</select>
-								</div>
-								<button
-									class="px-2.5 bg-gray-50 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={() => {
-										showModelDeleteConfirm = true;
-									}}
-								>
-									<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>
-
-						<div>
-							<div class=" mb-2 text-sm font-medium">{$i18n.t('Create a model')}</div>
-							<div class="flex w-full">
-								<div class="flex-1 mr-2 flex flex-col gap-2">
-									<input
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-										placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
-											modelTag: 'my-modelfile'
-										})}
-										bind:value={createModelTag}
-										disabled={createModelLoading}
-									/>
-
-									<textarea
-										bind:value={createModelContent}
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden"
-										rows="6"
-										placeholder={`TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`}
-										disabled={createModelLoading}
-									/>
-								</div>
-
-								<div class="flex self-start">
-									<button
-										class="px-2.5 py-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed"
-										on:click={() => {
-											createModelHandler();
-										}}
-										disabled={createModelLoading}
-									>
-										<svg
-											xmlns="http://www.w3.org/2000/svg"
-											viewBox="0 0 16 16"
-											fill="currentColor"
-											class="size-4"
-										>
-											<path
-												d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
-											/>
-											<path
-												d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
-											/>
-										</svg>
-									</button>
-								</div>
-							</div>
-
-							{#if createModelDigest !== ''}
-								<div class="flex flex-col mt-1">
-									<div class="font-medium mb-1">{createModelTag}</div>
-									<div class="">
-										<div class="flex flex-row justify-between space-x-4 pr-2">
-											<div class=" flex-1">
-												<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, createModelPullProgress ?? 0)}%"
-												>
-													{createModelPullProgress ?? 0}%
-												</div>
-											</div>
-										</div>
-										{#if createModelDigest}
-											<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
-												{createModelDigest}
-											</div>
-										{/if}
-									</div>
-								</div>
-							{/if}
-						</div>
-
-						<div class="pt-1">
-							<div class="flex justify-between items-center text-xs">
-								<div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
-								<button
-									class=" text-xs font-medium text-gray-500"
-									type="button"
-									on:click={() => {
-										showExperimentalOllama = !showExperimentalOllama;
-									}}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button
-								>
-							</div>
-						</div>
-
-						{#if showExperimentalOllama}
-							<form
-								on:submit|preventDefault={() => {
-									uploadModelHandler();
-								}}
-							>
-								<div class=" mb-2 flex w-full justify-between">
-									<div class="  text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
-
-									<button
-										class="p-1 px-3 text-xs flex rounded transition"
-										on:click={() => {
-											if (modelUploadMode === 'file') {
-												modelUploadMode = 'url';
-											} else {
-												modelUploadMode = 'file';
-											}
-										}}
-										type="button"
-									>
-										{#if modelUploadMode === 'file'}
-											<span class="ml-2 self-center">{$i18n.t('File Mode')}</span>
-										{:else}
-											<span class="ml-2 self-center">{$i18n.t('URL Mode')}</span>
-										{/if}
-									</button>
-								</div>
-
-								<div class="flex w-full mb-1.5">
-									<div class="flex flex-col w-full">
-										{#if modelUploadMode === 'file'}
-											<div
-												class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}"
-											>
-												<input
-													id="model-upload-input"
-													bind:this={modelUploadInputElement}
-													type="file"
-													bind:files={modelInputFile}
-													on:change={() => {
-														console.log(modelInputFile);
-													}}
-													accept=".gguf,.safetensors"
-													required
-													hidden
-												/>
-
-												<button
-													type="button"
-													class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850"
-													on:click={() => {
-														modelUploadInputElement.click();
-													}}
-												>
-													{#if modelInputFile && modelInputFile.length > 0}
-														{modelInputFile[0].name}
-													{:else}
-														{$i18n.t('Click here to select')}
-													{/if}
-												</button>
-											</div>
-										{:else}
-											<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
-												<input
-													class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
-													''
-														? 'mr-2'
-														: ''}"
-													type="url"
-													required
-													bind:value={modelFileUrl}
-													placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')}
-												/>
-											</div>
-										{/if}
-									</div>
-
-									{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
-										<button
-											class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition"
-											type="submit"
-											disabled={modelTransferring}
-										>
-											{#if modelTransferring}
-												<div class="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>
-											{:else}
-												<svg
-													xmlns="http://www.w3.org/2000/svg"
-													viewBox="0 0 16 16"
-													fill="currentColor"
-													class="w-4 h-4"
-												>
-													<path
-														d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
-													/>
-													<path
-														d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
-													/>
-												</svg>
-											{/if}
-										</button>
-									{/if}
-								</div>
-
-								{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
-									<div>
-										<div>
-											<div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
-											<textarea
-												bind:value={modelFileContent}
-												class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
-												rows="6"
-											/>
-										</div>
-									</div>
-								{/if}
-								<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
-									{$i18n.t('To access the GGUF models available for downloading,')}
-									<a
-										class=" text-gray-500 dark:text-gray-300 font-medium underline"
-										href="https://huggingface.co/models?search=gguf"
-										target="_blank">{$i18n.t('click here.')}</a
-									>
-								</div>
-
-								{#if uploadMessage}
-									<div class="mt-2">
-										<div class=" mb-2 text-xs">{$i18n.t('Upload 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: 100%"
-											>
-												{uploadMessage}
-											</div>
-										</div>
-										<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
-											{modelFileDigest}
-										</div>
-									</div>
-								{:else if uploadProgress !== null}
-									<div class="mt-2">
-										<div class=" mb-2 text-xs">{$i18n.t('Upload 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, uploadProgress ?? 0)}%"
-											>
-												{uploadProgress ?? 0}%
-											</div>
-										</div>
-										<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
-											{modelFileDigest}
-										</div>
-									</div>
-								{/if}
-							</form>
-						{/if}
-					</div>
-				</div>
-			{:else if ollamaVersion === false}
-				<div>Ollama Not Detected</div>
-			{:else}
-				<div class="flex h-full justify-center">
-					<div class="my-auto">
-						<Spinner className="size-6" />
-					</div>
-				</div>
-			{/if}
-		{:else if ollamaEnabled === false}
-			<div>{$i18n.t('Ollama API is disabled')}</div>
-		{:else}
-			<div class="flex h-full justify-center">
-				<div class="my-auto">
-					<Spinner className="size-6" />
-				</div>
-			</div>
-		{/if}
-	</div>
+	<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">Models</div>
 </div>

+ 30 - 2
src/lib/components/admin/Users/Groups.svelte

@@ -23,6 +23,7 @@
 	import GroupItem from './Groups/GroupItem.svelte';
 	import AddGroupModal from './Groups/AddGroupModal.svelte';
 	import { createNewGroup, getGroups } from '$lib/apis/groups';
+	import { getUserPermissions, updateUserPermissions } from '$lib/apis/users';
 
 	const i18n = getContext('i18n');
 
@@ -44,6 +45,19 @@
 	});
 
 	let search = '';
+	let defaultPermissions = {
+		workspace: {
+			models: false,
+			knowledge: false,
+			prompts: false,
+			tools: false
+		},
+		chat: {
+			delete: true,
+			edit: true,
+			temporary: true
+		}
+	};
 
 	let showCreateGroupModal = false;
 	let showDefaultPermissionsModal = false;
@@ -64,8 +78,20 @@
 		}
 	};
 
-	const updateDefaultPermissionsHandler = async (permissions) => {
-		console.log(permissions);
+	const updateDefaultPermissionsHandler = async (group) => {
+		console.log(group.permissions);
+
+		const res = await updateUserPermissions(localStorage.token, group.permissions).catch(
+			(error) => {
+				toast.error(error);
+				return null;
+			}
+		);
+
+		if (res) {
+			toast.success($i18n.t('Default permissions updated successfully'));
+			defaultPermissions = group.permissions;
+		}
 	};
 
 	onMount(async () => {
@@ -73,6 +99,7 @@
 			await goto('/');
 		} else {
 			await setGroups();
+			defaultPermissions = await getUserPermissions(localStorage.token);
 		}
 		loaded = true;
 	});
@@ -176,6 +203,7 @@
 		<GroupModal
 			bind:show={showDefaultPermissionsModal}
 			tabs={['permissions']}
+			permissions={defaultPermissions}
 			custom={false}
 			onSubmit={updateDefaultPermissionsHandler}
 		/>

+ 22 - 5
src/lib/components/admin/Users/Groups/EditGroupModal.svelte

@@ -26,10 +26,10 @@
 	let selectedTab = 'general';
 	let loading = false;
 
-	let name = '';
-	let description = '';
+	export let name = '';
+	export let description = '';
 
-	let permissions = {
+	export let permissions = {
 		workspace: {
 			models: false,
 			knowledge: false,
@@ -42,7 +42,7 @@
 			temporary: true
 		}
 	};
-	let userIds = [];
+	export let userIds = [];
 
 	const submitHandler = async () => {
 		loading = true;
@@ -60,7 +60,24 @@
 		show = false;
 
 		name = '';
-		permissions = {};
+		permissions = {
+			model: {
+				filter: false,
+				model_ids: [],
+				default_id: ''
+			},
+			workspace: {
+				models: false,
+				knowledge: false,
+				prompts: false,
+				tools: false
+			},
+			chat: {
+				delete: true,
+				edit: true,
+				temporary: true
+			}
+		};
 		userIds = [];
 	};
 

+ 9 - 9
src/lib/components/admin/Users/Groups/Permissions.svelte

@@ -28,11 +28,11 @@
 			<div class="flex justify-between items-center text-xs pr-2">
 				<div class=" text-xs font-medium">{$i18n.t('Model Filtering')}</div>
 
-				<Switch bind:state={filterEnabled} />
+				<Switch bind:state={permissions.model.filter} />
 			</div>
 		</div>
 
-		{#if filterEnabled}
+		{#if permissions.model.filter}
 			<div class="mb-2">
 				<div class=" space-y-1.5">
 					<div class="flex flex-col w-full">
@@ -40,9 +40,9 @@
 							<div class="text-xs text-gray-500">{$i18n.t('Model IDs')}</div>
 						</div>
 
-						{#if filterModelIds.length > 0}
+						{#if model_ids.length > 0}
 							<div class="flex flex-col">
-								{#each filterModelIds as modelId, modelIdx}
+								{#each model_ids as modelId, modelIdx}
 									<div class=" flex gap-2 w-full justify-between items-center">
 										<div class=" text-sm flex-1 rounded-lg">
 											{modelId}
@@ -51,7 +51,7 @@
 											<button
 												type="button"
 												on:click={() => {
-													filterModelIds = filterModelIds.filter((_, idx) => idx !== modelIdx);
+													model_ids = model_ids.filter((_, idx) => idx !== modelIdx);
 												}}
 											>
 												<Minus strokeWidth="2" className="size-3.5" />
@@ -86,8 +86,8 @@
 						<button
 							type="button"
 							on:click={() => {
-								if (selectedModelId && !filterModelIds.includes(selectedModelId)) {
-									filterModelIds = [...filterModelIds, selectedModelId];
+								if (selectedModelId && !permissions.model.model_ids.includes(selectedModelId)) {
+									permissions.model.model_ids = [...permissions.model.model_ids, selectedModelId];
 									selectedModelId = '';
 								}
 							}}
@@ -109,11 +109,11 @@
 			<div class="flex-1 mr-2">
 				<select
 					class="w-full bg-transparent outline-none py-0.5 text-sm"
-					bind:value={defaultModelId}
+					bind:value={permissions.model.default_id}
 					placeholder="Select a model"
 				>
 					<option value="" disabled selected>{$i18n.t('Select a model')}</option>
-					{#each filterEnabled ? $models.filter( (model) => filterModelIds.includes(model.id) ) : $models.filter((model) => model.id) as model}
+					{#each permissions.model.filter ? $models.filter( (model) => filterModelIds.includes(model.id) ) : $models.filter((model) => model.id) as model}
 						<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
 					{/each}
 				</select>

+ 0 - 1
src/lib/components/chat/MessageInput.svelte

@@ -557,7 +557,6 @@
 									<InputMenu
 										bind:webSearchEnabled
 										bind:selectedToolIds
-										{availableToolIds}
 										uploadFilesHandler={() => {
 											filesInputElement.click();
 										}}

+ 5 - 7
src/lib/components/chat/MessageInput/InputMenu.svelte

@@ -42,13 +42,11 @@
 		}
 
 		tools = $_tools.reduce((a, tool, i, arr) => {
-			if (availableToolIds.includes(tool.id) || ($user?.role ?? 'user') === 'admin') {
-				a[tool.id] = {
-					name: tool.name,
-					description: tool.meta.description,
-					enabled: selectedToolIds.includes(tool.id)
-				};
-			}
+			a[tool.id] = {
+				name: tool.name,
+				description: tool.meta.description,
+				enabled: selectedToolIds.includes(tool.id)
+			};
 			return a;
 		}, {});
 	};

+ 20 - 0
src/lib/components/icons/Wrench.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	export let className = 'w-4 h-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+	stroke-width={strokeWidth}
+	stroke="currentColor"
+	class={className}
+>
+	<path
+		stroke-linecap="round"
+		stroke-linejoin="round"
+		d="M21.75 6.75a4.5 4.5 0 0 1-4.884 4.484c-1.076-.091-2.264.071-2.95.904l-7.152 8.684a2.548 2.548 0 1 1-3.586-3.586l8.684-7.152c.833-.686.995-1.874.904-2.95a4.5 4.5 0 0 1 6.336-4.486l-3.276 3.276a3.004 3.004 0 0 0 2.25 2.25l3.276-3.276c.256.565.398 1.192.398 1.852Z"
+	/>
+	<path stroke-linecap="round" stroke-linejoin="round" d="M4.867 19.125h.008v.008h-.008v-.008Z" />
+</svg>

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

@@ -112,7 +112,7 @@
 	</div>
 </div>
 
-<div class="my-3 mb-5 grid md:grid-cols-2 lg:grid-cols-3 gap-2">
+<div class="my-3 mb-5 grid lg:grid-cols-2 xl:grid-cols-3 gap-2">
 	{#each filteredItems as item}
 		<button
 			class=" flex space-x-4 cursor-pointer text-left w-full px-4 py-3 border border-gray-50 dark:border-gray-850 dark:hover:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-xl"

+ 24 - 162
src/lib/components/workspace/Models.svelte

@@ -12,7 +12,12 @@
 	const i18n = getContext('i18n');
 
 	import { WEBUI_NAME, config, mobile, models as _models, settings, user } from '$lib/stores';
-	import { addNewModel, deleteModelById, getModelInfos, updateModelById } from '$lib/apis/models';
+	import {
+		createNewModel,
+		deleteModelById,
+		getWorkspaceModels,
+		updateModelById
+	} from '$lib/apis/models';
 
 	import { getModels } from '$lib/apis';
 
@@ -38,29 +43,15 @@
 	let showModelDeleteConfirm = false;
 
 	$: if (models) {
-		filteredModels = models
-			.filter((m) => m?.owned_by !== 'arena')
-			.filter(
-				(m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase())
-			);
+		filteredModels = models.filter(
+			(m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase())
+		);
 	}
 
-	let sortable = null;
 	let searchValue = '';
 
 	const deleteModelHandler = async (model) => {
-		console.log(model.info);
-		if (!model?.info) {
-			toast.error(
-				$i18n.t('{{ owner }}: You cannot delete a base model', {
-					owner: model.owned_by.toUpperCase()
-				})
-			);
-			return null;
-		}
-
 		const res = await deleteModelById(localStorage.token, model.id);
-
 		if (res) {
 			toast.success($i18n.t(`Deleted {{name}}`, { name: model.id }));
 		}
@@ -70,17 +61,12 @@
 	};
 
 	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');
-		}
+		sessionStorage.model = JSON.stringify({
+			...model,
+			id: `${model.id}-clone`,
+			name: `${model.name} (Clone)`
+		});
+		goto('/workspace/models/create');
 	};
 
 	const shareModelHandler = async (model) => {
@@ -104,58 +90,6 @@
 		window.addEventListener('message', messageHandler, false);
 	};
 
-	const moveToTopHandler = async (model) => {
-		// find models with position 0 and set them to 1
-		const topModels = models.filter((m) => m.info?.meta?.position === 0);
-		for (const m of topModels) {
-			let info = m.info;
-			if (!info) {
-				info = {
-					id: m.id,
-					name: m.name,
-					meta: {
-						position: 1
-					},
-					params: {}
-				};
-			}
-
-			info.meta = {
-				...info.meta,
-				position: 1
-			};
-
-			await updateModelById(localStorage.token, info.id, info);
-		}
-
-		let info = model.info;
-
-		if (!info) {
-			info = {
-				id: model.id,
-				name: model.name,
-				meta: {
-					position: 0
-				},
-				params: {}
-			};
-		}
-
-		info.meta = {
-			...info.meta,
-			position: 0
-		};
-
-		const res = await updateModelById(localStorage.token, info.id, info);
-
-		if (res) {
-			toast.success($i18n.t(`Model {{name}} is now at the top`, { name: info.id }));
-		}
-
-		await _models.set(await getModels(localStorage.token));
-		models = $_models;
-	};
-
 	const hideModelHandler = async (model) => {
 		let info = model.info;
 
@@ -206,65 +140,8 @@
 		saveAs(blob, `${model.id}-${Date.now()}.json`);
 	};
 
-	const positionChangeHandler = async () => {
-		// Get the new order of the models
-		const modelIds = Array.from(document.getElementById('model-list').children).map((child) =>
-			child.id.replace('model-item-', '')
-		);
-
-		// Update the position of the models
-		for (const [index, id] of modelIds.entries()) {
-			const model = $_models.find((m) => m.id === id);
-			if (model) {
-				let info = model.info;
-
-				if (!info) {
-					info = {
-						id: model.id,
-						name: model.name,
-						meta: {
-							position: index
-						},
-						params: {}
-					};
-				}
-
-				info.meta = {
-					...info.meta,
-					position: index
-				};
-				await updateModelById(localStorage.token, info.id, info);
-			}
-		}
-
-		await tick();
-		await _models.set(await getModels(localStorage.token));
-	};
-
 	onMount(async () => {
-		if ($user?.role === 'admin') {
-			models = $_models;
-
-			// Legacy code to sync localModelfiles with models
-			localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
-
-			if (localModelfiles) {
-				console.log(localModelfiles);
-			}
-
-			if (!$mobile) {
-				// SortableJS
-				sortable = new Sortable(document.getElementById('model-list'), {
-					animation: 150,
-					onUpdate: async (event) => {
-						console.log(event);
-						positionChangeHandler();
-					}
-				});
-			}
-		} else {
-			models = [];
-		}
+		models = await getWorkspaceModels(localStorage.token);
 
 		const onKeyDown = (event) => {
 			if (event.key === 'Shift') {
@@ -376,47 +253,35 @@
 			>
 				<div class=" self-start w-8 pt-0.5">
 					<div
-						class=" rounded-full object-cover {(model?.info?.meta?.hidden ?? false)
+						class=" rounded-full object-cover {(model?.meta?.hidden ?? false)
 							? 'brightness-90 dark:brightness-50'
 							: ''} "
 					>
 						<img
-							src={model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
+							src={model?.meta?.profile_image_url ?? '/static/favicon.png'}
 							alt="modelfile profile"
 							class=" rounded-full w-full h-auto object-cover"
 						/>
 					</div>
 				</div>
 
-				<div
-					class=" flex-1 self-center {(model?.info?.meta?.hidden ?? false) ? 'text-gray-500' : ''}"
-				>
+				<div class=" flex-1 self-center {(model?.meta?.hidden ?? false) ? 'text-gray-500' : ''}">
 					<Tooltip
-						content={marked.parse(
-							model?.ollama?.digest
-								? `${model?.ollama?.digest} *(${model?.ollama?.modified_at})*`
-								: ''
-						)}
+						content={marked.parse(model?.meta?.description ?? model.id)}
 						className=" w-fit"
 						placement="top-start"
 					>
 						<div class="  font-semibold line-clamp-1">{model.name}</div>
 					</Tooltip>
 					<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1 text-gray-500">
-						{!!model?.info?.meta?.description
-							? model?.info?.meta?.description
-							: model?.ollama?.digest
-								? `${model.id} (${model?.ollama?.digest})`
-								: model.id}
+						{model?.meta?.description ?? model.id}
 					</div>
 				</div>
 			</a>
 			<div class="flex flex-row gap-0.5 self-center">
 				{#if $user?.role === 'admin' && shiftKey}
 					<Tooltip
-						content={(model?.info?.meta?.hidden ?? false)
-							? $i18n.t('Show Model')
-							: $i18n.t('Hide Model')}
+						content={(model?.meta?.hidden ?? false) ? $i18n.t('Show Model') : $i18n.t('Hide Model')}
 					>
 						<button
 							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"
@@ -425,7 +290,7 @@
 								hideModelHandler(model);
 							}}
 						>
-							{#if model?.info?.meta?.hidden ?? false}
+							{#if model?.meta?.hidden ?? false}
 								<svg
 									xmlns="http://www.w3.org/2000/svg"
 									fill="none"
@@ -511,9 +376,6 @@
 						exportHandler={() => {
 							exportModelHandler(model);
 						}}
-						moveToTopHandler={() => {
-							moveToTopHandler(model);
-						}}
 						hideHandler={() => {
 							hideModelHandler(model);
 						}}
@@ -561,7 +423,7 @@
 										return null;
 									});
 								} else {
-									await addNewModel(localStorage.token, model.info).catch((error) => {
+									await createNewModel(localStorage.token, model.info).catch((error) => {
 										return null;
 									});
 								}

+ 36 - 37
src/lib/components/workspace/Models/ModelEditor.svelte

@@ -17,6 +17,7 @@
 	import { getTools } from '$lib/apis/tools';
 	import { getFunctions } from '$lib/apis/functions';
 	import { getKnowledgeItems } from '$lib/apis/knowledge';
+	import AccessPermissions from '../common/AccessPermissionsModal.svelte';
 
 	const i18n = getContext('i18n');
 
@@ -82,7 +83,7 @@
 
 		if (baseModel) {
 			if (baseModel.owned_by === 'openai') {
-				capabilities.usage = baseModel.info?.meta?.capabilities?.usage ?? false;
+				capabilities.usage = basemodel?.meta?.capabilities?.usage ?? false;
 			} else {
 				delete capabilities.usage;
 			}
@@ -159,33 +160,31 @@
 
 			id = model.id;
 
-			if (model.info.base_model_id) {
+			if (model.base_model_id) {
 				const base_model = $models
 					.filter((m) => !m?.preset && m?.owned_by !== 'arena')
-					.find((m) =>
-						[model.info.base_model_id, `${model.info.base_model_id}:latest`].includes(m.id)
-					);
+					.find((m) => [model.base_model_id, `${model.base_model_id}:latest`].includes(m.id));
 
 				console.log('base_model', base_model);
 
 				if (base_model) {
-					model.info.base_model_id = base_model.id;
+					model.base_model_id = base_model.id;
 				} else {
-					model.info.base_model_id = null;
+					model.base_model_id = null;
 				}
 			}
 
-			params = { ...params, ...model?.info?.params };
+			params = { ...params, ...model?.params };
 			params.stop = params?.stop
 				? (typeof params.stop === 'string' ? params.stop.split(',') : (params?.stop ?? [])).join(
 						','
 					)
 				: null;
 
-			toolIds = model?.info?.meta?.toolIds ?? [];
-			filterIds = model?.info?.meta?.filterIds ?? [];
-			actionIds = model?.info?.meta?.actionIds ?? [];
-			knowledge = (model?.info?.meta?.knowledge ?? []).map((item) => {
+			toolIds = model?.meta?.toolIds ?? [];
+			filterIds = model?.meta?.filterIds ?? [];
+			actionIds = model?.meta?.actionIds ?? [];
+			knowledge = (model?.meta?.knowledge ?? []).map((item) => {
 				if (item?.collection_name) {
 					return {
 						id: item.collection_name,
@@ -203,7 +202,7 @@
 					return item;
 				}
 			});
-			capabilities = { ...capabilities, ...(model?.info?.meta?.capabilities ?? {}) };
+			capabilities = { ...capabilities, ...(model?.meta?.capabilities ?? {}) };
 			if (model?.owned_by === 'openai') {
 				capabilities.usage = false;
 			}
@@ -212,8 +211,8 @@
 				...info,
 				...JSON.parse(
 					JSON.stringify(
-						model?.info
-							? model?.info
+						model
+							? model
 							: {
 									id: model.id,
 									name: model.name
@@ -441,6 +440,26 @@
 						{/if}
 					</div>
 
+					<div class="my-1">
+						<div class="">
+							<Tags
+								tags={info?.meta?.tags ?? []}
+								on:delete={(e) => {
+									const tagName = e.detail;
+									info.meta.tags = info.meta.tags.filter((tag) => tag.name !== tagName);
+								}}
+								on:add={(e) => {
+									const tagName = e.detail;
+									if (!(info?.meta?.tags ?? null)) {
+										info.meta.tags = [{ name: tagName }];
+									} else {
+										info.meta.tags = [...info.meta.tags, { name: tagName }];
+									}
+								}}
+							/>
+						</div>
+					</div>
+
 					<hr class=" dark:border-gray-850 my-1.5" />
 
 					<div class="my-2">
@@ -620,28 +639,8 @@
 						<Capabilities bind:capabilities />
 					</div>
 
-					<div class="my-1">
-						<div class="flex w-full justify-between items-center">
-							<div class=" self-center text-sm font-semibold">{$i18n.t('Tags')}</div>
-						</div>
-
-						<div class="mt-2">
-							<Tags
-								tags={info?.meta?.tags ?? []}
-								on:delete={(e) => {
-									const tagName = e.detail;
-									info.meta.tags = info.meta.tags.filter((tag) => tag.name !== tagName);
-								}}
-								on:add={(e) => {
-									const tagName = e.detail;
-									if (!(info?.meta?.tags ?? null)) {
-										info.meta.tags = [{ name: tagName }];
-									} else {
-										info.meta.tags = [...info.meta.tags, { name: tagName }];
-									}
-								}}
-							/>
-						</div>
+					<div class="my-2">
+						<AccessPermissions />
 					</div>
 
 					<div class="my-2 text-gray-300 dark:text-gray-700">

+ 0 - 66
src/lib/components/workspace/Models/ModelMenu.svelte

@@ -23,7 +23,6 @@
 	export let cloneHandler: Function;
 	export let exportHandler: Function;
 
-	export let moveToTopHandler: Function;
 	export let hideHandler: Function;
 	export let deleteHandler: Function;
 	export let onClose: Function;
@@ -83,71 +82,6 @@
 				<div class="flex items-center">{$i18n.t('Export')}</div>
 			</DropdownMenu.Item>
 
-			{#if user?.role === 'admin'}
-				<DropdownMenu.Item
-					class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
-					on:click={() => {
-						moveToTopHandler();
-					}}
-				>
-					<ArrowUpCircle />
-
-					<div class="flex items-center">{$i18n.t('Move to Top')}</div>
-				</DropdownMenu.Item>
-
-				<DropdownMenu.Item
-					class="flex  gap-2  items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
-					on:click={() => {
-						hideHandler();
-					}}
-				>
-					{#if model?.info?.meta?.hidden ?? false}
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							fill="none"
-							viewBox="0 0 24 24"
-							stroke-width="1.5"
-							stroke="currentColor"
-							class="size-4"
-						>
-							<path
-								stroke-linecap="round"
-								stroke-linejoin="round"
-								d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88"
-							/>
-						</svg>
-					{:else}
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							fill="none"
-							viewBox="0 0 24 24"
-							stroke-width="1.5"
-							stroke="currentColor"
-							class="size-4"
-						>
-							<path
-								stroke-linecap="round"
-								stroke-linejoin="round"
-								d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
-							/>
-							<path
-								stroke-linecap="round"
-								stroke-linejoin="round"
-								d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
-							/>
-						</svg>
-					{/if}
-
-					<div class="flex items-center">
-						{#if model?.info?.meta?.hidden ?? false}
-							{$i18n.t('Show Model')}
-						{:else}
-							{$i18n.t('Hide Model')}
-						{/if}
-					</div>
-				</DropdownMenu.Item>
-			{/if}
-
 			<hr class="border-gray-100 dark:border-gray-800 my-1" />
 
 			<DropdownMenu.Item

+ 101 - 0
src/lib/components/workspace/common/AccessPermissionsModal.svelte

@@ -0,0 +1,101 @@
+<script lang="ts">
+	import { getContext } from 'svelte';
+	const i18n = getContext('i18n');
+
+	export let type = 'private';
+
+	let query = '';
+</script>
+
+<div>
+	<div>
+		<div class=" text-sm font-semibold mb-0.5">{$i18n.t('People with access')}</div>
+
+		<div>
+			<div class="flex w-full py-1">
+				<div class="flex flex-1">
+					<div class=" self-center mr-3">
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							viewBox="0 0 20 20"
+							fill="currentColor"
+							class="w-4 h-4"
+						>
+							<path
+								fill-rule="evenodd"
+								d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
+								clip-rule="evenodd"
+							/>
+						</svg>
+					</div>
+					<input
+						class=" w-full text-sm pr-4 placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none bg-transparent"
+						bind:value={query}
+						placeholder={$i18n.t('Add user or groups')}
+					/>
+				</div>
+			</div>
+		</div>
+	</div>
+
+	<div class="">
+		<div class=" text-sm font-semibold mb-2">{$i18n.t('General access')}</div>
+
+		<div class="flex gap-2.5 items-center">
+			<div>
+				<div class=" p-2 bg-gray-700 rounded-full">
+					{#if type === 'private'}
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							fill="none"
+							viewBox="0 0 24 24"
+							stroke-width="1.5"
+							stroke="currentColor"
+							class="w-5 h-5"
+						>
+							<path
+								stroke-linecap="round"
+								stroke-linejoin="round"
+								d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"
+							/>
+						</svg>
+					{:else}
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							fill="none"
+							viewBox="0 0 24 24"
+							stroke-width="1.5"
+							stroke="currentColor"
+							class="w-5 h-5"
+						>
+							<path
+								stroke-linecap="round"
+								stroke-linejoin="round"
+								d="M6.115 5.19l.319 1.913A6 6 0 008.11 10.36L9.75 12l-.387.775c-.217.433-.132.956.21 1.298l1.348 1.348c.21.21.329.497.329.795v1.089c0 .426.24.815.622 1.006l.153.076c.433.217.956.132 1.298-.21l.723-.723a8.7 8.7 0 002.288-4.042 1.087 1.087 0 00-.358-1.099l-1.33-1.108c-.251-.21-.582-.299-.905-.245l-1.17.195a1.125 1.125 0 01-.98-.314l-.295-.295a1.125 1.125 0 010-1.591l.13-.132a1.125 1.125 0 011.3-.21l.603.302a.809.809 0 001.086-1.086L14.25 7.5l1.256-.837a4.5 4.5 0 001.528-1.732l.146-.292M6.115 5.19A9 9 0 1017.18 4.64M6.115 5.19A8.965 8.965 0 0112 3c1.929 0 3.716.607 5.18 1.64"
+							/>
+						</svg>
+					{/if}
+				</div>
+			</div>
+
+			<div>
+				<select
+					id="models"
+					class="outline-none bg-transparent text-sm font-medium rounded-lg block w-fit pr-10 max-w-full placeholder-gray-400"
+					bind:value={type}
+				>
+					<option class=" text-gray-700" value="private" selected>Private</option>
+					<option class=" text-gray-700" value="public" selected>Public</option>
+				</select>
+
+				<div class=" text-xs text-gray-400 font-medium">
+					{#if type === 'private'}
+						{$i18n.t('Only users with permission can access')}
+					{:else if type === 'public'}
+						{$i18n.t('Accessible to all users')}
+					{/if}
+				</div>
+			</div>
+		</div>
+	</div>
+</div>

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

@@ -5,7 +5,7 @@
 	import { models } from '$lib/stores';
 
 	import { onMount, tick, getContext } from 'svelte';
-	import { addNewModel, getModelById, getModelInfos } from '$lib/apis/models';
+	import { createNewModel, getModelById } from '$lib/apis/models';
 	import { getModels } from '$lib/apis';
 
 	import ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
@@ -21,7 +21,7 @@
 		}
 
 		if (modelInfo) {
-			const res = await addNewModel(localStorage.token, {
+			const res = await createNewModel(localStorage.token, {
 				...modelInfo,
 				meta: {
 					...modelInfo.meta,

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

@@ -8,17 +8,20 @@
 	import { page } from '$app/stores';
 	import { models } from '$lib/stores';
 
-	import { updateModelById } from '$lib/apis/models';
+	import { getModelById, updateModelById } from '$lib/apis/models';
 
 	import { getModels } from '$lib/apis';
 	import ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
 
 	let model = null;
 
-	onMount(() => {
+	onMount(async () => {
 		const _id = $page.url.searchParams.get('id');
 		if (_id) {
-			model = $models.find((m) => m.id === _id && m?.owned_by !== 'arena');
+			model = await getModelById(localStorage.token, _id).catch((e) => {
+				return null;
+			});
+
 			if (!model) {
 				goto('/workspace/models');
 			}