+page.svelte 19.5 KB
Newer Older
Timothy J. Baek's avatar
Timothy J. Baek committed
1
<script lang="ts">
2
	import { v4 as uuidv4 } from 'uuid';
Timothy J. Baek's avatar
Timothy J. Baek committed
3
4
	import toast from 'svelte-french-toast';

5
	import { onDestroy, onMount, tick } from 'svelte';
6
	import { goto } from '$app/navigation';
7
	import { page } from '$app/stores';
Timothy J. Baek's avatar
Timothy J. Baek committed
8

9
	import { config, models, modelfiles, user, settings, db, chats, chatId } from '$lib/stores';
10
11
	import { OLLAMA_API_BASE_URL } from '$lib/constants';
	import { splitStream } from '$lib/utils';
12
13
14
15

	import MessageInput from '$lib/components/chat/MessageInput.svelte';
	import Messages from '$lib/components/chat/Messages.svelte';
	import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
16
	import Navbar from '$lib/components/layout/Navbar.svelte';
17

18
19
	let stopResponseFlag = false;
	let autoScroll = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
20

Timothy J. Baek's avatar
Timothy J. Baek committed
21
	let selectedModels = [''];
22
23
24
25
26
27
	let selectedModelfile = null;
	$: selectedModelfile =
		selectedModels.length === 1 &&
		$modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0]).length > 0
			? $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0])[0]
			: null;
28

29
30
	let chat = null;

Timothy J. Baek's avatar
Timothy J. Baek committed
31
	let title = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
32
	let prompt = '';
33
	let files = [];
34
	let messages = [];
Timothy J. Baek's avatar
Timothy J. Baek committed
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
	let history = {
		messages: {},
		currentId: null
	};

	$: if (history.currentId !== null) {
		let _messages = [];

		let currentMessage = history.messages[history.currentId];
		while (currentMessage !== null) {
			_messages.unshift({ ...currentMessage });
			currentMessage =
				currentMessage.parentId !== null ? history.messages[currentMessage.parentId] : null;
		}
		messages = _messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
50
51
	} else {
		messages = [];
Timothy J. Baek's avatar
Timothy J. Baek committed
52
	}
Timothy J. Baek's avatar
Timothy J. Baek committed
53

54
	onMount(async () => {
55
		await initNewChat();
56
57
58
59
60
61
	});

	//////////////////////////
	// Web functions
	//////////////////////////

