forked from open-webui/open-webui
		
	feat: prompt preset support
This commit is contained in:
		
							parent
							
								
									27aa1e964d
								
							
						
					
					
						commit
						69a037257e
					
				
					 4 changed files with 220 additions and 28 deletions
				
			
		|  | @ -3,6 +3,8 @@ | |||
| 	import toast from 'svelte-french-toast'; | ||||
| 	import Suggestions from './MessageInput/Suggestions.svelte'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 	import Prompts from './MessageInput/PromptCommands.svelte'; | ||||
| 	import { findWordIndices } from '$lib/utils'; | ||||
| 
 | ||||
| 	export let submitPrompt: Function; | ||||
| 	export let stopResponse: Function; | ||||
|  | @ -11,6 +13,8 @@ | |||
| 	export let autoScroll = true; | ||||
| 
 | ||||
| 	let filesInputElement; | ||||
| 	let promptsElement; | ||||
| 
 | ||||
| 	let inputFiles; | ||||
| 	let dragged = false; | ||||
| 
 | ||||
|  | @ -154,36 +158,42 @@ | |||
| 
 | ||||
| <div class="fixed bottom-0 w-full"> | ||||
| 	<div class="px-2.5 pt-2.5 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center"> | ||||
| 		{#if messages.length == 0 && suggestionPrompts.length !== 0} | ||||
| 			<div class="max-w-3xl w-full"> | ||||
| 				<Suggestions {suggestionPrompts} {submitPrompt} /> | ||||
| 		<div class="flex flex-col max-w-3xl w-full"> | ||||
| 			<div> | ||||
| 				{#if autoScroll === false && messages.length > 0} | ||||
| 					<div class=" flex justify-center mb-4"> | ||||
| 						<button | ||||
| 							class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full" | ||||
| 							on:click={() => { | ||||
| 								autoScroll = true; | ||||
| 								window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); | ||||
| 							}} | ||||
| 						> | ||||
| 							<svg | ||||
| 								xmlns="http://www.w3.org/2000/svg" | ||||
| 								viewBox="0 0 20 20" | ||||
| 								fill="currentColor" | ||||
| 								class="w-5 h-5" | ||||
| 							> | ||||
| 								<path | ||||
| 									fill-rule="evenodd" | ||||
| 									d="M10 3a.75.75 0 01.75.75v10.638l3.96-4.158a.75.75 0 111.08 1.04l-5.25 5.5a.75.75 0 01-1.08 0l-5.25-5.5a.75.75 0 111.08-1.04l3.96 4.158V3.75A.75.75 0 0110 3z" | ||||
| 									clip-rule="evenodd" | ||||
| 								/> | ||||
| 							</svg> | ||||
| 						</button> | ||||
| 					</div> | ||||
| 				{/if} | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 
 | ||||
| 		{#if autoScroll === false && messages.length > 0} | ||||
| 			<div class=" flex justify-center mb-4"> | ||||
| 				<button | ||||
| 					class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full" | ||||
| 					on:click={() => { | ||||
| 						autoScroll = true; | ||||
| 						window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); | ||||
| 					}} | ||||
| 				> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						viewBox="0 0 20 20" | ||||
| 						fill="currentColor" | ||||
| 						class="w-5 h-5" | ||||
| 					> | ||||
| 						<path | ||||
| 							fill-rule="evenodd" | ||||
| 							d="M10 3a.75.75 0 01.75.75v10.638l3.96-4.158a.75.75 0 111.08 1.04l-5.25 5.5a.75.75 0 01-1.08 0l-5.25-5.5a.75.75 0 111.08-1.04l3.96 4.158V3.75A.75.75 0 0110 3z" | ||||
| 							clip-rule="evenodd" | ||||
| 						/> | ||||
| 					</svg> | ||||
| 				</button> | ||||
| 			<div class="w-full"> | ||||
| 				{#if prompt.charAt(0) === '/'} | ||||
| 					<Prompts bind:this={promptsElement} bind:prompt /> | ||||
| 				{:else if messages.length == 0 && suggestionPrompts.length !== 0} | ||||
| 					<Suggestions {suggestionPrompts} {submitPrompt} /> | ||||
| 				{/if} | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="bg-white dark:bg-gray-800"> | ||||
| 		<div class="max-w-3xl px-2.5 -mb-0.5 mx-auto inset-x-0"> | ||||
|  | @ -315,6 +325,52 @@ | |||
| 									userMessageElement.scrollIntoView({ block: 'center' }); | ||||
| 									editButton?.click(); | ||||
| 								} | ||||
| 
 | ||||
| 								if (prompt.charAt(0) === '/' && e.key === 'Tab') { | ||||
| 									e.preventDefault(); | ||||
| 
 | ||||
| 									const commandOptionButton = [ | ||||
| 										...document.getElementsByClassName('selected-command-option-button') | ||||
| 									]?.at(-1); | ||||
| 
 | ||||
| 									commandOptionButton?.click(); | ||||
| 								} | ||||
| 
 | ||||
| 								if (prompt.charAt(0) === '/' && e.key === 'ArrowUp') { | ||||
| 									promptsElement.selectUp(); | ||||
| 
 | ||||
| 									const commandOptionButton = [ | ||||
| 										...document.getElementsByClassName('selected-command-option-button') | ||||
| 									]?.at(-1); | ||||
| 									commandOptionButton.scrollIntoView({ block: 'center' }); | ||||
| 								} | ||||
| 
 | ||||
| 								if (prompt.charAt(0) === '/' && e.key === 'ArrowDown') { | ||||
| 									promptsElement.selectDown(); | ||||
| 
 | ||||
| 									const commandOptionButton = [ | ||||
| 										...document.getElementsByClassName('selected-command-option-button') | ||||
| 									]?.at(-1); | ||||
| 									commandOptionButton.scrollIntoView({ block: 'center' }); | ||||
| 								} | ||||
| 
 | ||||
| 								if (prompt.charAt(0) === '/' && e.key === 'Enter') { | ||||
| 									e.preventDefault(); | ||||
| 
 | ||||
| 									const commandOptionButton = [ | ||||
| 										...document.getElementsByClassName('selected-command-option-button') | ||||
| 									]?.at(-1); | ||||
| 
 | ||||
| 									commandOptionButton?.click(); | ||||
| 								} | ||||
| 
 | ||||
| 								const words = findWordIndices(prompt); | ||||
| 
 | ||||
| 								if (words.length > 0 && e.key === 'Tab') { | ||||
| 									const word = words.at(0); | ||||
| 									e.preventDefault(); | ||||
| 									e.target.setSelectionRange(word?.startIndex, word.endIndex + 1); | ||||
| 								} | ||||
| 							}} | ||||
| 							rows="1" | ||||
| 							on:input={(e) => { | ||||
|  |  | |||
							
								
								
									
										121
									
								
								src/lib/components/chat/MessageInput/PromptCommands.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/lib/components/chat/MessageInput/PromptCommands.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,121 @@ | |||
| <script lang="ts"> | ||||
| 	import { findWordIndices } from '$lib/utils'; | ||||
| 	import { tick } from 'svelte'; | ||||
| 
 | ||||
| 	export let prompt = ''; | ||||
| 
 | ||||
| 	let selectedCommandIdx = 0; | ||||
| 
 | ||||
| 	let promptCommands = [ | ||||
| 		{ | ||||
| 			command: '/article', | ||||
| 			title: 'Article Generator', | ||||
| 			content: `Write an article about [topic] | ||||
| 
 | ||||
| include relevant statistics (add the links of the sources you use) and consider diverse perspectives. Write it in a [X_tone] and mention the source links in the end.` | ||||
| 		}, | ||||
| 		{ | ||||
| 			command: '/backlink', | ||||
| 
 | ||||
| 			title: 'Backlink Outreach Email', | ||||
| 			content: `Write a link-exchange outreach email on behalf of [your name] from [your_company] to ask for a backlink from their [website_url] to [your website url].` | ||||
| 		}, | ||||
| 		{ | ||||
| 			command: '/faq', | ||||
| 
 | ||||
| 			title: 'FAQ Generator', | ||||
| 			content: `Create a list of [10] frequently asked questions about [keyword] and provide answers for each one of them considering the SERP and rich result guidelines.` | ||||
| 		}, | ||||
| 		{ | ||||
| 			command: '/headline', | ||||
| 
 | ||||
| 			title: 'Headline Generator', | ||||
| 			content: `Generate 10 attention-grabbing headlines for an article about [your topic]` | ||||
| 		}, | ||||
| 		{ | ||||
| 			command: '/product', | ||||
| 
 | ||||
| 			title: 'Product Description', | ||||
| 			content: `Craft an irresistible product description that highlights the benefits of [your product]` | ||||
| 		}, | ||||
| 		{ | ||||
| 			command: '/seo', | ||||
| 
 | ||||
| 			title: 'SEO Content Brief', | ||||
| 			content: `Create a SEO content brief for [keyword].` | ||||
| 		} | ||||
| 	]; | ||||
| 
 | ||||
| 	let filteredPromptCommands = []; | ||||
| 
 | ||||
| 	$: filteredPromptCommands = promptCommands.filter((p) => p.command.includes(prompt)); | ||||
| 
 | ||||
| 	$: if (prompt) { | ||||
| 		selectedCommandIdx = 0; | ||||
| 	} | ||||
| 
 | ||||
| 	export const selectUp = () => { | ||||
| 		selectedCommandIdx = Math.max(0, selectedCommandIdx - 1); | ||||
| 	}; | ||||
| 
 | ||||
| 	export const selectDown = () => { | ||||
| 		selectedCommandIdx = Math.min(selectedCommandIdx + 1, filteredPromptCommands.length - 1); | ||||
| 	}; | ||||
| 
 | ||||
| 	const confirmCommand = async (command) => { | ||||
| 		prompt = command.content; | ||||
| 
 | ||||
| 		const chatInputElement = document.getElementById('chat-textarea'); | ||||
| 
 | ||||
| 		await tick(); | ||||
| 
 | ||||
| 		chatInputElement.style.height = ''; | ||||
| 		chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px'; | ||||
| 
 | ||||
| 		chatInputElement?.focus(); | ||||
| 
 | ||||
| 		await tick(); | ||||
| 
 | ||||
| 		const words = findWordIndices(prompt); | ||||
| 
 | ||||
| 		if (words.length > 0) { | ||||
| 			const word = words.at(0); | ||||
| 			chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1); | ||||
| 		} | ||||
| 	}; | ||||
| </script> | ||||
| 
 | ||||
| {#if filteredPromptCommands.length > 0} | ||||
| 	<div class="md:px-2 mb-3 text-left w-full"> | ||||
| 		<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=" text-lg font-medium mt-2">/</div> | ||||
| 			</div> | ||||
| 			<div class=" max-h-60 overflow-y-auto bg-white w-full p-2 rounded-r-lg space-y-0.5"> | ||||
| 				{#each filteredPromptCommands as command, commandIdx} | ||||
| 					<button | ||||
| 						class=" px-3 py-1.5 rounded-lg w-full text-left {commandIdx === selectedCommandIdx | ||||
| 							? ' bg-gray-100 selected-command-option-button' | ||||
| 							: ''}" | ||||
| 						type="button" | ||||
| 						on:click={() => { | ||||
| 							confirmCommand(command); | ||||
| 						}} | ||||
| 						on:mousemove={() => { | ||||
| 							selectedCommandIdx = commandIdx; | ||||
| 						}} | ||||
| 						on:focus={() => {}} | ||||
| 					> | ||||
| 						<div class=" font-medium text-black"> | ||||
| 							{command.command} | ||||
| 						</div> | ||||
| 
 | ||||
| 						<div class=" text-xs text-gray-600"> | ||||
| 							{command.title} | ||||
| 						</div> | ||||
| 					</button> | ||||
| 				{/each} | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| {/if} | ||||
|  | @ -111,3 +111,19 @@ export const checkVersion = (required, current) => { | |||
| 				caseFirst: 'upper' | ||||
| 		  }) < 0; | ||||
| }; | ||||
| 
 | ||||
| export const findWordIndices = (text) => { | ||||
| 	const regex = /\[([^\]]+)\]/g; | ||||
| 	let matches = []; | ||||
| 	let match; | ||||
| 
 | ||||
| 	while ((match = regex.exec(text)) !== null) { | ||||
| 		matches.push({ | ||||
| 			word: match[1], | ||||
| 			startIndex: match.index, | ||||
| 			endIndex: regex.lastIndex - 1 | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	return matches; | ||||
| }; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy J. Baek
						Timothy J. Baek