"vscode:/vscode.git/clone" did not exist on "f32094dd4155b56ae8c924170db06051e0f5fcca"
Interface.svelte 12.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;
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;
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
37

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

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

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

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

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

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

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

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

		saveSettings({ userLocation });
	};

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

	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(
120
				$i18n.t(
SimonOriginal's avatar
SimonOriginal committed
121
122
					'Clipboard write permission denied. Please check your browser settings to grant the necessary access.'
				)
Timothy J. Baek's avatar
Timothy J. Baek committed
123
124
125
126
			);
		}
	};

127
128
	const toggleChangeChatDirection = async () => {
		chatDirection = chatDirection === 'LTR' ? 'RTL' : 'LTR';
Timothy J. Baek's avatar
Timothy J. Baek committed
129
		saveSettings({ chatDirection });
130
131
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
132
	const updateInterfaceHandler = async () => {
133
		saveSettings({
134
			models: [defaultModelId]
135
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
136
137
138
	};

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

141
142
		responseAutoCopy = $settings.responseAutoCopy ?? false;
		showUsername = $settings.showUsername ?? false;
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
143
144

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

147
		chatBubble = $settings.chatBubble ?? true;
148
		widescreenMode = $settings.widescreenMode ?? false;
149
		splitLargeChunks = $settings.splitLargeChunks ?? false;
150
		scrollOnBranchChange = $settings.scrollOnBranchChange ?? true;
151
		chatDirection = $settings.chatDirection ?? 'LTR';
Timothy J. Baek's avatar
Timothy J. Baek committed
152
		userLocation = $settings.userLocation ?? false;
153

154
155
156
157
		defaultModelId = $settings?.models?.at(0) ?? '';
		if ($config?.default_models) {
			defaultModelId = $config.default_models.split(',')[0];
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
158
159

		backgroundImageUrl = $settings.backgroundImageUrl ?? null;
Timothy J. Baek's avatar
Timothy J. Baek committed
160
161
162
163
164
165
166
167
168
169
	});
</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
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
214
215
216
217
218
219
220
	<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
221
		<div>
Timothy J. Baek's avatar
Timothy J. Baek committed
222
			<div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
223

Timothy J. Baek's avatar
Timothy J. Baek committed
224
225
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
226
					<div class=" self-center text-xs">{$i18n.t('Chat Bubble UI')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243

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

					<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
288
289
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
					<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
311
312
313
314

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

328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
			<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
350
351
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
352
353
					<div class=" self-center text-xs">
						{$i18n.t('Chat Background Image')}
354
					</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
355
356
357
358

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
359
360
361
362
363
364
							if (backgroundImageUrl !== null) {
								backgroundImageUrl = null;
								saveSettings({ backgroundImageUrl });
							} else {
								filesInputElement.click();
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
365
366
367
						}}
						type="button"
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
368
369
						{#if backgroundImageUrl !== null}
							<span class="ml-2 self-center">{$i18n.t('Reset')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
370
						{:else}
Timothy J. Baek's avatar
Timothy J. Baek committed
371
							<span class="ml-2 self-center">{$i18n.t('Upload')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
372
373
374
375
						{/if}
					</button>
				</div>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
376

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

Timothy J. Baek's avatar
Timothy J. Baek committed
379
380
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
381
					<div class=" self-center text-xs">{$i18n.t('Title Auto-Generation')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
382
383
384
385

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
386
							toggleTitleAutoGenerate();
Timothy J. Baek's avatar
Timothy J. Baek committed
387
388
389
						}}
						type="button"
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
390
						{#if titleAutoGenerate === true}
391
							<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
392
						{:else}
393
							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
394
395
396
397
398
						{/if}
					</button>
				</div>
			</div>

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
399
400
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
401
402
403
					<div class=" self-center text-xs">
						{$i18n.t('Response AutoCopy to Clipboard')}
					</div>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
404
405
406
407

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
408
							toggleResponseAutoCopy();
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
409
410
411
						}}
						type="button"
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
412
						{#if responseAutoCopy === true}
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
413
414
415
416
417
418
419
420
							<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>

421
422
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
423
					<div class=" self-center text-xs">{$i18n.t('Allow User Location')}</div>
424
425
426
427

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
428
							toggleUserLocation();
429
430
431
						}}
						type="button"
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
432
						{#if userLocation === true}
433
434
435
436
437
438
439
							<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
440

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

443
444
			<div>
				<div class=" py-0.5 flex w-full justify-between">
Timothy J. Baek's avatar
Timothy J. Baek committed
445
					<div class=" self-center text-xs">{$i18n.t('Allow Voice Interruption in Call')}</div>
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462

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

Timothy J. Baek's avatar
Timothy J. Baek committed
467
468
469
470
471
472
473
474
475
476
477
478
479
					<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>
480
481
482
				</div>
			</div>
		</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
483
484
	</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
485
	<div class="flex justify-end text-sm font-medium">
Timothy J. Baek's avatar
Timothy J. Baek committed
486
		<button
Timothy J. Baek's avatar
Timothy J. Baek committed
487
			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
488
489
			type="submit"
		>
Jannik Streidl's avatar
Jannik Streidl committed
490
			{$i18n.t('Save')}
Timothy J. Baek's avatar
Timothy J. Baek committed
491
492
493
		</button>
	</div>
</form>