62
	const initNewChat = async () => {
63
64
65
		console.log('initNewChat');

		await chatId.set('');
66
		console.log($chatId);
Timothy J. Baek's avatar
Timothy J. Baek committed
67

68
		autoScroll = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
69

70
71
72
73
74
		title = '';
		messages = [];
		history = {
			messages: {},
			currentId: null
Timothy J. Baek's avatar
Timothy J. Baek committed
75
		};
76
77
78
		selectedModels = $page.url.searchParams.get('models')
			? $page.url.searchParams.get('models')?.split(',')
			: $settings.models ?? [''];
79
80
81
82
83

		let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
		settings.set({
			..._settings
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
84
85
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
	const copyToClipboard = (text) => {
		if (!navigator.clipboard) {
			var textArea = document.createElement('textarea');
			textArea.value = text;

			// Avoid scrolling to bottom
			textArea.style.top = '0';
			textArea.style.left = '0';
			textArea.style.position = 'fixed';

			document.body.appendChild(textArea);
			textArea.focus();
			textArea.select();

			try {
				var successful = document.execCommand('copy');
				var msg = successful ? 'successful' : 'unsuccessful';
				console.log('Fallback: Copying text command was ' + msg);
			} catch (err) {
				console.error('Fallback: Oops, unable to copy', err);
			}

			document.body.removeChild(textArea);
			return;
		}
		navigator.clipboard.writeText(text).then(
			function () {
				console.log('Async: Copying to clipboard was successful!');
			},
			function (err) {
				console.error('Async: Could not copy text: ', err);
			}
		);
	};

121
122
123
124
	//////////////////////////
	// Ollama functions
	//////////////////////////

125
126
	const sendPrompt = async (prompt, parentId) => {
		const _chatId = JSON.parse(JSON.stringify($chatId));
Timothy J. Baek's avatar
Timothy J. Baek committed
127
128
		await Promise.all(
			selectedModels.map(async (model) => {
129
130
				console.log(model);
				if ($models.filter((m) => m.name === model)[0].external) {
131
					await sendPromptOpenAI(model, prompt, parentId, _chatId);
Timothy J. Baek's avatar
Timothy J. Baek committed
132
				} else {
133
					await sendPromptOllama(model, prompt, parentId, _chatId);
Timothy J. Baek's avatar
Timothy J. Baek committed
134
135
136
				}
			})
		);
137

138
		await chats.set(await $db.getChats());
139
140
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
141
	const sendPromptOllama = async (model, userPrompt, parentId, _chatId) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
142
		console.log('sendPromptOllama');
Timothy J. Baek's avatar
Timothy J. Baek committed
143
		let responseMessageId = uuidv4();
144
		let responseMessage = {
Timothy J. Baek's avatar
Timothy J. Baek committed
145
146
147
			parentId: parentId,
			id: responseMessageId,
			childrenIds: [],
148
			role: 'assistant',
Timothy J. Baek's avatar
Timothy J. Baek committed
149
150
			content: '',
			model: model
151
152
		};

Timothy J. Baek's avatar
Timothy J. Baek committed
153
154
155
156
157
158
159
160
161
		history.messages[responseMessageId] = responseMessage;
		history.currentId = responseMessageId;
		if (parentId !== null) {
			history.messages[parentId].childrenIds = [
				...history.messages[parentId].childrenIds,
				responseMessageId
			];
		}

Timothy J. Baek's avatar
Timothy J. Baek committed
162
163
		await tick();
		window.scrollTo({ top: document.body.scrollHeight });
164

Timothy J. Baek's avatar
Timothy J. Baek committed
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
		const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/chat`, {
			method: 'POST',
			headers: {
				'Content-Type': 'text/event-stream',
				...($settings.authHeader && { Authorization: $settings.authHeader }),
				...($user && { Authorization: `Bearer ${localStorage.token}` })
			},
			body: JSON.stringify({
				model: model,
				messages: [
					$settings.system
						? {
								role: 'system',
								content: $settings.system
						  }
						: undefined,
					...messages
				]
					.filter((message) => message)
Timothy J. Baek's avatar
Timothy J. Baek committed
184
185
186
187
188
189
190
191
192
					.map((message) => ({
						role: message.role,
						content: message.content,
						...(message.files && {
							images: message.files
								.filter((file) => file.type === 'image')
								.map((file) => file.url.slice(file.url.indexOf(',') + 1))
						})
					})),
Timothy J. Baek's avatar
Timothy J. Baek committed
193
194
195
196
197
198
199
200
201
202
203
				options: {
					seed: $settings.seed ?? undefined,
					temperature: $settings.temperature ?? undefined,
					repeat_penalty: $settings.repeat_penalty ?? undefined,
					top_k: $settings.top_k ?? undefined,
					top_p: $settings.top_p ?? undefined,
					num_ctx: $settings.num_ctx ?? undefined,
					...($settings.options ?? {})
				},
				format: $settings.requestFormat ?? undefined
			})
204
205
206
		}).catch((err) => {
			console.log(err);
			return null;
Timothy J. Baek's avatar
Timothy J. Baek committed
207
208
		});

209
		if (res && res.ok) {
Rohit Das's avatar
Rohit Das committed
210
211
212
213
214
215
216
217
218
219
220
221
			const reader = res.body
				.pipeThrough(new TextDecoderStream())
				.pipeThrough(splitStream('\n'))
				.getReader();

			while (true) {
				const { value, done } = await reader.read();
				if (done || stopResponseFlag || _chatId !== $chatId) {
					responseMessage.done = true;
					messages = messages;
					break;
				}
222

Rohit Das's avatar
Rohit Das committed
223
224
				try {
					let lines = value.split('\n');
225

Rohit Das's avatar
Rohit Das committed
226
227
228
229
					for (const line of lines) {
						if (line !== '') {
							console.log(line);
							let data = JSON.parse(line);
Timothy J. Baek's avatar
Timothy J. Baek committed
230

Rohit Das's avatar
Rohit Das committed
231
232
233
							if ('detail' in data) {
								throw data;
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
234

Rohit Das's avatar
Rohit Das committed
235
236
237
238
239
240
241
							if (data.done == false) {
								if (responseMessage.content == '' && data.message.content == '\n') {
									continue;
								} else {
									responseMessage.content += data.message.content;
									messages = messages;
								}
242
							} else {
Rohit Das's avatar
Rohit Das committed
243
								responseMessage.done = true;
244
245
246
247
248
249
250

								if (responseMessage.content == '') {
									responseMessage.error = true;
									responseMessage.content =
										'Oops! No text generated from Ollama, Please try again.';
								}

Rohit Das's avatar
Rohit Das committed
251
252
253
								responseMessage.context = data.context ?? null;
								responseMessage.info = {
									total_duration: data.total_duration,
Timothy J. Baek's avatar
Timothy J. Baek committed
254
255
256
									load_duration: data.load_duration,
									sample_count: data.sample_count,
									sample_duration: data.sample_duration,
Rohit Das's avatar
Rohit Das committed
257
258
259
260
261
									prompt_eval_count: data.prompt_eval_count,
									prompt_eval_duration: data.prompt_eval_duration,
									eval_count: data.eval_count,
									eval_duration: data.eval_duration
								};
262
								messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
263

Timothy J. Baek's avatar
Timothy J. Baek committed
264
								if ($settings.notificationEnabled && !document.hasFocus()) {
Timothy J. Baek's avatar
Timothy J. Baek committed
265
266
267
268
269
270
271
272
273
274
275
276
277
									const notification = new Notification(
										selectedModelfile
											? `${
													selectedModelfile.title.charAt(0).toUpperCase() +
													selectedModelfile.title.slice(1)
											  }`
											: `Ollama - ${model}`,
										{
											body: responseMessage.content,
											icon: selectedModelfile?.imageUrl ?? '/favicon.png'
										}
									);
								}
Timothy J. Baek's avatar
Timothy J. Baek committed
278
279
280
281

								if ($settings.responseAutoCopy) {
									copyToClipboard(responseMessage.content);
								}
282
283
284
							}
						}
					}
Rohit Das's avatar
Rohit Das committed
285
286
287
288
289
290
				} catch (error) {
					console.log(error);
					if ('detail' in error) {
						toast.error(error.detail);
					}
					break;
291
				}
Rohit Das's avatar
Rohit Das committed
292
293
294

				if (autoScroll) {
					window.scrollTo({ top: document.body.scrollHeight });
295
				}
296
			}
297

298
299
300
			if ($chatId == _chatId) {
				chat = await $db.updateChatById(_chatId, {
					...chat.chat,
Rohit Das's avatar
Rohit Das committed
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
					title: title === '' ? 'New Chat' : title,
					models: selectedModels,
					system: $settings.system ?? undefined,
					options: {
						seed: $settings.seed ?? undefined,
						temperature: $settings.temperature ?? undefined,
						repeat_penalty: $settings.repeat_penalty ?? undefined,
						top_k: $settings.top_k ?? undefined,
						top_p: $settings.top_p ?? undefined,
						num_ctx: $settings.num_ctx ?? undefined,
						...($settings.options ?? {})
					},
					messages: messages,
					history: history
				});
316
			}
317
318
319
		} else {
			if (res !== null) {
				const error = await res.json();
320
				console.log(error);
321
322
				if ('detail' in error) {
					toast.error(error.detail);
323
					responseMessage.content = error.detail;
324
325
				} else {
					toast.error(error.error);
326
					responseMessage.content = error.error;
327
				}
328
329
			} else {
				toast.error(`Uh-oh! There was an issue connecting to Ollama.`);
330
				responseMessage.content = `Uh-oh! There was an issue connecting to Ollama.`;
331
332
			}

333
334
335
336
			responseMessage.error = true;
			responseMessage.content = `Uh-oh! There was an issue connecting to Ollama.`;
			responseMessage.done = true;
			messages = messages;
337
338
339
340
341
342
343
		}

		stopResponseFlag = false;
		await tick();
		if (autoScroll) {
			window.scrollTo({ top: document.body.scrollHeight });
		}
344

345
		if (messages.length == 2 && messages.at(1).content !== '') {
Timothy J. Baek's avatar
Timothy J. Baek committed
346
347
			window.history.replaceState(history.state, '', `/c/${_chatId}`);
			await generateChatTitle(_chatId, userPrompt);
348
349
350
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
351
	const sendPromptOpenAI = async (model, userPrompt, parentId, _chatId) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
352
		if ($settings.OPENAI_API_KEY) {
353
			if (models) {
354
355
				let responseMessageId = uuidv4();

356
				let responseMessage = {
357
358
359
					parentId: parentId,
					id: responseMessageId,
					childrenIds: [],
360
					role: 'assistant',
Timothy J. Baek's avatar
Timothy J. Baek committed
361
362
					content: '',
					model: model
363
364
				};

365
366
367
368
369
370
371
372
373
				history.messages[responseMessageId] = responseMessage;
				history.currentId = responseMessageId;
				if (parentId !== null) {
					history.messages[parentId].childrenIds = [
						...history.messages[parentId].childrenIds,
						responseMessageId
					];
				}

374
375
				window.scrollTo({ top: document.body.scrollHeight });

376
377
378
379
380
381
				const res = await fetch(
					`${$settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1'}/chat/completions`,
					{
						method: 'POST',
						headers: {
							Authorization: `Bearer ${$settings.OPENAI_API_KEY}`,
Timothy J. Baek's avatar
Timothy J. Baek committed
382
							'Content-Type': 'application/json'
383
384
385
386
387
388
						},
						body: JSON.stringify({
							model: model,
							stream: true,
							messages: [
								$settings.system
389
									? {
390
391
											role: 'system',
											content: $settings.system
392
									  }
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
									: undefined,
								...messages
							]
								.filter((message) => message)
								.map((message) => ({
									role: message.role,
									...(message.files
										? {
												content: [
													{
														type: 'text',
														text: message.content
													},
													...message.files
														.filter((file) => file.type === 'image')
														.map((file) => ({
															type: 'image_url',
															image_url: {
																url: file.url
															}
														}))
												]
										  }
										: { content: message.content })
								})),
							temperature: $settings.temperature ?? undefined,
							top_p: $settings.top_p ?? undefined,
							num_ctx: $settings.num_ctx ?? undefined,
							frequency_penalty: $settings.repeat_penalty ?? undefined
						})
423
					}
424
425
426
427
				).catch((err) => {
					console.log(err);
					return null;
				});
428

429
430
431
432
433
434
435
436
437
438
439
440
441
				if (res && res.ok) {
					const reader = res.body
						.pipeThrough(new TextDecoderStream())
						.pipeThrough(splitStream('\n'))
						.getReader();

					while (true) {
						const { value, done } = await reader.read();
						if (done || stopResponseFlag || _chatId !== $chatId) {
							responseMessage.done = true;
							messages = messages;
							break;
						}
442

443
444
						try {
							let lines = value.split('\n');
445

446
447
448
449
450
							for (const line of lines) {
								if (line !== '') {
									console.log(line);
									if (line === 'data: [DONE]') {
										responseMessage.done = true;
451
										messages = messages;
452
453
454
455
456
457
458
459
460
461
									} else {
										let data = JSON.parse(line.replace(/^data: /, ''));
										console.log(data);

										if (responseMessage.content == '' && data.choices[0].delta.content == '\n') {
											continue;
										} else {
											responseMessage.content += data.choices[0].delta.content ?? '';
											messages = messages;
										}
462
463
464
									}
								}
							}
465
466
						} catch (error) {
							console.log(error);
467
468
						}

469
470
471
472
473
474
						if ($settings.notificationEnabled && !document.hasFocus()) {
							const notification = new Notification(`OpenAI ${model}`, {
								body: responseMessage.content,
								icon: '/favicon.png'
							});
						}
475

476
477
478
						if ($settings.responseAutoCopy) {
							copyToClipboard(responseMessage.content);
						}
479

480
481
482
						if (autoScroll) {
							window.scrollTo({ top: document.body.scrollHeight });
						}
483
					}
484

485
486
487
					if ($chatId == _chatId) {
						chat = await $db.updateChatById(_chatId, {
							...chat.chat,
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
							title: title === '' ? 'New Chat' : title,
							models: selectedModels,
							system: $settings.system ?? undefined,
							options: {
								seed: $settings.seed ?? undefined,
								temperature: $settings.temperature ?? undefined,
								repeat_penalty: $settings.repeat_penalty ?? undefined,
								top_k: $settings.top_k ?? undefined,
								top_p: $settings.top_p ?? undefined,
								num_ctx: $settings.num_ctx ?? undefined,
								...($settings.options ?? {})
							},
							messages: messages,
							history: history
						});
					}
				} else {
					if (res !== null) {
						const error = await res.json();
						console.log(error);
						if ('detail' in error) {
							toast.error(error.detail);
							responseMessage.content = error.detail;
						} else {
							if ('message' in error.error) {
								toast.error(error.error.message);
								responseMessage.content = error.error.message;
							} else {
								toast.error(error.error);
								responseMessage.content = error.error;
							}
						}
					} else {
						toast.error(`Uh-oh! There was an issue connecting to ${model}.`);
						responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`;
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
524

525
526
527
528
					responseMessage.error = true;
					responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`;
					responseMessage.done = true;
					messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
529
530
				}

531
532
				stopResponseFlag = false;
				await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
533

534
535
536
537
538
				if (autoScroll) {
					window.scrollTo({ top: document.body.scrollHeight });
				}

				if (messages.length == 2) {
Timothy J. Baek's avatar
Timothy J. Baek committed
539
540
					window.history.replaceState(history.state, '', `/c/${_chatId}`);
					await setChatTitle(_chatId, userPrompt);
541
542
543
				}
			}
		}
544
545
546
	};

	const submitPrompt = async (userPrompt) => {
547
		console.log('submitPrompt', $chatId);
548

Timothy J. Baek's avatar
Timothy J. Baek committed
549
		if (selectedModels.includes('')) {
Timothy J. Baek's avatar
Timothy J. Baek committed
550
			toast.error('Model not selected');
551
		} else if (messages.length != 0 && messages.at(-1).done != true) {
Timothy J. Baek's avatar
Timothy J. Baek committed
552
553
			console.log('wait');
		} else {
Timothy J. Baek's avatar
Timothy J. Baek committed
554
555
			document.getElementById('chat-textarea').style.height = '';

Timothy J. Baek's avatar
Timothy J. Baek committed
556
557
558
559
560
561
			let userMessageId = uuidv4();
			let userMessage = {
				id: userMessageId,
				parentId: messages.length !== 0 ? messages.at(-1).id : null,
				childrenIds: [],
				role: 'user',
562
563
				content: userPrompt,
				files: files.length > 0 ? files : undefined
Timothy J. Baek's avatar
Timothy J. Baek committed
564
565
566
567
568
569
570
571
			};

			if (messages.length !== 0) {
				history.messages[messages.at(-1).id].childrenIds.push(userMessageId);
			}

			history.messages[userMessageId] = userMessage;
			history.currentId = userMessageId;
Timothy J. Baek's avatar
Timothy J. Baek committed
572

Timothy J. Baek's avatar
Timothy J. Baek committed
573
			await tick();
574

Timothy J. Baek's avatar
Timothy J. Baek committed
575
			if (messages.length == 1) {
576
577
				chat = await $db.createNewChat({
					id: $chatId,
578
					title: 'New Chat',
Timothy J. Baek's avatar
Timothy J. Baek committed
579
					models: selectedModels,
580
					system: $settings.system ?? undefined,
581
					options: {
582
583
584
585
						seed: $settings.seed ?? undefined,
						temperature: $settings.temperature ?? undefined,
						repeat_penalty: $settings.repeat_penalty ?? undefined,
						top_k: $settings.top_k ?? undefined,
Anthony Cucci's avatar
Anthony Cucci committed
586
						top_p: $settings.top_p ?? undefined,
587
588
						num_ctx: $settings.num_ctx ?? undefined,
						...($settings.options ?? {})
589
					},
Timothy J. Baek's avatar
Timothy J. Baek committed
590
					messages: messages,
591
					history: history
592
				});
593
594
595
596
597

				console.log(chat);

				await chatId.set(chat.id);
				await tick();
598
			}
599

Timothy J. Baek's avatar
Timothy J. Baek committed
600
601
602
			prompt = '';
			files = [];

603
604
605
606
			setTimeout(() => {
				window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
			}, 50);

607
			await sendPrompt(userPrompt, userMessageId);
Timothy J. Baek's avatar
Timothy J. Baek committed
608
609
610
		}
	};

611
612
613
614
615
	const stopResponse = () => {
		stopResponseFlag = true;
		console.log('stopResponse');
	};

616
	const regenerateResponse = async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
617
618
619
		const _chatId = JSON.parse(JSON.stringify($chatId));
		console.log('regenerateResponse', _chatId);

620
621
622
		if (messages.length != 0 && messages.at(-1).done == true) {
			messages.splice(messages.length - 1, 1);
			messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
623

624
625
			let userMessage = messages.at(-1);
			let userPrompt = userMessage.content;
626

Timothy J. Baek's avatar
Timothy J. Baek committed
627
			await sendPrompt(userPrompt, userMessage.id, _chatId);
Timothy J. Baek's avatar
Timothy J. Baek committed
628
		}
629
	};
630

631
	const generateChatTitle = async (_chatId, userPrompt) => {
632
633
634
635
636
637
638
639
640
641
642
643
644
645
		if ($settings.titleAutoGenerate ?? true) {
			console.log('generateChatTitle');

			const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/generate`, {
				method: 'POST',
				headers: {
					'Content-Type': 'text/event-stream',
					...($user && { Authorization: `Bearer ${localStorage.token}` })
				},
				body: JSON.stringify({
					model: selectedModels[0],
					prompt: `Generate a brief 3-5 word title for this question, excluding the term 'title.' Then, please reply with only the title: ${userPrompt}`,
					stream: false
				})
646
			})
647
648
649
650
651
652
653
654
655
656
657
				.then(async (res) => {
					if (!res.ok) throw await res.json();
					return res.json();
				})
				.catch((error) => {
					if ('detail' in error) {
						toast.error(error.detail);
					}
					console.log(error);
					return null;
				});
658

659
660
661
662
663
			if (res) {
				await setChatTitle(_chatId, res.response === '' ? 'New Chat' : res.response);
			}
		} else {
			await setChatTitle(_chatId, `${userPrompt}`);
664
665
666
667
		}
	};

	const setChatTitle = async (_chatId, _title) => {
668
		chat = await $db.updateChatById(_chatId, { ...chat.chat, title: _title });
669
		if (_chatId === $chatId) {
670
			title = _title;
671
672
		}
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
673
674
</script>

675
676
<svelte:window
	on:scroll={(e) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
677
		autoScroll = window.innerHeight + window.scrollY >= document.body.offsetHeight - 40;
678
679
680
	}}
/>

681
<Navbar {title} shareEnabled={messages.length > 0} {initNewChat} />
682
683
684
685
<div class="min-h-screen w-full flex justify-center">
	<div class=" py-2.5 flex flex-col justify-between w-full">
		<div class="max-w-2xl mx-auto w-full px-3 md:px-0 mt-10">
			<ModelSelector bind:selectedModels disabled={messages.length > 0} />
Timothy J. Baek's avatar
Timothy J. Baek committed
686
		</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
687

688
		<div class=" h-full mt-10 mb-32 w-full flex flex-col">
689
			<Messages
690
				chatId={$chatId}
691
692
693
694
695
				{selectedModels}
				{selectedModelfile}
				bind:history
				bind:messages
				bind:autoScroll
Timothy J. Baek's avatar
Timothy J. Baek committed
696
				bottomPadding={files.length > 0}
697
698
699
				{sendPrompt}
				{regenerateResponse}
			/>
700
701
		</div>
	</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
702

703
704
	<MessageInput
		bind:files
Timothy J. Baek's avatar
Timothy J. Baek committed
705
		bind:prompt
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
		bind:autoScroll
		suggestionPrompts={selectedModelfile?.suggestionPrompts ?? [
			{
				title: ['Help me study', 'vocabulary for a college entrance exam'],
				content: `Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.`
			},
			{
				title: ['Give me ideas', `for what to do with my kids' art`],
				content: `What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.`
			},
			{
				title: ['Tell me a fun fact', 'about the Roman Empire'],
				content: 'Tell me a random fun fact about the Roman Empire'
			},
			{
				title: ['Show me a code snippet', `of a website's sticky header`],
				content: `Show me a code snippet of a website's sticky header in CSS and JavaScript.`
			}
		]}
		{messages}
		{submitPrompt}
		{stopResponse}
	/>
729
</div>