forked from open-webui/open-webui
		
	
						commit
						07fc6e4b11
					
				
					 14 changed files with 460 additions and 83 deletions
				
			
		|  | @ -71,6 +71,11 @@ Don't forget to explore our sibling project, [OllamaHub](https://ollamahub.com/) | |||
| 
 | ||||
| ## How to Install 🚀 | ||||
| 
 | ||||
| 🌟 **Important Note on User Roles:** | ||||
| 
 | ||||
| - **Admin Creation:** The very first account to sign up on the Ollama Web UI will be granted **Administrator privileges**. This account will have comprehensive control over the platform, including user management and system settings. | ||||
| - **User Registrations:** All subsequent users signing up will initially have their accounts set to **Pending** status by default. These accounts will require approval from the Administrator to gain access to the platform functionalities. | ||||
| 
 | ||||
| ### Installing Both Ollama and Ollama Web UI Using Docker Compose | ||||
| 
 | ||||
| If you don't have Ollama installed yet, you can use the provided Docker Compose file for a hassle-free installation. Simply run the following command: | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ If you're running ollama-webui and have chosen to install webui and ollama separ | |||
| Here's an example of the command you should run: | ||||
| 
 | ||||
| ```bash | ||||
| docker run -d --network=host -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main | ||||
| docker run -d --network=host -v ollama-webui:/app/backend/data -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main | ||||
| ``` | ||||
| 
 | ||||
| ## Connection Errors | ||||
|  |  | |||
|  | @ -88,7 +88,9 @@ async def signup(form_data: SignupForm): | |||
|         try: | ||||
|             role = "admin" if Users.get_num_users() == 0 else "pending" | ||||
|             hashed = get_password_hash(form_data.password) | ||||
|             user = Auths.insert_new_auth(form_data.email, hashed, form_data.name, role) | ||||
|             user = Auths.insert_new_auth( | ||||
|                 form_data.email.lower(), hashed, form_data.name, role | ||||
|             ) | ||||
| 
 | ||||
