Interface.svelte 13 KB
Newer Older
Timothy J. Baek's avatar
Timothy J. Baek committed
1
2
3
<script lang="ts">
	import { getBackendConfig } from '$lib/apis';
	import { setDefaultPromptSuggestions } from '$lib/apis/configs';
4
	import { config, models, settings, user } from '$lib/stores';
5
	import { createEventDispatcher, onMount, getContext } from 'svelte';
Jannik Streidl's avatar
Jannik Streidl committed
6
	import { toast } from 'svelte-sonner';
7
	import Tooltip from '$lib/components/common/Tooltip.svelte';
Timothy J. Baek's avatar
Timothy J. Baek committed
8
9
	const dispatch = createEventDispatcher();

10
11
	const i18n = getContext('i18n');

Timothy J. Baek's avatar
Timothy J. Baek committed
12
13
14
15
16
17
	export let saveSettings: Function;

	// Addons
	let titleAutoGenerate = true;
	let responseAutoCopy = false;
	let titleAutoGenerateModel = '';
18
	let titleAutoGenerateModelExternal = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
19
	let fullScreenMode = false;
20
	let titleGenerationPrompt = '';
21
	let splitLargeChunks = false;
Timothy J. Baek's avatar
Timothy J. Baek committed
22

Timothy J. Baek's avatar
Timothy J. Baek committed
23
24
	// Interface
	let promptSuggestions = [];
25
	let showUsername = false;
Timothy J. Baek's avatar
Timothy J. Baek committed
26
	let chatBubble = true;
27
	let chatDirection: 'LTR' | 'RTL' = 'LTR';
28

