forked from open-webui/open-webui
		
	refac: settings
addon removed, voice added
This commit is contained in:
		
							parent
							
								
									3e1c7e4e06
								
							
						
					
					
						commit
						efd546ff36
					
				
					 4 changed files with 353 additions and 341 deletions
				
			
		| 
						 | 
				
			
			@ -1,249 +0,0 @@
 | 
			
		|||
<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,13 +1,54 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { getBackendConfig } from '$lib/apis';
 | 
			
		||||
	import { setDefaultPromptSuggestions } from '$lib/apis/configs';
 | 
			
		||||
	import { config, user } from '$lib/stores';
 | 
			
		||||
	import { config, models, user } from '$lib/stores';
 | 
			
		||||
	import { createEventDispatcher, onMount } from 'svelte';
 | 
			
		||||
	import toast from 'svelte-french-toast';
 | 
			
		||||
	const dispatch = createEventDispatcher();
 | 
			
		||||
 | 
			
		||||
	export let saveSettings: Function;
 | 
			
		||||
 | 
			
		||||
	// Addons
 | 
			
		||||
	let titleAutoGenerate = true;
 | 
			
		||||
	let speechAutoSend = false;
 | 
			
		||||
	let responseAutoCopy = false;
 | 
			
		||||
	let titleAutoGenerateModel = '';
 | 
			
		||||
 | 
			
		||||
	// Interface
 | 
			
		||||
	let promptSuggestions = [];
 | 
			
		||||
 | 
			
		||||
	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.'
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const updateInterfaceHandler = async () => {
 | 
			
		||||
		promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
 | 
			
		||||
		await config.set(await getBackendConfig());
 | 
			
		||||
| 
						 | 
				
			
			@ -17,6 +58,14 @@
 | 
			
		|||
		if ($user.role === 'admin') {
 | 
			
		||||
			promptSuggestions = $config?.default_prompt_suggestions;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
 | 
			
		||||
 | 
			
		||||
		titleAutoGenerate = settings.titleAutoGenerate ?? true;
 | 
			
		||||
		speechAutoSend = settings.speechAutoSend ?? false;
 | 
			
		||||
		responseAutoCopy = settings.responseAutoCopy ?? false;
 | 
			
		||||
 | 
			
		||||
		titleAutoGenerateModel = settings.titleAutoGenerateModel ?? '';
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -27,62 +76,130 @@
 | 
			
		|||
		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>
 | 
			
		||||
	<div class=" space-y-3 pr-1.5 overflow-y-scroll h-80">
 | 
			
		||||
		<div>
 | 
			
		||||
			<div class=" mb-1 text-sm font-medium">WebUI Add-ons</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>
 | 
			
		||||
			<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="px-2"
 | 
			
		||||
						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>
 | 
			
		||||
 | 
			
		||||
		{#if $user.role === 'admin'}
 | 
			
		||||
			<hr class=" dark:border-gray-700" />
 | 
			
		||||
 | 
			
		||||
			<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={() => {
 | 
			
		||||
							promptSuggestions.splice(promptIdx, 1);
 | 
			
		||||
							promptSuggestions = promptSuggestions;
 | 
			
		||||
							if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
 | 
			
		||||
								promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
 | 
			
		||||
							}
 | 
			
		||||
						}}
 | 
			
		||||
					>
 | 
			
		||||
						<svg
 | 
			
		||||
| 
						 | 
				
			
			@ -92,17 +209,64 @@
 | 
			
		|||
							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"
 | 
			
		||||
								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>
 | 
			
		||||
			{/each}
 | 
			
		||||
		</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]}
 | 
			
		||||
									/>
 | 
			
		||||
 | 
			
		||||
		{#if promptSuggestions.length > 0}
 | 
			
		||||
			<div class="text-xs text-left w-full mt-2">
 | 
			
		||||
				Adjusting these settings will apply changes universally to all users.
 | 
			
		||||
									<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>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										93
									
								
								src/lib/components/chat/Settings/Voice.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/lib/components/chat/Settings/Voice.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,93 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { createEventDispatcher, onMount } from 'svelte';
 | 
			
		||||
	import { voices } from '$lib/stores';
 | 
			
		||||
	const dispatch = createEventDispatcher();
 | 
			
		||||
 | 
			
		||||
	export let saveSettings: Function;
 | 
			
		||||
 | 
			
		||||
	// Voice
 | 
			
		||||
	let speakVoice = '';
 | 
			
		||||
 | 
			
		||||
	onMount(async () => {
 | 
			
		||||
		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
 | 
			
		||||
 | 
			
		||||
		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 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>
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +13,7 @@
 | 
			
		|||
	import General from './Settings/General.svelte';
 | 
			
		||||
	import External from './Settings/External.svelte';
 | 
			
		||||
	import Interface from './Settings/Interface.svelte';
 | 
			
		||||
	import AddOns from './Settings/AddOns.svelte';
 | 
			
		||||
	import Voice from './Settings/Voice.svelte';
 | 
			
		||||
	import Chats from './Settings/Chats.svelte';
 | 
			
		||||
 | 
			
		||||
	export let show = false;
 | 
			
		||||
| 
						 | 
				
			
			@ -176,56 +176,59 @@
 | 
			
		|||
						</div>
 | 
			
		||||
						<div class=" self-center">External</div>
 | 
			
		||||
					</button>
 | 
			
		||||
 | 
			
		||||
					<button
 | 
			
		||||
						class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 | 
			
		||||
						'interface'
 | 
			
		||||
							? 'bg-gray-200 dark:bg-gray-700'
 | 
			
		||||
							: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
 | 
			
		||||
						on:click={() => {
 | 
			
		||||
							selectedTab = 'interface';
 | 
			
		||||
						}}
 | 
			
		||||
					>
 | 
			
		||||
						<div class=" self-center mr-2">
 | 
			
		||||
							<svg
 | 
			
		||||
								xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
								viewBox="0 0 16 16"
 | 
			
		||||
								fill="currentColor"
 | 
			
		||||
								class="w-4 h-4"
 | 
			
		||||
							>
 | 
			
		||||
								<path
 | 
			
		||||
									fill-rule="evenodd"
 | 
			
		||||
									d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
 | 
			
		||||
									clip-rule="evenodd"
 | 
			
		||||
								/>
 | 
			
		||||
							</svg>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class=" self-center">Interface</div>
 | 
			
		||||
					</button>
 | 
			
		||||
				{/if}
 | 
			
		||||
 | 
			
		||||
				<button
 | 
			
		||||
					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 | 
			
		||||
					'addons'
 | 
			
		||||
					'interface'
 | 
			
		||||
						? 'bg-gray-200 dark:bg-gray-700'
 | 
			
		||||
						: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
 | 
			
		||||
					on:click={() => {
 | 
			
		||||
						selectedTab = 'addons';
 | 
			
		||||
						selectedTab = 'interface';
 | 
			
		||||
					}}
 | 
			
		||||
				>
 | 
			
		||||
					<div class=" self-center mr-2">
 | 
			
		||||
						<svg
 | 
			
		||||
							xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
							viewBox="0 0 20 20"
 | 
			
		||||
							viewBox="0 0 16 16"
 | 
			
		||||
							fill="currentColor"
 | 
			
		||||
							class="w-4 h-4"
 | 
			
		||||
						>
 | 
			
		||||
							<path
 | 
			
		||||
								d="M12 4.467c0-.405.262-.75.559-1.027.276-.257.441-.584.441-.94 0-.828-.895-1.5-2-1.5s-2 .672-2 1.5c0 .362.171.694.456.953.29.265.544.6.544.994a.968.968 0 01-1.024.974 39.655 39.655 0 01-3.014-.306.75.75 0 00-.847.847c.14.993.242 1.999.306 3.014A.968.968 0 014.447 10c-.393 0-.729-.253-.994-.544C3.194 9.17 2.862 9 2.5 9 1.672 9 1 9.895 1 11s.672 2 1.5 2c.356 0 .683-.165.94-.441.276-.297.622-.559 1.027-.559a.997.997 0 011.004 1.03 39.747 39.747 0 01-.319 3.734.75.75 0 00.64.842c1.05.146 2.111.252 3.184.318A.97.97 0 0010 16.948c0-.394-.254-.73-.545-.995C9.171 15.693 9 15.362 9 15c0-.828.895-1.5 2-1.5s2 .672 2 1.5c0 .356-.165.683-.441.94-.297.276-.559.622-.559 1.027a.998.998 0 001.03 1.005c1.337-.05 2.659-.162 3.961-.337a.75.75 0 00.644-.644c.175-1.302.288-2.624.337-3.961A.998.998 0 0016.967 12c-.405 0-.75.262-1.027.559-.257.276-.584.441-.94.441-.828 0-1.5-.895-1.5-2s.672-2 1.5-2c.362 0 .694.17.953.455.265.291.601.545.995.545a.97.97 0 00.976-1.024 41.159 41.159 0 00-.318-3.184.75.75 0 00-.842-.64c-1.228.164-2.473.271-3.734.319A.997.997 0 0112 4.467z"
 | 
			
		||||
								fill-rule="evenodd"
 | 
			
		||||
								d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
 | 
			
		||||
								clip-rule="evenodd"
 | 
			
		||||
							/>
 | 
			
		||||
						</svg>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class=" self-center">Add-ons</div>
 | 
			
		||||
					<div class=" self-center">Interface</div>
 | 
			
		||||
				</button>
 | 
			
		||||
 | 
			
		||||
				<button
 | 
			
		||||
					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 | 
			
		||||
					'voice'
 | 
			
		||||
						? 'bg-gray-200 dark:bg-gray-700'
 | 
			
		||||
						: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
 | 
			
		||||
					on:click={() => {
 | 
			
		||||
						selectedTab = 'voice';
 | 
			
		||||
					}}
 | 
			
		||||
				>
 | 
			
		||||
					<div class=" self-center mr-2">
 | 
			
		||||
						<svg
 | 
			
		||||
							xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
							viewBox="0 0 16 16"
 | 
			
		||||
							fill="currentColor"
 | 
			
		||||
							class="w-4 h-4"
 | 
			
		||||
						>
 | 
			
		||||
							<path
 | 
			
		||||
								d="M7.557 2.066A.75.75 0 0 1 8 2.75v10.5a.75.75 0 0 1-1.248.56L3.59 11H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.59l3.162-2.81a.75.75 0 0 1 .805-.124ZM12.95 3.05a.75.75 0 1 0-1.06 1.06 5.5 5.5 0 0 1 0 7.78.75.75 0 1 0 1.06 1.06 7 7 0 0 0 0-9.9Z"
 | 
			
		||||
							/>
 | 
			
		||||
							<path
 | 
			
		||||
								d="M10.828 5.172a.75.75 0 1 0-1.06 1.06 2.5 2.5 0 0 1 0 3.536.75.75 0 1 0 1.06 1.06 4 4 0 0 0 0-5.656Z"
 | 
			
		||||
							/>
 | 
			
		||||
						</svg>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class=" self-center">Voice</div>
 | 
			
		||||
				</button>
 | 
			
		||||
 | 
			
		||||
				<button
 | 
			
		||||
| 
						 | 
				
			
			@ -333,12 +336,13 @@
 | 
			
		|||
					/>
 | 
			
		||||
				{:else if selectedTab === 'interface'}
 | 
			
		||||
					<Interface
 | 
			
		||||
						{saveSettings}
 | 
			
		||||
						on:save={() => {
 | 
			
		||||
							show = false;
 | 
			
		||||
						}}
 | 
			
		||||
					/>
 | 
			
		||||
				{:else if selectedTab === 'addons'}
 | 
			
		||||
					<AddOns
 | 
			
		||||
				{:else if selectedTab === 'voice'}
 | 
			
		||||
					<Voice
 | 
			
		||||
						{saveSettings}
 | 
			
		||||
						on:save={() => {
 | 
			
		||||
							show = false;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue