forked from open-webui/open-webui
		
	feat: editable prompt suggestions integration
This commit is contained in:
		
							parent
							
								
									4e1b52e91b
								
							
						
					
					
						commit
						c4a039326f
					
				
					 6 changed files with 172 additions and 52 deletions
				
			
		|  | @ -49,14 +49,15 @@ async def set_global_default_models( | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @router.post("/default/suggestions", response_model=str) | @router.post("/default/suggestions", response_model=List[PromptSuggestion]) | ||||||
| async def set_global_default_suggestions( | async def set_global_default_suggestions( | ||||||
|     request: Request, |     request: Request, | ||||||
|     form_data: SetDefaultSuggestionsForm, |     form_data: SetDefaultSuggestionsForm, | ||||||
|     user=Depends(get_current_user), |     user=Depends(get_current_user), | ||||||
| ): | ): | ||||||
|     if user.role == "admin": |     if user.role == "admin": | ||||||
|         request.app.state.DEFAULT_PROMPT_SUGGESTIONS = form_data.suggestions |         data = form_data.model_dump() | ||||||
|  |         request.app.state.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"] | ||||||
|         return request.app.state.DEFAULT_PROMPT_SUGGESTIONS |         return request.app.state.DEFAULT_PROMPT_SUGGESTIONS | ||||||
|     else: |     else: | ||||||
|         raise HTTPException( |         raise HTTPException( | ||||||
|  |  | ||||||
|  | @ -29,3 +29,33 @@ export const setDefaultModels = async (token: string, models: string) => { | ||||||
| 
 | 
 | ||||||
| 	return res; | 	return res; | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export const setDefaultPromptSuggestions = async (token: string, promptSuggestions: string) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/default/suggestions`, { | ||||||
|  | 		method: 'POST', | ||||||
|  | 		headers: { | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			Authorization: `Bearer ${token}` | ||||||
|  | 		}, | ||||||
|  | 		body: JSON.stringify({ | ||||||
|  | 			suggestions: promptSuggestions | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  | 		.then(async (res) => { | ||||||
|  | 			if (!res.ok) throw await res.json(); | ||||||
|  | 			return res.json(); | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			console.log(err); | ||||||
|  | 			error = err.detail; | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ | ||||||
| 				}} | 				}} | ||||||
| 			> | 			> | ||||||
| 				<div class="flex flex-col text-left self-center"> | 				<div class="flex flex-col text-left self-center"> | ||||||
| 					{#if prompt.title} | 					{#if prompt.title && prompt.title[0] !== ''} | ||||||
| 						<div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div> | 						<div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div> | ||||||
| 						<div class="text-sm text-gray-500">{prompt.title[1]}</div> | 						<div class="text-sm text-gray-500">{prompt.title[1]}</div> | ||||||
| 					{:else} | 					{:else} | ||||||
|  |  | ||||||
|  | @ -34,6 +34,8 @@ | ||||||
| 		updateOpenAIUrl | 		updateOpenAIUrl | ||||||
| 	} from '$lib/apis/openai'; | 	} from '$lib/apis/openai'; | ||||||
| 	import { resetVectorDB } from '$lib/apis/rag'; | 	import { resetVectorDB } from '$lib/apis/rag'; | ||||||
|  | 	import { setDefaultPromptSuggestions } from '$lib/apis/configs'; | ||||||
|  | 	import { getBackendConfig } from '$lib/apis'; | ||||||
| 
 | 
 | ||||||
| 	export let show = false; | 	export let show = false; | ||||||
| 
 | 
 | ||||||
|  | @ -99,6 +101,9 @@ | ||||||
| 	let OPENAI_API_KEY = ''; | 	let OPENAI_API_KEY = ''; | ||||||
| 	let OPENAI_API_BASE_URL = ''; | 	let OPENAI_API_BASE_URL = ''; | ||||||
| 
 | 
 | ||||||
|  | 	// Interface | ||||||
|  | 	let promptSuggestions = []; | ||||||
|  | 
 | ||||||
| 	// Addons | 	// Addons | ||||||
| 	let titleAutoGenerate = true; | 	let titleAutoGenerate = true; | ||||||
| 	let speechAutoSend = false; | 	let speechAutoSend = false; | ||||||
|  | @ -191,6 +196,11 @@ | ||||||
| 		await models.set(await getModels()); | 		await models.set(await getModels()); | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  | 	const updateInterfaceHandler = async () => { | ||||||
|  | 		promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions); | ||||||
|  | 		await config.set(await getBackendConfig()); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
| 	const toggleTheme = async () => { | 	const toggleTheme = async () => { | ||||||
| 		if (theme === 'dark') { | 		if (theme === 'dark') { | ||||||
| 			theme = 'light'; | 			theme = 'light'; | ||||||
|  | @ -577,6 +587,7 @@ | ||||||
| 			API_BASE_URL = await getOllamaAPIUrl(localStorage.token); | 			API_BASE_URL = await getOllamaAPIUrl(localStorage.token); | ||||||
| 			OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token); | 			OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token); | ||||||
| 			OPENAI_API_KEY = await getOpenAIKey(localStorage.token); | 			OPENAI_API_KEY = await getOpenAIKey(localStorage.token); | ||||||
|  | 			promptSuggestions = $config?.default_prompt_suggestions; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||||
|  | @ -745,6 +756,32 @@ | ||||||
| 						</div> | 						</div> | ||||||
| 						<div class=" self-center">External</div> | 						<div class=" self-center">External</div> | ||||||
| 					</button> | 					</button> | ||||||
|  | 
 | ||||||
|  | 					<button | ||||||
|  | 						class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === | ||||||
|  | 						'interface' | ||||||
|  | 							? 'bg-gray-200 dark:bg-gray-700' | ||||||
|  | 							: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" | ||||||
|  | 						on:click={() => { | ||||||
|  | 							selectedTab = 'interface'; | ||||||
|  | 						}} | ||||||
|  | 					> | ||||||
|  | 						<div class=" self-center mr-2"> | ||||||
|  | 							<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="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" | ||||||
|  | 									clip-rule="evenodd" | ||||||
|  | 								/> | ||||||
|  | 							</svg> | ||||||
|  | 						</div> | ||||||
|  | 						<div class=" self-center">Interface</div> | ||||||
|  | 					</button> | ||||||
| 				{/if} | 				{/if} | ||||||
| 
 | 
 | ||||||
| 				<button | 				<button | ||||||
|  | @ -797,34 +834,6 @@ | ||||||
| 					<div class=" self-center">Chats</div> | 					<div class=" self-center">Chats</div> | ||||||
| 				</button> | 				</button> | ||||||
| 
 | 
 | ||||||
| 				{#if !$config || ($config && !$config.auth)} |  | ||||||
| 					<button |  | ||||||
| 						class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === |  | ||||||
| 						'auth' |  | ||||||
| 							? 'bg-gray-200 dark:bg-gray-700' |  | ||||||
| 							: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" |  | ||||||
| 						on:click={() => { |  | ||||||
| 							selectedTab = 'auth'; |  | ||||||
| 						}} |  | ||||||
| 					> |  | ||||||
| 						<div class=" self-center mr-2"> |  | ||||||
| 							<svg |  | ||||||
| 								xmlns="http://www.w3.org/2000/svg" |  | ||||||
| 								viewBox="0 0 24 24" |  | ||||||
| 								fill="currentColor" |  | ||||||
| 								class="w-4 h-4" |  | ||||||
| 							> |  | ||||||
| 								<path |  | ||||||
| 									fill-rule="evenodd" |  | ||||||
| 									d="M12.516 2.17a.75.75 0 00-1.032 0 11.209 11.209 0 01-7.877 3.08.75.75 0 00-.722.515A12.74 12.74 0 002.25 9.75c0 5.942 4.064 10.933 9.563 12.348a.749.749 0 00.374 0c5.499-1.415 9.563-6.406 9.563-12.348 0-1.39-.223-2.73-.635-3.985a.75.75 0 00-.722-.516l-.143.001c-2.996 0-5.717-1.17-7.734-3.08zm3.094 8.016a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z" |  | ||||||
| 									clip-rule="evenodd" |  | ||||||
| 								/> |  | ||||||
| 							</svg> |  | ||||||
| 						</div> |  | ||||||
| 						<div class=" self-center">Authentication</div> |  | ||||||
| 					</button> |  | ||||||
| 				{/if} |  | ||||||
| 
 |  | ||||||
| 				<button | 				<button | ||||||
| 					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === | 					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === | ||||||
| 					'account' | 					'account' | ||||||
|  | @ -877,7 +886,7 @@ | ||||||
| 					<div class=" self-center">About</div> | 					<div class=" self-center">About</div> | ||||||
| 				</button> | 				</button> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div class="flex-1 md:min-h-[340px]"> | 			<div class="flex-1 md:min-h-[380px]"> | ||||||
| 				{#if selectedTab === 'general'} | 				{#if selectedTab === 'general'} | ||||||
| 					<div class="flex flex-col space-y-3"> | 					<div class="flex flex-col space-y-3"> | ||||||
| 						<div> | 						<div> | ||||||
|  | @ -1048,7 +1057,7 @@ | ||||||
| 					</div> | 					</div> | ||||||
| 				{:else if selectedTab === 'advanced'} | 				{:else if selectedTab === 'advanced'} | ||||||
| 					<div class="flex flex-col h-full justify-between text-sm"> | 					<div class="flex flex-col h-full justify-between text-sm"> | ||||||
| 						<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-72"> | 						<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> | ||||||
| 							<div class=" text-sm font-medium">Parameters</div> | 							<div class=" text-sm font-medium">Parameters</div> | ||||||
| 
 | 
 | ||||||
| 							<Advanced bind:options /> | 							<Advanced bind:options /> | ||||||
|  | @ -1483,6 +1492,103 @@ | ||||||
| 							</div> | 							</div> | ||||||
| 						</div> | 						</div> | ||||||
| 
 | 
 | ||||||
|  | 						<div class="flex justify-end pt-3 text-sm font-medium"> | ||||||
|  | 							<button | ||||||
|  | 								class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" | ||||||
|  | 								type="submit" | ||||||
|  | 							> | ||||||
|  | 								Save | ||||||
|  | 							</button> | ||||||
|  | 						</div> | ||||||
|  | 					</form> | ||||||
|  | 				{:else if selectedTab === 'interface'} | ||||||
|  | 					<form | ||||||
|  | 						class="flex flex-col h-full justify-between space-y-3 text-sm" | ||||||
|  | 						on:submit|preventDefault={() => { | ||||||
|  | 							updateInterfaceHandler(); | ||||||
|  | 							show = false; | ||||||
|  | 						}} | ||||||
|  | 					> | ||||||
|  | 						<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> | ||||||
|  | 							<div class="flex w-full justify-between mb-2"> | ||||||
|  | 								<div class=" self-center text-sm font-semibold">Default Prompt Suggestions</div> | ||||||
|  | 
 | ||||||
|  | 								<button | ||||||
|  | 									class="p-1 px-3 text-xs flex rounded transition" | ||||||
|  | 									type="button" | ||||||
|  | 									on:click={() => { | ||||||
|  | 										if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') { | ||||||
|  | 											promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }]; | ||||||
|  | 										} | ||||||
|  | 									}} | ||||||
|  | 								> | ||||||
|  | 									<svg | ||||||
|  | 										xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 										viewBox="0 0 20 20" | ||||||
|  | 										fill="currentColor" | ||||||
|  | 										class="w-4 h-4" | ||||||
|  | 									> | ||||||
|  | 										<path | ||||||
|  | 											d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" | ||||||
|  | 										/> | ||||||
|  | 									</svg> | ||||||
|  | 								</button> | ||||||
|  | 							</div> | ||||||
|  | 							<div class="flex flex-col space-y-1"> | ||||||
|  | 								{#each promptSuggestions as prompt, promptIdx} | ||||||
|  | 									<div class=" flex border dark:border-gray-600 rounded-lg"> | ||||||
|  | 										<div class="flex flex-col flex-1"> | ||||||
|  | 											<div class="flex border-b dark:border-gray-600 w-full"> | ||||||
|  | 												<input | ||||||
|  | 													class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600" | ||||||
|  | 													placeholder="Title (e.g. Tell me a fun fact)" | ||||||
|  | 													bind:value={prompt.title[0]} | ||||||
|  | 												/> | ||||||
|  | 
 | ||||||
|  | 												<input | ||||||
|  | 													class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600" | ||||||
|  | 													placeholder="Subtitle (e.g. about the Roman Empire)" | ||||||
|  | 													bind:value={prompt.title[1]} | ||||||
|  | 												/> | ||||||
|  | 											</div> | ||||||
|  | 
 | ||||||
|  | 											<input | ||||||
|  | 												class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600" | ||||||
|  | 												placeholder="Prompt (e.g. Tell me a fun fact about the Roman Empire)" | ||||||
|  | 												bind:value={prompt.content} | ||||||
|  | 											/> | ||||||
|  | 										</div> | ||||||
|  | 
 | ||||||
|  | 										<button | ||||||
|  | 											class="px-2" | ||||||
|  | 											type="button" | ||||||
|  | 											on:click={() => { | ||||||
|  | 												promptSuggestions.splice(promptIdx, 1); | ||||||
|  | 												promptSuggestions = promptSuggestions; | ||||||
|  | 											}} | ||||||
|  | 										> | ||||||
|  | 											<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> | ||||||
|  | 								{/each} | ||||||
|  | 							</div> | ||||||
|  | 
 | ||||||
|  | 							{#if promptSuggestions.length > 0} | ||||||
|  | 								<div class="text-xs text-left w-full mt-2"> | ||||||
|  | 									Adjusting these settings will apply changes universally to all users. | ||||||
|  | 								</div> | ||||||
|  | 							{/if} | ||||||
|  | 						</div> | ||||||
|  | 
 | ||||||
| 						<div class="flex justify-end pt-3 text-sm font-medium"> | 						<div class="flex justify-end pt-3 text-sm font-medium"> | ||||||
| 							<button | 							<button | ||||||
| 								class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" | 								class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ | ||||||
| 		} else if (size === 'sm') { | 		} else if (size === 'sm') { | ||||||
| 			return 'w-[30rem]'; | 			return 'w-[30rem]'; | ||||||
| 		} else { | 		} else { | ||||||
| 			return 'w-[40rem]'; | 			return 'w-[42rem]'; | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -765,24 +765,7 @@ | ||||||
| 		bind:files | 		bind:files | ||||||
| 		bind:prompt | 		bind:prompt | ||||||
| 		bind:autoScroll | 		bind:autoScroll | ||||||
| 		suggestionPrompts={selectedModelfile?.suggestionPrompts ?? [ | 		suggestionPrompts={selectedModelfile?.suggestionPrompts ?? $config.default_prompt_suggestions} | ||||||
| 			{ |  | ||||||
| 				title: ['Help me study', 'vocabulary for a college entrance exam'], |  | ||||||
| 				content: `Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.` |  | ||||||
| 			}, |  | ||||||
| 			{ |  | ||||||
| 				title: ['Give me ideas', `for what to do with my kids' art`], |  | ||||||
| 				content: `What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.` |  | ||||||
| 			}, |  | ||||||
| 			{ |  | ||||||
| 				title: ['Tell me a fun fact', 'about the Roman Empire'], |  | ||||||
| 				content: 'Tell me a random fun fact about the Roman Empire' |  | ||||||
| 			}, |  | ||||||
| 			{ |  | ||||||
| 				title: ['Show me a code snippet', `of a website's sticky header`], |  | ||||||
| 				content: `Show me a code snippet of a website's sticky header in CSS and JavaScript.` |  | ||||||
| 			} |  | ||||||
| 		]} |  | ||||||
| 		{messages} | 		{messages} | ||||||
| 		{submitPrompt} | 		{submitPrompt} | ||||||
| 		{stopResponse} | 		{stopResponse} | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy J. Baek
						Timothy J. Baek