Interface.svelte 13.7 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
	const dispatch = createEventDispatcher();

10
11
	const i18n = getContext('i18n');

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

	// Addons
	let titleAutoGenerate = true;
	let responseAutoCopy = false;
	let titleAutoGenerateModel = '';
18
	let titleAutoGenerateModelExternal = '';
19
	let widescreenMode = false;
20
	let titleGenerationPrompt = '';
21
	let splitLargeChunks = false;
Timothy J. Baek's avatar
Timothy J. Baek committed
22

Timothy J. Baek's avatar
Timothy J. Baek committed
23
	// Interface
24
	let defaultModelId = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
25
	let promptSuggestions = [];
26
	let showUsername = false;
Timothy J. Baek's avatar
Timothy J. Baek committed
27
	let chatBubble = true;
28
	let chatDirection: 'LTR' | 'RTL' = 'LTR';
29

30
31
32
33
34
	const toggleSplitLargeChunks = async () => {
		splitLargeChunks = !splitLargeChunks;
		saveSettings({ splitLargeChunks: splitLargeChunks });
	};

35
36
37
	const togglewidescreenMode = async () => {
		widescreenMode = !widescreenMode;
		saveSettings({ widescreenMode: widescreenMode });
Timothy J. Baek's avatar
Timothy J. Baek committed
38
39
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
40
41
42
43
44
	const toggleChatBubble = async () => {
		chatBubble = !chatBubble;
		saveSettings({ chatBubble: chatBubble });
	};

45
46
47
48
49
	const toggleShowUsername = async () => {
		showUsername = !showUsername;
		saveSettings({ showUsername: showUsername });
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
50
51
	const toggleTitleAutoGenerate = async () => {
		titleAutoGenerate = !titleAutoGenerate;
52
53
54
55
56
57
		saveSettings({
			title: {
				...$settings.title,
				auto: titleAutoGenerate
			}
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
	};

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

82
83
	const toggleChangeChatDirection = async () => {
		chatDirection = chatDirection === 'LTR' ? 'RTL' : 'LTR';
Timothy J. Baek's avatar
Timothy J. Baek committed
84
		saveSettings({ chatDirection });
85
86
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
87
	const updateInterfaceHandler = async () => {
88
89
90
91
92
93
		if ($user.role === 'admin') {
			promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
			await config.set(await getBackendConfig());
		}

		saveSettings({
94
95
96
97
98
99
			title: {
				...$settings.title,
				model: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
				modelExternal:
					titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined,
				prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
100
101
			},
			models: [defaultModelId]
102
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
103
104
105
106
107
108
	};

	onMount(async () => {
		if ($user.role === 'admin') {
			promptSuggestions = $config?.default_prompt_suggestions;
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
109

110
111
112
		titleAutoGenerate = $settings?.title?.auto ?? true;
		titleAutoGenerateModel = $settings?.title?.model ?? '';
		titleAutoGenerateModelExternal = $settings?.title?.modelExternal ?? '';
113
		titleGenerationPrompt =
114
115
116
117
118
			$settings?.title?.prompt ??
			`Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title': {{prompt}}`;
		responseAutoCopy = $settings.responseAutoCopy ?? false;
		showUsername = $settings.showUsername ?? false;
		chatBubble = $settings.chatBubble ?? true;
119
		widescreenMode = $settings.widescreenMode ?? false;
120
121
		splitLargeChunks = $settings.splitLargeChunks ?? false;
		chatDirection = $settings.chatDirection ?? 'LTR';
122
123

		defaultModelId = ($settings?.models ?? ['']).at(0);
Timothy J. Baek's avatar
Timothy J. Baek committed
124
125
126
127
128
129
130
131
132
133
	});
</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
134
	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]">
Timothy J. Baek's avatar
Timothy J. Baek committed
135
		<div>
136
			<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Add-ons')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
137

Timothy J. Baek's avatar
Timothy J. Baek committed
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
			<div>
				<div class=" py-0.5 flex w-full justify-between">
					<div class=" self-center text-xs font-medium">{$i18n.t('Chat Bubble UI')}</div>

					<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
158
159
			<div>
				<div class=" py-0.5 flex w-full justify-between">
160
					<div class=" self-center text-xs font-medium">{$i18n.t('Title Auto-Generation')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
161
162
163
164
165
166
167
168
169

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
							toggleTitleAutoGenerate();
						}}
						type="button"
					>
						{#if titleAutoGenerate === true}
170
							<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
171
						{:else}
172
							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
173
174
175
176
177
178
179
						{/if}
					</button>
				</div>
			</div>

			<div>
				<div class=" py-0.5 flex w-full justify-between">
180
181
182
					<div class=" self-center text-xs font-medium">
						{$i18n.t('Response AutoCopy to Clipboard')}
					</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
183
184
185
186
187
188
189
190
191

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
							toggleResponseAutoCopy();
						}}
						type="button"
					>
						{#if responseAutoCopy === true}
192
							<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
193
						{:else}
194
							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
195
196
197
198
						{/if}
					</button>
				</div>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
199

Timothy J. Baek's avatar
Timothy J. Baek committed
200
201
			<div>
				<div class=" py-0.5 flex w-full justify-between">
202
					<div class=" self-center text-xs font-medium">{$i18n.t('Widescreen Mode')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
203
204
205
206

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
207
							togglewidescreenMode();
Timothy J. Baek's avatar
Timothy J. Baek committed
208
209
210
						}}
						type="button"
					>
211
						{#if widescreenMode === true}
212
							<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
213
						{:else}
214
							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
215
216
217
218
219
						{/if}
					</button>
				</div>
			</div>

220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
			{#if !$settings.chatBubble}
				<div>
					<div class=" py-0.5 flex w-full justify-between">
						<div class=" self-center text-xs font-medium">
							{$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>
Timothy J. Baek's avatar
Timothy J. Baek committed
240
					</div>
241
				</div>
242
			{/if}
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264

			<div>
				<div class=" py-0.5 flex w-full justify-between">
					<div class=" self-center text-xs font-medium">
						{$i18n.t('Fluidly stream large external response chunks')}
					</div>

					<button
						class="p-1 px-3 text-xs flex rounded transition"
						on:click={() => {
							toggleSplitLargeChunks();
						}}
						type="button"
					>
						{#if splitLargeChunks === 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
265
266
		</div>

267
268
269
270
271
272
273
274
275
		<div>
			<div class=" py-0.5 flex w-full justify-between">
				<div class=" self-center text-xs font-medium">{$i18n.t('Chat direction')}</div>

				<button
					class="p-1 px-3 text-xs flex rounded transition"
					on:click={toggleChangeChatDirection}
					type="button"
				>
Timothy J. Baek's avatar
Timothy J. Baek committed
276
					{#if chatDirection === 'LTR'}
277
278
279
280
281
282
283
284
						<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>

285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
		<hr class=" dark:border-gray-850" />

		<div class=" space-y-1 mb-3">
			<div class="mb-2">
				<div class="flex justify-between items-center text-xs">
					<div class=" text-xs 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
309

Timothy J. Baek's avatar
Timothy J. Baek committed
310
		<div>
311
312
313
			<div class=" mb-2.5 text-sm font-medium flex">
				<div class=" mr-1">{$i18n.t('Set Task Model')}</div>
				<Tooltip
Jun Siang Cheah's avatar
Jun Siang Cheah committed
314
315
316
317
					content={$i18n.t(
						'A task model is used when performing tasks such as generating titles for chats and web search queries'
					)}
				>
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
					<svg
						xmlns="http://www.w3.org/2000/svg"
						fill="none"
						viewBox="0 0 24 24"
						stroke-width="1.5"
						stroke="currentColor"
						class="w-5 h-5"
					>
						<path
							stroke-linecap="round"
							stroke-linejoin="round"
							d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
						/>
					</svg>
				</Tooltip>
			</div>
334
335
			<div class="flex w-full gap-2 pr-2">
				<div class="flex-1">
Karl Lee's avatar
Karl Lee committed
336
					<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
337
					<select
Timothy J. Baek's avatar
Timothy J. Baek committed
338
						class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
Timothy J. Baek's avatar
Timothy J. Baek committed
339
						bind:value={titleAutoGenerateModel}
340
						placeholder={$i18n.t('Select a model')}
Timothy J. Baek's avatar
Timothy J. Baek committed
341
					>
342
						<option value="" selected>{$i18n.t('Current Model')}</option>
Timothy J. Baek's avatar
Timothy J. Baek committed
343
344
345
346
						{#each $models.filter((m) => m.owned_by === 'ollama') as model}
							<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
								{model.name}
							</option>
Timothy J. Baek's avatar
Timothy J. Baek committed
347
348
349
						{/each}
					</select>
				</div>
350
351

				<div class="flex-1">
Karl Lee's avatar
Karl Lee committed
352
					<div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
353
354
355
356
357
358
359
					<select
						class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
						bind:value={titleAutoGenerateModelExternal}
						placeholder={$i18n.t('Select a model')}
					>
						<option value="" selected>{$i18n.t('Current Model')}</option>
						{#each $models as model}
Timothy J. Baek's avatar
Timothy J. Baek committed
360
361
362
							<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
								{model.name}
							</option>
363
364
365
						{/each}
					</select>
				</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
366
			</div>
367

Timothy J. Baek's avatar
Timothy J. Baek committed
368
			<div class="mt-3 mr-2">
369
				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
370
371
				<textarea
					bind:value={titleGenerationPrompt}
Timothy J. Baek's avatar
Timothy J. Baek committed
372
					class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
373
374
375
					rows="3"
				/>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
376
377
378
379
380
		</div>

		{#if $user.role === 'admin'}
			<hr class=" dark:border-gray-700" />

Timothy J. Baek's avatar
Timothy J. Baek committed
381
			<div class=" space-y-3 pr-1.5">
Timothy J. Baek's avatar
Timothy J. Baek committed
382
				<div class="flex w-full justify-between mb-2">
383
384
385
					<div class=" self-center text-sm font-semibold">
						{$i18n.t('Default Prompt Suggestions')}
					</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
386
387

					<button
Timothy J. Baek's avatar
Timothy J. Baek committed
388
						class="p-1 px-3 text-xs flex rounded transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
389
390
						type="button"
						on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
391
392
393
							if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
								promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
394
395
396
397
398
399
400
401
402
						}}
					>
						<svg
							xmlns="http://www.w3.org/2000/svg"
							viewBox="0 0 20 20"
							fill="currentColor"
							class="w-4 h-4"
						>
							<path
Timothy J. Baek's avatar
Timothy J. Baek committed
403
								d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
Timothy J. Baek's avatar
Timothy J. Baek committed
404
405
406
407
							/>
						</svg>
					</button>
				</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
408
409
410
411
412
413
414
				<div class="flex flex-col space-y-1">
					{#each promptSuggestions as prompt, promptIdx}
						<div class=" flex border dark:border-gray-600 rounded-lg">
							<div class="flex flex-col flex-1">
								<div class="flex border-b dark:border-gray-600 w-full">
									<input
										class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
415
										placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
Timothy J. Baek's avatar
Timothy J. Baek committed
416
417
418
419
420
										bind:value={prompt.title[0]}
									/>

									<input
										class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
421
										placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
Timothy J. Baek's avatar
Timothy J. Baek committed
422
423
424
425
426
427
										bind:value={prompt.title[1]}
									/>
								</div>

								<input
									class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
428
									placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
Timothy J. Baek's avatar
Timothy J. Baek committed
429
430
431
									bind:value={prompt.content}
								/>
							</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
432

Timothy J. Baek's avatar
Timothy J. Baek committed
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
							<button
								class="px-2"
								type="button"
								on:click={() => {
									promptSuggestions.splice(promptIdx, 1);
									promptSuggestions = promptSuggestions;
								}}
							>
								<svg
									xmlns="http://www.w3.org/2000/svg"
									viewBox="0 0 20 20"
									fill="currentColor"
									class="w-4 h-4"
								>
									<path
										d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
									/>
								</svg>
							</button>
						</div>
					{/each}
				</div>

				{#if promptSuggestions.length > 0}
					<div class="text-xs text-left w-full mt-2">
Jannik Streidl's avatar
Jannik Streidl committed
458
						{$i18n.t('Adjusting these settings will apply changes universally to all users.')}
Timothy J. Baek's avatar
Timothy J. Baek committed
459
460
					</div>
				{/if}
Timothy J. Baek's avatar
Timothy J. Baek committed
461
462
463
464
			</div>
		{/if}
	</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
465
	<div class="flex justify-end text-sm font-medium">
Timothy J. Baek's avatar
Timothy J. Baek committed
466
		<button
Timothy J. Baek's avatar
Timothy J. Baek committed
467
			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
468
469
			type="submit"
		>
Jannik Streidl's avatar
Jannik Streidl committed
470
			{$i18n.t('Save')}
Timothy J. Baek's avatar
Timothy J. Baek committed
471
472
473
		</button>
	</div>
</form>