Interface.svelte 13.5 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
	import { updateUserInfo } from '$lib/apis/users';
	import { getUserPosition } from '$lib/utils';
Timothy J. Baek's avatar
Timothy J. Baek committed
10
11
	const dispatch = createEventDispatcher();

12
13
	const i18n = getContext('i18n');

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

Timothy J. Baek's avatar
Timothy J. Baek committed
16
17
18
19
	let backgroundImageUrl = null;
	let inputFiles = null;
	let filesInputElement;

Timothy J. Baek's avatar
Timothy J. Baek committed
20
21
22
	// Addons
	let titleAutoGenerate = true;
	let responseAutoCopy = false;
23
	let widescreenMode = false;
24
	let splitLargeChunks = false;
25
	let scrollOnBranchChange = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
26
	let userLocation = false;
Timothy J. Baek's avatar
Timothy J. Baek committed
27

Timothy J. Baek's avatar
Timothy J. Baek committed
28
	// Interface
29
	let defaultModelId = '';
30
	let showUsername = false;
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
31

Timothy J. Baek's avatar
Timothy J. Baek committed
32
	let chatBubble = true;
33
	let chatDirection: 'LTR' | 'RTL' = 'LTR';
34

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
35
	let showEmojiInCall = false;
36
	let voiceInterruption = false;
37
	let hapticFeedback = false;
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
38

39
40
41
42
43
	const toggleSplitLargeChunks = async () => {
		splitLargeChunks = !splitLargeChunks;
		saveSettings({ splitLargeChunks: splitLargeChunks });
	};

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

