forked from open-webui/open-webui
		
	Merge pull request #751 from ollama-webui/styling
feat: fullscreen mode
This commit is contained in:
		
						commit
						a7bb692a54
					
				
					 13 changed files with 259 additions and 222 deletions
				
			
		|  | @ -55,6 +55,11 @@ | ||||||
| 	let isRecording = false; | 	let isRecording = false; | ||||||
| 	const MIN_DECIBELS = -45; | 	const MIN_DECIBELS = -45; | ||||||
| 
 | 
 | ||||||
|  | 	const scrollToBottom = () => { | ||||||
|  | 		const element = document.getElementById('messages-container'); | ||||||
|  | 		element.scrollTop = element.scrollHeight; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
| 	const startRecording = async () => { | 	const startRecording = async () => { | ||||||
| 		const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | 		const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | ||||||
| 		mediaRecorder = new MediaRecorder(stream); | 		mediaRecorder = new MediaRecorder(stream); | ||||||
|  | @ -371,17 +376,17 @@ | ||||||
| 	</div> | 	</div> | ||||||
| {/if} | {/if} | ||||||
| 
 | 
 | ||||||
| <div class="fixed bottom-0 w-full"> | <div class="w-full"> | ||||||
| 	<div class="px-2.5 pt-2.5 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center"> | 	<div class="px-2.5 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center"> | ||||||
| 		<div class="flex flex-col max-w-3xl w-full"> | 		<div class="flex flex-col max-w-3xl w-full"> | ||||||
| 			<div> | 			<div class="relative"> | ||||||
| 				{#if autoScroll === false && messages.length > 0} | 				{#if autoScroll === false && messages.length > 0} | ||||||
| 					<div class=" flex justify-center mb-4"> | 					<div class=" absolute -top-12 left-0 right-0 flex justify-center"> | ||||||
| 						<button | 						<button | ||||||
| 							class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full" | 							class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full" | ||||||
| 							on:click={() => { | 							on:click={() => { | ||||||
| 								autoScroll = true; | 								autoScroll = true; | ||||||
| 								window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); | 								scrollToBottom(); | ||||||
| 							}} | 							}} | ||||||
| 						> | 						> | ||||||
| 							<svg | 							<svg | ||||||
|  | @ -401,7 +406,7 @@ | ||||||
| 				{/if} | 				{/if} | ||||||
| 			</div> | 			</div> | ||||||
| 
 | 
 | ||||||
| 			<div class="w-full"> | 			<div class="w-full relative"> | ||||||
| 				{#if prompt.charAt(0) === '/'} | 				{#if prompt.charAt(0) === '/'} | ||||||
| 					<Prompts bind:this={promptsElement} bind:prompt /> | 					<Prompts bind:this={promptsElement} bind:prompt /> | ||||||
| 				{:else if prompt.charAt(0) === '#'} | 				{:else if prompt.charAt(0) === '#'} | ||||||
|  | @ -432,14 +437,16 @@ | ||||||
| 						bind:chatInputPlaceholder | 						bind:chatInputPlaceholder | ||||||
| 						{messages} | 						{messages} | ||||||
| 					/> | 					/> | ||||||
| 				{:else if messages.length == 0 && suggestionPrompts.length !== 0} | 				{/if} | ||||||
|  | 
 | ||||||
|  | 				{#if messages.length == 0 && suggestionPrompts.length !== 0} | ||||||
| 					<Suggestions {suggestionPrompts} {submitPrompt} /> | 					<Suggestions {suggestionPrompts} {submitPrompt} /> | ||||||
| 				{/if} | 				{/if} | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="bg-white dark:bg-gray-900"> | 	<div class="bg-white dark:bg-gray-900"> | ||||||
| 		<div class="max-w-3xl px-2.5 -mb-0.5 mx-auto inset-x-0"> | 		<div class="max-w-3xl px-2.5 mx-auto inset-x-0"> | ||||||
| 			<div class=" pb-2"> | 			<div class=" pb-2"> | ||||||
| 				<input | 				<input | ||||||
| 					bind:this={filesInputElement} | 					bind:this={filesInputElement} | ||||||
|  |  | ||||||
|  | @ -88,7 +88,7 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')} | {#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')} | ||||||
| 	<div class="md:px-2 mb-3 text-left w-full"> | 	<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0"> | ||||||
| 		<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700"> | 		<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700"> | ||||||
| 			<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center"> | 			<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center"> | ||||||
| 				<div class=" text-lg font-semibold mt-2">#</div> | 				<div class=" text-lg font-semibold mt-2">#</div> | ||||||
|  |  | ||||||
|  | @ -120,7 +120,7 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if filteredModels.length > 0} | {#if filteredModels.length > 0} | ||||||
| 	<div class="md:px-2 mb-3 text-left w-full"> | 	<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0"> | ||||||
| 		<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700"> | 		<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700"> | ||||||
| 			<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center"> | 			<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center"> | ||||||
| 				<div class=" text-lg font-semibold mt-2">@</div> | 				<div class=" text-lg font-semibold mt-2">@</div> | ||||||
|  |  | ||||||
|  | @ -47,7 +47,7 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if filteredPromptCommands.length > 0} | {#if filteredPromptCommands.length > 0} | ||||||
| 	<div class="md:px-2 mb-3 text-left w-full"> | 	<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0"> | ||||||
| 		<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700"> | 		<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700"> | ||||||
| 			<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center"> | 			<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center"> | ||||||
| 				<div class=" text-lg font-semibold mt-2">/</div> | 				<div class=" text-lg font-semibold mt-2">/</div> | ||||||
|  |  | ||||||
|  | @ -29,10 +29,15 @@ | ||||||
| 	$: if (autoScroll && bottomPadding) { | 	$: if (autoScroll && bottomPadding) { | ||||||
| 		(async () => { | 		(async () => { | ||||||
| 			await tick(); | 			await tick(); | ||||||
| 			window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); | 			scrollToBottom(); | ||||||
| 		})(); | 		})(); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	const scrollToBottom = () => { | ||||||
|  | 		const element = document.getElementById('messages-container'); | ||||||
|  | 		element.scrollTop = element.scrollHeight; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
| 	const copyToClipboard = (text) => { | 	const copyToClipboard = (text) => { | ||||||
| 		if (!navigator.clipboard) { | 		if (!navigator.clipboard) { | ||||||
| 			var textArea = document.createElement('textarea'); | 			var textArea = document.createElement('textarea'); | ||||||
|  | @ -160,10 +165,11 @@ | ||||||
| 
 | 
 | ||||||
| 		await tick(); | 		await tick(); | ||||||
| 
 | 
 | ||||||
| 		autoScroll = window.innerHeight + window.scrollY >= document.body.offsetHeight - 40; | 		const element = document.getElementById('messages-container'); | ||||||
|  | 		autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50; | ||||||
| 
 | 
 | ||||||
| 		setTimeout(() => { | 		setTimeout(() => { | ||||||
| 			window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); | 			scrollToBottom(); | ||||||
| 		}, 100); | 		}, 100); | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  | @ -208,9 +214,11 @@ | ||||||
| 
 | 
 | ||||||
| 		await tick(); | 		await tick(); | ||||||
| 
 | 
 | ||||||
| 		autoScroll = window.innerHeight + window.scrollY >= document.body.offsetHeight - 40; | 		const element = document.getElementById('messages-container'); | ||||||
|  | 		autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50; | ||||||
|  | 
 | ||||||
| 		setTimeout(() => { | 		setTimeout(() => { | ||||||
| 			window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); | 			scrollToBottom(); | ||||||
| 		}, 100); | 		}, 100); | ||||||
| 	}; | 	}; | ||||||
| </script> | </script> | ||||||
|  | @ -218,95 +226,97 @@ | ||||||
| {#if messages.length == 0} | {#if messages.length == 0} | ||||||
| 	<Placeholder models={selectedModels} modelfiles={selectedModelfiles} /> | 	<Placeholder models={selectedModels} modelfiles={selectedModelfiles} /> | ||||||
| {:else} | {:else} | ||||||
| 	{#key chatId} | 	<div class=" pb-10"> | ||||||
| 		{#each messages as message, messageIdx} | 		{#key chatId} | ||||||
| 			<div class=" w-full"> | 			{#each messages as message, messageIdx} | ||||||
| 				<div | 				<div class=" w-full"> | ||||||
| 					class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null | 					<div | ||||||
| 						? 'max-w-full' | 						class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null | ||||||
| 						: 'max-w-3xl'} mx-auto rounded-lg group" | 							? 'max-w-full' | ||||||
| 				> | 							: 'max-w-3xl'} mx-auto rounded-lg group" | ||||||
| 					{#if message.role === 'user'} | 					> | ||||||
| 						<UserMessage | 						{#if message.role === 'user'} | ||||||
| 							user={$user} | 							<UserMessage | ||||||
| 							{message} | 								user={$user} | ||||||
| 							siblings={message.parentId !== null | 								{message} | ||||||
| 								? history.messages[message.parentId]?.childrenIds ?? [] | 								siblings={message.parentId !== null | ||||||
| 								: Object.values(history.messages) | 									? history.messages[message.parentId]?.childrenIds ?? [] | ||||||
| 										.filter((message) => message.parentId === null) | 									: Object.values(history.messages) | ||||||
| 										.map((message) => message.id) ?? []} | 											.filter((message) => message.parentId === null) | ||||||
| 							{confirmEditMessage} | 											.map((message) => message.id) ?? []} | ||||||
| 							{showPreviousMessage} | 								{confirmEditMessage} | ||||||
| 							{showNextMessage} | 								{showPreviousMessage} | ||||||
| 							{copyToClipboard} | 								{showNextMessage} | ||||||
| 						/> | 								{copyToClipboard} | ||||||
|  | 							/> | ||||||
| 
 | 
 | ||||||
| 						{#if messages.length - 1 === messageIdx && processing !== ''} | 							{#if messages.length - 1 === messageIdx && processing !== ''} | ||||||
| 							<div class="flex my-2.5 ml-12 items-center w-fit space-x-2.5"> | 								<div class="flex my-2.5 ml-12 items-center w-fit space-x-2.5"> | ||||||
| 								<div class=" dark:text-blue-100"> | 									<div class=" dark:text-blue-100"> | ||||||
| 									<svg | 										<svg | ||||||
| 										class=" w-4 h-4 translate-y-[0.5px]" | 											class=" w-4 h-4 translate-y-[0.5px]" | ||||||
| 										fill="currentColor" | 											fill="currentColor" | ||||||
| 										viewBox="0 0 24 24" | 											viewBox="0 0 24 24" | ||||||
| 										xmlns="http://www.w3.org/2000/svg" | 											xmlns="http://www.w3.org/2000/svg" | ||||||
| 										><style> | 											><style> | ||||||
| 											.spinner_qM83 { | 												.spinner_qM83 { | ||||||
| 												animation: spinner_8HQG 1.05s infinite; | 													animation: spinner_8HQG 1.05s infinite; | ||||||
| 											} |  | ||||||
| 											.spinner_oXPr { |  | ||||||
| 												animation-delay: 0.1s; |  | ||||||
| 											} |  | ||||||
| 											.spinner_ZTLf { |  | ||||||
| 												animation-delay: 0.2s; |  | ||||||
| 											} |  | ||||||
| 											@keyframes spinner_8HQG { |  | ||||||
| 												0%, |  | ||||||
| 												57.14% { |  | ||||||
| 													animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1); |  | ||||||
| 													transform: translate(0); |  | ||||||
| 												} | 												} | ||||||
| 												28.57% { | 												.spinner_oXPr { | ||||||
| 													animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33); | 													animation-delay: 0.1s; | ||||||
| 													transform: translateY(-6px); |  | ||||||
| 												} | 												} | ||||||
| 												100% { | 												.spinner_ZTLf { | ||||||
| 													transform: translate(0); | 													animation-delay: 0.2s; | ||||||
| 												} | 												} | ||||||
| 											} | 												@keyframes spinner_8HQG { | ||||||
| 										</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle | 													0%, | ||||||
| 											class="spinner_qM83 spinner_oXPr" | 													57.14% { | ||||||
| 											cx="12" | 														animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1); | ||||||
| 											cy="12" | 														transform: translate(0); | ||||||
| 											r="2.5" | 													} | ||||||
| 										/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg | 													28.57% { | ||||||
| 									> | 														animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33); | ||||||
|  | 														transform: translateY(-6px); | ||||||
|  | 													} | ||||||
|  | 													100% { | ||||||
|  | 														transform: translate(0); | ||||||
|  | 													} | ||||||
|  | 												} | ||||||
|  | 											</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle | ||||||
|  | 												class="spinner_qM83 spinner_oXPr" | ||||||
|  | 												cx="12" | ||||||
|  | 												cy="12" | ||||||
|  | 												r="2.5" | ||||||
|  | 											/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg | ||||||
|  | 										> | ||||||
|  | 									</div> | ||||||
|  | 									<div class=" text-sm font-medium"> | ||||||
|  | 										{processing} | ||||||
|  | 									</div> | ||||||
| 								</div> | 								</div> | ||||||
| 								<div class=" text-sm font-medium"> | 							{/if} | ||||||
| 									{processing} | 						{:else} | ||||||
| 								</div> | 							<ResponseMessage | ||||||
| 							</div> | 								{message} | ||||||
|  | 								modelfiles={selectedModelfiles} | ||||||
|  | 								siblings={history.messages[message.parentId]?.childrenIds ?? []} | ||||||
|  | 								isLastMessage={messageIdx + 1 === messages.length} | ||||||
|  | 								{confirmEditResponseMessage} | ||||||
|  | 								{showPreviousMessage} | ||||||
|  | 								{showNextMessage} | ||||||
|  | 								{rateMessage} | ||||||
|  | 								{copyToClipboard} | ||||||
|  | 								{continueGeneration} | ||||||
|  | 								{regenerateResponse} | ||||||
|  | 							/> | ||||||
| 						{/if} | 						{/if} | ||||||
| 					{:else} | 					</div> | ||||||
| 						<ResponseMessage |  | ||||||
| 							{message} |  | ||||||
| 							modelfiles={selectedModelfiles} |  | ||||||
| 							siblings={history.messages[message.parentId]?.childrenIds ?? []} |  | ||||||
| 							isLastMessage={messageIdx + 1 === messages.length} |  | ||||||
| 							{confirmEditResponseMessage} |  | ||||||
| 							{showPreviousMessage} |  | ||||||
| 							{showNextMessage} |  | ||||||
| 							{rateMessage} |  | ||||||
| 							{copyToClipboard} |  | ||||||
| 							{continueGeneration} |  | ||||||
| 							{regenerateResponse} |  | ||||||
| 						/> |  | ||||||
| 					{/if} |  | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			{/each} | ||||||
| 		{/each} |  | ||||||
| 
 | 
 | ||||||
| 		{#if bottomPadding} | 			{#if bottomPadding} | ||||||
| 			<div class=" mb-10" /> | 				<div class=" mb-10" /> | ||||||
| 		{/if} | 			{/if} | ||||||
| 	{/key} | 		{/key} | ||||||
|  | 	</div> | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
|  | @ -16,7 +16,7 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if models.length > 0} | {#if models.length > 0} | ||||||
| 	<div class="m-auto text-center max-w-md pb-56 px-2"> | 	<div class="m-auto text-center max-w-md px-2"> | ||||||
| 		<div class="flex justify-center mt-8"> | 		<div class="flex justify-center mt-8"> | ||||||
| 			<div class="flex -space-x-4 mb-1"> | 			<div class="flex -space-x-4 mb-1"> | ||||||
| 				{#each models as model, modelIdx} | 				{#each models as model, modelIdx} | ||||||
|  |  | ||||||
|  | @ -270,9 +270,7 @@ | ||||||
| 				{#if message.model in modelfiles} | 				{#if message.model in modelfiles} | ||||||
| 					{modelfiles[message.model]?.title} | 					{modelfiles[message.model]?.title} | ||||||
| 				{:else} | 				{:else} | ||||||
| 					Ollama <span class=" text-gray-500 text-sm font-medium" | 					{message.model ? ` ${message.model}` : ''} | ||||||
| 						>{message.model ? ` ${message.model}` : ''}</span |  | ||||||
| 					> |  | ||||||
| 				{/if} | 				{/if} | ||||||
| 
 | 
 | ||||||
| 				{#if message.timestamp} | 				{#if message.timestamp} | ||||||
|  | @ -365,7 +363,7 @@ | ||||||
| 								{#if message.done} | 								{#if message.done} | ||||||
| 									<div class=" flex justify-start space-x-1 -mt-2 overflow-x-auto buttons"> | 									<div class=" flex justify-start space-x-1 -mt-2 overflow-x-auto buttons"> | ||||||
| 										{#if siblings.length > 1} | 										{#if siblings.length > 1} | ||||||
| 											<div class="flex self-center"> | 											<div class="flex self-center min-w-fit"> | ||||||
| 												<button | 												<button | ||||||
| 													class="self-center" | 													class="self-center" | ||||||
| 													on:click={() => { | 													on:click={() => { | ||||||
|  | @ -386,7 +384,7 @@ | ||||||
| 													</svg> | 													</svg> | ||||||
| 												</button> | 												</button> | ||||||
| 
 | 
 | ||||||
| 												<div class="text-xs font-bold self-center"> | 												<div class="text-xs font-bold self-center min-w-fit"> | ||||||
| 													{siblings.indexOf(message.id) + 1} / {siblings.length} | 													{siblings.indexOf(message.id) + 1} / {siblings.length} | ||||||
| 												</div> | 												</div> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
| 	import toast from 'svelte-french-toast'; | 	import toast from 'svelte-french-toast'; | ||||||
| 
 | 
 | ||||||
| 	import { createModel, deleteModel, pullModel } from '$lib/apis/ollama'; | 	import { createModel, deleteModel, pullModel } from '$lib/apis/ollama'; | ||||||
| 	import { WEBUI_API_BASE_URL } from '$lib/constants'; | 	import { WEBUI_API_BASE_URL, WEBUI_NAME } from '$lib/constants'; | ||||||
| 	import { models, user } from '$lib/stores'; | 	import { models, user } from '$lib/stores'; | ||||||
| 	import { splitStream } from '$lib/utils'; | 	import { splitStream } from '$lib/utils'; | ||||||
| 
 | 
 | ||||||
|  | @ -59,7 +59,7 @@ | ||||||
| 				} else { | 				} else { | ||||||
| 					toast.success(`Model '${modelName}' has been successfully downloaded.`); | 					toast.success(`Model '${modelName}' has been successfully downloaded.`); | ||||||
| 
 | 
 | ||||||
| 					const notification = new Notification(`Ollama`, { | 					const notification = new Notification(WEBUI_NAME, { | ||||||
| 						body: `Model '${modelName}' has been successfully downloaded.`, | 						body: `Model '${modelName}' has been successfully downloaded.`, | ||||||
| 						icon: '/favicon.png' | 						icon: '/favicon.png' | ||||||
| 					}); | 					}); | ||||||
|  |  | ||||||
|  | @ -69,7 +69,7 @@ | ||||||
| <ShareChatModal bind:show={showShareChatModal} {downloadChat} {shareChat} /> | <ShareChatModal bind:show={showShareChatModal} {downloadChat} {shareChat} /> | ||||||
| <nav | <nav | ||||||
| 	id="nav" | 	id="nav" | ||||||
| 	class=" fixed py-2.5 top-0 flex flex-row justify-center bg-white/95 dark:bg-gray-900/90 dark:text-gray-200 backdrop-blur-xl w-screen z-30" | 	class=" sticky py-2.5 top-0 flex flex-row justify-center bg-white/95 dark:bg-gray-900/90 dark:text-gray-200 backdrop-blur-xl z-30" | ||||||
| > | > | ||||||
| 	<div | 	<div | ||||||
| 		class=" flex {$settings?.fullScreenMode ?? null | 		class=" flex {$settings?.fullScreenMode ?? null | ||||||
|  |  | ||||||
|  | @ -89,10 +89,14 @@ | ||||||
| 	bind:this={navElement} | 	bind:this={navElement} | ||||||
| 	class="h-screen {show | 	class="h-screen {show | ||||||
| 		? '' | 		? '' | ||||||
| 		: '-translate-x-[260px]'}  w-[260px] fixed top-0 left-0 z-40 transition bg-black text-gray-200 shadow-2xl text-sm | 		: '-translate-x-[260px] w-[0px]'}  w-[260px] min-w[260px] bg-black text-gray-200 shadow-2xl text-sm transition z-40 fixed top-0 left-0 lg:relative | ||||||
|         " |         " | ||||||
| > | > | ||||||
| 	<div class="py-2.5 my-auto flex flex-col justify-between h-screen"> | 	<div | ||||||
|  | 		class="py-2.5 my-auto flex flex-col justify-between h-screen w-[260px] {show | ||||||
|  | 			? '' | ||||||
|  | 			: 'invisible'}" | ||||||
|  | 	> | ||||||
| 		<div class="px-2.5 flex justify-center space-x-2"> | 		<div class="px-2.5 flex justify-center space-x-2"> | ||||||
| 			<button | 			<button | ||||||
| 				id="sidebar-new-chat-button" | 				id="sidebar-new-chat-button" | ||||||
|  |  | ||||||
|  | @ -137,6 +137,11 @@ | ||||||
| 		}); | 		}); | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  | 	const scrollToBottom = () => { | ||||||
|  | 		const element = document.getElementById('messages-container'); | ||||||
|  | 		element.scrollTop = element.scrollHeight; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
| 	////////////////////////// | 	////////////////////////// | ||||||
| 	// Ollama functions | 	// Ollama functions | ||||||
| 	////////////////////////// | 	////////////////////////// | ||||||
|  | @ -316,7 +321,7 @@ | ||||||
| 		await tick(); | 		await tick(); | ||||||
| 
 | 
 | ||||||
| 		// Scroll down | 		// Scroll down | ||||||
| 		window.scrollTo({ top: document.body.scrollHeight }); | 		scrollToBottom(); | ||||||
| 
 | 
 | ||||||
| 		const messagesBody = [ | 		const messagesBody = [ | ||||||
| 			$settings.system | 			$settings.system | ||||||
|  | @ -440,7 +445,7 @@ | ||||||
| 														selectedModelfile.title.charAt(0).toUpperCase() + | 														selectedModelfile.title.charAt(0).toUpperCase() + | ||||||
| 														selectedModelfile.title.slice(1) | 														selectedModelfile.title.slice(1) | ||||||
| 												  }` | 												  }` | ||||||
| 												: `Ollama - ${model}`, | 												: `${model}`, | ||||||
| 											{ | 											{ | ||||||
| 												body: responseMessage.content, | 												body: responseMessage.content, | ||||||
| 												icon: selectedModelfile?.imageUrl ?? '/favicon.png' | 												icon: selectedModelfile?.imageUrl ?? '/favicon.png' | ||||||
|  | @ -469,7 +474,7 @@ | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				if (autoScroll) { | 				if (autoScroll) { | ||||||
| 					window.scrollTo({ top: document.body.scrollHeight }); | 					scrollToBottom(); | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  | @ -508,7 +513,7 @@ | ||||||
| 		await tick(); | 		await tick(); | ||||||
| 
 | 
 | ||||||
| 		if (autoScroll) { | 		if (autoScroll) { | ||||||
| 			window.scrollTo({ top: document.body.scrollHeight }); | 			scrollToBottom(); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if (messages.length == 2 && messages.at(1).content !== '') { | 		if (messages.length == 2 && messages.at(1).content !== '') { | ||||||
|  | @ -519,8 +524,7 @@ | ||||||
| 
 | 
 | ||||||
| 	const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => { | 	const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => { | ||||||
| 		const responseMessage = history.messages[responseMessageId]; | 		const responseMessage = history.messages[responseMessageId]; | ||||||
| 
 | 		scrollToBottom(); | ||||||
| 		window.scrollTo({ top: document.body.scrollHeight }); |  | ||||||
| 
 | 
 | ||||||
| 		const res = await generateOpenAIChatCompletion(localStorage.token, { | 		const res = await generateOpenAIChatCompletion(localStorage.token, { | ||||||
| 			model: model, | 			model: model, | ||||||
|  | @ -628,7 +632,7 @@ | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				if (autoScroll) { | 				if (autoScroll) { | ||||||
| 					window.scrollTo({ top: document.body.scrollHeight }); | 					scrollToBottom(); | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  | @ -672,7 +676,7 @@ | ||||||
| 		await tick(); | 		await tick(); | ||||||
| 
 | 
 | ||||||
| 		if (autoScroll) { | 		if (autoScroll) { | ||||||
| 			window.scrollTo({ top: document.body.scrollHeight }); | 			scrollToBottom(); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if (messages.length == 2) { | 		if (messages.length == 2) { | ||||||
|  | @ -783,47 +787,52 @@ | ||||||
| 	}; | 	}; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <svelte:window | <div class="min-h-screen max-h-screen w-full flex flex-col"> | ||||||
| 	on:scroll={(e) => { | 	<Navbar {title} shareEnabled={messages.length > 0} {initNewChat} {tags} {addTag} {deleteTag} /> | ||||||
| 		autoScroll = window.innerHeight + window.scrollY >= document.body.offsetHeight - 40; | 	<div class="flex flex-col flex-auto"> | ||||||
| 	}} |  | ||||||
| /> |  | ||||||
| 
 |  | ||||||
| <Navbar {title} shareEnabled={messages.length > 0} {initNewChat} {tags} {addTag} {deleteTag} /> |  | ||||||
| <div class="min-h-screen w-full flex justify-center"> |  | ||||||
| 	<div class=" py-2.5 flex flex-col justify-between w-full"> |  | ||||||
| 		<div | 		<div | ||||||
| 			class="{$settings?.fullScreenMode ?? null | 			class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0" | ||||||
| 				? 'max-w-full' | 			id="messages-container" | ||||||
| 				: 'max-w-2xl md:px-0'} mx-auto w-full px-4 mt-10" | 			on:scroll={(e) => { | ||||||
|  | 				autoScroll = e.target.scrollHeight - e.target.scrollTop <= e.target.clientHeight + 50; | ||||||
|  | 			}} | ||||||
| 		> | 		> | ||||||
| 			<ModelSelector bind:selectedModels disabled={messages.length > 0} /> | 			<div | ||||||
|  | 				class="{$settings?.fullScreenMode ?? null | ||||||
|  | 					? 'max-w-full' | ||||||
|  | 					: 'max-w-2xl md:px-0'} mx-auto w-full px-4" | ||||||
|  | 			> | ||||||
|  | 				<ModelSelector | ||||||
|  | 					bind:selectedModels | ||||||
|  | 					disabled={messages.length > 0 && !selectedModels.includes('')} | ||||||
|  | 				/> | ||||||
|  | 			</div> | ||||||
|  | 
 | ||||||
|  | 			<div class=" h-full w-full flex flex-col py-8"> | ||||||
|  | 				<Messages | ||||||
|  | 					chatId={$chatId} | ||||||
|  | 					{selectedModels} | ||||||
|  | 					{selectedModelfiles} | ||||||
|  | 					{processing} | ||||||
|  | 					bind:history | ||||||
|  | 					bind:messages | ||||||
|  | 					bind:autoScroll | ||||||
|  | 					bottomPadding={files.length > 0} | ||||||
|  | 					{sendPrompt} | ||||||
|  | 					{continueGeneration} | ||||||
|  | 					{regenerateResponse} | ||||||
|  | 				/> | ||||||
|  | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 
 | 
 | ||||||
| 		<div class=" h-full mt-10 mb-32 w-full flex flex-col"> | 		<MessageInput | ||||||
| 			<Messages | 			bind:files | ||||||
| 				chatId={$chatId} | 			bind:prompt | ||||||
| 				{selectedModels} | 			bind:autoScroll | ||||||
| 				{selectedModelfiles} | 			suggestionPrompts={selectedModelfile?.suggestionPrompts ?? $config.default_prompt_suggestions} | ||||||
| 				{processing} | 			{messages} | ||||||
| 				bind:history | 			{submitPrompt} | ||||||
| 				bind:messages | 			{stopResponse} | ||||||
| 				bind:autoScroll | 		/> | ||||||
| 				bottomPadding={files.length > 0} |  | ||||||
| 				{sendPrompt} |  | ||||||
| 				{continueGeneration} |  | ||||||
| 				{regenerateResponse} |  | ||||||
| 			/> |  | ||||||
| 		</div> |  | ||||||
| 	</div> | 	</div> | ||||||
| 
 |  | ||||||
| 	<MessageInput |  | ||||||
| 		bind:files |  | ||||||
| 		bind:prompt |  | ||||||
| 		bind:autoScroll |  | ||||||
| 		suggestionPrompts={selectedModelfile?.suggestionPrompts ?? $config.default_prompt_suggestions} |  | ||||||
| 		{messages} |  | ||||||
| 		{submitPrompt} |  | ||||||
| 		{stopResponse} |  | ||||||
| 	/> |  | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|  | @ -153,6 +153,11 @@ | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  | 	const scrollToBottom = () => { | ||||||
|  | 		const element = document.getElementById('messages-container'); | ||||||
|  | 		element.scrollTop = element.scrollHeight; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
| 	////////////////////////// | 	////////////////////////// | ||||||
| 	// Ollama functions | 	// Ollama functions | ||||||
| 	////////////////////////// | 	////////////////////////// | ||||||
|  | @ -330,7 +335,7 @@ | ||||||
| 		await tick(); | 		await tick(); | ||||||
| 
 | 
 | ||||||
| 		// Scroll down | 		// Scroll down | ||||||
| 		window.scrollTo({ top: document.body.scrollHeight }); | 		scrollToBottom(); | ||||||
| 
 | 
 | ||||||
| 		const messagesBody = [ | 		const messagesBody = [ | ||||||
| 			$settings.system | 			$settings.system | ||||||
|  | @ -454,7 +459,7 @@ | ||||||
| 														selectedModelfile.title.charAt(0).toUpperCase() + | 														selectedModelfile.title.charAt(0).toUpperCase() + | ||||||
| 														selectedModelfile.title.slice(1) | 														selectedModelfile.title.slice(1) | ||||||
| 												  }` | 												  }` | ||||||
| 												: `Ollama - ${model}`, | 												: `${model}`, | ||||||
| 											{ | 											{ | ||||||
| 												body: responseMessage.content, | 												body: responseMessage.content, | ||||||
| 												icon: selectedModelfile?.imageUrl ?? '/favicon.png' | 												icon: selectedModelfile?.imageUrl ?? '/favicon.png' | ||||||
|  | @ -483,7 +488,7 @@ | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				if (autoScroll) { | 				if (autoScroll) { | ||||||
| 					window.scrollTo({ top: document.body.scrollHeight }); | 					scrollToBottom(); | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  | @ -522,7 +527,7 @@ | ||||||
| 		await tick(); | 		await tick(); | ||||||
| 
 | 
 | ||||||
| 		if (autoScroll) { | 		if (autoScroll) { | ||||||
| 			window.scrollTo({ top: document.body.scrollHeight }); | 			scrollToBottom(); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if (messages.length == 2 && messages.at(1).content !== '') { | 		if (messages.length == 2 && messages.at(1).content !== '') { | ||||||
|  | @ -534,7 +539,7 @@ | ||||||
| 	const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => { | 	const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => { | ||||||
| 		const responseMessage = history.messages[responseMessageId]; | 		const responseMessage = history.messages[responseMessageId]; | ||||||
| 
 | 
 | ||||||
| 		window.scrollTo({ top: document.body.scrollHeight }); | 		scrollToBottom(); | ||||||
| 
 | 
 | ||||||
| 		const res = await generateOpenAIChatCompletion(localStorage.token, { | 		const res = await generateOpenAIChatCompletion(localStorage.token, { | ||||||
| 			model: model, | 			model: model, | ||||||
|  | @ -642,7 +647,7 @@ | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				if (autoScroll) { | 				if (autoScroll) { | ||||||
| 					window.scrollTo({ top: document.body.scrollHeight }); | 					scrollToBottom(); | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  | @ -686,7 +691,7 @@ | ||||||
| 		await tick(); | 		await tick(); | ||||||
| 
 | 
 | ||||||
| 		if (autoScroll) { | 		if (autoScroll) { | ||||||
| 			window.scrollTo({ top: document.body.scrollHeight }); | 			scrollToBottom(); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if (messages.length == 2) { | 		if (messages.length == 2) { | ||||||
|  | @ -797,66 +802,69 @@ | ||||||
| 	}); | 	}); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <svelte:window |  | ||||||
| 	on:scroll={(e) => { |  | ||||||
| 		autoScroll = window.innerHeight + window.scrollY >= document.body.offsetHeight - 40; |  | ||||||
| 	}} |  | ||||||
| /> |  | ||||||
| 
 |  | ||||||
| {#if loaded} | {#if loaded} | ||||||
| 	<Navbar | 	<div class="min-h-screen max-h-screen w-full flex flex-col"> | ||||||
| 		{title} | 		<Navbar | ||||||
| 		shareEnabled={messages.length > 0} | 			{title} | ||||||
| 		initNewChat={async () => { | 			shareEnabled={messages.length > 0} | ||||||
| 			if (currentRequestId !== null) { | 			initNewChat={async () => { | ||||||
| 				await cancelChatCompletion(localStorage.token, currentRequestId); | 				if (currentRequestId !== null) { | ||||||
| 				currentRequestId = null; | 					await cancelChatCompletion(localStorage.token, currentRequestId); | ||||||
| 			} | 					currentRequestId = null; | ||||||
|  | 				} | ||||||
| 
 | 
 | ||||||
| 			goto('/'); | 				goto('/'); | ||||||
| 		}} | 			}} | ||||||
| 		{tags} | 			{tags} | ||||||
| 		{addTag} | 			{addTag} | ||||||
| 		{deleteTag} | 			{deleteTag} | ||||||
| 	/> |  | ||||||
| 	<div class="min-h-screen w-full flex justify-center"> |  | ||||||
| 		<div class=" py-2.5 flex flex-col justify-between w-full"> |  | ||||||
| 			<div |  | ||||||
| 				class="{$settings?.fullScreenMode ?? null |  | ||||||
| 					? 'max-w-full' |  | ||||||
| 					: 'max-w-2xl md:px-0'} mx-auto w-full px-4 mt-10" |  | ||||||
| 			> |  | ||||||
| 				<ModelSelector |  | ||||||
| 					bind:selectedModels |  | ||||||
| 					disabled={messages.length > 0 && !selectedModels.includes('')} |  | ||||||
| 				/> |  | ||||||
| 			</div> |  | ||||||
| 
 |  | ||||||
| 			<div class=" h-full mt-10 mb-32 w-full flex flex-col"> |  | ||||||
| 				<Messages |  | ||||||
| 					chatId={$chatId} |  | ||||||
| 					{selectedModels} |  | ||||||
| 					{selectedModelfiles} |  | ||||||
| 					{processing} |  | ||||||
| 					bind:history |  | ||||||
| 					bind:messages |  | ||||||
| 					bind:autoScroll |  | ||||||
| 					bottomPadding={files.length > 0} |  | ||||||
| 					{sendPrompt} |  | ||||||
| 					{continueGeneration} |  | ||||||
| 					{regenerateResponse} |  | ||||||
| 				/> |  | ||||||
| 			</div> |  | ||||||
| 		</div> |  | ||||||
| 
 |  | ||||||
| 		<MessageInput |  | ||||||
| 			bind:files |  | ||||||
| 			bind:prompt |  | ||||||
| 			bind:autoScroll |  | ||||||
| 			suggestionPrompts={selectedModelfile?.suggestionPrompts ?? $config.default_prompt_suggestions} |  | ||||||
| 			{messages} |  | ||||||
| 			{submitPrompt} |  | ||||||
| 			{stopResponse} |  | ||||||
| 		/> | 		/> | ||||||
|  | 		<div class="flex flex-col flex-auto"> | ||||||
|  | 			<div | ||||||
|  | 				class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0" | ||||||
|  | 				id="messages-container" | ||||||
|  | 				on:scroll={(e) => { | ||||||
|  | 					autoScroll = e.target.scrollHeight - e.target.scrollTop <= e.target.clientHeight + 50; | ||||||
|  | 				}} | ||||||
|  | 			> | ||||||
|  | 				<div | ||||||
|  | 					class="{$settings?.fullScreenMode ?? null | ||||||
|  | 						? 'max-w-full' | ||||||
|  | 						: 'max-w-2xl md:px-0'} mx-auto w-full px-4" | ||||||
|  | 				> | ||||||
|  | 					<ModelSelector | ||||||
|  | 						bind:selectedModels | ||||||
|  | 						disabled={messages.length > 0 && !selectedModels.includes('')} | ||||||
|  | 					/> | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div class=" h-full w-full flex flex-col py-8"> | ||||||
|  | 					<Messages | ||||||
|  | 						chatId={$chatId} | ||||||
|  | 						{selectedModels} | ||||||
|  | 						{selectedModelfiles} | ||||||
|  | 						{processing} | ||||||
|  | 						bind:history | ||||||
|  | 						bind:messages | ||||||
|  | 						bind:autoScroll | ||||||
|  | 						bottomPadding={files.length > 0} | ||||||
|  | 						{sendPrompt} | ||||||
|  | 						{continueGeneration} | ||||||
|  | 						{regenerateResponse} | ||||||
|  | 					/> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 
 | ||||||
|  | 			<MessageInput | ||||||
|  | 				bind:files | ||||||
|  | 				bind:prompt | ||||||
|  | 				bind:autoScroll | ||||||
|  | 				suggestionPrompts={selectedModelfile?.suggestionPrompts ?? | ||||||
|  | 					$config.default_prompt_suggestions} | ||||||
|  | 				{messages} | ||||||
|  | 				{submitPrompt} | ||||||
|  | 				{stopResponse} | ||||||
|  | 			/> | ||||||
|  | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| <script> | <script> | ||||||
| 	import { goto } from '$app/navigation'; | 	import { goto } from '$app/navigation'; | ||||||
|  | 	import { WEBUI_NAME } from '$lib/constants'; | ||||||
| 	import { config } from '$lib/stores'; | 	import { config } from '$lib/stores'; | ||||||
| 	import { onMount } from 'svelte'; | 	import { onMount } from 'svelte'; | ||||||
| 
 | 
 | ||||||
|  | @ -19,7 +20,7 @@ | ||||||
| 		<div class="absolute rounded-xl w-full h-full backdrop-blur flex justify-center"> | 		<div class="absolute rounded-xl w-full h-full backdrop-blur flex justify-center"> | ||||||
| 			<div class="m-auto pb-44 flex flex-col justify-center"> | 			<div class="m-auto pb-44 flex flex-col justify-center"> | ||||||
| 				<div class="max-w-md"> | 				<div class="max-w-md"> | ||||||
| 					<div class="text-center text-2xl font-medium z-50">Ollama WebUI Backend Required</div> | 					<div class="text-center text-2xl font-medium z-50">{WEBUI_NAME} Backend Required</div> | ||||||
| 
 | 
 | ||||||
| 					<div class=" mt-4 text-center text-sm w-full"> | 					<div class=" mt-4 text-center text-sm w-full"> | ||||||
| 						Oops! You're using an unsupported method (frontend only). Please serve the WebUI from | 						Oops! You're using an unsupported method (frontend only). Please serve the WebUI from | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy Jaeryang Baek
						Timothy Jaeryang Baek