forked from open-webui/open-webui
		
	refac: settings
This commit is contained in:
		
							parent
							
								
									87a1927447
								
							
						
					
					
						commit
						cd9f0135f6
					
				
					 12 changed files with 2381 additions and 2325 deletions
				
			
		
							
								
								
									
										63
									
								
								src/lib/components/chat/Settings/About.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/lib/components/chat/Settings/About.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | |||
| <script lang="ts"> | ||||
| 	import { getOllamaVersion } from '$lib/apis/ollama'; | ||||
| 	import { WEB_UI_VERSION } from '$lib/constants'; | ||||
| 	import { config } from '$lib/stores'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 
 | ||||
| 	let ollamaVersion = ''; | ||||
| 	onMount(async () => { | ||||
| 		ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => { | ||||
| 			return ''; | ||||
| 		}); | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6"> | ||||
| 	<div class=" space-y-3"> | ||||
| 		<div> | ||||
| 			<div class=" mb-2.5 text-sm font-medium">Ollama Web UI Version</div> | ||||
| 			<div class="flex w-full"> | ||||
| 				<div class="flex-1 text-xs text-gray-700 dark:text-gray-200"> | ||||
| 					{$config && $config.version ? $config.version : WEB_UI_VERSION} | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 		<div> | ||||
| 			<div class=" mb-2.5 text-sm font-medium">Ollama Version</div> | ||||
| 			<div class="flex w-full"> | ||||
| 				<div class="flex-1 text-xs text-gray-700 dark:text-gray-200"> | ||||
| 					{ollamaVersion ?? 'N/A'} | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<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" | ||||
| 				href="https://github.com/tjbck" | ||||
| 				target="_blank">Timothy J. Baek</a | ||||
| 			> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
							
								
								
									
										249
									
								
								src/lib/components/chat/Settings/AddOns.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										249
									
								
								src/lib/components/chat/Settings/AddOns.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,249 @@ | |||
| <script lang="ts"> | ||||
| 	import toast from 'svelte-french-toast'; | ||||
| 	import { createEventDispatcher, onMount } from 'svelte'; | ||||
| 	import { models, voices } from '$lib/stores'; | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
| 	export let saveSettings: Function; | ||||
| 	// Addons | ||||
| 	let titleAutoGenerate = true; | ||||
| 	let speechAutoSend = false; | ||||
| 	let responseAutoCopy = false; | ||||
| 
 | ||||
| 	let gravatarEmail = ''; | ||||
| 	let titleAutoGenerateModel = ''; | ||||
| 
 | ||||
| 	// Voice | ||||
| 	let speakVoice = ''; | ||||
| 
 | ||||
| 	const toggleSpeechAutoSend = async () => { | ||||
| 		speechAutoSend = !speechAutoSend; | ||||
| 		saveSettings({ speechAutoSend: speechAutoSend }); | ||||
| 	}; | ||||
| 
 | ||||
| 	const toggleTitleAutoGenerate = async () => { | ||||
| 		titleAutoGenerate = !titleAutoGenerate; | ||||
| 		saveSettings({ titleAutoGenerate: titleAutoGenerate }); | ||||
| 	}; | ||||
| 
 | ||||
| 	const toggleResponseAutoCopy = async () => { | ||||
| 		const permission = await navigator.clipboard | ||||
| 			.readText() | ||||
| 			.then(() => { | ||||
| 				return 'granted'; | ||||
| 			}) | ||||
| 			.catch(() => { | ||||
| 				return ''; | ||||
| 			}); | ||||
| 
 | ||||
| 		console.log(permission); | ||||
| 
 | ||||
| 		if (permission === 'granted') { | ||||
| 			responseAutoCopy = !responseAutoCopy; | ||||
| 			saveSettings({ responseAutoCopy: responseAutoCopy }); | ||||
| 		} else { | ||||
| 			toast.error( | ||||
| 				'Clipboard write permission denied. Please check your browser settings to grant the necessary access.' | ||||
| 			); | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	onMount(async () => { | ||||
| 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||
| 
 | ||||
| 		titleAutoGenerate = settings.titleAutoGenerate ?? true; | ||||
| 		speechAutoSend = settings.speechAutoSend ?? false; | ||||
| 		responseAutoCopy = settings.responseAutoCopy ?? false; | ||||
| 		titleAutoGenerateModel = settings.titleAutoGenerateModel ?? ''; | ||||
| 		gravatarEmail = settings.gravatarEmail ?? ''; | ||||
| 		speakVoice = settings.speakVoice ?? ''; | ||||
| 
 | ||||
| 		const getVoicesLoop = setInterval(async () => { | ||||
| 			const _voices = await speechSynthesis.getVoices(); | ||||
| 			await voices.set(_voices); | ||||
| 
 | ||||
| 			// do your loop | ||||
| 			if (_voices.length > 0) { | ||||
| 				clearInterval(getVoicesLoop); | ||||
| 			} | ||||
| 		}, 100); | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <form | ||||
| 	class="flex flex-col h-full justify-between space-y-3 text-sm" | ||||
| 	on:submit|preventDefault={() => { | ||||
| 		saveSettings({ | ||||
| 			speakVoice: speakVoice !== '' ? speakVoice : undefined | ||||
| 		}); | ||||
| 		dispatch('save'); | ||||
| 	}} | ||||
| > | ||||
| 	<div class=" space-y-3"> | ||||
| 		<div> | ||||
| 			<div class=" mb-1 text-sm font-medium">WebUI Add-ons</div> | ||||
| 
 | ||||
| 			<div> | ||||
| 				<div class=" py-0.5 flex w-full justify-between"> | ||||
| 					<div class=" self-center text-xs font-medium">Title Auto-Generation</div> | ||||
| 
 | ||||
| 					<button | ||||
| 						class="p-1 px-3 text-xs flex rounded transition" | ||||
| 						on:click={() => { | ||||
| 							toggleTitleAutoGenerate(); | ||||
| 						}} | ||||
| 						type="button" | ||||
| 					> | ||||
| 						{#if titleAutoGenerate === true} | ||||
| 							<span class="ml-2 self-center">On</span> | ||||
| 						{:else} | ||||
| 							<span class="ml-2 self-center">Off</span> | ||||
| 						{/if} | ||||
| 					</button> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div> | ||||
| 				<div class=" py-0.5 flex w-full justify-between"> | ||||
| 					<div class=" self-center text-xs font-medium">Voice Input Auto-Send</div> | ||||
| 
 | ||||
| 					<button | ||||
| 						class="p-1 px-3 text-xs flex rounded transition" | ||||
| 						on:click={() => { | ||||
| 							toggleSpeechAutoSend(); | ||||
| 						}} | ||||
| 						type="button" | ||||
| 					> | ||||
| 						{#if speechAutoSend === true} | ||||
| 							<span class="ml-2 self-center">On</span> | ||||
| 						{:else} | ||||
| 							<span class="ml-2 self-center">Off</span> | ||||
| 						{/if} | ||||
| 					</button> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div> | ||||
| 				<div class=" py-0.5 flex w-full justify-between"> | ||||
| 					<div class=" self-center text-xs font-medium">Response AutoCopy to Clipboard</div> | ||||
| 
 | ||||
| 					<button | ||||
| 						class="p-1 px-3 text-xs flex rounded transition" | ||||
| 						on:click={() => { | ||||
| 							toggleResponseAutoCopy(); | ||||
| 						}} | ||||
| 						type="button" | ||||
| 					> | ||||
| 						{#if responseAutoCopy === true} | ||||
| 							<span class="ml-2 self-center">On</span> | ||||
| 						{:else} | ||||
| 							<span class="ml-2 self-center">Off</span> | ||||
| 						{/if} | ||||
| 					</button> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 		<div> | ||||
| 			<div class=" mb-2.5 text-sm font-medium">Set Title Auto-Generation Model</div> | ||||
| 			<div class="flex w-full"> | ||||
| 				<div class="flex-1 mr-2"> | ||||
| 					<select | ||||
| 						class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" | ||||
| 						bind:value={titleAutoGenerateModel} | ||||
| 						placeholder="Select a model" | ||||
| 					> | ||||
| 						<option value="" selected>Current Model</option> | ||||
| 						{#each $models.filter((m) => m.size != null) as model} | ||||
| 							<option value={model.name} class="bg-gray-100 dark:bg-gray-700" | ||||
| 								>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option | ||||
| 							> | ||||
| 						{/each} | ||||
| 					</select> | ||||
| 				</div> | ||||
| 				<button | ||||
| 					class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition" | ||||
| 					on:click={() => { | ||||
| 						saveSettings({ | ||||
| 							titleAutoGenerateModel: | ||||
| 								titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined | ||||
| 						}); | ||||
| 					}} | ||||
| 					type="button" | ||||
| 				> | ||||
| 					<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> | ||||
| 				</button> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 		<div class=" space-y-3"> | ||||
| 			<div> | ||||
| 				<div class=" mb-2.5 text-sm font-medium">Set Default Voice</div> | ||||
| 				<div class="flex w-full"> | ||||
| 					<div class="flex-1"> | ||||
| 						<select | ||||
| 							class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" | ||||
| 							bind:value={speakVoice} | ||||
| 							placeholder="Select a voice" | ||||
| 						> | ||||
| 							<option value="" selected>Default</option> | ||||
| 							{#each $voices.filter((v) => v.localService === true) as voice} | ||||
| 								<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option | ||||
| 								> | ||||
| 							{/each} | ||||
| 						</select> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<!-- | ||||
| 							<div> | ||||
| 								<div class=" mb-2.5 text-sm font-medium"> | ||||
| 									Gravatar Email <span class=" text-gray-400 text-sm">(optional)</span> | ||||
| 								</div> | ||||
| 								<div class="flex w-full"> | ||||
| 									<div class="flex-1"> | ||||
| 										<input | ||||
| 											class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" | ||||
| 											placeholder="Enter Your Email" | ||||
| 											bind:value={gravatarEmail} | ||||
| 											autocomplete="off" | ||||
| 											type="email" | ||||
| 										/> | ||||
| 									</div> | ||||
| 								</div> | ||||
| 								<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> | ||||
| 									Changes user profile image to match your <a | ||||
| 										class=" text-gray-500 dark:text-gray-300 font-medium" | ||||
| 										href="https://gravatar.com/" | ||||
| 										target="_blank">Gravatar.</a | ||||
| 									> | ||||
| 								</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> | ||||
|  | @ -1,8 +1,15 @@ | |||
| <script lang="ts"> | ||||
| 	export let options = { | ||||
| 	import { createEventDispatcher, onMount } from 'svelte'; | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
| 	import AdvancedParams from './Advanced/AdvancedParams.svelte'; | ||||
| 	export let saveSettings: Function; | ||||
| 
 | ||||
| 	// Advanced | ||||
| 	let requestFormat = ''; | ||||
| 	let options = { | ||||
| 		// Advanced | ||||
| 		seed: 0, | ||||
| 		stop: '', | ||||
| 		temperature: '', | ||||
| 		repeat_penalty: '', | ||||
| 		repeat_last_n: '', | ||||
|  | @ -11,546 +18,101 @@ | |||
| 		mirostat_tau: '', | ||||
| 		top_k: '', | ||||
| 		top_p: '', | ||||
| 		stop: '', | ||||
| 		tfs_z: '', | ||||
| 		num_ctx: '', | ||||
| 		num_predict: '' | ||||
| 	}; | ||||
| 
 | ||||
| 	const toggleRequestFormat = async () => { | ||||
| 		if (requestFormat === '') { | ||||
| 			requestFormat = 'json'; | ||||
| 		} else { | ||||
| 			requestFormat = ''; | ||||
| 		} | ||||
| 
 | ||||
| 		saveSettings({ requestFormat: requestFormat !== '' ? requestFormat : undefined }); | ||||
| 	}; | ||||
| 
 | ||||
| 	onMount(() => { | ||||
| 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||
| 
 | ||||
| 		requestFormat = settings.requestFormat ?? ''; | ||||
| 
 | ||||
| 		options.seed = settings.seed ?? 0; | ||||
| 		options.temperature = settings.temperature ?? ''; | ||||
| 		options.repeat_penalty = settings.repeat_penalty ?? ''; | ||||
| 		options.top_k = settings.top_k ?? ''; | ||||
| 		options.top_p = settings.top_p ?? ''; | ||||
| 		options.num_ctx = settings.num_ctx ?? ''; | ||||
| 		options = { ...options, ...settings.options }; | ||||
| 		options.stop = (settings?.options?.stop ?? []).join(','); | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <div class=" space-y-3 text-xs"> | ||||
| 	<div> | ||||
| 		<div class=" py-0.5 flex w-full justify-between"> | ||||
| 			<div class=" w-20 text-xs font-medium self-center">Seed</div> | ||||
| 			<div class=" flex-1 self-center"> | ||||
| 				<input | ||||
| 					class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" | ||||
| 					type="number" | ||||
| 					placeholder="Enter Seed" | ||||
| 					bind:value={options.seed} | ||||
| 					autocomplete="off" | ||||
| 					min="0" | ||||
| 				/> | ||||
| <div class="flex flex-col h-full justify-between text-sm"> | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> | ||||
| 		<div class=" text-sm font-medium">Parameters</div> | ||||
| 
 | ||||
| 		<AdvancedParams bind:options /> | ||||
| 		<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 		<div> | ||||
| 			<div class=" py-1 flex w-full justify-between"> | ||||
| 				<div class=" self-center text-sm font-medium">Request Mode</div> | ||||
| 
 | ||||
| 				<button | ||||
| 					class="p-1 px-3 text-xs flex rounded transition" | ||||
| 					on:click={() => { | ||||
| 						toggleRequestFormat(); | ||||
| 					}} | ||||
| 				> | ||||
| 					{#if requestFormat === ''} | ||||
| 						<span class="ml-2 self-center"> Default </span> | ||||
| 					{:else if requestFormat === 'json'} | ||||
| 						<!-- <svg | ||||
|                             xmlns="http://www.w3.org/2000/svg" | ||||
|                             viewBox="0 0 20 20" | ||||
|                             fill="currentColor" | ||||
|                             class="w-4 h-4 self-center" | ||||
|                         > | ||||
|                             <path | ||||
|                                 d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z" | ||||
|                             /> | ||||
|                         </svg> --> | ||||
| 						<span class="ml-2 self-center"> JSON </span> | ||||
| 					{/if} | ||||
| 				</button> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div> | ||||
| 		<div class=" py-0.5 flex w-full justify-between"> | ||||
| 			<div class=" w-20 text-xs font-medium self-center">Stop Sequence</div> | ||||
| 			<div class=" flex-1 self-center"> | ||||
| 				<input | ||||
| 					class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" | ||||
| 					type="text" | ||||
| 					placeholder="Enter Stop Sequence" | ||||
| 					bind:value={options.stop} | ||||
| 					autocomplete="off" | ||||
| 				/> | ||||
| 			</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" | ||||
| 			on:click={() => { | ||||
| 				saveSettings({ | ||||
| 					options: { | ||||
| 						seed: (options.seed !== 0 ? options.seed : undefined) ?? 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, | ||||
| 						repeat_last_n: options.repeat_last_n !== '' ? options.repeat_last_n : undefined, | ||||
| 						mirostat: options.mirostat !== '' ? options.mirostat : undefined, | ||||
| 						mirostat_eta: options.mirostat_eta !== '' ? options.mirostat_eta : undefined, | ||||
| 						mirostat_tau: options.mirostat_tau !== '' ? options.mirostat_tau : undefined, | ||||
| 						top_k: options.top_k !== '' ? options.top_k : undefined, | ||||
| 						top_p: options.top_p !== '' ? options.top_p : undefined, | ||||
| 						tfs_z: options.tfs_z !== '' ? options.tfs_z : undefined, | ||||
| 						num_ctx: options.num_ctx !== '' ? options.num_ctx : undefined, | ||||
| 						num_predict: options.num_predict !== '' ? options.num_predict : undefined | ||||
| 					} | ||||
| 				}); | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Temperature</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.temperature = options.temperature === '' ? 0.8 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.temperature === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.temperature !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="0" | ||||
| 						max="1" | ||||
| 						step="0.05" | ||||
| 						bind:value={options.temperature} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.temperature} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="0" | ||||
| 						max="1" | ||||
| 						step="0.05" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Mirostat</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.mirostat = options.mirostat === '' ? 0 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.mirostat === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.mirostat !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="0" | ||||
| 						max="2" | ||||
| 						step="1" | ||||
| 						bind:value={options.mirostat} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.mirostat} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="0" | ||||
| 						max="2" | ||||
| 						step="1" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Mirostat Eta</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.mirostat_eta = options.mirostat_eta === '' ? 0.1 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.mirostat_eta === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.mirostat_eta !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="0" | ||||
| 						max="1" | ||||
| 						step="0.05" | ||||
| 						bind:value={options.mirostat_eta} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.mirostat_eta} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="0" | ||||
| 						max="1" | ||||
| 						step="0.05" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Mirostat Tau</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.mirostat_tau = options.mirostat_tau === '' ? 5.0 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.mirostat_tau === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.mirostat_tau !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="0" | ||||
| 						max="10" | ||||
| 						step="0.5" | ||||
| 						bind:value={options.mirostat_tau} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.mirostat_tau} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="0" | ||||
| 						max="10" | ||||
| 						step="0.5" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Top K</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.top_k = options.top_k === '' ? 40 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.top_k === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.top_k !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="0" | ||||
| 						max="100" | ||||
| 						step="0.5" | ||||
| 						bind:value={options.top_k} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.top_k} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="0" | ||||
| 						max="100" | ||||
| 						step="0.5" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Top P</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.top_p = options.top_p === '' ? 0.9 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.top_p === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.top_p !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="0" | ||||
| 						max="1" | ||||
| 						step="0.05" | ||||
| 						bind:value={options.top_p} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.top_p} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="0" | ||||
| 						max="1" | ||||
| 						step="0.05" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Repeat Penalty</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.repeat_penalty = options.repeat_penalty === '' ? 1.1 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.repeat_penalty === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.repeat_penalty !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="0" | ||||
| 						max="2" | ||||
| 						step="0.05" | ||||
| 						bind:value={options.repeat_penalty} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.repeat_penalty} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="0" | ||||
| 						max="2" | ||||
| 						step="0.05" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Repeat Last N</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.repeat_last_n = options.repeat_last_n === '' ? 64 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.repeat_last_n === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.repeat_last_n !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="-1" | ||||
| 						max="128" | ||||
| 						step="1" | ||||
| 						bind:value={options.repeat_last_n} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.repeat_last_n} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="-1" | ||||
| 						max="128" | ||||
| 						step="1" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Tfs Z</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.tfs_z = options.tfs_z === '' ? 1 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.tfs_z === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.tfs_z !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="0" | ||||
| 						max="2" | ||||
| 						step="0.05" | ||||
| 						bind:value={options.tfs_z} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.tfs_z} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="0" | ||||
| 						max="2" | ||||
| 						step="0.05" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Context Length</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.num_ctx = options.num_ctx === '' ? 2048 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.num_ctx === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.num_ctx !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="1" | ||||
| 						max="16000" | ||||
| 						step="1" | ||||
| 						bind:value={options.num_ctx} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div class=""> | ||||
| 					<input | ||||
| 						bind:value={options.num_ctx} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="1" | ||||
| 						max="16000" | ||||
| 						step="1" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Max Tokens</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.num_predict = options.num_predict === '' ? 128 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.num_predict === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.num_predict !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="-2" | ||||
| 						max="16000" | ||||
| 						step="1" | ||||
| 						bind:value={options.num_predict} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div class=""> | ||||
| 					<input | ||||
| 						bind:value={options.num_predict} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="-2" | ||||
| 						max="16000" | ||||
| 						step="1" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 				dispatch('save'); | ||||
| 			}} | ||||
| 		> | ||||
| 			Save | ||||
| 		</button> | ||||
| 	</div> | ||||
| </div> | ||||
|  |  | |||
							
								
								
									
										556
									
								
								src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										556
									
								
								src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,556 @@ | |||
| <script lang="ts"> | ||||
| 	export let options = { | ||||
| 		// Advanced | ||||
| 		seed: 0, | ||||
| 		stop: '', | ||||
| 		temperature: '', | ||||
| 		repeat_penalty: '', | ||||
| 		repeat_last_n: '', | ||||
| 		mirostat: '', | ||||
| 		mirostat_eta: '', | ||||
| 		mirostat_tau: '', | ||||
| 		top_k: '', | ||||
| 		top_p: '', | ||||
| 		tfs_z: '', | ||||
| 		num_ctx: '', | ||||
| 		num_predict: '' | ||||
| 	}; | ||||
| </script> | ||||
| 
 | ||||
| <div class=" space-y-3 text-xs"> | ||||
| 	<div> | ||||
| 		<div class=" py-0.5 flex w-full justify-between"> | ||||
| 			<div class=" w-20 text-xs font-medium self-center">Seed</div> | ||||
| 			<div class=" flex-1 self-center"> | ||||
| 				<input | ||||
| 					class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" | ||||
| 					type="number" | ||||
| 					placeholder="Enter Seed" | ||||
| 					bind:value={options.seed} | ||||
| 					autocomplete="off" | ||||
| 					min="0" | ||||
| 				/> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div> | ||||
| 		<div class=" py-0.5 flex w-full justify-between"> | ||||
| 			<div class=" w-20 text-xs font-medium self-center">Stop Sequence</div> | ||||
| 			<div class=" flex-1 self-center"> | ||||
| 				<input | ||||
| 					class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" | ||||
| 					type="text" | ||||
| 					placeholder="Enter Stop Sequence" | ||||
| 					bind:value={options.stop} | ||||
| 					autocomplete="off" | ||||
| 				/> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Temperature</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.temperature = options.temperature === '' ? 0.8 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.temperature === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.temperature !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="0" | ||||
| 						max="1" | ||||
| 						step="0.05" | ||||
| 						bind:value={options.temperature} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.temperature} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="0" | ||||
| 						max="1" | ||||
| 						step="0.05" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Mirostat</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.mirostat = options.mirostat === '' ? 0 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.mirostat === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.mirostat !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="0" | ||||
| 						max="2" | ||||
| 						step="1" | ||||
| 						bind:value={options.mirostat} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.mirostat} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="0" | ||||
| 						max="2" | ||||
| 						step="1" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Mirostat Eta</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.mirostat_eta = options.mirostat_eta === '' ? 0.1 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.mirostat_eta === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.mirostat_eta !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="0" | ||||
| 						max="1" | ||||
| 						step="0.05" | ||||
| 						bind:value={options.mirostat_eta} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.mirostat_eta} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="0" | ||||
| 						max="1" | ||||
| 						step="0.05" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Mirostat Tau</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.mirostat_tau = options.mirostat_tau === '' ? 5.0 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.mirostat_tau === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.mirostat_tau !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="0" | ||||
| 						max="10" | ||||
| 						step="0.5" | ||||
| 						bind:value={options.mirostat_tau} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.mirostat_tau} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="0" | ||||
| 						max="10" | ||||
| 						step="0.5" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Top K</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.top_k = options.top_k === '' ? 40 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.top_k === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.top_k !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="0" | ||||
| 						max="100" | ||||
| 						step="0.5" | ||||
| 						bind:value={options.top_k} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.top_k} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="0" | ||||
| 						max="100" | ||||
| 						step="0.5" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Top P</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.top_p = options.top_p === '' ? 0.9 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.top_p === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.top_p !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="0" | ||||
| 						max="1" | ||||
| 						step="0.05" | ||||
| 						bind:value={options.top_p} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.top_p} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="0" | ||||
| 						max="1" | ||||
| 						step="0.05" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Repeat Penalty</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.repeat_penalty = options.repeat_penalty === '' ? 1.1 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.repeat_penalty === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.repeat_penalty !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="0" | ||||
| 						max="2" | ||||
| 						step="0.05" | ||||
| 						bind:value={options.repeat_penalty} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.repeat_penalty} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="0" | ||||
| 						max="2" | ||||
| 						step="0.05" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Repeat Last N</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.repeat_last_n = options.repeat_last_n === '' ? 64 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.repeat_last_n === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.repeat_last_n !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="-1" | ||||
| 						max="128" | ||||
| 						step="1" | ||||
| 						bind:value={options.repeat_last_n} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.repeat_last_n} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="-1" | ||||
| 						max="128" | ||||
| 						step="1" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Tfs Z</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.tfs_z = options.tfs_z === '' ? 1 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.tfs_z === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.tfs_z !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="0" | ||||
| 						max="2" | ||||
| 						step="0.05" | ||||
| 						bind:value={options.tfs_z} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<input | ||||
| 						bind:value={options.tfs_z} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="0" | ||||
| 						max="2" | ||||
| 						step="0.05" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Context Length</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.num_ctx = options.num_ctx === '' ? 2048 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.num_ctx === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.num_ctx !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="1" | ||||
| 						max="16000" | ||||
| 						step="1" | ||||
| 						bind:value={options.num_ctx} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div class=""> | ||||
| 					<input | ||||
| 						bind:value={options.num_ctx} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="1" | ||||
| 						max="16000" | ||||
| 						step="1" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Max Tokens</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					options.num_predict = options.num_predict === '' ? 128 : ''; | ||||
| 				}} | ||||
| 			> | ||||
| 				{#if options.num_predict === ''} | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		{#if options.num_predict !== ''} | ||||
| 			<div class="flex mt-0.5 space-x-2"> | ||||
| 				<div class=" flex-1"> | ||||
| 					<input | ||||
| 						id="steps-range" | ||||
| 						type="range" | ||||
| 						min="-2" | ||||
| 						max="16000" | ||||
| 						step="1" | ||||
| 						bind:value={options.num_predict} | ||||
| 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div class=""> | ||||
| 					<input | ||||
| 						bind:value={options.num_predict} | ||||
| 						type="number" | ||||
| 						class=" bg-transparent text-center w-14" | ||||
| 						min="-2" | ||||
| 						max="16000" | ||||
| 						step="1" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| </div> | ||||
							
								
								
									
										353
									
								
								src/lib/components/chat/Settings/Chats.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										353
									
								
								src/lib/components/chat/Settings/Chats.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,353 @@ | |||
| <script lang="ts"> | ||||
| 	import fileSaver from 'file-saver'; | ||||
| 	const { saveAs } = fileSaver; | ||||
| 
 | ||||
| 	import { resetVectorDB } from '$lib/apis/rag'; | ||||
| 	import { chats, user } from '$lib/stores'; | ||||
| 
 | ||||
| 	import { | ||||
| 		createNewChat, | ||||
| 		deleteAllChats, | ||||
| 		getAllChats, | ||||
| 		getAllUserChats, | ||||
| 		getChatList | ||||
| 	} from '$lib/apis/chats'; | ||||
| 	import { getImportOrigin, convertOpenAIChats } from '$lib/utils'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import toast from 'svelte-french-toast'; | ||||
| 
 | ||||
| 	export let saveSettings: Function; | ||||
| 	// Chats | ||||
| 	let saveChatHistory = true; | ||||
| 	let importFiles; | ||||
| 	let showDeleteConfirm = false; | ||||
| 
 | ||||
| 	$: if (importFiles) { | ||||
| 		console.log(importFiles); | ||||
| 
 | ||||
| 		let reader = new FileReader(); | ||||
| 		reader.onload = (event) => { | ||||
| 			let chats = JSON.parse(event.target.result); | ||||
| 			console.log(chats); | ||||
| 			if (getImportOrigin(chats) == 'openai') { | ||||
| 				try { | ||||
| 					chats = convertOpenAIChats(chats); | ||||
| 				} catch (error) { | ||||
| 					console.log('Unable to import chats:', error); | ||||
| 				} | ||||
| 			} | ||||
| 			importChats(chats); | ||||
| 		}; | ||||
| 
 | ||||
| 		if (importFiles.length > 0) { | ||||
| 			reader.readAsText(importFiles[0]); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	const importChats = async (_chats) => { | ||||
| 		for (const chat of _chats) { | ||||
| 			console.log(chat); | ||||
| 
 | ||||
| 			if (chat.chat) { | ||||
| 				await createNewChat(localStorage.token, chat.chat); | ||||
| 			} else { | ||||
| 				await createNewChat(localStorage.token, chat); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		await chats.set(await getChatList(localStorage.token)); | ||||
| 	}; | ||||
| 
 | ||||
| 	const exportChats = async () => { | ||||
| 		let blob = new Blob([JSON.stringify(await getAllChats(localStorage.token))], { | ||||
| 			type: 'application/json' | ||||
| 		}); | ||||
| 		saveAs(blob, `chat-export-${Date.now()}.json`); | ||||
| 	}; | ||||
| 
 | ||||
| 	const exportAllUserChats = async () => { | ||||
| 		let blob = new Blob([JSON.stringify(await getAllUserChats(localStorage.token))], { | ||||
| 			type: 'application/json' | ||||
| 		}); | ||||
| 		saveAs(blob, `all-chats-export-${Date.now()}.json`); | ||||
| 	}; | ||||
| 
 | ||||
| 	const deleteChats = async () => { | ||||
| 		await goto('/'); | ||||
| 		await deleteAllChats(localStorage.token); | ||||
| 		await chats.set(await getChatList(localStorage.token)); | ||||
| 	}; | ||||
| 
 | ||||
| 	const toggleSaveChatHistory = async () => { | ||||
| 		saveChatHistory = !saveChatHistory; | ||||
| 		console.log(saveChatHistory); | ||||
| 
 | ||||
| 		if (saveChatHistory === false) { | ||||
| 			await goto('/'); | ||||
| 		} | ||||
| 		saveSettings({ saveChatHistory: saveChatHistory }); | ||||
| 	}; | ||||
| 
 | ||||
| 	onMount(async () => { | ||||
| 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||
| 
 | ||||
| 		saveChatHistory = settings.saveChatHistory ?? true; | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex flex-col h-full justify-between space-y-3 text-sm"> | ||||
| 	<div class=" space-y-2"> | ||||
| 		<div | ||||
| 			class="flex flex-col justify-between rounded-md items-center py-2 px-3.5 w-full transition" | ||||
| 		> | ||||
| 			<div class="flex w-full justify-between"> | ||||
| 				<div class=" self-center text-sm font-medium">Chat History</div> | ||||
| 
 | ||||
| 				<button | ||||
| 					class="p-1 px-3 text-xs flex rounded transition" | ||||
| 					type="button" | ||||
| 					on:click={() => { | ||||
| 						toggleSaveChatHistory(); | ||||
| 					}} | ||||
| 				> | ||||
| 					{#if saveChatHistory === true} | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							viewBox="0 0 16 16" | ||||
| 							fill="currentColor" | ||||
| 							class="w-4 h-4" | ||||
| 						> | ||||
| 							<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" /> | ||||
| 							<path | ||||
| 								fill-rule="evenodd" | ||||
| 								d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" | ||||
| 								clip-rule="evenodd" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 
 | ||||
| 						<span class="ml-2 self-center"> On </span> | ||||
| 					{:else} | ||||
| 						<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="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z" | ||||
| 								clip-rule="evenodd" | ||||
| 							/> | ||||
| 							<path | ||||
| 								d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 
 | ||||
| 						<span class="ml-2 self-center">Off</span> | ||||
| 					{/if} | ||||
| 				</button> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class="text-xs text-left w-full font-medium mt-0.5"> | ||||
| 				This setting does not sync across browsers or devices. | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 		<div class="flex flex-col"> | ||||
| 			<input id="chat-import-input" bind:files={importFiles} type="file" accept=".json" hidden /> | ||||
| 			<button | ||||
| 				class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" | ||||
| 				on:click={() => { | ||||
| 					document.getElementById('chat-import-input').click(); | ||||
| 				}} | ||||
| 			> | ||||
| 				<div class=" self-center mr-3"> | ||||
| 					<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="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> | ||||
| 				<div class=" self-center text-sm font-medium">Import Chats</div> | ||||
| 			</button> | ||||
| 			<button | ||||
| 				class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" | ||||
| 				on:click={() => { | ||||
| 					exportChats(); | ||||
| 				}} | ||||
| 			> | ||||
| 				<div class=" self-center mr-3"> | ||||
| 					<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="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 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z" | ||||
| 							clip-rule="evenodd" | ||||
| 						/> | ||||
| 					</svg> | ||||
| 				</div> | ||||
| 				<div class=" self-center text-sm font-medium">Export Chats</div> | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 		{#if showDeleteConfirm} | ||||
| 			<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition"> | ||||
| 				<div class="flex items-center space-x-3"> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						viewBox="0 0 16 16" | ||||
| 						fill="currentColor" | ||||
| 						class="w-4 h-4" | ||||
| 					> | ||||
| 						<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" /> | ||||
| 						<path | ||||
| 							fill-rule="evenodd" | ||||
| 							d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z" | ||||
| 							clip-rule="evenodd" | ||||
| 						/> | ||||
| 					</svg> | ||||
| 					<span>Are you sure?</span> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<div class="flex space-x-1.5 items-center"> | ||||
| 					<button | ||||
| 						class="hover:text-white transition" | ||||
| 						on:click={() => { | ||||
| 							deleteChats(); | ||||
| 							showDeleteConfirm = false; | ||||
| 						}} | ||||
| 					> | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							viewBox="0 0 20 20" | ||||
| 							fill="currentColor" | ||||
| 							class="w-4 h-4" | ||||
| 						> | ||||
| 							<path | ||||
| 								fill-rule="evenodd" | ||||
| 								d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" | ||||
| 								clip-rule="evenodd" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 					</button> | ||||
| 					<button | ||||
| 						class="hover:text-white transition" | ||||
| 						on:click={() => { | ||||
| 							showDeleteConfirm = false; | ||||
| 						}} | ||||
| 					> | ||||
| 						<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> | ||||
| 		{:else} | ||||
| 			<button | ||||
| 				class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" | ||||
| 				on:click={() => { | ||||
| 					showDeleteConfirm = true; | ||||
| 				}} | ||||
| 			> | ||||
| 				<div class=" self-center mr-3"> | ||||
| 					<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="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 2H4Zm7 7a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1 0-1.5h4.5A.75.75 0 0 1 11 9Z" | ||||
| 							clip-rule="evenodd" | ||||
| 						/> | ||||
| 					</svg> | ||||
| 				</div> | ||||
| 				<div class=" self-center text-sm font-medium">Delete Chats</div> | ||||
| 			</button> | ||||
| 		{/if} | ||||
| 
 | ||||
| 		{#if $user?.role === 'admin'} | ||||
| 			<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 			<button | ||||
| 				class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" | ||||
| 				on:click={() => { | ||||
| 					exportAllUserChats(); | ||||
| 				}} | ||||
| 			> | ||||
| 				<div class=" self-center mr-3"> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						viewBox="0 0 16 16" | ||||
| 						fill="currentColor" | ||||
| 						class="w-4 h-4" | ||||
| 					> | ||||
| 						<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" /> | ||||
| 						<path | ||||
| 							fill-rule="evenodd" | ||||
| 							d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z" | ||||
| 							clip-rule="evenodd" | ||||
| 						/> | ||||
| 					</svg> | ||||
| 				</div> | ||||
| 				<div class=" self-center text-sm font-medium">Export All Chats (All Users)</div> | ||||
| 			</button> | ||||
| 
 | ||||
| 			<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 			<button | ||||
| 				class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" | ||||
| 				on:click={() => { | ||||
| 					const res = resetVectorDB(localStorage.token).catch((error) => { | ||||
| 						toast.error(error); | ||||
| 						return null; | ||||
| 					}); | ||||
| 
 | ||||
| 					if (res) { | ||||
| 						toast.success('Success'); | ||||
| 					} | ||||
| 				}} | ||||
| 			> | ||||
| 				<div class=" self-center mr-3"> | ||||
| 					<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="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z" | ||||
| 							clip-rule="evenodd" | ||||
| 						/> | ||||
| 					</svg> | ||||
| 				</div> | ||||
| 				<div class=" self-center text-sm font-medium">Reset Vector Storage</div> | ||||
| 			</button> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| </div> | ||||
							
								
								
									
										86
									
								
								src/lib/components/chat/Settings/External.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/lib/components/chat/Settings/External.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,86 @@ | |||
| <script lang="ts"> | ||||
| 	import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai'; | ||||
| 	import { models, user } from '$lib/stores'; | ||||
| 	import { createEventDispatcher, onMount } from 'svelte'; | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
| 	export let getModels: Function; | ||||
| 
 | ||||
| 	// External | ||||
| 	let OPENAI_API_KEY = ''; | ||||
| 	let OPENAI_API_BASE_URL = ''; | ||||
| 
 | ||||
| 	const updateOpenAIHandler = async () => { | ||||
| 		OPENAI_API_BASE_URL = await updateOpenAIUrl(localStorage.token, OPENAI_API_BASE_URL); | ||||
| 		OPENAI_API_KEY = await updateOpenAIKey(localStorage.token, OPENAI_API_KEY); | ||||
| 
 | ||||
| 		await models.set(await getModels()); | ||||
| 	}; | ||||
| 
 | ||||
| 	onMount(async () => { | ||||
| 		if ($user.role === 'admin') { | ||||
| 			OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token); | ||||
| 			OPENAI_API_KEY = await getOpenAIKey(localStorage.token); | ||||
| 		} | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <form | ||||
| 	class="flex flex-col h-full justify-between space-y-3 text-sm" | ||||
| 	on:submit|preventDefault={() => { | ||||
| 		updateOpenAIHandler(); | ||||
| 		dispatch('save'); | ||||
| 
 | ||||
| 		// saveSettings({ | ||||
| 		// 	OPENAI_API_KEY: OPENAI_API_KEY !== '' ? OPENAI_API_KEY : undefined, | ||||
| 		// 	OPENAI_API_BASE_URL: OPENAI_API_BASE_URL !== '' ? OPENAI_API_BASE_URL : undefined | ||||
| 		// }); | ||||
| 	}} | ||||
| > | ||||
| 	<div class=" space-y-3"> | ||||
| 		<div> | ||||
| 			<div class=" mb-2.5 text-sm font-medium">OpenAI API Key</div> | ||||
| 			<div class="flex w-full"> | ||||
| 				<div class="flex-1"> | ||||
| 					<input | ||||
| 						class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" | ||||
| 						placeholder="Enter OpenAI API Key" | ||||
| 						bind:value={OPENAI_API_KEY} | ||||
| 						autocomplete="off" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> | ||||
| 				Adds optional support for online models. | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 		<div> | ||||
| 			<div class=" mb-2.5 text-sm font-medium">OpenAI API Base URL</div> | ||||
| 			<div class="flex w-full"> | ||||
| 				<div class="flex-1"> | ||||
| 					<input | ||||
| 						class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" | ||||
| 						placeholder="Enter OpenAI API Key" | ||||
| 						bind:value={OPENAI_API_BASE_URL} | ||||
| 						autocomplete="off" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> | ||||
| 				WebUI will make requests to <span class=" text-gray-200">'{OPENAI_API_BASE_URL}/chat'</span> | ||||
| 			</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> | ||||
							
								
								
									
										224
									
								
								src/lib/components/chat/Settings/General.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								src/lib/components/chat/Settings/General.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,224 @@ | |||
| <script lang="ts"> | ||||
| 	import toast from 'svelte-french-toast'; | ||||
| 	import { createEventDispatcher, onMount } from 'svelte'; | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
| 	import { getOllamaAPIUrl, updateOllamaAPIUrl } from '$lib/apis/ollama'; | ||||
| 	import { models, user } from '$lib/stores'; | ||||
| 
 | ||||
| 	export let saveSettings: Function; | ||||
| 	export let getModels: Function; | ||||
| 
 | ||||
| 	// General | ||||
| 	let API_BASE_URL = ''; | ||||
| 	let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light']; | ||||
| 	let theme = 'dark'; | ||||
| 	let notificationEnabled = false; | ||||
| 	let system = ''; | ||||
| 
 | ||||
| 	const toggleTheme = async () => { | ||||
| 		if (theme === 'dark') { | ||||
| 			theme = 'light'; | ||||
| 		} else { | ||||
| 			theme = 'dark'; | ||||
| 		} | ||||
| 
 | ||||
| 		localStorage.theme = theme; | ||||
| 
 | ||||
| 		document.documentElement.classList.remove(theme === 'dark' ? 'light' : 'dark'); | ||||
| 		document.documentElement.classList.add(theme); | ||||
| 	}; | ||||
| 
 | ||||
| 	const toggleNotification = async () => { | ||||
| 		const permission = await Notification.requestPermission(); | ||||
| 
 | ||||
| 		if (permission === 'granted') { | ||||
| 			notificationEnabled = !notificationEnabled; | ||||
| 			saveSettings({ notificationEnabled: notificationEnabled }); | ||||
| 		} else { | ||||
| 			toast.error( | ||||
| 				'Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.' | ||||
| 			); | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	const updateOllamaAPIUrlHandler = async () => { | ||||
| 		API_BASE_URL = await updateOllamaAPIUrl(localStorage.token, API_BASE_URL); | ||||
| 		const _models = await getModels('ollama'); | ||||
| 
 | ||||
| 		if (_models.length > 0) { | ||||
| 			toast.success('Server connection verified'); | ||||
| 			await models.set(_models); | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	onMount(async () => { | ||||
| 		if ($user.role === 'admin') { | ||||
| 			API_BASE_URL = await getOllamaAPIUrl(localStorage.token); | ||||
| 		} | ||||
| 
 | ||||
| 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||
| 
 | ||||
| 		theme = localStorage.theme ?? 'dark'; | ||||
| 		notificationEnabled = settings.notificationEnabled ?? false; | ||||
| 		system = settings.system ?? ''; | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex flex-col space-y-3"> | ||||
| 	<div> | ||||
| 		<div class=" mb-1 text-sm font-medium">WebUI Settings</div> | ||||
| 
 | ||||
| 		<div class=" py-0.5 flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">Theme</div> | ||||
| 			<div class="flex items-center relative"> | ||||
| 				<div class=" absolute right-16"> | ||||
| 					{#if theme === 'dark'} | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							viewBox="0 0 20 20" | ||||
| 							fill="currentColor" | ||||
| 							class="w-4 h-4" | ||||
| 						> | ||||
| 							<path | ||||
| 								fill-rule="evenodd" | ||||
| 								d="M7.455 2.004a.75.75 0 01.26.77 7 7 0 009.958 7.967.75.75 0 011.067.853A8.5 8.5 0 116.647 1.921a.75.75 0 01.808.083z" | ||||
| 								clip-rule="evenodd" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 					{:else if theme === 'light'} | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							viewBox="0 0 20 20" | ||||
| 							fill="currentColor" | ||||
| 							class="w-4 h-4 self-center" | ||||
| 						> | ||||
| 							<path | ||||
| 								d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 					{/if} | ||||
| 				</div> | ||||
| 
 | ||||
| 				<select | ||||
| 					class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right" | ||||
| 					bind:value={theme} | ||||
| 					placeholder="Select a theme" | ||||
| 					on:change={(e) => { | ||||
| 						localStorage.theme = theme; | ||||
| 
 | ||||
| 						themes | ||||
| 							.filter((e) => e !== theme) | ||||
| 							.forEach((e) => { | ||||
| 								e.split(' ').forEach((e) => { | ||||
| 									document.documentElement.classList.remove(e); | ||||
| 								}); | ||||
| 							}); | ||||
| 
 | ||||
| 						theme.split(' ').forEach((e) => { | ||||
| 							document.documentElement.classList.add(e); | ||||
| 						}); | ||||
| 
 | ||||
| 						console.log(theme); | ||||
| 					}} | ||||
| 				> | ||||
| 					<option value="dark">Dark</option> | ||||
| 					<option value="light">Light</option> | ||||
| 					<option value="rose-pine dark">Rosé Pine</option> | ||||
| 					<option value="rose-pine-dawn light">Rosé Pine Dawn</option> | ||||
| 				</select> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<div> | ||||
| 			<div class=" py-0.5 flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium">Notification</div> | ||||
| 
 | ||||
| 				<button | ||||
| 					class="p-1 px-3 text-xs flex rounded transition" | ||||
| 					on:click={() => { | ||||
| 						toggleNotification(); | ||||
| 					}} | ||||
| 					type="button" | ||||
| 				> | ||||
| 					{#if notificationEnabled === true} | ||||
| 						<span class="ml-2 self-center">On</span> | ||||
| 					{:else} | ||||
| 						<span class="ml-2 self-center">Off</span> | ||||
| 					{/if} | ||||
| 				</button> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	{#if $user.role === 'admin'} | ||||
| 		<hr class=" dark:border-gray-700" /> | ||||
| 		<div> | ||||
| 			<div class=" mb-2.5 text-sm font-medium">Ollama API URL</div> | ||||
| 			<div class="flex w-full"> | ||||
| 				<div class="flex-1 mr-2"> | ||||
| 					<input | ||||
| 						class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" | ||||
| 						placeholder="Enter URL (e.g. http://localhost:11434/api)" | ||||
| 						bind:value={API_BASE_URL} | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<button | ||||
| 					class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition" | ||||
| 					on:click={() => { | ||||
| 						updateOllamaAPIUrlHandler(); | ||||
| 					}} | ||||
| 				> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						viewBox="0 0 20 20" | ||||
| 						fill="currentColor" | ||||
| 						class="w-4 h-4" | ||||
| 					> | ||||
| 						<path | ||||
| 							fill-rule="evenodd" | ||||
| 							d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" | ||||
| 							clip-rule="evenodd" | ||||
| 						/> | ||||
| 					</svg> | ||||
| 				</button> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> | ||||
| 				Trouble accessing Ollama? | ||||
| 				<a | ||||
| 					class=" text-gray-300 font-medium" | ||||
| 					href="https://github.com/ollama-webui/ollama-webui#troubleshooting" | ||||
| 					target="_blank" | ||||
| 				> | ||||
| 					Click here for help. | ||||
| 				</a> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	{/if} | ||||
| 
 | ||||
| 	<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 	<div> | ||||
| 		<div class=" mb-2.5 text-sm font-medium">System Prompt</div> | ||||
| 		<textarea | ||||
| 			bind:value={system} | ||||
| 			class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none" | ||||
| 			rows="4" | ||||
| 		/> | ||||
| 	</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" | ||||
| 			on:click={() => { | ||||
| 				saveSettings({ | ||||
| 					system: system !== '' ? system : undefined | ||||
| 				}); | ||||
| 				dispatch('save'); | ||||
| 			}} | ||||
| 		> | ||||
| 			Save | ||||
| 		</button> | ||||
| 	</div> | ||||
| </div> | ||||
							
								
								
									
										118
									
								
								src/lib/components/chat/Settings/Interface.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								src/lib/components/chat/Settings/Interface.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,118 @@ | |||
| <script lang="ts"> | ||||
| 	import { getBackendConfig } from '$lib/apis'; | ||||
| 	import { setDefaultPromptSuggestions } from '$lib/apis/configs'; | ||||
| 	import { config, user } from '$lib/stores'; | ||||
| 	import { createEventDispatcher, onMount } from 'svelte'; | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
| 	// Interface | ||||
| 	let promptSuggestions = []; | ||||
| 
 | ||||
| 	const updateInterfaceHandler = async () => { | ||||
| 		promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions); | ||||
| 		await config.set(await getBackendConfig()); | ||||
| 	}; | ||||
| 
 | ||||
| 	onMount(async () => { | ||||
| 		if ($user.role === 'admin') { | ||||
| 			promptSuggestions = $config?.default_prompt_suggestions; | ||||
| 		} | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <form | ||||
| 	class="flex flex-col h-full justify-between space-y-3 text-sm" | ||||
| 	on:submit|preventDefault={() => { | ||||
| 		updateInterfaceHandler(); | ||||
| 		dispatch('save'); | ||||
| 	}} | ||||
| > | ||||
| 	<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"> | ||||
| 		<button | ||||
| 			class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" | ||||
| 			type="submit" | ||||
| 		> | ||||
| 			Save | ||||
| 		</button> | ||||
| 	</div> | ||||
| </form> | ||||
							
								
								
									
										596
									
								
								src/lib/components/chat/Settings/Models.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										596
									
								
								src/lib/components/chat/Settings/Models.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,596 @@ | |||
| <script lang="ts"> | ||||
| 	import queue from 'async/queue'; | ||||
| 	import toast from 'svelte-french-toast'; | ||||
| 
 | ||||
| 	import { createModel, deleteModel, pullModel } from '$lib/apis/ollama'; | ||||
| 	import { WEBUI_API_BASE_URL } from '$lib/constants'; | ||||
| 	import { models, user } from '$lib/stores'; | ||||
| 	import { splitStream } from '$lib/utils'; | ||||
| 
 | ||||
| 	export let getModels: Function; | ||||
| 
 | ||||
| 	// Models | ||||
| 	const MAX_PARALLEL_DOWNLOADS = 3; | ||||
| 	const modelDownloadQueue = queue( | ||||
| 		(task: { modelName: string }, cb) => | ||||
| 			pullModelHandlerProcessor({ modelName: task.modelName, callback: cb }), | ||||
| 		MAX_PARALLEL_DOWNLOADS | ||||
| 	); | ||||
| 	let modelDownloadStatus: Record<string, any> = {}; | ||||
| 
 | ||||
| 	let modelTransferring = false; | ||||
| 	let modelTag = ''; | ||||
| 	let digest = ''; | ||||
| 	let pullProgress = null; | ||||
| 
 | ||||
| 	let modelUploadMode = 'file'; | ||||
| 	let modelInputFile = ''; | ||||
| 	let modelFileUrl = ''; | ||||
| 	let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSSISTANT:"`; | ||||
| 	let modelFileDigest = ''; | ||||
| 	let uploadProgress = null; | ||||
| 
 | ||||
| 	let deleteModelTag = ''; | ||||
| 
 | ||||
| 	const pullModelHandler = async () => { | ||||
| 		const sanitizedModelTag = modelTag.trim(); | ||||
| 		if (modelDownloadStatus[sanitizedModelTag]) { | ||||
| 			toast.error(`Model '${sanitizedModelTag}' is already in queue for downloading.`); | ||||
| 			return; | ||||
| 		} | ||||
| 		if (Object.keys(modelDownloadStatus).length === 3) { | ||||
| 			toast.error('Maximum of 3 models can be downloaded simultaneously. Please try again later.'); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		modelTransferring = true; | ||||
| 
 | ||||
| 		modelDownloadQueue.push( | ||||
| 			{ modelName: sanitizedModelTag }, | ||||
| 			async (data: { modelName: string; success: boolean; error?: Error }) => { | ||||
| 				const { modelName } = data; | ||||
| 				// Remove the downloaded model | ||||
| 				delete modelDownloadStatus[modelName]; | ||||
| 
 | ||||
| 				console.log(data); | ||||
| 
 | ||||
| 				if (!data.success) { | ||||
| 					toast.error(data.error); | ||||
| 				} else { | ||||
| 					toast.success(`Model '${modelName}' has been successfully downloaded.`); | ||||
| 
 | ||||
| 					const notification = new Notification(`Ollama`, { | ||||
| 						body: `Model '${modelName}' has been successfully downloaded.`, | ||||
| 						icon: '/favicon.png' | ||||
| 					}); | ||||
| 
 | ||||
| 					models.set(await getModels()); | ||||
| 				} | ||||
| 			} | ||||
| 		); | ||||
| 
 | ||||
| 		modelTag = ''; | ||||
| 		modelTransferring = false; | ||||
| 	}; | ||||
| 
 | ||||
| 	const uploadModelHandler = async () => { | ||||
| 		modelTransferring = true; | ||||
| 		uploadProgress = 0; | ||||
| 
 | ||||
| 		let uploaded = false; | ||||
| 		let fileResponse = null; | ||||
| 		let name = ''; | ||||
| 
 | ||||
| 		if (modelUploadMode === 'file') { | ||||
| 			const file = modelInputFile[0]; | ||||
| 			const formData = new FormData(); | ||||
| 			formData.append('file', file); | ||||
| 
 | ||||
| 			fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/upload`, { | ||||
| 				method: 'POST', | ||||
| 				headers: { | ||||
| 					...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||
| 				}, | ||||
| 				body: formData | ||||
| 			}).catch((error) => { | ||||
| 				console.log(error); | ||||
| 				return null; | ||||
| 			}); | ||||
| 		} else { | ||||
| 			fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/download?url=${modelFileUrl}`, { | ||||
| 				method: 'GET', | ||||
| 				headers: { | ||||
| 					...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||
| 				} | ||||
| 			}).catch((error) => { | ||||
| 				console.log(error); | ||||
| 				return null; | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		if (fileResponse && fileResponse.ok) { | ||||
| 			const reader = fileResponse.body | ||||
| 				.pipeThrough(new TextDecoderStream()) | ||||
| 				.pipeThrough(splitStream('\n')) | ||||
| 				.getReader(); | ||||
| 
 | ||||
| 			while (true) { | ||||
| 				const { value, done } = await reader.read(); | ||||
| 				if (done) break; | ||||
| 
 | ||||
| 				try { | ||||
| 					let lines = value.split('\n'); | ||||
| 
 | ||||
| 					for (const line of lines) { | ||||
| 						if (line !== '') { | ||||
| 							let data = JSON.parse(line.replace(/^data: /, '')); | ||||
| 
 | ||||
| 							if (data.progress) { | ||||
| 								uploadProgress = data.progress; | ||||
| 							} | ||||
| 
 | ||||
| 							if (data.error) { | ||||
| 								throw data.error; | ||||
| 							} | ||||
| 
 | ||||
| 							if (data.done) { | ||||
| 								modelFileDigest = data.blob; | ||||
| 								name = data.name; | ||||
| 								uploaded = true; | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 				} catch (error) { | ||||
| 					console.log(error); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if (uploaded) { | ||||
| 			const res = await createModel( | ||||
| 				localStorage.token, | ||||
| 				`${name}:latest`, | ||||
| 				`FROM @${modelFileDigest}\n${modelFileContent}` | ||||
| 			); | ||||
| 
 | ||||
| 			if (res && res.ok) { | ||||
| 				const reader = res.body | ||||
| 					.pipeThrough(new TextDecoderStream()) | ||||
| 					.pipeThrough(splitStream('\n')) | ||||
| 					.getReader(); | ||||
| 
 | ||||
| 				while (true) { | ||||
| 					const { value, done } = await reader.read(); | ||||
| 					if (done) break; | ||||
| 
 | ||||
| 					try { | ||||
| 						let lines = value.split('\n'); | ||||
| 
 | ||||
| 						for (const line of lines) { | ||||
| 							if (line !== '') { | ||||
| 								console.log(line); | ||||
| 								let data = JSON.parse(line); | ||||
| 								console.log(data); | ||||
| 
 | ||||
| 								if (data.error) { | ||||
| 									throw data.error; | ||||
| 								} | ||||
| 								if (data.detail) { | ||||
| 									throw data.detail; | ||||
| 								} | ||||
| 
 | ||||
| 								if (data.status) { | ||||
| 									if ( | ||||
| 										!data.digest && | ||||
| 										!data.status.includes('writing') && | ||||
| 										!data.status.includes('sha256') | ||||
| 									) { | ||||
| 										toast.success(data.status); | ||||
| 									} else { | ||||
| 										if (data.digest) { | ||||
| 											digest = data.digest; | ||||
| 
 | ||||
| 											if (data.completed) { | ||||
| 												pullProgress = Math.round((data.completed / data.total) * 1000) / 10; | ||||
| 											} else { | ||||
| 												pullProgress = 100; | ||||
| 											} | ||||
| 										} | ||||
| 									} | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 					} catch (error) { | ||||
| 						console.log(error); | ||||
| 						toast.error(error); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		modelFileUrl = ''; | ||||
| 		modelInputFile = ''; | ||||
| 		modelTransferring = false; | ||||
| 		uploadProgress = null; | ||||
| 
 | ||||
| 		models.set(await getModels()); | ||||
| 	}; | ||||
| 
 | ||||
| 	const deleteModelHandler = async () => { | ||||
| 		const res = await deleteModel(localStorage.token, deleteModelTag).catch((error) => { | ||||
| 			toast.error(error); | ||||
| 		}); | ||||
| 
 | ||||
| 		if (res) { | ||||
| 			toast.success(`Deleted ${deleteModelTag}`); | ||||
| 		} | ||||
| 
 | ||||
| 		deleteModelTag = ''; | ||||
| 		models.set(await getModels()); | ||||
| 	}; | ||||
| 
 | ||||
| 	const pullModelHandlerProcessor = async (opts: { modelName: string; callback: Function }) => { | ||||
| 		const res = await pullModel(localStorage.token, opts.modelName).catch((error) => { | ||||
| 			opts.callback({ success: false, error, modelName: opts.modelName }); | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 		if (res) { | ||||
| 			const reader = res.body | ||||
| 				.pipeThrough(new TextDecoderStream()) | ||||
| 				.pipeThrough(splitStream('\n')) | ||||
| 				.getReader(); | ||||
| 
 | ||||
| 			while (true) { | ||||
| 				try { | ||||
| 					const { value, done } = await reader.read(); | ||||
| 					if (done) break; | ||||
| 
 | ||||
| 					let lines = value.split('\n'); | ||||
| 
 | ||||
| 					for (const line of lines) { | ||||
| 						if (line !== '') { | ||||
| 							let data = JSON.parse(line); | ||||
| 							if (data.error) { | ||||
| 								throw data.error; | ||||
| 							} | ||||
| 							if (data.detail) { | ||||
| 								throw data.detail; | ||||
| 							} | ||||
| 							if (data.status) { | ||||
| 								if (data.digest) { | ||||
| 									let downloadProgress = 0; | ||||
| 									if (data.completed) { | ||||
| 										downloadProgress = Math.round((data.completed / data.total) * 1000) / 10; | ||||
| 									} else { | ||||
| 										downloadProgress = 100; | ||||
| 									} | ||||
| 									modelDownloadStatus[opts.modelName] = { | ||||
| 										pullProgress: downloadProgress, | ||||
| 										digest: data.digest | ||||
| 									}; | ||||
| 								} else { | ||||
| 									toast.success(data.status); | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 				} catch (error) { | ||||
| 					console.log(error); | ||||
| 					if (typeof error !== 'string') { | ||||
| 						error = error.message; | ||||
| 					} | ||||
| 					opts.callback({ success: false, error, modelName: opts.modelName }); | ||||
| 				} | ||||
| 			} | ||||
| 			opts.callback({ success: true, modelName: opts.modelName }); | ||||
| 		} | ||||
| 	}; | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex flex-col h-full justify-between text-sm"> | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll h-80"> | ||||
| 		<div> | ||||
| 			<div class=" mb-2.5 text-sm font-medium">Pull a model from Ollama.ai</div> | ||||
| 			<div class="flex w-full"> | ||||
| 				<div class="flex-1 mr-2"> | ||||
| 					<input | ||||
| 						class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" | ||||
| 						placeholder="Enter model tag (e.g. mistral:7b)" | ||||
| 						bind:value={modelTag} | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<button | ||||
| 					class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition" | ||||
| 					on:click={() => { | ||||
| 						pullModelHandler(); | ||||
| 					}} | ||||
| 					disabled={modelTransferring} | ||||
| 				> | ||||
| 					{#if modelTransferring} | ||||
| 						<div class="self-center"> | ||||
| 							<svg | ||||
| 								class=" w-4 h-4" | ||||
| 								viewBox="0 0 24 24" | ||||
| 								fill="currentColor" | ||||
| 								xmlns="http://www.w3.org/2000/svg" | ||||
| 								><style> | ||||
| 									.spinner_ajPY { | ||||
| 										transform-origin: center; | ||||
| 										animation: spinner_AtaB 0.75s infinite linear; | ||||
| 									} | ||||
| 									@keyframes spinner_AtaB { | ||||
| 										100% { | ||||
| 											transform: rotate(360deg); | ||||
| 										} | ||||
| 									} | ||||
| 								</style><path | ||||
| 									d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" | ||||
| 									opacity=".25" | ||||
| 								/><path | ||||
| 									d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" | ||||
| 									class="spinner_ajPY" | ||||
| 								/></svg | ||||
| 							> | ||||
| 						</div> | ||||
| 					{:else} | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							viewBox="0 0 16 16" | ||||
| 							fill="currentColor" | ||||
| 							class="w-4 h-4" | ||||
| 						> | ||||
| 							<path | ||||
| 								d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z" | ||||
| 							/> | ||||
| 							<path | ||||
| 								d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 					{/if} | ||||
| 				</button> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500"> | ||||
| 				To access the available model names for downloading, <a | ||||
| 					class=" text-gray-500 dark:text-gray-300 font-medium" | ||||
| 					href="https://ollama.ai/library" | ||||
| 					target="_blank">click here.</a | ||||
| 				> | ||||
| 			</div> | ||||
| 
 | ||||
| 			{#if Object.keys(modelDownloadStatus).length > 0} | ||||
| 				{#each Object.keys(modelDownloadStatus) as model} | ||||
| 					<div class="flex flex-col"> | ||||
| 						<div class="font-medium mb-1">{model}</div> | ||||
| 						<div class=""> | ||||
| 							<div | ||||
| 								class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full" | ||||
| 								style="width: {Math.max(15, modelDownloadStatus[model].pullProgress ?? 0)}%" | ||||
| 							> | ||||
| 								{modelDownloadStatus[model].pullProgress ?? 0}% | ||||
| 							</div> | ||||
| 							<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> | ||||
| 								{modelDownloadStatus[model].digest} | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				{/each} | ||||
| 			{/if} | ||||
| 		</div> | ||||
| 
 | ||||
| 		<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 		<div> | ||||
| 			<div class=" mb-2.5 text-sm font-medium">Delete a model</div> | ||||
| 			<div class="flex w-full"> | ||||
| 				<div class="flex-1 mr-2"> | ||||
| 					<select | ||||
| 						class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" | ||||
| 						bind:value={deleteModelTag} | ||||
| 						placeholder="Select a model" | ||||
| 					> | ||||
| 						{#if !deleteModelTag} | ||||
| 							<option value="" disabled selected>Select a model</option> | ||||
| 						{/if} | ||||
| 						{#each $models.filter((m) => m.size != null) as model} | ||||
| 							<option value={model.name} class="bg-gray-100 dark:bg-gray-700" | ||||
| 								>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option | ||||
| 							> | ||||
| 						{/each} | ||||
| 					</select> | ||||
| 				</div> | ||||
| 				<button | ||||
| 					class="px-3 bg-red-700 hover:bg-red-800 text-gray-100 rounded transition" | ||||
| 					on:click={() => { | ||||
| 						deleteModelHandler(); | ||||
| 					}} | ||||
| 				> | ||||
| 					<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="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z" | ||||
| 							clip-rule="evenodd" | ||||
| 						/> | ||||
| 					</svg> | ||||
| 				</button> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 		<form | ||||
| 			on:submit|preventDefault={() => { | ||||
| 				uploadModelHandler(); | ||||
| 			}} | ||||
| 		> | ||||
| 			<div class=" mb-2 flex w-full justify-between"> | ||||
| 				<div class="  text-sm font-medium"> | ||||
| 					Upload a GGUF model <a | ||||
| 						class=" text-xs font-medium text-gray-500 underline" | ||||
| 						href="https://github.com/jmorganca/ollama/blob/main/README.md#import-from-gguf" | ||||
| 						target="_blank">(Experimental)</a | ||||
| 					> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<button | ||||
| 					class="p-1 px-3 text-xs flex rounded transition" | ||||
| 					on:click={() => { | ||||
| 						if (modelUploadMode === 'file') { | ||||
| 							modelUploadMode = 'url'; | ||||
| 						} else { | ||||
| 							modelUploadMode = 'file'; | ||||
| 						} | ||||
| 					}} | ||||
| 					type="button" | ||||
| 				> | ||||
| 					{#if modelUploadMode === 'file'} | ||||
| 						<span class="ml-2 self-center">File Mode</span> | ||||
| 					{:else} | ||||
| 						<span class="ml-2 self-center">URL Mode</span> | ||||
| 					{/if} | ||||
| 				</button> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class="flex w-full mb-1.5"> | ||||
| 				<div class="flex flex-col w-full"> | ||||
| 					{#if modelUploadMode === 'file'} | ||||
| 						<div class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}"> | ||||
| 							<input | ||||
| 								id="model-upload-input" | ||||
| 								type="file" | ||||
| 								bind:files={modelInputFile} | ||||
| 								on:change={() => { | ||||
| 									console.log(modelInputFile); | ||||
| 								}} | ||||
| 								accept=".gguf" | ||||
| 								required | ||||
| 								hidden | ||||
| 							/> | ||||
| 
 | ||||
| 							<button | ||||
| 								type="button" | ||||
| 								class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-800" | ||||
| 								on:click={() => { | ||||
| 									document.getElementById('model-upload-input').click(); | ||||
| 								}} | ||||
| 							> | ||||
| 								{#if modelInputFile && modelInputFile.length > 0} | ||||
| 									{modelInputFile[0].name} | ||||
| 								{:else} | ||||
| 									Click here to select | ||||
| 								{/if} | ||||
| 							</button> | ||||
| 						</div> | ||||
| 					{:else} | ||||
| 						<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}"> | ||||
| 							<input | ||||
| 								class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-800 outline-none {modelFileUrl !== | ||||
| 								'' | ||||
| 									? 'mr-2' | ||||
| 									: ''}" | ||||
| 								type="url" | ||||
| 								required | ||||
| 								bind:value={modelFileUrl} | ||||
| 								placeholder="Type HuggingFace Resolve (Download) URL" | ||||
| 							/> | ||||
| 						</div> | ||||
| 					{/if} | ||||
| 				</div> | ||||
| 
 | ||||
| 				{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} | ||||
| 					<button | ||||
| 						class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition" | ||||
| 						type="submit" | ||||
| 						disabled={modelTransferring} | ||||
| 					> | ||||
| 						{#if modelTransferring} | ||||
| 							<div class="self-center"> | ||||
| 								<svg | ||||
| 									class=" w-4 h-4" | ||||
| 									viewBox="0 0 24 24" | ||||
| 									fill="currentColor" | ||||
| 									xmlns="http://www.w3.org/2000/svg" | ||||
| 									><style> | ||||
| 										.spinner_ajPY { | ||||
| 											transform-origin: center; | ||||
| 											animation: spinner_AtaB 0.75s infinite linear; | ||||
| 										} | ||||
| 										@keyframes spinner_AtaB { | ||||
| 											100% { | ||||
| 												transform: rotate(360deg); | ||||
| 											} | ||||
| 										} | ||||
| 									</style><path | ||||
| 										d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" | ||||
| 										opacity=".25" | ||||
| 									/><path | ||||
| 										d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" | ||||
| 										class="spinner_ajPY" | ||||
| 									/></svg | ||||
| 								> | ||||
| 							</div> | ||||
| 						{:else} | ||||
| 							<svg | ||||
| 								xmlns="http://www.w3.org/2000/svg" | ||||
| 								viewBox="0 0 16 16" | ||||
| 								fill="currentColor" | ||||
| 								class="w-4 h-4" | ||||
| 							> | ||||
| 								<path | ||||
| 									d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z" | ||||
| 								/> | ||||
| 								<path | ||||
| 									d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" | ||||
| 								/> | ||||
| 							</svg> | ||||
| 						{/if} | ||||
| 					</button> | ||||
| 				{/if} | ||||
| 			</div> | ||||
| 
 | ||||
| 			{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} | ||||
| 				<div> | ||||
| 					<div> | ||||
| 						<div class=" my-2.5 text-sm font-medium">Modelfile Content</div> | ||||
| 						<textarea | ||||
| 							bind:value={modelFileContent} | ||||
| 							class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none" | ||||
| 							rows="6" | ||||
| 						/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			{/if} | ||||
| 			<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500"> | ||||
| 				To access the GGUF models available for downloading, <a | ||||
| 					class=" text-gray-500 dark:text-gray-300 font-medium" | ||||
| 					href="https://huggingface.co/models?search=gguf" | ||||
| 					target="_blank">click here.</a | ||||
| 				> | ||||
| 			</div> | ||||
| 
 | ||||
| 			{#if uploadProgress !== null} | ||||
| 				<div class="mt-2"> | ||||
| 					<div class=" mb-2 text-xs">Upload Progress</div> | ||||
| 
 | ||||
| 					<div class="w-full rounded-full dark:bg-gray-800"> | ||||
| 						<div | ||||
| 							class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full" | ||||
| 							style="width: {Math.max(15, uploadProgress ?? 0)}%" | ||||
| 						> | ||||
| 							{uploadProgress ?? 0}% | ||||
| 						</div> | ||||
| 					</div> | ||||
| 					<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> | ||||
| 						{modelFileDigest} | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			{/if} | ||||
| 		</form> | ||||
| 	</div> | ||||
| </div> | ||||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy J. Baek
						Timothy J. Baek