49
50
51
	const togglewidescreenMode = async () => {
		widescreenMode = !widescreenMode;
		saveSettings({ widescreenMode: widescreenMode });
Timothy J. Baek's avatar
Timothy J. Baek committed
52
53
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
54
55
56
57
58
	const toggleChatBubble = async () => {
		chatBubble = !chatBubble;
		saveSettings({ chatBubble: chatBubble });
	};

59
60
61
62
63
	const toggleShowUsername = async () => {
		showUsername = !showUsername;
		saveSettings({ showUsername: showUsername });
	};

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
64
65
66
67
68
	const toggleEmojiInCall = async () => {
		showEmojiInCall = !showEmojiInCall;
		saveSettings({ showEmojiInCall: showEmojiInCall });
	};

69
70
71
72
73
	const toggleVoiceInterruption = async () => {
		voiceInterruption = !voiceInterruption;
		saveSettings({ voiceInterruption: voiceInterruption });
	};

74
75
76
77
78
	const toggleHapticFeedback = async () => {
		hapticFeedback = !hapticFeedback;
		saveSettings({ hapticFeedback: hapticFeedback });
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
79
80
81
82
83
84
85
86
87
88
89
	const toggleUserLocation = async () => {
		userLocation = !userLocation;

		if (userLocation) {
			const position = await getUserPosition().catch((error) => {
				toast.error(error.message);
				return null;
			});

			if (position) {
				await updateUserInfo(localStorage.token, { location: position });
SimonOriginal's avatar
SimonOriginal committed
90
				toast.success($i18n.t('User location successfully retrieved.'));
Timothy J. Baek's avatar
Timothy J. Baek committed
91
92
93
94
95
96
97
98
			} else {
				userLocation = false;
			}
		}

		saveSettings({ userLocation });
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
99
100
	const toggleTitleAutoGenerate = async () => {
		titleAutoGenerate = !titleAutoGenerate;
101
102
103
104
105
106
		saveSettings({
			title: {
				...$settings.title,
				auto: titleAutoGenerate
			}
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
	};

	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(
126
				$i18n.t(
SimonOriginal's avatar
SimonOriginal committed
127
128
					'Clipboard write permission denied. Please check your browser settings to grant the necessary access.'
				)
Timothy J. Baek's avatar
Timothy J. Baek committed
129
130
131
132
			);
		}
	};

133
134
	const toggleChangeChatDirection = async () => {
		chatDirection = chatDirection === 'LTR' ? 'RTL' : 'LTR';
Timothy J. Baek's avatar
Timothy J. Baek committed
135
		saveSettings({ chatDirection });
136
137
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
138
	const updateInterfaceHandler = async () => {
139
		saveSettings({
140
			models: [defaultModelId]
141
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
142
143
144
	};

	onMount(async () => {
145
		titleAutoGenerate = $settings?.title?.auto ?? true;
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
146

147
148
		responseAutoCopy = $settings.responseAutoCopy ?? false;
		showUsername = $settings.showUsername ?? false;
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
149
150

		showEmojiInCall = $settings.showEmojiInCall ?? false;
151
		voiceInterruption = $settings.voiceInterruption ?? false;
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
152

153
		chatBubble = $settings.chatBubble ?? true;
154
		widescreenMode = $settings.widescreenMode ?? false;
155
		splitLargeChunks = $settings.splitLargeChunks ?? false;
156
		scrollOnBranchChange = $settings.scrollOnBranchChange ?? true;
157
		chatDirection = $settings.chatDirection ?? 'LTR';
Timothy J. Baek's avatar
Timothy J. Baek committed
158
		userLocation = $settings.userLocation ?? false;
159

160
161
		hapticFeedback = $settings.hapticFeedback ?? false;

162
163
164
165
		defaultModelId = $settings?.models?.at(0) ?? '';
		if ($config?.default_models) {
			defaultModelId = $config.default_models.split(',')[0];
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
166
167

		backgroundImageUrl = $settings.backgroundImageUrl ?? null;
Timothy J. Baek's avatar
Timothy J. Baek committed
168
169
170
171
172
173
174
175
176
177
	});
</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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
	<input
		bind:this={filesInputElement}
		bind:files={inputFiles}
		type="file"
		hidden
		accept="image/*"
		on:change={() => {
			let reader = new FileReader();
			reader.onload = (event) => {
				let originalImageUrl = `${event.target.result}`;

				backgroundImageUrl = originalImageUrl;
				saveSettings({ backgroundImageUrl });
			};

			if (
				inputFiles &&
				inputFiles.length > 0 &&
				['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(inputFiles[0]['type'])
			) {
				reader.readAsDataURL(inputFiles[0]);
			} else {
				console.log(`Unsupported File Type '${inputFiles[0]['type']}'.`);
				inputFiles = null;
			}
		}}
	/>

	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem] scrollbar-hidden">
		<div class=" space-y-1 mb-3">
			<div class="mb-2">
				<div class="flex justify-between items-center text-xs">
					<div class=" text-sm font-medium">{$i18n.t('Default Model')}</div>
				</div>
			</div>

			<div class="flex-1 mr-2">
				<select
					class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
					bind:value={defaultModelId}
					placeholder="Select a model"
				>
					<option value="" disabled selected>{$i18n.t('Select a model')}</option>
					{#each $models.filter((model) => model.id) as model}
						<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
					{/each}
				</select>
			</div>
		</div>
		<hr class=" dark:border-gray-850" />

Timothy J. Baek's avatar
Timothy J. Baek committed
229
		<div>
Timothy J. Baek's avatar
Timothy J. Baek committed
230
			<div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
231

Timothy J. Baek's avatar
Timothy J. Baek committed
232
233
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
234
					<div class=" self-center text-xs">{$i18n.t('Chat Bubble UI')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251

					<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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
			{#if !$settings.chatBubble}
				<div>
					<div class=" py-0.5 flex w-full justify-between">
						<div class=" self-center text-xs">
							{$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>
					</div>
				</div>
			{/if}

Timothy J. Baek's avatar
Timothy J. Baek committed
276
277
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
278
					<div class=" self-center text-xs">{$i18n.t('Widescreen Mode')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
							togglewidescreenMode();
						}}
						type="button"
					>
						{#if widescreenMode === 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
296
297
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
					<div class=" self-center text-xs">{$i18n.t('Chat direction')}</div>

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={toggleChangeChatDirection}
						type="button"
					>
						{#if chatDirection === 'LTR'}
							<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>

			<div>
				<div class=" py-0.5 flex w-full justify-between">
					<div class=" self-center text-xs">
						{$i18n.t('Fluidly stream large external response chunks')}
					</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
319
320
321
322

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
323
							toggleSplitLargeChunks();
Timothy J. Baek's avatar
Timothy J. Baek committed
324
325
326
						}}
						type="button"
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
327
						{#if splitLargeChunks === true}
328
							<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
329
						{:else}
330
							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
331
332
333
334
335
						{/if}
					</button>
				</div>
			</div>

336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
			<div>
				<div class=" py-0.5 flex w-full justify-between">
					<div class=" self-center text-xs">
						{$i18n.t('Scroll to bottom when switching between branches')}
					</div>

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
							togglesScrollOnBranchChange();
						}}
						type="button"
					>
						{#if scrollOnBranchChange === 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
358
359
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
360
361
					<div class=" self-center text-xs">
						{$i18n.t('Chat Background Image')}
362
					</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
363
364
365
366

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
367
368
369
370
371
372
							if (backgroundImageUrl !== null) {
								backgroundImageUrl = null;
								saveSettings({ backgroundImageUrl });
							} else {
								filesInputElement.click();
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
373
374
375
						}}
						type="button"
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
376
377
						{#if backgroundImageUrl !== null}
							<span class="ml-2 self-center">{$i18n.t('Reset')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
378
						{:else}
Timothy J. Baek's avatar
Timothy J. Baek committed
379
							<span class="ml-2 self-center">{$i18n.t('Upload')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
380
381
382
383
						{/if}
					</button>
				</div>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
384

Timothy J. Baek's avatar
Timothy J. Baek committed
385
386
			<div class=" my-1.5 text-sm font-medium">{$i18n.t('Chat')}</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
387
388
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
389
					<div class=" self-center text-xs">{$i18n.t('Title Auto-Generation')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
390
391
392
393

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
394
							toggleTitleAutoGenerate();
Timothy J. Baek's avatar
Timothy J. Baek committed
395
396
397
						}}
						type="button"
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
398
						{#if titleAutoGenerate === true}
399
							<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
400
						{:else}
401
							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
402
403
404
405
406
						{/if}
					</button>
				</div>
			</div>

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
407
408
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
409
410
411
					<div class=" self-center text-xs">
						{$i18n.t('Response AutoCopy to Clipboard')}
					</div>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
412
413
414
415

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
416
							toggleResponseAutoCopy();
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
417
418
419
						}}
						type="button"
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
420
						{#if responseAutoCopy === true}
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
421
422
423
424
425
426
427
428
							<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>

429
430
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
431
					<div class=" self-center text-xs">{$i18n.t('Allow User Location')}</div>
432
433
434
435

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
436
							toggleUserLocation();
437
438
439
						}}
						type="button"
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
440
						{#if userLocation === true}
441
442
443
444
445
446
447
							<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
448

449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
			<div>
				<div class=" py-0.5 flex w-full justify-between">
					<div class=" self-center text-xs">{$i18n.t('Haptic Feedback')}</div>

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
							toggleHapticFeedback();
						}}
						type="button"
					>
						{#if hapticFeedback === 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
469
			<div class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div>
470

471
472
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
473
					<div class=" self-center text-xs">{$i18n.t('Allow Voice Interruption in Call')}</div>
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
							toggleVoiceInterruption();
						}}
						type="button"
					>
						{#if voiceInterruption === 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
491
492
493
			<div>
				<div class=" py-0.5 flex w-full justify-between">
					<div class=" self-center text-xs">{$i18n.t('Display Emoji in Call')}</div>
494

Timothy J. Baek's avatar
Timothy J. Baek committed
495
496
497
498
499
500
501
502
503
504
505
506
507
					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
							toggleEmojiInCall();
						}}
						type="button"
					>
						{#if showEmojiInCall === true}
							<span class="ml-2 self-center">{$i18n.t('On')}</span>
						{:else}
							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
						{/if}
					</button>
508
509
510
				</div>
			</div>
		</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
511
512
	</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
513
	<div class="flex justify-end text-sm font-medium">
Timothy J. Baek's avatar
Timothy J. Baek committed
514
		<button
Timothy J. Baek's avatar
Timothy J. Baek committed
515
			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
516
517
			type="submit"
		>
Jannik Streidl's avatar
Jannik Streidl committed
518
			{$i18n.t('Save')}
Timothy J. Baek's avatar
Timothy J. Baek committed
519
520
521
		</button>
	</div>
</form>