Interface.svelte 11.9 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;
Timothy J. Baek's avatar
Timothy J. Baek committed
25
	let userLocation = false;
Timothy J. Baek's avatar
Timothy J. Baek committed
26

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

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

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

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

42
43
44
	const togglewidescreenMode = async () => {
		widescreenMode = !widescreenMode;
		saveSettings({ widescreenMode: widescreenMode });
Timothy J. Baek's avatar
Timothy J. Baek committed
45
46
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
47
48
49
50
51
	const toggleChatBubble = async () => {
		chatBubble = !chatBubble;
		saveSettings({ chatBubble: chatBubble });
	};

52
53
54
55
56
	const toggleShowUsername = async () => {
		showUsername = !showUsername;
		saveSettings({ showUsername: showUsername });
	};

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
57
58
59
60
61
	const toggleEmojiInCall = async () => {
		showEmojiInCall = !showEmojiInCall;
		saveSettings({ showEmojiInCall: showEmojiInCall });
	};

62
63
64
65
66
	const toggleVoiceInterruption = async () => {
		voiceInterruption = !voiceInterruption;
		saveSettings({ voiceInterruption: voiceInterruption });
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
67
68
69
70
71
72
73
74
75
76
77
	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
78
				toast.success($i18n.t('User location successfully retrieved.'));
Timothy J. Baek's avatar
Timothy J. Baek committed
79
80
81
82
83
84
85
86
			} else {
				userLocation = false;
			}
		}

		saveSettings({ userLocation });
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
87
88
	const toggleTitleAutoGenerate = async () => {
		titleAutoGenerate = !titleAutoGenerate;
89
90
91
92
93
94
		saveSettings({
			title: {
				...$settings.title,
				auto: titleAutoGenerate
			}
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
	};

	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(
114
				$i18n.t(
SimonOriginal's avatar
SimonOriginal committed
115
116
					'Clipboard write permission denied. Please check your browser settings to grant the necessary access.'
				)
Timothy J. Baek's avatar
Timothy J. Baek committed
117
118
119
120
			);
		}
	};

121
122
	const toggleChangeChatDirection = async () => {
		chatDirection = chatDirection === 'LTR' ? 'RTL' : 'LTR';
Timothy J. Baek's avatar
Timothy J. Baek committed
123
		saveSettings({ chatDirection });
124
125
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
126
	const updateInterfaceHandler = async () => {
127
		saveSettings({
128
			models: [defaultModelId]
129
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
130
131
132
	};

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

135
136
		responseAutoCopy = $settings.responseAutoCopy ?? false;
		showUsername = $settings.showUsername ?? false;
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
137
138

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

141
		chatBubble = $settings.chatBubble ?? true;
142
		widescreenMode = $settings.widescreenMode ?? false;
143
144
		splitLargeChunks = $settings.splitLargeChunks ?? false;
		chatDirection = $settings.chatDirection ?? 'LTR';
Timothy J. Baek's avatar
Timothy J. Baek committed
145
		userLocation = $settings.userLocation ?? false;
146

147
148
149
150
		defaultModelId = $settings?.models?.at(0) ?? '';
		if ($config?.default_models) {
			defaultModelId = $config.default_models.split(',')[0];
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
151
152

		backgroundImageUrl = $settings.backgroundImageUrl ?? null;
Timothy J. Baek's avatar
Timothy J. Baek committed
153
154
155
156
157
158
159
160
161
162
	});
</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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
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
	<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
214
		<div>
Timothy J. Baek's avatar
Timothy J. Baek committed
215
			<div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
216

Timothy J. Baek's avatar
Timothy J. Baek committed
217
218
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
219
					<div class=" self-center text-xs">{$i18n.t('Chat Bubble UI')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236

					<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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
			{#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
261
262
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
263
					<div class=" self-center text-xs">{$i18n.t('Widescreen Mode')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280

					<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
281
282
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
					<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
304
305
306
307

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
308
							toggleSplitLargeChunks();
Timothy J. Baek's avatar
Timothy J. Baek committed
309
310
311
						}}
						type="button"
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
312
						{#if splitLargeChunks === true}
313
							<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
314
						{:else}
315
							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
316
317
318
319
320
321
322
						{/if}
					</button>
				</div>
			</div>

			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
323
324
					<div class=" self-center text-xs">
						{$i18n.t('Chat Background Image')}
325
					</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
326
327
328
329

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
330
331
332
333
334
335
							if (backgroundImageUrl !== null) {
								backgroundImageUrl = null;
								saveSettings({ backgroundImageUrl });
							} else {
								filesInputElement.click();
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
336
337
338
						}}
						type="button"
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
339
340
						{#if backgroundImageUrl !== null}
							<span class="ml-2 self-center">{$i18n.t('Reset')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
341
						{:else}
Timothy J. Baek's avatar
Timothy J. Baek committed
342
							<span class="ml-2 self-center">{$i18n.t('Upload')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
343
344
345
346
						{/if}
					</button>
				</div>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
347

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

Timothy J. Baek's avatar
Timothy J. Baek committed
350
351
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
352
					<div class=" self-center text-xs">{$i18n.t('Title Auto-Generation')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
353
354
355
356

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
357
							toggleTitleAutoGenerate();
Timothy J. Baek's avatar
Timothy J. Baek committed
358
359
360
						}}
						type="button"
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
361
						{#if titleAutoGenerate === true}
362
							<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
363
						{:else}
364
							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
365
366
367
368
369
						{/if}
					</button>
				</div>
			</div>

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
370
371
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
372
373
374
					<div class=" self-center text-xs">
						{$i18n.t('Response AutoCopy to Clipboard')}
					</div>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
375
376
377
378

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
379
							toggleResponseAutoCopy();
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
380
381
382
						}}
						type="button"
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
383
						{#if responseAutoCopy === true}
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
384
385
386
387
388
389
390
391
							<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>

392
393
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
394
					<div class=" self-center text-xs">{$i18n.t('Allow User Location')}</div>
395
396
397
398

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
399
							toggleUserLocation();
400
401
402
						}}
						type="button"
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
403
						{#if userLocation === true}
404
405
406
407
408
409
410
							<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
411

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

414
415
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
416
					<div class=" self-center text-xs">{$i18n.t('Allow Voice Interruption in Call')}</div>
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433

					<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
434
435
436
			<div>
				<div class=" py-0.5 flex w-full justify-between">
					<div class=" self-center text-xs">{$i18n.t('Display Emoji in Call')}</div>
437

Timothy J. Baek's avatar
Timothy J. Baek committed
438
439
440
441
442
443
444
445
446
447
448
449
450
					<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>
451
452
453
				</div>
			</div>
		</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
454
455
	</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
456
	<div class="flex justify-end text-sm font-medium">
Timothy J. Baek's avatar
Timothy J. Baek committed
457
		<button
Timothy J. Baek's avatar
Timothy J. Baek committed
458
			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
459
460
			type="submit"
		>
Jannik Streidl's avatar
Jannik Streidl committed
461
			{$i18n.t('Save')}
Timothy J. Baek's avatar
Timothy J. Baek committed
462
463
464
		</button>
	</div>
</form>