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
				
			
		|  | @ -1,9 +1,9 @@ | |||
| import { OLLAMA_API_BASE_URL } from '$lib/constants'; | ||||
| 
 | ||||
| export const getOllamaAPIUrl = async (token: string = '') => { | ||||
| export const getOllamaUrls = async (token: string = '') => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${OLLAMA_API_BASE_URL}/url`, { | ||||
| 	const res = await fetch(`${OLLAMA_API_BASE_URL}/urls`, { | ||||
| 		method: 'GET', | ||||
| 		headers: { | ||||
| 			Accept: 'application/json', | ||||
|  | @ -29,13 +29,13 @@ export const getOllamaAPIUrl = async (token: string = '') => { | |||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res.OLLAMA_BASE_URL; | ||||
| 	return res.OLLAMA_BASE_URLS; | ||||
| }; | ||||
| 
 | ||||
| export const updateOllamaAPIUrl = async (token: string = '', url: string) => { | ||||
| export const updateOllamaUrls = async (token: string = '', urls: string[]) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${OLLAMA_API_BASE_URL}/url/update`, { | ||||
| 	const res = await fetch(`${OLLAMA_API_BASE_URL}/urls/update`, { | ||||
| 		method: 'POST', | ||||
| 		headers: { | ||||
| 			Accept: 'application/json', | ||||
|  | @ -43,7 +43,7 @@ export const updateOllamaAPIUrl = async (token: string = '', url: string) => { | |||
| 			...(token && { authorization: `Bearer ${token}` }) | ||||
| 		}, | ||||
| 		body: JSON.stringify({ | ||||
| 			url: url | ||||
| 			urls: urls | ||||
| 		}) | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
|  | @ -64,7 +64,7 @@ export const updateOllamaAPIUrl = async (token: string = '', url: string) => { | |||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res.OLLAMA_BASE_URL; | ||||
| 	return res.OLLAMA_BASE_URLS; | ||||
| }; | ||||
| 
 | ||||
| export const getOllamaVersion = async (token: string = '') => { | ||||
|  | @ -151,7 +151,8 @@ export const generateTitle = async ( | |||
| 	const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, { | ||||
| 		method: 'POST', | ||||
| 		headers: { | ||||
| 			'Content-Type': 'text/event-stream', | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			Authorization: `Bearer ${token}` | ||||
| 		}, | ||||
| 		body: JSON.stringify({ | ||||
|  | @ -189,7 +190,8 @@ export const generatePrompt = async (token: string = '', model: string, conversa | |||
| 	const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, { | ||||
| 		method: 'POST', | ||||
| 		headers: { | ||||
| 			'Content-Type': 'text/event-stream', | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			Authorization: `Bearer ${token}` | ||||
| 		}, | ||||
| 		body: JSON.stringify({ | ||||
|  | @ -223,7 +225,8 @@ export const generateTextCompletion = async (token: string = '', model: string, | |||
| 	const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, { | ||||
| 		method: 'POST', | ||||
| 		headers: { | ||||
| 			'Content-Type': 'text/event-stream', | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			Authorization: `Bearer ${token}` | ||||
| 		}, | ||||
| 		body: JSON.stringify({ | ||||
|  | @ -251,7 +254,8 @@ export const generateChatCompletion = async (token: string = '', body: object) = | |||
| 		signal: controller.signal, | ||||
| 		method: 'POST', | ||||
| 		headers: { | ||||
| 			'Content-Type': 'text/event-stream', | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			Authorization: `Bearer ${token}` | ||||
| 		}, | ||||
| 		body: JSON.stringify(body) | ||||
|  | @ -294,7 +298,8 @@ export const createModel = async (token: string, tagName: string, content: strin | |||
| 	const res = await fetch(`${OLLAMA_API_BASE_URL}/api/create`, { | ||||
| 		method: 'POST', | ||||
| 		headers: { | ||||
| 			'Content-Type': 'text/event-stream', | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			Authorization: `Bearer ${token}` | ||||
| 		}, | ||||
| 		body: JSON.stringify({ | ||||
|  | @ -313,19 +318,23 @@ export const createModel = async (token: string, tagName: string, content: strin | |||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const deleteModel = async (token: string, tagName: string) => { | ||||
| export const deleteModel = async (token: string, tagName: string, urlIdx: string | null = null) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${OLLAMA_API_BASE_URL}/api/delete`, { | ||||
| 		method: 'DELETE', | ||||
| 		headers: { | ||||
| 			'Content-Type': 'text/event-stream', | ||||
| 			Authorization: `Bearer ${token}` | ||||
| 		}, | ||||
| 		body: JSON.stringify({ | ||||
| 			name: tagName | ||||
| 		}) | ||||
| 	}) | ||||
| 	const res = await fetch( | ||||
| 		`${OLLAMA_API_BASE_URL}/api/delete${urlIdx !== null ? `/${urlIdx}` : ''}`, | ||||
| 		{ | ||||
| 			method: 'DELETE', | ||||
| 			headers: { | ||||
| 				Accept: 'application/json', | ||||
| 				'Content-Type': 'application/json', | ||||
| 				Authorization: `Bearer ${token}` | ||||
| 			}, | ||||
| 			body: JSON.stringify({ | ||||
| 				name: tagName | ||||
| 			}) | ||||
| 		} | ||||
| 	) | ||||
| 		.then(async (res) => { | ||||
| 			if (!res.ok) throw await res.json(); | ||||
| 			return res.json(); | ||||
|  | @ -336,7 +345,12 @@ export const deleteModel = async (token: string, tagName: string) => { | |||
| 		}) | ||||
| 		.catch((err) => { | ||||
| 			console.log(err); | ||||
| 			error = err.error; | ||||
| 			error = err; | ||||
| 
 | ||||
| 			if ('detail' in err) { | ||||
| 				error = err.detail; | ||||
| 			} | ||||
| 
 | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
|  | @ -347,13 +361,14 @@ export const deleteModel = async (token: string, tagName: string) => { | |||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const pullModel = async (token: string, tagName: string) => { | ||||
| export const pullModel = async (token: string, tagName: string, urlIdx: string | null = null) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${OLLAMA_API_BASE_URL}/api/pull`, { | ||||
| 	const res = await fetch(`${OLLAMA_API_BASE_URL}/api/pull${urlIdx !== null ? `/${urlIdx}` : ''}`, { | ||||
| 		method: 'POST', | ||||
| 		headers: { | ||||
| 			'Content-Type': 'text/event-stream', | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			Authorization: `Bearer ${token}` | ||||
| 		}, | ||||
| 		body: JSON.stringify({ | ||||
|  |  | |||
|  | @ -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'; | ||||
| 					}} | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``; | |||
| export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`; | ||||
| 
 | ||||
| export const LITELLM_API_BASE_URL = `${WEBUI_BASE_URL}/litellm/api`; | ||||
| export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama/api`; | ||||
| export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama`; | ||||
| export const OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai/api`; | ||||
| export const AUDIO_API_BASE_URL = `${WEBUI_BASE_URL}/audio/api/v1`; | ||||
| export const IMAGES_API_BASE_URL = `${WEBUI_BASE_URL}/images/api/v1`; | ||||
|  |  | |||
|  | @ -34,12 +34,13 @@ | |||
| 	import Sidebar from '$lib/components/layout/Sidebar.svelte'; | ||||
| 	import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte'; | ||||
| 	import ChangelogModal from '$lib/components/ChangelogModal.svelte'; | ||||
| 	import Tooltip from '$lib/components/common/Tooltip.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	let ollamaVersion = ''; | ||||
| 	let loaded = false; | ||||
| 
 | ||||
| 	let showShortcutsButtonElement: HTMLButtonElement; | ||||
| 	let DB = null; | ||||
| 	let localDBChats = []; | ||||
| 
 | ||||
|  | @ -186,7 +187,7 @@ | |||
| 				if (isCtrlPressed && event.key === '/') { | ||||
| 					event.preventDefault(); | ||||
| 					console.log('showShortcuts'); | ||||
| 					document.getElementById('show-shortcuts-button')?.click(); | ||||
| 					showShortcutsButtonElement.click(); | ||||
| 				} | ||||
| 			}); | ||||
| 
 | ||||
|  | @ -203,15 +204,18 @@ | |||
| 
 | ||||
| {#if loaded} | ||||
| 	<div class=" hidden lg:flex fixed bottom-0 right-0 px-3 py-3 z-10"> | ||||
| 		<button | ||||
| 			id="show-shortcuts-button" | ||||
| 			class="text-gray-600 dark:text-gray-300 bg-gray-300/20 w-6 h-6 flex items-center justify-center text-xs rounded-full" | ||||
| 			on:click={() => { | ||||
| 				showShortcuts = !showShortcuts; | ||||
| 			}} | ||||
| 		> | ||||
| 			? | ||||
| 		</button> | ||||
| 		<Tooltip content="help" placement="left"> | ||||
| 			<button | ||||
| 				id="show-shortcuts-button" | ||||
| 				bind:this={showShortcutsButtonElement} | ||||
| 				class="text-gray-600 dark:text-gray-300 bg-gray-300/20 w-6 h-6 flex items-center justify-center text-xs rounded-full" | ||||
| 				on:click={() => { | ||||
| 					showShortcuts = !showShortcuts; | ||||
| 				}} | ||||
| 			> | ||||
| 				? | ||||
| 			</button> | ||||
| 		</Tooltip> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<ShortcutsModal bind:show={showShortcuts} /> | ||||
|  |  | |||
|  | @ -45,7 +45,7 @@ | |||
| 	let stopResponseFlag = false; | ||||
| 	let autoScroll = true; | ||||
| 	let processing = ''; | ||||
| 
 | ||||
| 	let messagesContainerElement: HTMLDivElement; | ||||
| 	let currentRequestId = null; | ||||
| 
 | ||||
| 	let selectedModels = ['']; | ||||
|  | @ -143,8 +143,7 @@ | |||
| 	}; | ||||
| 
 | ||||
| 	const scrollToBottom = () => { | ||||
| 		const element = document.getElementById('messages-container'); | ||||
| 		element.scrollTop = element.scrollHeight; | ||||
| 		messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight; | ||||
| 	}; | ||||
| 
 | ||||
| 	////////////////////////// | ||||
|  | @ -837,8 +836,11 @@ | |||
| 		<div | ||||
| 			class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0" | ||||
| 			id="messages-container" | ||||
| 			bind:this={messagesContainerElement} | ||||
| 			on:scroll={(e) => { | ||||
| 				autoScroll = e.target.scrollHeight - e.target.scrollTop <= e.target.clientHeight + 50; | ||||
| 				autoScroll = | ||||
| 					messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <= | ||||
| 					messagesContainerElement.clientHeight + 50; | ||||
| 			}} | ||||
| 		> | ||||
| 			<div | ||||
|  | @ -846,10 +848,7 @@ | |||
| 					? 'max-w-full' | ||||
| 					: 'max-w-2xl md:px-0'} mx-auto w-full px-4" | ||||
| 			> | ||||
| 				<ModelSelector | ||||
| 					bind:selectedModels | ||||
| 					disabled={messages.length > 0 && !selectedModels.includes('')} | ||||
| 				/> | ||||
| 				<ModelSelector bind:selectedModels /> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class=" h-full w-full flex flex-col py-8"> | ||||
|  |  | |||
|  | @ -47,7 +47,7 @@ | |||
| 	let stopResponseFlag = false; | ||||
| 	let autoScroll = true; | ||||
| 	let processing = ''; | ||||
| 
 | ||||
| 	let messagesContainerElement: HTMLDivElement; | ||||
| 	let currentRequestId = null; | ||||
| 
 | ||||
| 	// let chatId = $page.params.id; | ||||
|  | @ -162,8 +162,7 @@ | |||
| 	}; | ||||
| 
 | ||||
| 	const scrollToBottom = () => { | ||||
| 		const element = document.getElementById('messages-container'); | ||||
| 		element.scrollTop = element.scrollHeight; | ||||
| 		messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight; | ||||
| 	}; | ||||
| 
 | ||||
| 	////////////////////////// | ||||
|  | @ -865,8 +864,11 @@ | |||
| 			<div | ||||
| 				class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0" | ||||
| 				id="messages-container" | ||||
| 				bind:this={messagesContainerElement} | ||||
| 				on:scroll={(e) => { | ||||
| 					autoScroll = e.target.scrollHeight - e.target.scrollTop <= e.target.clientHeight + 50; | ||||
| 					autoScroll = | ||||
| 						messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <= | ||||
| 						messagesContainerElement.clientHeight + 50; | ||||
| 				}} | ||||
| 			> | ||||
| 				<div | ||||
|  | @ -874,10 +876,7 @@ | |||
| 						? 'max-w-full' | ||||
| 						: 'max-w-2xl md:px-0'} mx-auto w-full px-4" | ||||
| 				> | ||||
| 					<ModelSelector | ||||
| 						bind:selectedModels | ||||
| 						disabled={messages.length > 0 && !selectedModels.includes('')} | ||||
| 					/> | ||||
| 					<ModelSelector bind:selectedModels /> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<div class=" h-full w-full flex flex-col py-8"> | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ | |||
| 
 | ||||
| 	let inputFiles = ''; | ||||
| 	let query = ''; | ||||
| 
 | ||||
| 	let documentsImportInputElement: HTMLInputElement; | ||||
| 	let tags = []; | ||||
| 
 | ||||
| 	let showSettingsModal = false; | ||||
|  | @ -527,6 +527,7 @@ | |||
| 				<div class="flex space-x-2"> | ||||
| 					<input | ||||
| 						id="documents-import-input" | ||||
| 						bind:this={documentsImportInputElement} | ||||
| 						bind:files={importFiles} | ||||
| 						type="file" | ||||
| 						accept=".json" | ||||
|  | @ -561,9 +562,7 @@ | |||
| 
 | ||||
| 					<button | ||||
| 						class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition" | ||||
| 						on:click={async () => { | ||||
| 							document.getElementById('documents-import-input')?.click(); | ||||
| 						}} | ||||
| 						on:click={documentsImportInputElement.click} | ||||
| 					> | ||||
| 						<div class=" self-center mr-2 font-medium">{$i18n.t('Import Documents Mapping')}</div> | ||||
| 
 | ||||
|  |  | |||
|  | @ -18,11 +18,14 @@ | |||
| 
 | ||||
| 	let localModelfiles = []; | ||||
| 	let importFiles; | ||||
| 
 | ||||
| 	let modelfilesImportInputElement: HTMLInputElement; | ||||
| 	const deleteModelHandler = async (tagName) => { | ||||
| 		let success = null; | ||||
| 
 | ||||
| 		success = await deleteModel(localStorage.token, tagName); | ||||
| 		success = await deleteModel(localStorage.token, tagName).catch((err) => { | ||||
| 			toast.error(err); | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 		if (success) { | ||||
| 			toast.success($i18n.t(`Deleted {tagName}`, { tagName })); | ||||
|  | @ -237,6 +240,7 @@ | |||
| 				<div class="flex space-x-1"> | ||||
| 					<input | ||||
| 						id="modelfiles-import-input" | ||||
| 						bind:this={modelfilesImportInputElement} | ||||
| 						bind:files={importFiles} | ||||
| 						type="file" | ||||
| 						accept=".json" | ||||
|  | @ -264,9 +268,7 @@ | |||
| 
 | ||||
| 					<button | ||||
| 						class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition" | ||||
| 						on:click={async () => { | ||||
| 							document.getElementById('modelfiles-import-input')?.click(); | ||||
| 						}} | ||||
| 						on:click={modelfilesImportInputElement.click} | ||||
| 					> | ||||
| 						<div class=" self-center mr-2 font-medium">{$i18n.t('Import Modelfiles')}</div> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| <script> | ||||
| <script lang="ts"> | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 
 | ||||
| 	import { onMount, tick, getContext } from 'svelte'; | ||||
|  | @ -23,15 +23,17 @@ | |||
| 
 | ||||
| 	let mode = 'chat'; | ||||
| 	let loaded = false; | ||||
| 
 | ||||
| 	let text = ''; | ||||
| 
 | ||||
| 	let selectedModelId = ''; | ||||
| 
 | ||||
| 	let loading = false; | ||||
| 	let currentRequestId; | ||||
| 	let currentRequestId = null; | ||||
| 	let stopResponseFlag = false; | ||||
| 
 | ||||
| 	let messagesContainerElement: HTMLDivElement; | ||||
| 	let textCompletionAreaElement: HTMLTextAreaElement; | ||||
| 
 | ||||
| 	let system = ''; | ||||
| 	let messages = [ | ||||
| 		{ | ||||
|  | @ -41,13 +43,7 @@ | |||
| 	]; | ||||
| 
 | ||||
| 	const scrollToBottom = () => { | ||||
| 		let element; | ||||
| 
 | ||||
| 		if (mode === 'chat') { | ||||
| 			element = document.getElementById('messages-container'); | ||||
| 		} else { | ||||
| 			element = document.getElementById('text-completion-textarea'); | ||||
| 		} | ||||
| 		const element = mode === 'chat' ? messagesContainerElement : textCompletionAreaElement; | ||||
| 
 | ||||
| 		if (element) { | ||||
| 			element.scrollTop = element?.scrollHeight; | ||||
|  | @ -98,6 +94,10 @@ | |||
| 			while (true) { | ||||
| 				const { value, done } = await reader.read(); | ||||
| 				if (done || stopResponseFlag) { | ||||
| 					if (stopResponseFlag) { | ||||
| 						await cancelChatCompletion(localStorage.token, currentRequestId); | ||||
| 					} | ||||
| 
 | ||||
| 					currentRequestId = null; | ||||
| 					break; | ||||
| 				} | ||||
|  | @ -114,7 +114,11 @@ | |||
| 								let data = JSON.parse(line.replace(/^data: /, '')); | ||||
| 								console.log(data); | ||||
| 
 | ||||
| 								text += data.choices[0].delta.content ?? ''; | ||||
| 								if ('request_id' in data) { | ||||
| 									currentRequestId = data.request_id; | ||||
| 								} else { | ||||
| 									text += data.choices[0].delta.content ?? ''; | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
|  | @ -152,16 +156,6 @@ | |||
| 				: `${OLLAMA_API_BASE_URL}/v1` | ||||
| 		); | ||||
| 
 | ||||
| 		// const [res, controller] = await generateChatCompletion(localStorage.token, { | ||||
| 		// 	model: selectedModelId, | ||||
| 		// 	messages: [ | ||||
| 		// 		{ | ||||
| 		// 			role: 'assistant', | ||||
| 		// 			content: text | ||||
| 		// 		} | ||||
| 		// 	] | ||||
| 		// }); | ||||
| 
 | ||||
| 		let responseMessage; | ||||
| 		if (messages.at(-1)?.role === 'assistant') { | ||||
| 			responseMessage = messages.at(-1); | ||||
|  | @ -186,6 +180,11 @@ | |||
| 			while (true) { | ||||
| 				const { value, done } = await reader.read(); | ||||
| 				if (done || stopResponseFlag) { | ||||
| 					if (stopResponseFlag) { | ||||
| 						await cancelChatCompletion(localStorage.token, currentRequestId); | ||||
| 					} | ||||
| 
 | ||||
| 					currentRequestId = null; | ||||
| 					break; | ||||
| 				} | ||||
| 
 | ||||
|  | @ -202,17 +201,21 @@ | |||
| 								let data = JSON.parse(line.replace(/^data: /, '')); | ||||
| 								console.log(data); | ||||
| 
 | ||||
| 								if (responseMessage.content == '' && data.choices[0].delta.content == '\n') { | ||||
| 									continue; | ||||
| 								if ('request_id' in data) { | ||||
| 									currentRequestId = data.request_id; | ||||
| 								} else { | ||||
| 									textareaElement.style.height = textareaElement.scrollHeight + 'px'; | ||||
| 									if (responseMessage.content == '' && data.choices[0].delta.content == '\n') { | ||||
| 										continue; | ||||
| 									} else { | ||||
| 										textareaElement.style.height = textareaElement.scrollHeight + 'px'; | ||||
| 
 | ||||
| 									responseMessage.content += data.choices[0].delta.content ?? ''; | ||||
| 									messages = messages; | ||||
| 										responseMessage.content += data.choices[0].delta.content ?? ''; | ||||
| 										messages = messages; | ||||
| 
 | ||||
| 									textareaElement.style.height = textareaElement.scrollHeight + 'px'; | ||||
| 										textareaElement.style.height = textareaElement.scrollHeight + 'px'; | ||||
| 
 | ||||
| 									await tick(); | ||||
| 										await tick(); | ||||
| 									} | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
|  | @ -223,48 +226,6 @@ | |||
| 
 | ||||
| 				scrollToBottom(); | ||||
| 			} | ||||
| 
 | ||||
| 			// while (true) { | ||||
| 			// 	const { value, done } = await reader.read(); | ||||
| 			// 	if (done || stopResponseFlag) { | ||||
| 			// 		if (stopResponseFlag) { | ||||
| 			// 			await cancelChatCompletion(localStorage.token, currentRequestId); | ||||
| 			// 		} | ||||
| 
 | ||||
| 			// 		currentRequestId = null; | ||||
| 			// 		break; | ||||
| 			// 	} | ||||
| 
 | ||||
| 			// 	try { | ||||
| 			// 		let lines = value.split('\n'); | ||||
| 
 | ||||
| 			// 		for (const line of lines) { | ||||
| 			// 			if (line !== '') { | ||||
| 			// 				console.log(line); | ||||
| 			// 				let data = JSON.parse(line); | ||||
| 
 | ||||
| 			// 				if ('detail' in data) { | ||||
| 			// 					throw data; | ||||
| 			// 				} | ||||
| 
 | ||||
| 			// 				if ('id' in data) { | ||||
| 			// 					console.log(data); | ||||
| 			// 					currentRequestId = data.id; | ||||
| 			// 				} else { | ||||
| 			// 					if (data.done == false) { | ||||
| 			// 						text += data.message.content; | ||||
| 			// 					} else { | ||||
| 			// 						console.log('done'); | ||||
| 			// 					} | ||||
| 			// 				} | ||||
| 			// 			} | ||||
| 			// 		} | ||||
| 			// 	} catch (error) { | ||||
| 			// 		console.log(error); | ||||
| 			// 	} | ||||
| 
 | ||||
| 			// 	scrollToBottom(); | ||||
| 			// } | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
|  | @ -422,12 +383,14 @@ | |||
| 				<div | ||||
| 					class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0" | ||||
| 					id="messages-container" | ||||
| 					bind:this={messagesContainerElement} | ||||
| 				> | ||||
| 					<div class=" h-full w-full flex flex-col"> | ||||
| 						<div class="flex-1 p-1"> | ||||
| 							{#if mode === 'complete'} | ||||
| 								<textarea | ||||
| 									id="text-completion-textarea" | ||||
| 									bind:this={textCompletionAreaElement} | ||||
| 									class="w-full h-full p-3 bg-transparent outline outline-1 outline-gray-200 dark:outline-gray-800 resize-none rounded-lg text-sm" | ||||
| 									bind:value={text} | ||||
| 									placeholder={$i18n.t("You're a helpful assistant.")} | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ | |||
| 
 | ||||
| 	let importFiles = ''; | ||||
| 	let query = ''; | ||||
| 
 | ||||
| 	let promptsImportInputElement: HTMLInputElement; | ||||
| 	const sharePrompt = async (prompt) => { | ||||
| 		toast.success($i18n.t('Redirecting you to OpenWebUI Community')); | ||||
| 
 | ||||
|  | @ -210,6 +210,7 @@ | |||
| 				<div class="flex space-x-2"> | ||||
| 					<input | ||||
| 						id="prompts-import-input" | ||||
| 						bind:this={promptsImportInputElement} | ||||
| 						bind:files={importFiles} | ||||
| 						type="file" | ||||
| 						accept=".json" | ||||
|  | @ -243,9 +244,7 @@ | |||
| 
 | ||||
| 					<button | ||||
| 						class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition" | ||||
| 						on:click={async () => { | ||||
| 							document.getElementById('prompts-import-input')?.click(); | ||||
| 						}} | ||||
| 						on:click={promptsImportInputElement.click} | ||||
| 					> | ||||
| 						<div class=" self-center mr-2 font-medium">{$i18n.t('Import Prompts')}</div> | ||||
| 
 | ||||
|  | @ -268,7 +267,7 @@ | |||
| 					<button | ||||
| 						class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition" | ||||
| 						on:click={async () => { | ||||
| 							// document.getElementById('modelfiles-import-input')?.click(); | ||||
| 							// promptsImportInputElement.click(); | ||||
| 							let blob = new Blob([JSON.stringify($prompts)], { | ||||
| 								type: 'application/json' | ||||
| 							}); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Ased Mammad
						Ased Mammad