Interface.svelte 11.8 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
115
116
				$i18n.t(
+					'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

		defaultModelId = ($settings?.models ?? ['']).at(0);
Timothy J. Baek's avatar
Timothy J. Baek committed
148
149

		backgroundImageUrl = $settings.backgroundImageUrl ?? null;
Timothy J. Baek's avatar
Timothy J. Baek committed
150
151
152
153
154
155
156
157
158
159
	});
</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
160
161
162
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
	<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
211
		<div>
Timothy J. Baek's avatar
Timothy J. Baek committed
212
			<div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
213

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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