29
30
31
32
33
	const toggleSplitLargeChunks = async () => {
		splitLargeChunks = !splitLargeChunks;
		saveSettings({ splitLargeChunks: splitLargeChunks });
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
34
35
36
37
38
	const toggleFullScreenMode = async () => {
		fullScreenMode = !fullScreenMode;
		saveSettings({ fullScreenMode: fullScreenMode });
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
39
40
41
42
43
	const toggleChatBubble = async () => {
		chatBubble = !chatBubble;
		saveSettings({ chatBubble: chatBubble });
	};

44
45
46
47
48
	const toggleShowUsername = async () => {
		showUsername = !showUsername;
		saveSettings({ showUsername: showUsername });
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
49
50
	const toggleTitleAutoGenerate = async () => {
		titleAutoGenerate = !titleAutoGenerate;
51
52
53
54
55
56
		saveSettings({
			title: {
				...$settings.title,
				auto: titleAutoGenerate
			}
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
	};

	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.'
			);
		}
	};

81
82
	const toggleChangeChatDirection = async () => {
		chatDirection = chatDirection === 'LTR' ? 'RTL' : 'LTR';
Timothy J. Baek's avatar
Timothy J. Baek committed
83
		saveSettings({ chatDirection });
84
85
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
86
	const updateInterfaceHandler = async () => {
87
88
89
90
91
92
		if ($user.role === 'admin') {
			promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
			await config.set(await getBackendConfig());
		}

		saveSettings({
93
94
95
96
97
98
99
			title: {
				...$settings.title,
				model: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
				modelExternal:
					titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined,
				prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
			}
100
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
101
102
103
104
105
106
	};

	onMount(async () => {
		if ($user.role === 'admin') {
			promptSuggestions = $config?.default_prompt_suggestions;
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
107
108
109

		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');

110
111
112
		titleAutoGenerate = settings?.title?.auto ?? true;
		titleAutoGenerateModel = settings?.title?.model ?? '';
		titleAutoGenerateModelExternal = settings?.title?.modelExternal ?? '';
113
		titleGenerationPrompt =
114
			settings?.title?.prompt ??
Ased Mammad's avatar
Ased Mammad committed
115
116
117
			$i18n.t(
				"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':"
			) + ' {{prompt}}';
118
119
120

		responseAutoCopy = settings.responseAutoCopy ?? false;
		showUsername = settings.showUsername ?? false;
Timothy J. Baek's avatar
Timothy J. Baek committed
121
		chatBubble = settings.chatBubble ?? true;
122
		fullScreenMode = settings.fullScreenMode ?? false;
123
		splitLargeChunks = settings.splitLargeChunks ?? false;
124
		chatDirection = settings.chatDirection ?? 'LTR';
Timothy J. Baek's avatar
Timothy J. Baek committed
125
126
127
128
129
130
131
132
133
134
	});
</script>

<form
	class="flex flex-col h-full justify-between space-y-3 text-sm"
	on:submit|preventDefault={() => {
		updateInterfaceHandler();
		dispatch('save');
	}}
>
Timothy J. Baek's avatar
Timothy J. Baek committed
135
	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]">
Timothy J. Baek's avatar
Timothy J. Baek committed
136
		<div>
137
			<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Add-ons')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
138

Timothy J. Baek's avatar
Timothy J. Baek committed
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
			<div>
				<div class=" py-0.5 flex w-full justify-between">
					<div class=" self-center text-xs font-medium">{$i18n.t('Chat Bubble UI')}</div>

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
							toggleChatBubble();
						}}
						type="button"
					>
						{#if chatBubble === true}
							<span class="ml-2 self-center">{$i18n.t('On')}</span>
						{:else}
							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
						{/if}
					</button>
				</div>
			</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
159
160
			<div>
				<div class=" py-0.5 flex w-full justify-between">
161
					<div class=" self-center text-xs font-medium">{$i18n.t('Title Auto-Generation')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
162
163
164
165
166
167
168
169
170

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
							toggleTitleAutoGenerate();
						}}
						type="button"
					>
						{#if titleAutoGenerate === true}
171
							<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
172
						{:else}
173
							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
174
175
176
177
178
179
180
						{/if}
					</button>
				</div>
			</div>

			<div>
				<div class=" py-0.5 flex w-full justify-between">
181
182
183
					<div class=" self-center text-xs font-medium">
						{$i18n.t('Response AutoCopy to Clipboard')}
					</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
184
185
186
187
188
189
190
191
192

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
							toggleResponseAutoCopy();
						}}
						type="button"
					>
						{#if responseAutoCopy === true}
193
							<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
194
						{:else}
195
							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
196
197
198
199
						{/if}
					</button>
				</div>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
200

Timothy J. Baek's avatar
Timothy J. Baek committed
201
202
			<div>
				<div class=" py-0.5 flex w-full justify-between">
203
					<div class=" self-center text-xs font-medium">{$i18n.t('Full Screen Mode')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
204
205
206
207
208
209
210
211
212

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
							toggleFullScreenMode();
						}}
						type="button"
					>
						{#if fullScreenMode === true}
213
							<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
214
						{:else}
215
							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
216
217
218
219
220
						{/if}
					</button>
				</div>
			</div>

221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
			{#if !$settings.chatBubble}
				<div>
					<div class=" py-0.5 flex w-full justify-between">
						<div class=" self-center text-xs font-medium">
							{$i18n.t('Display the username instead of You in the Chat')}
						</div>

						<button
							class="p-1 px-3 text-xs flex rounded transition"
							on:click={() => {
								toggleShowUsername();
							}}
							type="button"
						>
							{#if showUsername === true}
								<span class="ml-2 self-center">{$i18n.t('On')}</span>
							{:else}
								<span class="ml-2 self-center">{$i18n.t('Off')}</span>
							{/if}
						</button>
Timothy J. Baek's avatar
Timothy J. Baek committed
241
					</div>
242
				</div>
243
			{/if}
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265

			<div>
				<div class=" py-0.5 flex w-full justify-between">
					<div class=" self-center text-xs font-medium">
						{$i18n.t('Fluidly stream large external response chunks')}
					</div>

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
							toggleSplitLargeChunks();
						}}
						type="button"
					>
						{#if splitLargeChunks === true}
							<span class="ml-2 self-center">{$i18n.t('On')}</span>
						{:else}
							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
						{/if}
					</button>
				</div>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
266
267
		</div>

268
269
270
271
272
273
274
275
276
		<div>
			<div class=" py-0.5 flex w-full justify-between">
				<div class=" self-center text-xs font-medium">{$i18n.t('Chat direction')}</div>

				<button
					class="p-1 px-3 text-xs flex rounded transition"
					on:click={toggleChangeChatDirection}
					type="button"
				>
Timothy J. Baek's avatar
Timothy J. Baek committed
277
					{#if chatDirection === 'LTR'}
278
279
280
281
282
283
284
285
						<span class="ml-2 self-center">{$i18n.t('LTR')}</span>
					{:else}
						<span class="ml-2 self-center">{$i18n.t('RTL')}</span>
					{/if}
				</button>
			</div>
		</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
286
		<hr class=" dark:border-gray-700" />
Timothy J. Baek's avatar
Timothy J. Baek committed
287

Timothy J. Baek's avatar
Timothy J. Baek committed
288
		<div>
289
290
291
			<div class=" mb-2.5 text-sm font-medium flex">
				<div class=" mr-1">{$i18n.t('Set Task Model')}</div>
				<Tooltip
Jun Siang Cheah's avatar
Jun Siang Cheah committed
292
293
294
295
					content={$i18n.t(
						'A task model is used when performing tasks such as generating titles for chats and web search queries'
					)}
				>
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
					<svg
						xmlns="http://www.w3.org/2000/svg"
						fill="none"
						viewBox="0 0 24 24"
						stroke-width="1.5"
						stroke="currentColor"
						class="w-5 h-5"
					>
						<path
							stroke-linecap="round"
							stroke-linejoin="round"
							d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
						/>
					</svg>
				</Tooltip>
			</div>
312
313
314
			<div class="flex w-full gap-2 pr-2">
				<div class="flex-1">
					<div class=" text-xs mb-1">Local Models</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
315
					<select
Timothy J. Baek's avatar
Timothy J. Baek committed
316
						class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
Timothy J. Baek's avatar
Timothy J. Baek committed
317
						bind:value={titleAutoGenerateModel}
318
						placeholder={$i18n.t('Select a model')}
Timothy J. Baek's avatar
Timothy J. Baek committed
319
					>
320
						<option value="" selected>{$i18n.t('Current Model')}</option>
321
322
323
324
325
326
						{#each $models as model}
							{#if model.size != null}
								<option value={model.name} class="bg-gray-100 dark:bg-gray-700">
									{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}
								</option>
							{/if}
Timothy J. Baek's avatar
Timothy J. Baek committed
327
328
329
						{/each}
					</select>
				</div>
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347

				<div class="flex-1">
					<div class=" text-xs mb-1">External Models</div>
					<select
						class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
						bind:value={titleAutoGenerateModelExternal}
						placeholder={$i18n.t('Select a model')}
					>
						<option value="" selected>{$i18n.t('Current Model')}</option>
						{#each $models as model}
							{#if model.name !== 'hr'}
								<option value={model.name} class="bg-gray-100 dark:bg-gray-700">
									{model.name}
								</option>
							{/if}
						{/each}
					</select>
				</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
348
			</div>
349

Timothy J. Baek's avatar
Timothy J. Baek committed
350
			<div class="mt-3 mr-2">
351
				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
352
353
				<textarea
					bind:value={titleGenerationPrompt}
Timothy J. Baek's avatar
Timothy J. Baek committed
354
					class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
355
356
357
					rows="3"
				/>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
358
359
360
361
362
		</div>

		{#if $user.role === 'admin'}
			<hr class=" dark:border-gray-700" />

Timothy J. Baek's avatar
Timothy J. Baek committed
363
			<div class=" space-y-3 pr-1.5">
Timothy J. Baek's avatar
Timothy J. Baek committed
364
				<div class="flex w-full justify-between mb-2">
365
366
367
					<div class=" self-center text-sm font-semibold">
						{$i18n.t('Default Prompt Suggestions')}
					</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
368
369

					<button
Timothy J. Baek's avatar
Timothy J. Baek committed
370
						class="p-1 px-3 text-xs flex rounded transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
371
372
						type="button"
						on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
373
374
375
							if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
								promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
376
377
378
379
380
381
382
383
384
						}}
					>
						<svg
							xmlns="http://www.w3.org/2000/svg"
							viewBox="0 0 20 20"
							fill="currentColor"
							class="w-4 h-4"
						>
							<path
Timothy J. Baek's avatar
Timothy J. Baek committed
385
								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"
Timothy J. Baek's avatar
Timothy J. Baek committed
386
387
388
389
							/>
						</svg>
					</button>
				</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
390
391
392
393
394
395
396
				<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"
397
										placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
Timothy J. Baek's avatar
Timothy J. Baek committed
398
399
400
401
402
										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"
403
										placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
Timothy J. Baek's avatar
Timothy J. Baek committed
404
405
406
407
408
409
										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"
410
									placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
Timothy J. Baek's avatar
Timothy J. Baek committed
411
412
413
									bind:value={prompt.content}
								/>
							</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
414

Timothy J. Baek's avatar
Timothy J. Baek committed
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
							<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">
Jannik Streidl's avatar
Jannik Streidl committed
440
						{$i18n.t('Adjusting these settings will apply changes universally to all users.')}
Timothy J. Baek's avatar
Timothy J. Baek committed
441
442
					</div>
				{/if}
Timothy J. Baek's avatar
Timothy J. Baek committed
443
444
445
446
			</div>
		{/if}
	</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
447
	<div class="flex justify-end text-sm font-medium">
Timothy J. Baek's avatar
Timothy J. Baek committed
448
		<button
Timothy J. Baek's avatar
Timothy J. Baek committed
449
			class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
Timothy J. Baek's avatar
Timothy J. Baek committed
450
451
			type="submit"
		>
Jannik Streidl's avatar
Jannik Streidl committed
452
			{$i18n.t('Save')}
Timothy J. Baek's avatar
Timothy J. Baek committed
453
454
455
		</button>
	</div>
</form>