forked from open-webui/open-webui
		
	feat: keyboard shortcuts
This commit is contained in:
		
							parent
							
								
									349e3074d7
								
							
						
					
					
						commit
						1f5bfe67a1
					
				
					 4 changed files with 303 additions and 1 deletions
				
			
		|  | @ -119,6 +119,7 @@ | ||||||
| 				langDiv.style.fontSize = '0.75rem'; | 				langDiv.style.fontSize = '0.75rem'; | ||||||
| 
 | 
 | ||||||
| 				let button = document.createElement('button'); | 				let button = document.createElement('button'); | ||||||
|  | 				button.className = 'copy-code-button'; | ||||||
| 				button.textContent = 'Copy Code'; | 				button.textContent = 'Copy Code'; | ||||||
| 				button.style.background = 'none'; | 				button.style.background = 'none'; | ||||||
| 				button.style.fontSize = '0.75rem'; | 				button.style.fontSize = '0.75rem'; | ||||||
|  | @ -832,7 +833,7 @@ | ||||||
| 														<button | 														<button | ||||||
| 															class="{messageIdx + 1 === messages.length | 															class="{messageIdx + 1 === messages.length | ||||||
| 																? 'visible' | 																? 'visible' | ||||||
| 																: 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition" | 																: 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition copy-response-button" | ||||||
| 															on:click={() => { | 															on:click={() => { | ||||||
| 																copyToClipboard(message.content); | 																copyToClipboard(message.content); | ||||||
| 															}} | 															}} | ||||||
|  |  | ||||||
							
								
								
									
										216
									
								
								src/lib/components/chat/ShortcutsModal.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								src/lib/components/chat/ShortcutsModal.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,216 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  | 	import Modal from '../common/Modal.svelte'; | ||||||
|  | 
 | ||||||
|  | 	export let show = false; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <Modal bind:show> | ||||||
|  | 	<div> | ||||||
|  | 		<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> | ||||||
|  | 			<div class=" text-lg font-medium self-center">Keyboard shortcuts</div> | ||||||
|  | 			<button | ||||||
|  | 				class="self-center" | ||||||
|  | 				on:click={() => { | ||||||
|  | 					show = false; | ||||||
|  | 				}} | ||||||
|  | 			> | ||||||
|  | 				<svg | ||||||
|  | 					xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 					viewBox="0 0 20 20" | ||||||
|  | 					fill="currentColor" | ||||||
|  | 					class="w-5 h-5" | ||||||
|  | 				> | ||||||
|  | 					<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> | ||||||
|  | 		<hr class=" dark:border-gray-800" /> | ||||||
|  | 
 | ||||||
|  | 		<div class="flex flex-col md:flex-row w-full p-5 md:space-x-4 dark:text-gray-200"> | ||||||
|  | 			<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"> | ||||||
|  | 				<div class="flex flex-col space-y-3 w-full self-start"> | ||||||
|  | 					<div class="w-full flex justify-between items-center"> | ||||||
|  | 						<div class=" text-sm">Open new chat</div> | ||||||
|  | 
 | ||||||
|  | 						<div class="flex space-x-1 text-xs"> | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								Ctrl/⌘ | ||||||
|  | 							</div> | ||||||
|  | 
 | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								Shift | ||||||
|  | 							</div> | ||||||
|  | 
 | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								O | ||||||
|  | 							</div> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 
 | ||||||
|  | 					<div class="w-full flex justify-between items-center"> | ||||||
|  | 						<div class=" text-sm">Focus chat input</div> | ||||||
|  | 
 | ||||||
|  | 						<div class="flex space-x-1 text-xs"> | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								Shift | ||||||
|  | 							</div> | ||||||
|  | 
 | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								Esc | ||||||
|  | 							</div> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 
 | ||||||
|  | 					<div class="w-full flex justify-between items-center"> | ||||||
|  | 						<div class=" text-sm">Copy last code block</div> | ||||||
|  | 
 | ||||||
|  | 						<div class="flex space-x-1 text-xs"> | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								Ctrl/⌘ | ||||||
|  | 							</div> | ||||||
|  | 
 | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								Shift | ||||||
|  | 							</div> | ||||||
|  | 
 | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								; | ||||||
|  | 							</div> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 
 | ||||||
|  | 					<div class="w-full flex justify-between items-center"> | ||||||
|  | 						<div class=" text-sm">Copy last response</div> | ||||||
|  | 
 | ||||||
|  | 						<div class="flex space-x-1 text-xs"> | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								Ctrl/⌘ | ||||||
|  | 							</div> | ||||||
|  | 
 | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								Shift | ||||||
|  | 							</div> | ||||||
|  | 
 | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								C | ||||||
|  | 							</div> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div class="flex flex-col space-y-3 w-full self-start"> | ||||||
|  | 					<div class="w-full flex justify-between items-center"> | ||||||
|  | 						<div class=" text-sm">Toggle sidebar</div> | ||||||
|  | 
 | ||||||
|  | 						<div class="flex space-x-1 text-xs"> | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								Ctrl/⌘ | ||||||
|  | 							</div> | ||||||
|  | 
 | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								Shift | ||||||
|  | 							</div> | ||||||
|  | 
 | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								S | ||||||
|  | 							</div> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 
 | ||||||
|  | 					<div class="w-full flex justify-between items-center"> | ||||||
|  | 						<div class=" text-sm">Delete chat</div> | ||||||
|  | 
 | ||||||
|  | 						<div class="flex space-x-1 text-xs"> | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								Ctrl/⌘ | ||||||
|  | 							</div> | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								Shift | ||||||
|  | 							</div> | ||||||
|  | 
 | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								⌫ | ||||||
|  | 							</div> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 
 | ||||||
|  | 					<div class="w-full flex justify-between items-center"> | ||||||
|  | 						<div class=" text-sm">Show shortcuts</div> | ||||||
|  | 
 | ||||||
|  | 						<div class="flex space-x-1 text-xs"> | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								Ctrl/⌘ | ||||||
|  | 							</div> | ||||||
|  | 
 | ||||||
|  | 							<div | ||||||
|  | 								class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" | ||||||
|  | 							> | ||||||
|  | 								/ | ||||||
|  | 							</div> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | </Modal> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | 	input::-webkit-outer-spin-button, | ||||||
|  | 	input::-webkit-inner-spin-button { | ||||||
|  | 		/* display: none; <- Crashes Chrome on hover */ | ||||||
|  | 		-webkit-appearance: none; | ||||||
|  | 		margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.tabs::-webkit-scrollbar { | ||||||
|  | 		display: none; /* for Chrome, Safari and Opera */ | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.tabs { | ||||||
|  | 		-ms-overflow-style: none; /* IE and Edge */ | ||||||
|  | 		scrollbar-width: none; /* Firefox */ | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	input[type='number'] { | ||||||
|  | 		-moz-appearance: textfield; /* Firefox */ | ||||||
|  | 	} | ||||||
|  | </style> | ||||||
|  | @ -61,6 +61,7 @@ | ||||||
| 	<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"> | ||||||
| 		<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" | ||||||
| 				class="flex-grow flex justify-between rounded-md px-3 py-1.5 mt-2 hover:bg-gray-900 transition" | 				class="flex-grow flex justify-between rounded-md px-3 py-1.5 mt-2 hover:bg-gray-900 transition" | ||||||
| 				on:click={async () => { | 				on:click={async () => { | ||||||
| 					goto('/'); | 					goto('/'); | ||||||
|  | @ -320,6 +321,13 @@ | ||||||
| 								</div> | 								</div> | ||||||
| 							{:else} | 							{:else} | ||||||
| 								<div class="flex self-center space-x-1.5"> | 								<div class="flex self-center space-x-1.5"> | ||||||
|  | 									<button | ||||||
|  | 										id="delete-chat-button" | ||||||
|  | 										class=" hidden" | ||||||
|  | 										on:click={() => { | ||||||
|  | 											deleteChat(chat.id); | ||||||
|  | 										}} | ||||||
|  | 									/> | ||||||
| 									<button | 									<button | ||||||
| 										class=" self-center hover:text-white transition" | 										class=" self-center hover:text-white transition" | ||||||
| 										on:click={() => { | 										on:click={() => { | ||||||
|  | @ -533,6 +541,7 @@ | ||||||
| 		class="fixed left-0 top-[50dvh] z-40 -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0" | 		class="fixed left-0 top-[50dvh] z-40 -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0" | ||||||
| 	> | 	> | ||||||
| 		<button | 		<button | ||||||
|  | 			id="sidebar-toggle-button" | ||||||
| 			class=" group" | 			class=" group" | ||||||
| 			on:click={() => { | 			on:click={() => { | ||||||
| 				show = !show; | 				show = !show; | ||||||
|  |  | ||||||
|  | @ -18,6 +18,7 @@ | ||||||
| 	import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; | 	import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; | ||||||
| 	import Sidebar from '$lib/components/layout/Sidebar.svelte'; | 	import Sidebar from '$lib/components/layout/Sidebar.svelte'; | ||||||
| 	import { checkVersion } from '$lib/utils'; | 	import { checkVersion } from '$lib/utils'; | ||||||
|  | 	import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte'; | ||||||
| 
 | 
 | ||||||
| 	let ollamaVersion = ''; | 	let ollamaVersion = ''; | ||||||
| 	let loaded = false; | 	let loaded = false; | ||||||
|  | @ -25,6 +26,8 @@ | ||||||
| 	let DB = null; | 	let DB = null; | ||||||
| 	let localDBChats = []; | 	let localDBChats = []; | ||||||
| 
 | 
 | ||||||
|  | 	let showShortcuts = false; | ||||||
|  | 
 | ||||||
| 	const getModels = async () => { | 	const getModels = async () => { | ||||||
| 		let models = []; | 		let models = []; | ||||||
| 		models.push( | 		models.push( | ||||||
|  | @ -106,6 +109,65 @@ | ||||||
| 			}); | 			}); | ||||||
| 
 | 
 | ||||||
| 			await setOllamaVersion(); | 			await setOllamaVersion(); | ||||||
|  | 
 | ||||||
|  | 			document.addEventListener('keydown', function (event) { | ||||||
|  | 				const isCtrlPressed = event.ctrlKey || event.metaKey; // metaKey is for Cmd key on Mac | ||||||
|  | 				// Check if the Shift key is pressed | ||||||
|  | 				const isShiftPressed = event.shiftKey; | ||||||
|  | 
 | ||||||
|  | 				// Check if Ctrl + Shift + O is pressed | ||||||
|  | 				if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 'o') { | ||||||
|  | 					event.preventDefault(); | ||||||
|  | 					console.log('newChat'); | ||||||
|  | 					document.getElementById('sidebar-new-chat-button')?.click(); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				// Check if Shift + Esc is pressed | ||||||
|  | 				if (isShiftPressed && event.key === 'Escape') { | ||||||
|  | 					event.preventDefault(); | ||||||
|  | 					console.log('focusInput'); | ||||||
|  | 					document.getElementById('chat-textarea')?.focus(); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				// Check if Ctrl + Shift + ; is pressed | ||||||
|  | 				if (isCtrlPressed && isShiftPressed && event.key === ';') { | ||||||
|  | 					event.preventDefault(); | ||||||
|  | 					console.log('copyLastCodeBlock'); | ||||||
|  | 					const button = [...document.getElementsByClassName('copy-code-button')]?.at(-1); | ||||||
|  | 					button?.click(); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				// Check if Ctrl + Shift + C is pressed | ||||||
|  | 				if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 'c') { | ||||||
|  | 					event.preventDefault(); | ||||||
|  | 					console.log('copyLastResponse'); | ||||||
|  | 					const button = [...document.getElementsByClassName('copy-response-button')]?.at(-1); | ||||||
|  | 					console.log(button); | ||||||
|  | 					button?.click(); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				// Check if Ctrl + Shift + S is pressed | ||||||
|  | 				if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 's') { | ||||||
|  | 					event.preventDefault(); | ||||||
|  | 					console.log('toggleSidebar'); | ||||||
|  | 					document.getElementById('sidebar-toggle-button')?.click(); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				// Check if Ctrl + Shift + Backspace is pressed | ||||||
|  | 				if (isCtrlPressed && isShiftPressed && event.key === 'Backspace') { | ||||||
|  | 					event.preventDefault(); | ||||||
|  | 					console.log('deleteChat'); | ||||||
|  | 					document.getElementById('delete-chat-button')?.click(); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				// Check if Ctrl + / is pressed | ||||||
|  | 				if (isCtrlPressed && event.key === '/') { | ||||||
|  | 					event.preventDefault(); | ||||||
|  | 					console.log('showShortcuts'); | ||||||
|  | 					document.getElementById('show-shortcuts-button')?.click(); | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
| 			await tick(); | 			await tick(); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -114,6 +176,20 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if loaded} | {#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> | ||||||
|  | 	</div> | ||||||
|  | 
 | ||||||
|  | 	<ShortcutsModal bind:show={showShortcuts} /> | ||||||
|  | 
 | ||||||
| 	<div class="app relative"> | 	<div class="app relative"> | ||||||
| 		{#if !['user', 'admin'].includes($user.role)} | 		{#if !['user', 'admin'].includes($user.role)} | ||||||
| 			<div class="fixed w-full h-full flex z-50"> | 			<div class="fixed w-full h-full flex z-50"> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy J. Baek
						Timothy J. Baek