|             if user: | ||||
|                 token = create_token(data={"email": user.email}) | ||||
|  |  | |||
|  | @ -1 +1 @@ | |||
| uvicorn main:app --port 8080 --host 0.0.0.0 --reload | ||||
| uvicorn main:app --port 8080 --host 0.0.0.0 --forwarded-allow-ips '*' --reload | ||||
|  | @ -1 +1 @@ | |||
| uvicorn main:app --host 0.0.0.0 --port 8080 | ||||
| uvicorn main:app --host 0.0.0.0 --port 8080 --forwarded-allow-ips '*' | ||||
|  | @ -3,7 +3,7 @@ import { WEBUI_API_BASE_URL } from '$lib/constants'; | |||
| export const getSessionUser = async (token: string) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${WEBUI_API_BASE_URL}/auths`, { | ||||
| 	const res = await fetch(`${WEBUI_API_BASE_URL}/auths/`, { | ||||
| 		method: 'GET', | ||||
| 		headers: { | ||||
| 			'Content-Type': 'application/json', | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ export const updateUserRole = async (token: string, id: string, role: string) => | |||
| export const getUsers = async (token: string) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${WEBUI_API_BASE_URL}/users`, { | ||||
| 	const res = await fetch(`${WEBUI_API_BASE_URL}/users/`, { | ||||
| 		method: 'GET', | ||||
| 		headers: { | ||||
| 			'Content-Type': 'application/json', | ||||
|  |  | |||
|  | @ -119,6 +119,7 @@ | |||
| 				langDiv.style.fontSize = '0.75rem'; | ||||
| 
 | ||||
| 				let button = document.createElement('button'); | ||||
| 				button.className = 'copy-code-button'; | ||||
| 				button.textContent = 'Copy Code'; | ||||
| 				button.style.background = 'none'; | ||||
| 				button.style.fontSize = '0.75rem'; | ||||
|  | @ -832,7 +833,7 @@ | |||
| 														<button | ||||
| 															class="{messageIdx + 1 === messages.length | ||||
| 																? '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={() => { | ||||
| 																copyToClipboard(message.content); | ||||
| 															}} | ||||
|  |  | |||
|  | @ -6,6 +6,11 @@ | |||
| 	export let disabled = false; | ||||
| 
 | ||||
| 	const saveDefaultModel = () => { | ||||
| 		const hasEmptyModel = selectedModels.filter(it => it === ''); | ||||
| 		if(hasEmptyModel.length){ | ||||
| 			toast.error('Choose a model before saving...'); | ||||
| 			return; | ||||
| 		} | ||||
| 		settings.set({ ...$settings, models: selectedModels }); | ||||
| 		localStorage.setItem('settings', JSON.stringify($settings)); | ||||
| 		toast.success('Default model updated'); | ||||
|  | @ -21,7 +26,7 @@ | |||
| 				bind:value={selectedModel} | ||||
| 				{disabled} | ||||
| 			> | ||||
| 				<option class=" text-gray-700" value="" selected>Select a model</option> | ||||
| 				<option class=" text-gray-700" value="" selected disabled>Select a model</option> | ||||
| 
 | ||||
| 				{#each $models as model} | ||||
| 					{#if model.name === 'hr'} | ||||
|  |  | |||
|  | @ -614,6 +614,7 @@ | |||
| 		options.top_p = settings.top_p ?? ''; | ||||
| 		options.num_ctx = settings.num_ctx ?? ''; | ||||
| 		options = { ...options, ...settings.options }; | ||||
| 		options.stop = (settings?.options?.stop ?? []).join(','); | ||||
| 
 | ||||
| 		titleAutoGenerate = settings.titleAutoGenerate ?? true; | ||||
| 		speechAutoSend = settings.speechAutoSend ?? false; | ||||
|  | @ -1051,7 +1052,8 @@ | |||
| 									saveSettings({ | ||||
| 										options: { | ||||
| 											seed: (options.seed !== 0 ? options.seed : undefined) ?? undefined, | ||||
| 											stop: options.stop !== '' ? options.stop : undefined, | ||||
| 											stop: | ||||
| 												options.stop !== '' ? options.stop.split(',').filter((e) => e) : undefined, | ||||
| 											temperature: options.temperature !== '' ? options.temperature : undefined, | ||||
| 											repeat_penalty: | ||||
| 												options.repeat_penalty !== '' ? options.repeat_penalty : undefined, | ||||
|  | @ -1840,6 +1842,22 @@ | |||
| 
 | ||||
| 							<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 							<div class="flex space-x-1"> | ||||
| 								<a href="https://discord.gg/5rJgQTnV4s" target="_blank"> | ||||
| 									<img | ||||
| 										alt="Discord" | ||||
| 										src="https://img.shields.io/badge/Discord-Ollama_Web_UI-blue?logo=discord&logoColor=white" | ||||
| 									/> | ||||
| 								</a> | ||||
| 
 | ||||
| 								<a href="https://github.com/ollama-webui/ollama-webui" target="_blank"> | ||||
| 									<img | ||||
| 										alt="Github Repo" | ||||
| 										src="https://img.shields.io/github/stars/ollama-webui/ollama-webui?style=social&label=Star us on Github" | ||||
| 									/> | ||||
| 								</a> | ||||
| 							</div> | ||||
| 
 | ||||
| 							<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> | ||||
| 								Created by <a | ||||
| 									class=" text-gray-500 dark:text-gray-300 font-medium" | ||||
|  | @ -1847,15 +1865,6 @@ | |||
| 									target="_blank">Timothy J. Baek</a | ||||
| 								> | ||||
| 							</div> | ||||
| 
 | ||||
| 							<div> | ||||
| 								<a href="https://github.com/ollama-webui/ollama-webui"> | ||||
| 									<img | ||||
| 										alt="Github Repo" | ||||
| 										src="https://img.shields.io/github/stars/ollama-webui/ollama-webui?style=social&label=Star us on Github" | ||||
| 									/> | ||||
| 								</a> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				{/if} | ||||
|  |  | |||
							
								
								
									
										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="px-2.5 flex justify-center space-x-2"> | ||||
| 			<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" | ||||
| 				on:click={async () => { | ||||
| 					goto('/'); | ||||
|  | @ -320,6 +321,13 @@ | |||
| 								</div> | ||||
| 							{:else} | ||||
| 								<div class="flex self-center space-x-1.5"> | ||||
| 									<button | ||||
| 										id="delete-chat-button" | ||||
| 										class=" hidden" | ||||
| 										on:click={() => { | ||||
| 											deleteChat(chat.id); | ||||
| 										}} | ||||
| 									/> | ||||
| 									<button | ||||
| 										class=" self-center hover:text-white transition" | ||||
| 										on:click={() => { | ||||
|  | @ -382,11 +390,6 @@ | |||
| 						on:click={() => { | ||||
| 							showDropdown = !showDropdown; | ||||
| 						}} | ||||
| 						on:focusout={() => { | ||||
| 							setTimeout(() => { | ||||
| 								showDropdown = false; | ||||
| 							}, 150); | ||||
| 						}} | ||||
| 					> | ||||
| 						<div class=" self-center mr-3"> | ||||
| 							<img | ||||
|  | @ -409,6 +412,7 @@ | |||
| 										class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition" | ||||
| 										on:click={() => { | ||||
| 											goto('/admin'); | ||||
| 											showDropdown = false; | ||||
| 										}} | ||||
| 									> | ||||
| 										<div class=" self-center mr-3"> | ||||
|  | @ -435,6 +439,7 @@ | |||
| 									class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition" | ||||
| 									on:click={async () => { | ||||
| 										await showSettings.set(true); | ||||
| 										showDropdown = false; | ||||
| 									}} | ||||
| 								> | ||||
| 									<div class=" self-center mr-3"> | ||||
|  | @ -470,6 +475,7 @@ | |||
| 									on:click={() => { | ||||
| 										localStorage.removeItem('token'); | ||||
| 										location.href = '/auth'; | ||||
| 										showDropdown = false; | ||||
| 									}} | ||||
| 								> | ||||
| 									<div class=" self-center mr-3"> | ||||
|  | @ -535,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" | ||||
| 	> | ||||
| 		<button | ||||
| 			id="sidebar-toggle-button" | ||||
| 			class=" group" | ||||
| 			on:click={() => { | ||||
| 				show = !show; | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ | |||
| 	import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; | ||||
| 	import Sidebar from '$lib/components/layout/Sidebar.svelte'; | ||||
| 	import { checkVersion } from '$lib/utils'; | ||||
| 	import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte'; | ||||
| 
 | ||||
| 	let ollamaVersion = ''; | ||||
| 	let loaded = false; | ||||
|  | @ -25,6 +26,8 @@ | |||
| 	let DB = null; | ||||
| 	let localDBChats = []; | ||||
| 
 | ||||
| 	let showShortcuts = false; | ||||
| 
 | ||||
| 	const getModels = async () => { | ||||
| 		let models = []; | ||||
| 		models.push( | ||||
|  | @ -106,6 +109,65 @@ | |||
| 			}); | ||||
| 
 | ||||
| 			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(); | ||||
| 		} | ||||
| 
 | ||||
|  | @ -114,6 +176,20 @@ | |||
| </script> | ||||
| 
 | ||||
| {#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"> | ||||
| 		{#if !['user', 'admin'].includes($user.role)} | ||||
| 			<div class="fixed w-full h-full flex z-50"> | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ | |||
| 	} from '$lib/apis/modelfiles'; | ||||
| 
 | ||||
| 	let localModelfiles = []; | ||||
| 	let importFiles; | ||||
| 
 | ||||
| 	const deleteModelHandler = async (tagName) => { | ||||
| 		let success = null; | ||||
|  | @ -199,78 +200,133 @@ | |||
| 				</div> | ||||
| 			{/each} | ||||
| 
 | ||||
| 			{#if localModelfiles.length > 0} | ||||
| 				<hr class=" dark:border-gray-700 my-2.5" /> | ||||
| 			<hr class=" dark:border-gray-700 my-2.5" /> | ||||
| 
 | ||||
| 				<div class=" flex justify-end space-x-4 w-full mb-3"> | ||||
| 					<div class=" self-center text-sm font-medium"> | ||||
| 						{localModelfiles.length} Local Modelfiles Detected | ||||
| 					</div> | ||||
| 			<div class=" flex justify-between w-full mb-3"> | ||||
| 				<div class="flex space-x-1"> | ||||
| 					<input | ||||
| 						id="modelfiles-import-input" | ||||
| 						bind:files={importFiles} | ||||
| 						type="file" | ||||
| 						accept=".json" | ||||
| 						hidden | ||||
| 						on:change={() => { | ||||
| 							console.log(importFiles); | ||||
| 
 | ||||
| 					<div class="flex space-x-1"> | ||||
| 						<button | ||||
| 							class="self-center w-fit text-sm px-3 py-1 border dark:border-gray-600 rounded-xl flex" | ||||
| 							on:click={async () => { | ||||
| 								for (const modelfile of localModelfiles) { | ||||
| 							let reader = new FileReader(); | ||||
| 							reader.onload = async (event) => { | ||||
| 								let savedModelfiles = JSON.parse(event.target.result); | ||||
| 								console.log(savedModelfiles); | ||||
| 
 | ||||
| 								for (const modelfile of savedModelfiles) { | ||||
| 									await createNewModelfile(localStorage.token, modelfile).catch((error) => { | ||||
| 										return null; | ||||
| 									}); | ||||
| 								} | ||||
| 
 | ||||
| 								saveModelfiles(localModelfiles); | ||||
| 								localStorage.removeItem('modelfiles'); | ||||
| 								localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]'); | ||||
| 								await modelfiles.set(await getModelfiles(localStorage.token)); | ||||
| 							}} | ||||
| 						> | ||||
| 							<div class=" self-center mr-2 font-medium">Sync All</div> | ||||
| 							}; | ||||
| 
 | ||||
| 							<div class=" self-center"> | ||||
| 								<svg | ||||
| 									xmlns="http://www.w3.org/2000/svg" | ||||
| 									viewBox="0 0 16 16" | ||||
| 									fill="currentColor" | ||||
| 									class="w-3.5 h-3.5" | ||||
| 								> | ||||
| 									<path | ||||
| 										fill-rule="evenodd" | ||||
| 										d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z" | ||||
| 										clip-rule="evenodd" | ||||
| 									/> | ||||
| 								</svg> | ||||
| 							</div> | ||||
| 						</button> | ||||
| 							reader.readAsText(importFiles[0]); | ||||
| 						}} | ||||
| 					/> | ||||
| 
 | ||||
| 						<button | ||||
| 							class="self-center w-fit text-sm p-1.5 border dark:border-gray-600 rounded-xl flex" | ||||
| 							on:click={async () => { | ||||
| 								saveModelfiles(localModelfiles); | ||||
| 					<button | ||||
| 						class="self-center w-fit text-sm px-3 py-1 border dark:border-gray-600 rounded-xl flex" | ||||
| 						on:click={async () => { | ||||
| 							document.getElementById('modelfiles-import-input')?.click(); | ||||
| 						}} | ||||
| 					> | ||||
| 						<div class=" self-center mr-2 font-medium">Import Modelfiles</div> | ||||
| 
 | ||||
| 								localStorage.removeItem('modelfiles'); | ||||
| 								localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]'); | ||||
| 								await modelfiles.set(await getModelfiles(localStorage.token)); | ||||
| 							}} | ||||
| 						> | ||||
| 							<div class=" self-center"> | ||||
| 								<svg | ||||
| 									xmlns="http://www.w3.org/2000/svg" | ||||
| 									fill="none" | ||||
| 									viewBox="0 0 24 24" | ||||
| 									stroke-width="1.5" | ||||
| 									stroke="currentColor" | ||||
| 									class="w-4 h-4" | ||||
| 								> | ||||
| 									<path | ||||
| 										stroke-linecap="round" | ||||
| 										stroke-linejoin="round" | ||||
| 										d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" | ||||
| 									/> | ||||
| 								</svg> | ||||
| 							</div> | ||||
| 						</button> | ||||
| 					</div> | ||||
| 						<div class=" self-center"> | ||||
| 							<svg | ||||
| 								xmlns="http://www.w3.org/2000/svg" | ||||
| 								viewBox="0 0 16 16" | ||||
| 								fill="currentColor" | ||||
| 								class="w-3.5 h-3.5" | ||||
| 							> | ||||
| 								<path | ||||
| 									fill-rule="evenodd" | ||||
| 									d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z" | ||||
| 									clip-rule="evenodd" | ||||
| 								/> | ||||
| 							</svg> | ||||
| 						</div> | ||||
| 					</button> | ||||
| 				</div> | ||||
| 			{/if} | ||||
| 
 | ||||
| 				{#if localModelfiles.length > 0} | ||||
| 					<div class="flex"> | ||||
| 						<div class=" self-center text-sm font-medium mr-4"> | ||||
| 							{localModelfiles.length} Local Modelfiles Detected | ||||
| 						</div> | ||||
| 
 | ||||
| 						<div class="flex space-x-1"> | ||||
| 							<button | ||||
| 								class="self-center w-fit text-sm px-3 py-1 border dark:border-gray-600 rounded-xl flex" | ||||
| 								on:click={async () => { | ||||
| 									for (const modelfile of localModelfiles) { | ||||
| 										await createNewModelfile(localStorage.token, modelfile).catch((error) => { | ||||
| 											return null; | ||||
| 										}); | ||||
| 									} | ||||
| 
 | ||||
| 									saveModelfiles(localModelfiles); | ||||
| 									localStorage.removeItem('modelfiles'); | ||||
| 									localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]'); | ||||
| 									await modelfiles.set(await getModelfiles(localStorage.token)); | ||||
| 								}} | ||||
| 							> | ||||
| 								<div class=" self-center mr-2 font-medium">Sync All</div> | ||||
| 
 | ||||
| 								<div class=" self-center"> | ||||
| 									<svg | ||||
| 										xmlns="http://www.w3.org/2000/svg" | ||||
| 										viewBox="0 0 16 16" | ||||
| 										fill="currentColor" | ||||
| 										class="w-3.5 h-3.5" | ||||
| 									> | ||||
| 										<path | ||||
| 											fill-rule="evenodd" | ||||
| 											d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z" | ||||
| 											clip-rule="evenodd" | ||||
| 										/> | ||||
| 									</svg> | ||||
| 								</div> | ||||
| 							</button> | ||||
| 
 | ||||
| 							<button | ||||
| 								class="self-center w-fit text-sm p-1.5 border dark:border-gray-600 rounded-xl flex" | ||||
| 								on:click={async () => { | ||||
| 									saveModelfiles(localModelfiles); | ||||
| 
 | ||||
| 									localStorage.removeItem('modelfiles'); | ||||
| 									localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]'); | ||||
| 									await modelfiles.set(await getModelfiles(localStorage.token)); | ||||
| 								}} | ||||
| 							> | ||||
| 								<div class=" self-center"> | ||||
| 									<svg | ||||
| 										xmlns="http://www.w3.org/2000/svg" | ||||
| 										fill="none" | ||||
| 										viewBox="0 0 24 24" | ||||
| 										stroke-width="1.5" | ||||
| 										stroke="currentColor" | ||||
| 										class="w-4 h-4" | ||||
| 									> | ||||
| 										<path | ||||
| 											stroke-linecap="round" | ||||
| 											stroke-linejoin="round" | ||||
| 											d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" | ||||
| 										/> | ||||
| 									</svg> | ||||
| 								</div> | ||||
| 							</button> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				{/if} | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class=" my-16"> | ||||
| 				<div class=" text-2xl font-semibold mb-6">Made by OllamaHub Community</div> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy Jaeryang Baek
						Timothy Jaeryang Baek