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
78
79
80
81
82
83
84
85
86
	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 });
				toast.success('User location successfully retrieved.');
			} 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
114
115
116
117
118
	};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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('Allow User Location')}</div>
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
							toggleUserLocation();
395
396
397
						}}
						type="button"
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
398
						{#if userLocation === true}
399
400
401
402
403
404
405
							<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
406

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

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

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

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

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