forked from open-webui/open-webui
		
	Merge remote-tracking branch 'upstream/dev' into feat/add-i18n
This commit is contained in:
		
						commit
						25e0f0de42
					
				
					 26 changed files with 1445 additions and 668 deletions
				
			
		| 
						 | 
				
			
			@ -21,7 +21,7 @@
 | 
			
		|||
 | 
			
		||||
	export let suggestionPrompts = [];
 | 
			
		||||
	export let autoScroll = true;
 | 
			
		||||
 | 
			
		||||
	let chatTextAreaElement:HTMLTextAreaElement
 | 
			
		||||
	let filesInputElement;
 | 
			
		||||
 | 
			
		||||
	let promptsElement;
 | 
			
		||||
| 
						 | 
				
			
			@ -45,11 +45,9 @@
 | 
			
		|||
	let speechRecognition;
 | 
			
		||||
 | 
			
		||||
	$: if (prompt) {
 | 
			
		||||
		const chatInput = document.getElementById('chat-textarea');
 | 
			
		||||
 | 
			
		||||
		if (chatInput) {
 | 
			
		||||
			chatInput.style.height = '';
 | 
			
		||||
			chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + 'px';
 | 
			
		||||
		if (chatTextAreaElement) {
 | 
			
		||||
			chatTextAreaElement.style.height = '';
 | 
			
		||||
			chatTextAreaElement.style.height = Math.min(chatTextAreaElement.scrollHeight, 200) + 'px';
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -88,9 +86,7 @@
 | 
			
		|||
			if (res) {
 | 
			
		||||
				prompt = res.text;
 | 
			
		||||
				await tick();
 | 
			
		||||
 | 
			
		||||
				const inputElement = document.getElementById('chat-textarea');
 | 
			
		||||
				inputElement?.focus();
 | 
			
		||||
				chatTextAreaElement?.focus();
 | 
			
		||||
 | 
			
		||||
				if (prompt !== '' && $settings?.speechAutoSend === true) {
 | 
			
		||||
					submitPrompt(prompt, user);
 | 
			
		||||
| 
						 | 
				
			
			@ -193,8 +189,7 @@
 | 
			
		|||
						prompt = `${prompt}${transcript}`;
 | 
			
		||||
 | 
			
		||||
						await tick();
 | 
			
		||||
						const inputElement = document.getElementById('chat-textarea');
 | 
			
		||||
						inputElement?.focus();
 | 
			
		||||
						chatTextAreaElement?.focus();
 | 
			
		||||
 | 
			
		||||
						// Restart the inactivity timeout
 | 
			
		||||
						timeoutId = setTimeout(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -296,8 +291,7 @@
 | 
			
		|||
	};
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		const chatInput = document.getElementById('chat-textarea');
 | 
			
		||||
		window.setTimeout(() => chatInput?.focus(), 0);
 | 
			
		||||
		window.setTimeout(() => chatTextAreaElement?.focus(), 0);
 | 
			
		||||
 | 
			
		||||
		const dropZone = document.querySelector('body');
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -671,6 +665,7 @@
 | 
			
		|||
 | 
			
		||||
						<textarea
 | 
			
		||||
							id="chat-textarea"
 | 
			
		||||
							bind:this={chatTextAreaElement}
 | 
			
		||||
							class=" dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-3 {fileUploadEnabled
 | 
			
		||||
								? ''
 | 
			
		||||
								: ' pl-4'} rounded-xl resize-none h-[48px]"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,7 +42,7 @@
 | 
			
		|||
 | 
			
		||||
	let edit = false;
 | 
			
		||||
	let editedContent = '';
 | 
			
		||||
 | 
			
		||||
	let editTextAreaElement: HTMLTextAreaElement;
 | 
			
		||||
	let tooltipInstance = null;
 | 
			
		||||
 | 
			
		||||
	let sentencesAudio = {};
 | 
			
		||||
| 
						 | 
				
			
			@ -249,10 +249,9 @@
 | 
			
		|||
		editedContent = message.content;
 | 
			
		||||
 | 
			
		||||
		await tick();
 | 
			
		||||
		const editElement = document.getElementById(`message-edit-${message.id}`);
 | 
			
		||||
 | 
			
		||||
		editElement.style.height = '';
 | 
			
		||||
		editElement.style.height = `${editElement.scrollHeight}px`;
 | 
			
		||||
		editTextAreaElement.style.height = '';
 | 
			
		||||
		editTextAreaElement.style.height = `${editTextAreaElement.scrollHeight}px`;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const editMessageConfirmHandler = async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -343,6 +342,7 @@
 | 
			
		|||
							<div class=" w-full">
 | 
			
		||||
								<textarea
 | 
			
		||||
									id="message-edit-{message.id}"
 | 
			
		||||
									bind:this={editTextAreaElement}
 | 
			
		||||
									class=" bg-transparent outline-none w-full resize-none"
 | 
			
		||||
									bind:value={editedContent}
 | 
			
		||||
									on:input={(e) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,18 +22,17 @@
 | 
			
		|||
 | 
			
		||||
	let edit = false;
 | 
			
		||||
	let editedContent = '';
 | 
			
		||||
 | 
			
		||||
	let messageEditTextAreaElement: HTMLTextAreaElement;
 | 
			
		||||
	const editMessageHandler = async () => {
 | 
			
		||||
		edit = true;
 | 
			
		||||
		editedContent = message.content;
 | 
			
		||||
 | 
			
		||||
		await tick();
 | 
			
		||||
		const editElement = document.getElementById(`message-edit-${message.id}`);
 | 
			
		||||
 | 
			
		||||
		editElement.style.height = '';
 | 
			
		||||
		editElement.style.height = `${editElement.scrollHeight}px`;
 | 
			
		||||
		messageEditTextAreaElement.style.height = '';
 | 
			
		||||
		messageEditTextAreaElement.style.height = `${messageEditTextAreaElement.scrollHeight}px`;
 | 
			
		||||
 | 
			
		||||
		editElement?.focus();
 | 
			
		||||
		messageEditTextAreaElement?.focus();
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const editMessageConfirmHandler = async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -168,10 +167,11 @@
 | 
			
		|||
				<div class=" w-full">
 | 
			
		||||
					<textarea
 | 
			
		||||
						id="message-edit-{message.id}"
 | 
			
		||||
						bind:this={messageEditTextAreaElement}
 | 
			
		||||
						class=" bg-transparent outline-none w-full resize-none"
 | 
			
		||||
						bind:value={editedContent}
 | 
			
		||||
						on:input={(e) => {
 | 
			
		||||
							e.target.style.height = `${e.target.scrollHeight}px`;
 | 
			
		||||
							messageEditTextAreaElement.style.height = `${messageEditTextAreaElement.scrollHeight}px`;
 | 
			
		||||
						}}
 | 
			
		||||
					/>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,7 @@
 | 
			
		|||
	let name = '';
 | 
			
		||||
	let showJWTToken = false;
 | 
			
		||||
	let JWTTokenCopied = false;
 | 
			
		||||
	let profileImageInputElement: HTMLInputElement;
 | 
			
		||||
 | 
			
		||||
	const submitHandler = async () => {
 | 
			
		||||
		const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
 | 
			
		||||
| 
						 | 
				
			
			@ -42,11 +43,12 @@
 | 
			
		|||
	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
 | 
			
		||||
		<input
 | 
			
		||||
			id="profile-image-input"
 | 
			
		||||
			bind:this={profileImageInputElement}
 | 
			
		||||
			type="file"
 | 
			
		||||
			hidden
 | 
			
		||||
			accept="image/*"
 | 
			
		||||
			on:change={(e) => {
 | 
			
		||||
				const files = e?.target?.files ?? [];
 | 
			
		||||
				const files = profileImageInputElement.files ?? [];
 | 
			
		||||
				let reader = new FileReader();
 | 
			
		||||
				reader.onload = (event) => {
 | 
			
		||||
					let originalImageUrl = `${event.target.result}`;
 | 
			
		||||
| 
						 | 
				
			
			@ -88,7 +90,7 @@
 | 
			
		|||
						// Display the compressed image
 | 
			
		||||
						profileImageUrl = compressedSrc;
 | 
			
		||||
 | 
			
		||||
						e.target.files = null;
 | 
			
		||||
						profileImageInputElement.files = null;
 | 
			
		||||
					};
 | 
			
		||||
				};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -109,9 +111,7 @@
 | 
			
		|||
					<button
 | 
			
		||||
						class="relative rounded-full dark:bg-gray-700"
 | 
			
		||||
						type="button"
 | 
			
		||||
						on:click={() => {
 | 
			
		||||
							document.getElementById('profile-image-input')?.click();
 | 
			
		||||
						}}
 | 
			
		||||
						on:click={profileImageInputElement.click}
 | 
			
		||||
					>
 | 
			
		||||
						<img
 | 
			
		||||
							src={profileImageUrl !== '' ? profileImageUrl : '/user.png'}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,6 +24,7 @@
 | 
			
		|||
	let saveChatHistory = true;
 | 
			
		||||
	let importFiles;
 | 
			
		||||
	let showDeleteConfirm = false;
 | 
			
		||||
	let chatImportInputElement: HTMLInputElement;
 | 
			
		||||
 | 
			
		||||
	$: if (importFiles) {
 | 
			
		||||
		console.log(importFiles);
 | 
			
		||||
| 
						 | 
				
			
			@ -161,12 +162,17 @@
 | 
			
		|||
		<hr class=" dark:border-gray-700" />
 | 
			
		||||
 | 
			
		||||
		<div class="flex flex-col">
 | 
			
		||||
			<input id="chat-import-input" bind:files={importFiles} type="file" accept=".json" hidden />
 | 
			
		||||
			<input
 | 
			
		||||
				id="chat-import-input"
 | 
			
		||||
				bind:this={chatImportInputElement}
 | 
			
		||||
				bind:files={importFiles}
 | 
			
		||||
				type="file"
 | 
			
		||||
				accept=".json"
 | 
			
		||||
				hidden
 | 
			
		||||
			/>
 | 
			
		||||
			<button
 | 
			
		||||
				class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
 | 
			
		||||
				on:click={() => {
 | 
			
		||||
					document.getElementById('chat-import-input').click();
 | 
			
		||||
				}}
 | 
			
		||||
				on:click={chatImportInputElement.click}
 | 
			
		||||
			>
 | 
			
		||||
				<div class=" self-center mr-3">
 | 
			
		||||
					<svg
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
	import { createEventDispatcher, onMount, getContext } from 'svelte';
 | 
			
		||||
	const dispatch = createEventDispatcher();
 | 
			
		||||
 | 
			
		||||
	import { getOllamaAPIUrl, getOllamaVersion, updateOllamaAPIUrl } from '$lib/apis/ollama';
 | 
			
		||||
	import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama';
 | 
			
		||||
	import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai';
 | 
			
		||||
	import { toast } from 'svelte-sonner';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -12,7 +12,8 @@
 | 
			
		|||
	export let getModels: Function;
 | 
			
		||||
 | 
			
		||||
	// External
 | 
			
		||||
	let API_BASE_URL = '';
 | 
			
		||||
	let OLLAMA_BASE_URL = '';
 | 
			
		||||
	let OLLAMA_BASE_URLS = [''];
 | 
			
		||||
 | 
			
		||||
	let OPENAI_API_KEY = '';
 | 
			
		||||
	let OPENAI_API_BASE_URL = '';
 | 
			
		||||
| 
						 | 
				
			
			@ -27,8 +28,8 @@
 | 
			
		|||
		await models.set(await getModels());
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const updateOllamaAPIUrlHandler = async () => {
 | 
			
		||||
		API_BASE_URL = await updateOllamaAPIUrl(localStorage.token, API_BASE_URL);
 | 
			
		||||
	const updateOllamaUrlsHandler = async () => {
 | 
			
		||||
		OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS);
 | 
			
		||||
 | 
			
		||||
		const ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
 | 
			
		||||
			toast.error(error);
 | 
			
		||||
| 
						 | 
				
			
			@ -43,7 +44,7 @@
 | 
			
		|||
 | 
			
		||||
	onMount(async () => {
 | 
			
		||||
		if ($user.role === 'admin') {
 | 
			
		||||
			API_BASE_URL = await getOllamaAPIUrl(localStorage.token);
 | 
			
		||||
			OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token);
 | 
			
		||||
			OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token);
 | 
			
		||||
			OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -55,11 +56,6 @@
 | 
			
		|||
	on:submit|preventDefault={() => {
 | 
			
		||||
		updateOpenAIHandler();
 | 
			
		||||
		dispatch('save');
 | 
			
		||||
 | 
			
		||||
		// saveSettings({
 | 
			
		||||
		// 	OPENAI_API_KEY: OPENAI_API_KEY !== '' ? OPENAI_API_KEY : undefined,
 | 
			
		||||
		// 	OPENAI_API_BASE_URL: OPENAI_API_BASE_URL !== '' ? OPENAI_API_BASE_URL : undefined
 | 
			
		||||
		// });
 | 
			
		||||
	}}
 | 
			
		||||
>
 | 
			
		||||
	<div class="  pr-1.5 overflow-y-scroll max-h-[20.5rem] space-y-3">
 | 
			
		||||
| 
						 | 
				
			
			@ -116,35 +112,82 @@
 | 
			
		|||
		<hr class=" dark:border-gray-700" />
 | 
			
		||||
 | 
			
		||||
		<div>
 | 
			
		||||
			<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama API URL')}</div>
 | 
			
		||||
			<div class="flex w-full">
 | 
			
		||||
				<div class="flex-1 mr-2">
 | 
			
		||||
					<input
 | 
			
		||||
						class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
 | 
			
		||||
						placeholder="Enter URL (e.g. http://localhost:11434)"
 | 
			
		||||
						bind:value={API_BASE_URL}
 | 
			
		||||
					/>
 | 
			
		||||
			<div class=" mb-2.5 text-sm font-medium">Ollama Base URL</div>
 | 
			
		||||
			<div class="flex w-full gap-1.5">
 | 
			
		||||
				<div class="flex-1 flex flex-col gap-2">
 | 
			
		||||
					{#each OLLAMA_BASE_URLS as url, idx}
 | 
			
		||||
						<div class="flex gap-1.5">
 | 
			
		||||
							<input
 | 
			
		||||
								class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 | 
			
		||||
								placeholder="Enter URL (e.g. http://localhost:11434)"
 | 
			
		||||
								bind:value={url}
 | 
			
		||||
							/>
 | 
			
		||||
 | 
			
		||||
							<div class="self-center flex items-center">
 | 
			
		||||
								{#if idx === 0}
 | 
			
		||||
									<button
 | 
			
		||||
										class="px-1"
 | 
			
		||||
										on:click={() => {
 | 
			
		||||
											OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, ''];
 | 
			
		||||
										}}
 | 
			
		||||
										type="button"
 | 
			
		||||
									>
 | 
			
		||||
										<svg
 | 
			
		||||
											xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
											viewBox="0 0 16 16"
 | 
			
		||||
											fill="currentColor"
 | 
			
		||||
											class="w-4 h-4"
 | 
			
		||||
										>
 | 
			
		||||
											<path
 | 
			
		||||
												d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
 | 
			
		||||
											/>
 | 
			
		||||
										</svg>
 | 
			
		||||
									</button>
 | 
			
		||||
								{:else}
 | 
			
		||||
									<button
 | 
			
		||||
										class="px-1"
 | 
			
		||||
										on:click={() => {
 | 
			
		||||
											OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
 | 
			
		||||
										}}
 | 
			
		||||
										type="button"
 | 
			
		||||
									>
 | 
			
		||||
										<svg
 | 
			
		||||
											xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
											viewBox="0 0 16 16"
 | 
			
		||||
											fill="currentColor"
 | 
			
		||||
											class="w-4 h-4"
 | 
			
		||||
										>
 | 
			
		||||
											<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
 | 
			
		||||
										</svg>
 | 
			
		||||
									</button>
 | 
			
		||||
								{/if}
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					{/each}
 | 
			
		||||
				</div>
 | 
			
		||||
				<button
 | 
			
		||||
					class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
 | 
			
		||||
					on:click={() => {
 | 
			
		||||
						updateOllamaAPIUrlHandler();
 | 
			
		||||
					}}
 | 
			
		||||
					type="button"
 | 
			
		||||
				>
 | 
			
		||||
					<svg
 | 
			
		||||
						xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
						viewBox="0 0 20 20"
 | 
			
		||||
						fill="currentColor"
 | 
			
		||||
						class="w-4 h-4"
 | 
			
		||||
 | 
			
		||||
				<div class="">
 | 
			
		||||
					<button
 | 
			
		||||
						class="p-2.5 bg-gray-200 hover:bg-gray-300 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-lg transition"
 | 
			
		||||
						on:click={() => {
 | 
			
		||||
							updateOllamaUrlsHandler();
 | 
			
		||||
						}}
 | 
			
		||||
						type="button"
 | 
			
		||||
					>
 | 
			
		||||
						<path
 | 
			
		||||
							fill-rule="evenodd"
 | 
			
		||||
							d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
 | 
			
		||||
							clip-rule="evenodd"
 | 
			
		||||
						/>
 | 
			
		||||
					</svg>
 | 
			
		||||
				</button>
 | 
			
		||||
						<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="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
 | 
			
		||||
								clip-rule="evenodd"
 | 
			
		||||
							/>
 | 
			
		||||
						</svg>
 | 
			
		||||
					</button>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,13 @@
 | 
			
		|||
	import queue from 'async/queue';
 | 
			
		||||
	import { toast } from 'svelte-sonner';
 | 
			
		||||
 | 
			
		||||
	import { createModel, deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
 | 
			
		||||
	import {
 | 
			
		||||
		createModel,
 | 
			
		||||
		deleteModel,
 | 
			
		||||
		getOllamaUrls,
 | 
			
		||||
		getOllamaVersion,
 | 
			
		||||
		pullModel
 | 
			
		||||
	} from '$lib/apis/ollama';
 | 
			
		||||
	import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
 | 
			
		||||
	import { WEBUI_NAME, models, user } from '$lib/stores';
 | 
			
		||||
	import { splitStream } from '$lib/utils';
 | 
			
		||||
| 
						 | 
				
			
			@ -15,7 +21,7 @@
 | 
			
		|||
 | 
			
		||||
	let showLiteLLM = false;
 | 
			
		||||
	let showLiteLLMParams = false;
 | 
			
		||||
 | 
			
		||||
	let modelUploadInputElement: HTMLInputElement;
 | 
			
		||||
	let liteLLMModelInfo = [];
 | 
			
		||||
 | 
			
		||||
	let liteLLMModel = '';
 | 
			
		||||
| 
						 | 
				
			
			@ -29,6 +35,9 @@
 | 
			
		|||
	$: liteLLMModelName = liteLLMModel;
 | 
			
		||||
 | 
			
		||||
	// Models
 | 
			
		||||
 | 
			
		||||
	let OLLAMA_URLS = [];
 | 
			
		||||
	let selectedOllamaUrlIdx: string | null = null;
 | 
			
		||||
	let showExperimentalOllama = false;
 | 
			
		||||
	let ollamaVersion = '';
 | 
			
		||||
	const MAX_PARALLEL_DOWNLOADS = 3;
 | 
			
		||||
| 
						 | 
				
			
			@ -246,9 +255,11 @@
 | 
			
		|||
	};
 | 
			
		||||
 | 
			
		||||
	const deleteModelHandler = async () => {
 | 
			
		||||
		const res = await deleteModel(localStorage.token, deleteModelTag).catch((error) => {
 | 
			
		||||
			toast.error(error);
 | 
			
		||||
		});
 | 
			
		||||
		const res = await deleteModel(localStorage.token, deleteModelTag, selectedOllamaUrlIdx).catch(
 | 
			
		||||
			(error) => {
 | 
			
		||||
				toast.error(error);
 | 
			
		||||
			}
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		if (res) {
 | 
			
		||||
			toast.success($i18n.t(`Deleted {{deleteModelTag}}`, { deleteModelTag }));
 | 
			
		||||
| 
						 | 
				
			
			@ -259,10 +270,12 @@
 | 
			
		|||
	};
 | 
			
		||||
 | 
			
		||||
	const pullModelHandlerProcessor = async (opts: { modelName: string; callback: Function }) => {
 | 
			
		||||
		const res = await pullModel(localStorage.token, opts.modelName).catch((error) => {
 | 
			
		||||
			opts.callback({ success: false, error, modelName: opts.modelName });
 | 
			
		||||
			return null;
 | 
			
		||||
		});
 | 
			
		||||
		const res = await pullModel(localStorage.token, opts.modelName, selectedOllamaUrlIdx).catch(
 | 
			
		||||
			(error) => {
 | 
			
		||||
				opts.callback({ success: false, error, modelName: opts.modelName });
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		if (res) {
 | 
			
		||||
			const reader = res.body
 | 
			
		||||
| 
						 | 
				
			
			@ -368,6 +381,15 @@
 | 
			
		|||
	};
 | 
			
		||||
 | 
			
		||||
	onMount(async () => {
 | 
			
		||||
		OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
 | 
			
		||||
			toast.error(error);
 | 
			
		||||
			return [];
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (OLLAMA_URLS.length > 1) {
 | 
			
		||||
			selectedOllamaUrlIdx = 0;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
 | 
			
		||||
		liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
 | 
			
		||||
	});
 | 
			
		||||
| 
						 | 
				
			
			@ -377,52 +399,137 @@
 | 
			
		|||
	<div class=" space-y-3 pr-1.5 overflow-y-scroll h-[23rem]">
 | 
			
		||||
		{#if ollamaVersion}
 | 
			
		||||
			<div class="space-y-2 pr-1.5">
 | 
			
		||||
				<div>
 | 
			
		||||
					<div class=" mb-2 text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
 | 
			
		||||
				<div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</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 py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 | 
			
		||||
								placeholder="Enter model tag (e.g. mistral:7b)"
 | 
			
		||||
								bind:value={modelTag}
 | 
			
		||||
							/>
 | 
			
		||||
						</div>
 | 
			
		||||
						<button
 | 
			
		||||
							class="px-3 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
 | 
			
		||||
							on:click={() => {
 | 
			
		||||
								pullModelHandler();
 | 
			
		||||
							}}
 | 
			
		||||
							disabled={modelTransferring}
 | 
			
		||||
				{#if OLLAMA_URLS.length > 1}
 | 
			
		||||
					<div class="flex-1 pb-1">
 | 
			
		||||
						<select
 | 
			
		||||
							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 | 
			
		||||
							bind:value={selectedOllamaUrlIdx}
 | 
			
		||||
							placeholder="Select an Ollama instance"
 | 
			
		||||
						>
 | 
			
		||||
							{#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);
 | 
			
		||||
							{#each OLLAMA_URLS as url, idx}
 | 
			
		||||
								<option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option>
 | 
			
		||||
							{/each}
 | 
			
		||||
						</select>
 | 
			
		||||
					</div>
 | 
			
		||||
				{/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 dark:text-gray-300 dark:bg-gray-850 outline-none"
 | 
			
		||||
									placeholder="Enter model tag (e.g. mistral:7b)"
 | 
			
		||||
									bind:value={modelTag}
 | 
			
		||||
								/>
 | 
			
		||||
							</div>
 | 
			
		||||
							<button
 | 
			
		||||
								class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
 | 
			
		||||
								on:click={() => {
 | 
			
		||||
									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;
 | 
			
		||||
												}
 | 
			
		||||
											}
 | 
			
		||||
										</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
 | 
			
		||||
												@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">
 | 
			
		||||
							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">click here.</a
 | 
			
		||||
							>
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
						{#if Object.keys(modelDownloadStatus).length > 0}
 | 
			
		||||
							{#each Object.keys(modelDownloadStatus) as model}
 | 
			
		||||
								<div class="flex flex-col">
 | 
			
		||||
									<div class="font-medium mb-1">{model}</div>
 | 
			
		||||
									<div class="">
 | 
			
		||||
										<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, modelDownloadStatus[model].pullProgress ?? 0)}%"
 | 
			
		||||
										>
 | 
			
		||||
											{modelDownloadStatus[model].pullProgress ?? 0}%
 | 
			
		||||
										</div>
 | 
			
		||||
										<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
 | 
			
		||||
											{modelDownloadStatus[model].digest}
 | 
			
		||||
										</div>
 | 
			
		||||
									</div>
 | 
			
		||||
								</div>
 | 
			
		||||
							{:else}
 | 
			
		||||
							{/each}
 | 
			
		||||
						{/if}
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div>
 | 
			
		||||
						<div class=" mb-2 text-sm font-medium">Delete a model</div>
 | 
			
		||||
						<div class="flex w-full">
 | 
			
		||||
							<div class="flex-1 mr-2">
 | 
			
		||||
								<select
 | 
			
		||||
									class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 | 
			
		||||
									bind:value={deleteModelTag}
 | 
			
		||||
									placeholder="Select a model"
 | 
			
		||||
								>
 | 
			
		||||
									{#if !deleteModelTag}
 | 
			
		||||
										<option value="" disabled selected>Select a model</option>
 | 
			
		||||
									{/if}
 | 
			
		||||
									{#each $models.filter((m) => m.size != null) as model}
 | 
			
		||||
										<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
 | 
			
		||||
											>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
 | 
			
		||||
										>
 | 
			
		||||
									{/each}
 | 
			
		||||
								</select>
 | 
			
		||||
							</div>
 | 
			
		||||
							<button
 | 
			
		||||
								class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
 | 
			
		||||
								on:click={() => {
 | 
			
		||||
									deleteModelHandler();
 | 
			
		||||
								}}
 | 
			
		||||
							>
 | 
			
		||||
								<svg
 | 
			
		||||
									xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
									viewBox="0 0 16 16"
 | 
			
		||||
| 
						 | 
				
			
			@ -430,263 +537,192 @@
 | 
			
		|||
									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"
 | 
			
		||||
										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>
 | 
			
		||||
							{/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(modelDownloadStatus).length > 0}
 | 
			
		||||
						{#each Object.keys(modelDownloadStatus) as model}
 | 
			
		||||
							<div class="flex flex-col">
 | 
			
		||||
								<div class="font-medium mb-1">{model}</div>
 | 
			
		||||
								<div class="">
 | 
			
		||||
									<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, modelDownloadStatus[model].pullProgress ?? 0)}%"
 | 
			
		||||
									>
 | 
			
		||||
										{modelDownloadStatus[model].pullProgress ?? 0}%
 | 
			
		||||
									</div>
 | 
			
		||||
									<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
 | 
			
		||||
										{modelDownloadStatus[model].digest}
 | 
			
		||||
									</div>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						{/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 py-2 px-4 text-sm 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.size != null) as model}
 | 
			
		||||
									<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
 | 
			
		||||
										>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
 | 
			
		||||
									>
 | 
			
		||||
								{/each}
 | 
			
		||||
							</select>
 | 
			
		||||
						</div>
 | 
			
		||||
						<button
 | 
			
		||||
							class="px-3 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
 | 
			
		||||
							on:click={() => {
 | 
			
		||||
								deleteModelHandler();
 | 
			
		||||
							}}
 | 
			
		||||
						>
 | 
			
		||||
							<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="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('Show') : $i18n.t('Hide')}</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>
 | 
			
		||||
 | 
			
		||||
						<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"
 | 
			
		||||
											type="file"
 | 
			
		||||
											bind:files={modelInputFile}
 | 
			
		||||
											on:change={() => {
 | 
			
		||||
												console.log(modelInputFile);
 | 
			
		||||
											}}
 | 
			
		||||
											accept=".gguf"
 | 
			
		||||
											required
 | 
			
		||||
											hidden
 | 
			
		||||
										/>
 | 
			
		||||
					<div class="pt-1">
 | 
			
		||||
						<div class="flex justify-between items-center text-xs">
 | 
			
		||||
							<div class=" text-sm font-medium">Experimental</div>
 | 
			
		||||
							<button
 | 
			
		||||
								class=" text-xs font-medium text-gray-500"
 | 
			
		||||
								type="button"
 | 
			
		||||
								on:click={() => {
 | 
			
		||||
									showExperimentalOllama = !showExperimentalOllama;
 | 
			
		||||
								}}>{showExperimentalOllama ? 'Hide' : 'Show'}</button
 | 
			
		||||
							>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
										<button
 | 
			
		||||
											type="button"
 | 
			
		||||
											class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850"
 | 
			
		||||
											on:click={() => {
 | 
			
		||||
												document.getElementById('model-upload-input').click();
 | 
			
		||||
											}}
 | 
			
		||||
										>
 | 
			
		||||
											{#if modelInputFile && modelInputFile.length > 0}
 | 
			
		||||
												{modelInputFile[0].name}
 | 
			
		||||
											{:else}
 | 
			
		||||
												Click here to select
 | 
			
		||||
											{/if}
 | 
			
		||||
										</button>
 | 
			
		||||
									</div>
 | 
			
		||||
								{:else}
 | 
			
		||||
									<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
 | 
			
		||||
										<input
 | 
			
		||||
											class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
 | 
			
		||||
											''
 | 
			
		||||
												? 'mr-2'
 | 
			
		||||
												: ''}"
 | 
			
		||||
											type="url"
 | 
			
		||||
											required
 | 
			
		||||
											bind:value={modelFileUrl}
 | 
			
		||||
											placeholder="Type Hugging Face Resolve (Download) URL"
 | 
			
		||||
										/>
 | 
			
		||||
									</div>
 | 
			
		||||
					{#if showExperimentalOllama}
 | 
			
		||||
						<form
 | 
			
		||||
							on:submit|preventDefault={() => {
 | 
			
		||||
								uploadModelHandler();
 | 
			
		||||
							}}
 | 
			
		||||
						>
 | 
			
		||||
							<div class=" mb-2 flex w-full justify-between">
 | 
			
		||||
								<div class="  text-sm font-medium">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">File Mode</span>
 | 
			
		||||
									{:else}
 | 
			
		||||
										<span class="ml-2 self-center">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"
 | 
			
		||||
												required
 | 
			
		||||
												hidden
 | 
			
		||||
											/>
 | 
			
		||||
 | 
			
		||||
											<button
 | 
			
		||||
												type="button"
 | 
			
		||||
												class="w-full rounded-lg text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850"
 | 
			
		||||
												on:click={modelUploadInputElement.click}
 | 
			
		||||
											>
 | 
			
		||||
												{#if modelInputFile && modelInputFile.length > 0}
 | 
			
		||||
													{modelInputFile[0].name}
 | 
			
		||||
												{:else}
 | 
			
		||||
													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 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
 | 
			
		||||
												''
 | 
			
		||||
													? 'mr-2'
 | 
			
		||||
													: ''}"
 | 
			
		||||
												type="url"
 | 
			
		||||
												required
 | 
			
		||||
												bind:value={modelFileUrl}
 | 
			
		||||
												placeholder="Type Hugging Face Resolve (Download) URL"
 | 
			
		||||
											/>
 | 
			
		||||
										</div>
 | 
			
		||||
									{/if}
 | 
			
		||||
								</div>
 | 
			
		||||
 | 
			
		||||
								{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
 | 
			
		||||
									<button
 | 
			
		||||
										class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded 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 !== '')}
 | 
			
		||||
								<button
 | 
			
		||||
									class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded 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 py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
 | 
			
		||||
										rows="6"
 | 
			
		||||
									/>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						{/if}
 | 
			
		||||
						<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
 | 
			
		||||
							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 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 class=" my-2.5 text-sm font-medium">Modelfile Content</div>
 | 
			
		||||
										<textarea
 | 
			
		||||
											bind:value={modelFileContent}
 | 
			
		||||
											class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
 | 
			
		||||
											rows="6"
 | 
			
		||||
										/>
 | 
			
		||||
									</div>
 | 
			
		||||
								</div>
 | 
			
		||||
								<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
 | 
			
		||||
									{modelFileDigest}
 | 
			
		||||
								</div>
 | 
			
		||||
							{/if}
 | 
			
		||||
							<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
 | 
			
		||||
								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">click here.</a
 | 
			
		||||
								>
 | 
			
		||||
							</div>
 | 
			
		||||
						{/if}
 | 
			
		||||
					</form>
 | 
			
		||||
				{/if}
 | 
			
		||||
 | 
			
		||||
							{#if uploadProgress !== null}
 | 
			
		||||
								<div class="mt-2">
 | 
			
		||||
									<div class=" mb-2 text-xs">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>
 | 
			
		||||
			<hr class=" dark:border-gray-700 my-2" />
 | 
			
		||||
		{/if}
 | 
			
		||||
| 
						 | 
				
			
			@ -704,7 +740,7 @@
 | 
			
		|||
								type="button"
 | 
			
		||||
								on:click={() => {
 | 
			
		||||
									showLiteLLMParams = !showLiteLLMParams;
 | 
			
		||||
								}}>{showLiteLLMParams ? $i18n.t('Advanced') : $i18n.t('Default')}</button
 | 
			
		||||
								}}>{showLiteLLMParams ? 'Hide Additional Params' : 'Show Additional Params'}</button
 | 
			
		||||
							>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -713,7 +749,7 @@
 | 
			
		|||
						<div class="flex w-full mb-1.5">
 | 
			
		||||
							<div class="flex-1 mr-2">
 | 
			
		||||
								<input
 | 
			
		||||
									class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 | 
			
		||||
									class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 | 
			
		||||
									placeholder="Enter LiteLLM Model (litellm_params.model)"
 | 
			
		||||
									bind:value={liteLLMModel}
 | 
			
		||||
									autocomplete="off"
 | 
			
		||||
| 
						 | 
				
			
			@ -721,7 +757,7 @@
 | 
			
		|||
							</div>
 | 
			
		||||
 | 
			
		||||
							<button
 | 
			
		||||
								class="px-3 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
 | 
			
		||||
								class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
 | 
			
		||||
								on:click={() => {
 | 
			
		||||
									addLiteLLMModelHandler();
 | 
			
		||||
								}}
 | 
			
		||||
| 
						 | 
				
			
			@ -745,7 +781,7 @@
 | 
			
		|||
								<div class="flex w-full">
 | 
			
		||||
									<div class="flex-1">
 | 
			
		||||
										<input
 | 
			
		||||
											class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 | 
			
		||||
											class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 | 
			
		||||
											placeholder="Enter Model Name (model_name)"
 | 
			
		||||
											bind:value={liteLLMModelName}
 | 
			
		||||
											autocomplete="off"
 | 
			
		||||
| 
						 | 
				
			
			@ -759,7 +795,7 @@
 | 
			
		|||
								<div class="flex w-full">
 | 
			
		||||
									<div class="flex-1">
 | 
			
		||||
										<input
 | 
			
		||||
											class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 | 
			
		||||
											class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 | 
			
		||||
											placeholder="Enter LiteLLM API Base URL (litellm_params.api_base)"
 | 
			
		||||
											bind:value={liteLLMAPIBase}
 | 
			
		||||
											autocomplete="off"
 | 
			
		||||
| 
						 | 
				
			
			@ -773,7 +809,7 @@
 | 
			
		|||
								<div class="flex w-full">
 | 
			
		||||
									<div class="flex-1">
 | 
			
		||||
										<input
 | 
			
		||||
											class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 | 
			
		||||
											class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 | 
			
		||||
											placeholder="Enter LiteLLM API Key (litellm_params.api_key)"
 | 
			
		||||
											bind:value={liteLLMAPIKey}
 | 
			
		||||
											autocomplete="off"
 | 
			
		||||
| 
						 | 
				
			
			@ -787,7 +823,7 @@
 | 
			
		|||
								<div class="flex w-full">
 | 
			
		||||
									<div class="flex-1">
 | 
			
		||||
										<input
 | 
			
		||||
											class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 | 
			
		||||
											class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 | 
			
		||||
											placeholder="Enter LiteLLM API RPM (litellm_params.rpm)"
 | 
			
		||||
											bind:value={liteLLMRPM}
 | 
			
		||||
											autocomplete="off"
 | 
			
		||||
| 
						 | 
				
			
			@ -814,7 +850,7 @@
 | 
			
		|||
						<div class="flex w-full">
 | 
			
		||||
							<div class="flex-1 mr-2">
 | 
			
		||||
								<select
 | 
			
		||||
									class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 | 
			
		||||
									class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 | 
			
		||||
									bind:value={deleteLiteLLMModelId}
 | 
			
		||||
									placeholder={$i18n.t('Select a model')}
 | 
			
		||||
								>
 | 
			
		||||
| 
						 | 
				
			
			@ -829,7 +865,7 @@
 | 
			
		|||
								</select>
 | 
			
		||||
							</div>
 | 
			
		||||
							<button
 | 
			
		||||
								class="px-3 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
 | 
			
		||||
								class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
 | 
			
		||||
								on:click={() => {
 | 
			
		||||
									deleteLiteLLMModelHandler();
 | 
			
		||||
								}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,6 +29,6 @@
 | 
			
		|||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div bind:this={tooltipElement}>
 | 
			
		||||
<div bind:this={tooltipElement} aria-label={content}>
 | 
			
		||||
	<slot />
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,7 +17,7 @@
 | 
			
		|||
 | 
			
		||||
	export let show = false;
 | 
			
		||||
	export let selectedDoc;
 | 
			
		||||
 | 
			
		||||
	let uploadDocInputElement: HTMLInputElement;
 | 
			
		||||
	let inputFiles;
 | 
			
		||||
	let tags = [];
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -71,7 +71,7 @@
 | 
			
		|||
			}
 | 
			
		||||
 | 
			
		||||
			inputFiles = null;
 | 
			
		||||
			document.getElementById('upload-doc-input').value = '';
 | 
			
		||||
			uploadDocInputElement.value = '';
 | 
			
		||||
		} else {
 | 
			
		||||
			toast.error($i18n.t(`File not found.`));
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -128,14 +128,19 @@
 | 
			
		|||
					}}
 | 
			
		||||
				>
 | 
			
		||||
					<div class="mb-3 w-full">
 | 
			
		||||
						<input id="upload-doc-input" hidden bind:files={inputFiles} type="file" multiple />
 | 
			
		||||
						<input
 | 
			
		||||
							id="upload-doc-input"
 | 
			
		||||
							bind:this={uploadDocInputElement}
 | 
			
		||||
							hidden
 | 
			
		||||
							bind:files={inputFiles}
 | 
			
		||||
							type="file"
 | 
			
		||||
							multiple
 | 
			
		||||
						/>
 | 
			
		||||
 | 
			
		||||
						<button
 | 
			
		||||
							class="w-full text-sm font-medium py-3 bg-gray-850 hover:bg-gray-800 text-center rounded-xl"
 | 
			
		||||
							type="button"
 | 
			
		||||
							on:click={() => {
 | 
			
		||||
								document.getElementById('upload-doc-input')?.click();
 | 
			
		||||
							}}
 | 
			
		||||
							on:click={uploadDocInputElement.click}
 | 
			
		||||
						>
 | 
			
		||||
							{#if inputFiles}
 | 
			
		||||
								{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,12 +4,11 @@
 | 
			
		|||
	const i18n = getContext('i18n');
 | 
			
		||||
 | 
			
		||||
	export let messages = [];
 | 
			
		||||
 | 
			
		||||
	let textAreaElement: HTMLTextAreaElement;
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		messages.forEach((message, idx) => {
 | 
			
		||||
			let textareaElement = document.getElementById(`${message.role}-${idx}-textarea`);
 | 
			
		||||
			textareaElement.style.height = '';
 | 
			
		||||
			textareaElement.style.height = textareaElement.scrollHeight + 'px';
 | 
			
		||||
			textAreaElement.style.height = '';
 | 
			
		||||
			textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -29,18 +28,19 @@
 | 
			
		|||
			<div class="flex-1">
 | 
			
		||||
				<textarea
 | 
			
		||||
					id="{message.role}-{idx}-textarea"
 | 
			
		||||
					bind:this={textAreaElement}
 | 
			
		||||
					class="w-full bg-transparent outline-none rounded-lg p-2 text-sm resize-none overflow-hidden"
 | 
			
		||||
					placeholder={$i18n.t(
 | 
			
		||||
						`Enter ${message.role === 'user' ? 'a user' : 'an assistant'} message here`
 | 
			
		||||
					)}
 | 
			
		||||
					rows="1"
 | 
			
		||||
					on:input={(e) => {
 | 
			
		||||
						e.target.style.height = '';
 | 
			
		||||
						e.target.style.height = e.target.scrollHeight + 'px';
 | 
			
		||||
						textAreaElement.style.height = '';
 | 
			
		||||
						textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
 | 
			
		||||
					}}
 | 
			
		||||
					on:focus={(e) => {
 | 
			
		||||
						e.target.style.height = '';
 | 
			
		||||
						e.target.style.height = e.target.scrollHeight + 'px';
 | 
			
		||||
						textAreaElement.style.height = '';
 | 
			
		||||
						textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
 | 
			
		||||
 | 
			
		||||
						// e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
 | 
			
		||||
					}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue