forked from open-webui/open-webui
		
	Merge pull request #144 from ollama-webui/dev
feat: multimodal support prep
This commit is contained in:
		
						commit
						055bc7a8c3
					
				
					 4 changed files with 117 additions and 38 deletions
				
			
		|  | @ -1,5 +1,6 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 	import { settings } from '$lib/stores'; | 	import { settings } from '$lib/stores'; | ||||||
|  | 	import toast from 'svelte-french-toast'; | ||||||
| 	import Suggestions from './MessageInput/Suggestions.svelte'; | 	import Suggestions from './MessageInput/Suggestions.svelte'; | ||||||
| 
 | 
 | ||||||
| 	export let submitPrompt: Function; | 	export let submitPrompt: Function; | ||||||
|  | @ -8,6 +9,11 @@ | ||||||
| 	export let suggestions = 'true'; | 	export let suggestions = 'true'; | ||||||
| 	export let autoScroll = true; | 	export let autoScroll = true; | ||||||
| 
 | 
 | ||||||
|  | 	let filesInputElement; | ||||||
|  | 	let inputFiles; | ||||||
|  | 
 | ||||||
|  | 	export let files = []; | ||||||
|  | 
 | ||||||
| 	export let fileUploadEnabled = false; | 	export let fileUploadEnabled = false; | ||||||
| 	export let speechRecognitionEnabled = true; | 	export let speechRecognitionEnabled = true; | ||||||
| 	export let speechRecognitionListening = false; | 	export let speechRecognitionListening = false; | ||||||
|  | @ -111,43 +117,82 @@ | ||||||
| 			{/if} | 			{/if} | ||||||
| 
 | 
 | ||||||
| 			<div class="bg-gradient-to-t from-white dark:from-gray-800 from-40% pb-2"> | 			<div class="bg-gradient-to-t from-white dark:from-gray-800 from-40% pb-2"> | ||||||
|  | 				<input | ||||||
|  | 					bind:this={filesInputElement} | ||||||
|  | 					bind:files={inputFiles} | ||||||
|  | 					type="file" | ||||||
|  | 					hidden | ||||||
|  | 					on:change={() => { | ||||||
|  | 						let reader = new FileReader(); | ||||||
|  | 						reader.onload = (event) => { | ||||||
|  | 							files = [ | ||||||
|  | 								...files, | ||||||
|  | 								{ | ||||||
|  | 									type: 'image', | ||||||
|  | 									url: `${event.target.result}` | ||||||
|  | 								} | ||||||
|  | 							]; | ||||||
|  | 							inputFiles = null; | ||||||
|  | 						}; | ||||||
|  | 
 | ||||||
|  | 						if ( | ||||||
|  | 							inputFiles && | ||||||
|  | 							inputFiles.length > 0 && | ||||||
|  | 							['image/gif', 'image/jpeg', 'image/png'].includes(inputFiles[0]['type']) | ||||||
|  | 						) { | ||||||
|  | 							reader.readAsDataURL(inputFiles[0]); | ||||||
|  | 						} else { | ||||||
|  | 							toast.error(`Unsupported File Type '${inputFiles[0]['type']}'.`); | ||||||
|  | 							inputFiles = null; | ||||||
|  | 						} | ||||||
|  | 					}} | ||||||
|  | 				/> | ||||||
| 				<form | 				<form | ||||||
| 					class=" flex relative w-full" | 					class=" flex flex-col relative w-full rounded-xl border dark:border-gray-600" | ||||||
| 					on:submit|preventDefault={() => { | 					on:submit|preventDefault={() => { | ||||||
| 						submitPrompt(prompt); | 						submitPrompt(prompt); | ||||||
| 					}} | 					}} | ||||||
| 				> | 				> | ||||||
| 					<textarea | 					{#if files.length > 0} | ||||||
| 						id="chat-textarea" | 						<div class="ml-2 mt-2 mb-1 flex space-x-2"> | ||||||
| 						class="rounded-xl dark:bg-gray-800 dark:text-gray-100 outline-none border dark:border-gray-600 w-full py-3 | 							{#each files as file, fileIdx} | ||||||
|                         {fileUploadEnabled ? 'pl-12' : 'pl-5'} {speechRecognitionEnabled | 								<div class=" relative group"> | ||||||
| 							? 'pr-20' | 									<img src={file.url} alt="input" class=" h-16 w-16 rounded-xl bg-cover" /> | ||||||
| 							: 'pr-12'} resize-none" |  | ||||||
| 						placeholder={speechRecognitionListening ? 'Listening...' : 'Send a message'} |  | ||||||
| 						bind:value={prompt} |  | ||||||
| 						on:keypress={(e) => { |  | ||||||
| 							if (e.keyCode == 13 && !e.shiftKey) { |  | ||||||
| 								e.preventDefault(); |  | ||||||
| 							} |  | ||||||
| 							if (prompt !== '' && e.keyCode == 13 && !e.shiftKey) { |  | ||||||
| 								submitPrompt(prompt); |  | ||||||
| 							} |  | ||||||
| 						}} |  | ||||||
| 						rows="1" |  | ||||||
| 						on:input={(e) => { |  | ||||||
| 							e.target.style.height = ''; |  | ||||||
| 							e.target.style.height = Math.min(e.target.scrollHeight, 200) + 2 + 'px'; |  | ||||||
| 						}} |  | ||||||
| 					/> |  | ||||||
| 
 | 
 | ||||||
| 					{#if fileUploadEnabled} | 									<div class=" absolute -top-1 -right-1"> | ||||||
| 						<div class=" absolute left-0 bottom-0"> | 										<button | ||||||
| 							<div class="pl-2.5 pb-[9px]"> | 											class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition" | ||||||
|  | 											type="button" | ||||||
|  | 											on:click={() => { | ||||||
|  | 												files.splice(fileIdx, 1); | ||||||
|  | 												files = files; | ||||||
|  | 											}} | ||||||
|  | 										> | ||||||
|  | 											<svg | ||||||
|  | 												xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 												viewBox="0 0 20 20" | ||||||
|  | 												fill="currentColor" | ||||||
|  | 												class="w-4 h-4" | ||||||
|  | 											> | ||||||
|  | 												<path | ||||||
|  | 													d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" | ||||||
|  | 												/> | ||||||
|  | 											</svg> | ||||||
|  | 										</button> | ||||||
|  | 									</div> | ||||||
|  | 								</div> | ||||||
|  | 							{/each} | ||||||
|  | 						</div> | ||||||
|  | 					{/if} | ||||||
|  | 
 | ||||||
|  | 					<div class=" flex"> | ||||||
|  | 						{#if fileUploadEnabled} | ||||||
|  | 							<div class=" self-end mb-2 ml-1.5"> | ||||||
| 								<button | 								<button | ||||||
| 									class="  text-gray-600 dark:text-gray-200 transition rounded-lg p-1.5" | 									class="  text-gray-600 dark:text-gray-200 transition rounded-lg p-1 ml-1" | ||||||
| 									type="button" | 									type="button" | ||||||
| 									on:click={() => { | 									on:click={() => { | ||||||
| 										console.log('file'); | 										filesInputElement.click(); | ||||||
| 									}} | 									}} | ||||||
| 								> | 								> | ||||||
| 									<svg | 									<svg | ||||||
|  | @ -164,15 +209,35 @@ | ||||||
| 									</svg> | 									</svg> | ||||||
| 								</button> | 								</button> | ||||||
| 							</div> | 							</div> | ||||||
| 						</div> | 						{/if} | ||||||
| 					{/if} |  | ||||||
| 
 | 
 | ||||||
| 					<div class=" absolute right-0 bottom-0"> | 						<textarea | ||||||
| 						<div class="pr-2.5 pb-[9px]"> | 							id="chat-textarea" | ||||||
|  | 							class=" dark:bg-gray-800 dark:text-gray-100 outline-none w-full py-3 px-2 {fileUploadEnabled | ||||||
|  | 								? '' | ||||||
|  | 								: ' pl-4'} rounded-xl resize-none" | ||||||
|  | 							placeholder={speechRecognitionListening ? 'Listening...' : 'Send a message'} | ||||||
|  | 							bind:value={prompt} | ||||||
|  | 							on:keypress={(e) => { | ||||||
|  | 								if (e.keyCode == 13 && !e.shiftKey) { | ||||||
|  | 									e.preventDefault(); | ||||||
|  | 								} | ||||||
|  | 								if (prompt !== '' && e.keyCode == 13 && !e.shiftKey) { | ||||||
|  | 									submitPrompt(prompt); | ||||||
|  | 								} | ||||||
|  | 							}} | ||||||
|  | 							rows="1" | ||||||
|  | 							on:input={(e) => { | ||||||
|  | 								e.target.style.height = ''; | ||||||
|  | 								e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px'; | ||||||
|  | 							}} | ||||||
|  | 						/> | ||||||
|  | 
 | ||||||
|  | 						<div class="self-end mb-2 flex space-x-0.5 mr-2"> | ||||||
| 							{#if messages.length == 0 || messages.at(-1).done == true} | 							{#if messages.length == 0 || messages.at(-1).done == true} | ||||||
| 								{#if speechRecognitionEnabled} | 								{#if speechRecognitionEnabled} | ||||||
| 									<button | 									<button | ||||||
| 										class=" text-gray-600 dark:text-gray-300 transition rounded-lg p-1 mr-0.5" | 										class=" text-gray-600 dark:text-gray-300 transition rounded-lg p-1.5 mr-0.5 self-center" | ||||||
| 										type="button" | 										type="button" | ||||||
| 										on:click={() => { | 										on:click={() => { | ||||||
| 											speechRecognitionHandler(); | 											speechRecognitionHandler(); | ||||||
|  | @ -233,7 +298,7 @@ | ||||||
| 								<button | 								<button | ||||||
| 									class="{prompt !== '' | 									class="{prompt !== '' | ||||||
| 										? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 ' | 										? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 ' | ||||||
| 										: 'text-white bg-gray-100 dark:text-gray-800 dark:bg-gray-600 disabled'} transition rounded-lg p-1" | 										: 'text-white bg-gray-100 dark:text-gray-800 dark:bg-gray-600 disabled'} transition rounded-lg p-1 mr-0.5 w-7 h-7 self-center" | ||||||
| 									type="submit" | 									type="submit" | ||||||
| 									disabled={prompt === ''} | 									disabled={prompt === ''} | ||||||
| 								> | 								> | ||||||
|  |  | ||||||
|  | @ -409,6 +409,17 @@ | ||||||
| 										</div> | 										</div> | ||||||
| 									{:else} | 									{:else} | ||||||
| 										<div class="w-full"> | 										<div class="w-full"> | ||||||
|  | 											{#if message.files} | ||||||
|  | 												<div class="my-3"> | ||||||
|  | 													{#each message.files as file} | ||||||
|  | 														<div> | ||||||
|  | 															{#if file.type === 'image'} | ||||||
|  | 																<img src={file.url} alt="input" class=" max-h-96" /> | ||||||
|  | 															{/if} | ||||||
|  | 														</div> | ||||||
|  | 													{/each} | ||||||
|  | 												</div> | ||||||
|  | 											{/if} | ||||||
| 											{message.content} | 											{message.content} | ||||||
| 
 | 
 | ||||||
| 											<div class=" flex justify-start space-x-1"> | 											<div class=" flex justify-start space-x-1"> | ||||||
|  |  | ||||||
|  | @ -119,7 +119,7 @@ | ||||||
| 			</button> | 			</button> | ||||||
| 		</div> | 		</div> | ||||||
| 
 | 
 | ||||||
| 		<div class="px-2.5 flex justify-center my-1"> | 		<!-- <div class="px-2.5 flex justify-center my-1"> | ||||||
| 			<button | 			<button | ||||||
| 				class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition" | 				class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition" | ||||||
| 				on:click={async () => { | 				on:click={async () => { | ||||||
|  | @ -147,7 +147,7 @@ | ||||||
| 					<div class=" self-center font-medium text-sm">Presets</div> | 					<div class=" self-center font-medium text-sm">Presets</div> | ||||||
| 				</div> | 				</div> | ||||||
| 			</button> | 			</button> | ||||||
| 		</div> | 		</div> --> | ||||||
| 
 | 
 | ||||||
| 		<div class="px-2.5 mt-1 mb-2 flex justify-center space-x-2"> | 		<div class="px-2.5 mt-1 mb-2 flex justify-center space-x-2"> | ||||||
| 			<div class="flex w-full"> | 			<div class="flex w-full"> | ||||||
|  |  | ||||||
|  | @ -21,6 +21,7 @@ | ||||||
| 
 | 
 | ||||||
| 	let title = ''; | 	let title = ''; | ||||||
| 	let prompt = ''; | 	let prompt = ''; | ||||||
|  | 	let files = []; | ||||||
| 
 | 
 | ||||||
| 	let messages = []; | 	let messages = []; | ||||||
| 	let history = { | 	let history = { | ||||||
|  | @ -358,7 +359,8 @@ | ||||||
| 				parentId: messages.length !== 0 ? messages.at(-1).id : null, | 				parentId: messages.length !== 0 ? messages.at(-1).id : null, | ||||||
| 				childrenIds: [], | 				childrenIds: [], | ||||||
| 				role: 'user', | 				role: 'user', | ||||||
| 				content: userPrompt | 				content: userPrompt, | ||||||
|  | 				files: files.length > 0 ? files : undefined | ||||||
| 			}; | 			}; | ||||||
| 
 | 
 | ||||||
| 			if (messages.length !== 0) { | 			if (messages.length !== 0) { | ||||||
|  | @ -369,6 +371,7 @@ | ||||||
| 			history.currentId = userMessageId; | 			history.currentId = userMessageId; | ||||||
| 
 | 
 | ||||||
| 			prompt = ''; | 			prompt = ''; | ||||||
|  | 			files = []; | ||||||
| 
 | 
 | ||||||
| 			if (messages.length == 0) { | 			if (messages.length == 0) { | ||||||
| 				await $db.createNewChat({ | 				await $db.createNewChat({ | ||||||
|  | @ -477,5 +480,5 @@ | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<MessageInput bind:prompt bind:autoScroll {messages} {submitPrompt} {stopResponse} /> | 	<MessageInput bind:prompt bind:files bind:autoScroll {messages} {submitPrompt} {stopResponse} /> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy Jaeryang Baek
						Timothy Jaeryang Baek