+page.svelte 20.3 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';

Timothy J. Baek's avatar
Timothy J. Baek committed
5
	import { 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

Timothy J. Baek's avatar
Timothy J. Baek committed
9
	import { models, modelfiles, user, settings, chats, chatId, config } from '$lib/stores';
10
	import { copyToClipboard, splitStream } from '$lib/utils';
Timothy J. Baek's avatar
Timothy J. Baek committed
11

12
	import { generateChatCompletion, cancelChatCompletion, generateTitle } from '$lib/apis/ollama';
13
14
15
16
17
18
19
20
	import {
		addTagById,
		createNewChat,
		deleteTagById,
		getChatList,
		getTagsById,
		updateChatById
	} from '$lib/apis/chats';
21
22
	import { queryVectorDB } from '$lib/apis/rag';
	import { generateOpenAIChatCompletion } from '$lib/apis/openai';
23
24
25
26

	import MessageInput from '$lib/components/chat/MessageInput.svelte';
	import Messages from '$lib/components/chat/Messages.svelte';
	import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
27
	import Navbar from '$lib/components/layout/Navbar.svelte';
28
	import { RAGTemplate } from '$lib/utils/rag';
29

30
31
	let stopResponseFlag = false;
	let autoScroll = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
32
	let processing = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
33

34
35
	let currentRequestId = null;

Timothy J. Baek's avatar
Timothy J. Baek committed
36
	let selectedModels = [''];
Timothy J. Baek's avatar
Timothy J. Baek committed
37

38
39
40
41
42
43
	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;
44

45
46
47
48
49
50
51
52
53
54
55
	let selectedModelfiles = {};
	$: selectedModelfiles = selectedModels.reduce((a, tagName, i, arr) => {
		const modelfile =
			$modelfiles.filter((modelfile) => modelfile.tagName === tagName)?.at(0) ?? undefined;

		return {
			...a,
			...(modelfile && { [tagName]: modelfile })
		};
	}, {});

56
	let chat = null;
57
	let tags = [];
58

Timothy J. Baek's avatar
Timothy J. Baek committed
59
	let title = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
60
	let prompt = '';
61
	let files = [];
62
	let messages = [];
Timothy J. Baek's avatar
Timothy J. Baek committed
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
	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
78
79
	} else {
		messages = [];
Timothy J. Baek's avatar
Timothy J. Baek committed
80
	}
Timothy J. Baek's avatar
Timothy J. Baek committed
81

82
	onMount(async () => {
83
		await initNewChat();
84
85
86
87
88
89
	});

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

90
	const initNewChat = async () => {
91
92
93
94
95
		if (currentRequestId !== null) {
			await cancelChatCompletion(localStorage.token, currentRequestId);
			currentRequestId = null;
		}

Timothy J. Baek's avatar
Timothy J. Baek committed
96
97
		window.history.replaceState(history.state, '', `/`);

98
99
100
		console.log('initNewChat');

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

103
		autoScroll = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
104

105
106
107
108
109
		title = '';
		messages = [];
		history = {
			messages: {},
			currentId: null
Timothy J. Baek's avatar
Timothy J. Baek committed
110
		};
Timothy J. Baek's avatar
Timothy J. Baek committed
111
112
113
114
115
116
117
118
119
120
121
122

		console.log($config);

		if ($page.url.searchParams.get('models')) {
			selectedModels = $page.url.searchParams.get('models')?.split(',');
		} else if ($settings?.models) {
			selectedModels = $settings?.models;
		} else if ($config?.default_models) {
			selectedModels = $config?.default_models.split(',');
		} else {
			selectedModels = [''];
		}
123
124
125
126
127

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

130
131
132
133
	//////////////////////////
	// Ollama functions
	//////////////////////////

Timothy J. Baek's avatar
Timothy J. Baek committed
134
	const submitPrompt = async (userPrompt, _user = null) => {
135
136
137
138
139
140
141
		console.log('submitPrompt', $chatId);

		if (selectedModels.includes('')) {
			toast.error('Model not selected');
		} else if (messages.length != 0 && messages.at(-1).done != true) {
			// Response not done
			console.log('wait');
142
143
144
145
146
147
148
149
		} else if (
			files.length > 0 &&
			files.filter((file) => file.upload_status === false).length > 0
		) {
			// Upload not done
			toast.error(
				`Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.`
			);
150
151
152
153
154
155
156
157
158
159
160
		} else {
			// Reset chat message textarea height
			document.getElementById('chat-textarea').style.height = '';

			// Create user message
			let userMessageId = uuidv4();
			let userMessage = {
				id: userMessageId,
				parentId: messages.length !== 0 ? messages.at(-1).id : null,
				childrenIds: [],
				role: 'user',
Timothy J. Baek's avatar
Timothy J. Baek committed
161
				user: _user ?? undefined,
162
				content: userPrompt,
Timothy J. Baek's avatar
Timothy J. Baek committed
163
				files: files.length > 0 ? files : undefined,
Timothy J. Baek's avatar
Timothy J. Baek committed
164
				timestamp: Math.floor(Date.now() / 1000) // Unix epoch
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
			};

			// Add message to history and Set currentId to messageId
			history.messages[userMessageId] = userMessage;
			history.currentId = userMessageId;

			// Append messageId to childrenIds of parent message
			if (messages.length !== 0) {
				history.messages[messages.at(-1).id].childrenIds.push(userMessageId);
			}

			// Wait until history/message have been updated
			await tick();

			// Create new chat if only one message in messages
			if (messages.length == 1) {
				if ($settings.saveChatHistory ?? true) {
					chat = await createNewChat(localStorage.token, {
						id: $chatId,
						title: 'New Chat',
						models: selectedModels,
						system: $settings.system ?? undefined,
						options: {
							...($settings.options ?? {})
						},
						messages: messages,
						history: history,
						timestamp: Date.now()
					});
					await chats.set(await getChatList(localStorage.token));
					await chatId.set(chat.id);
				} else {
					await chatId.set('local');
				}
				await tick();
			}

			// Reset chat input textarea
			prompt = '';
			files = [];

			// Send prompt
			await sendPrompt(userPrompt, userMessageId);
		}
	};

211
212
	const sendPrompt = async (prompt, parentId) => {
		const _chatId = JSON.parse(JSON.stringify($chatId));
213

214
215
216
217
		const docs = messages
			.filter((message) => message?.files ?? null)
			.map((message) => message.files.filter((item) => item.type === 'doc'))
			.flat(1);
Timothy J. Baek's avatar
Timothy J. Baek committed
218
219

		console.log(docs);
220
		if (docs.length > 0) {
Timothy J. Baek's avatar
Timothy J. Baek committed
221
			processing = 'Reading';
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
			const query = history.messages[parentId].content;

			let relevantContexts = await Promise.all(
				docs.map(async (doc) => {
					return await queryVectorDB(localStorage.token, doc.collection_name, query, 4).catch(
						(error) => {
							console.log(error);
							return null;
						}
					);
				})
			);
			relevantContexts = relevantContexts.filter((context) => context);

			const contextString = relevantContexts.reduce((a, context, i, arr) => {
				return `${a}${context.documents.join(' ')}\n`;
			}, '');

Timothy J. Baek's avatar
Timothy J. Baek committed
240
241
			console.log(contextString);

242
243
244
			history.messages[parentId].raContent = RAGTemplate(contextString, query);
			history.messages[parentId].contexts = relevantContexts;
			await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
245
			processing = '';
246
247
		}

Timothy J. Baek's avatar
Timothy J. Baek committed
248
249
		await Promise.all(
			selectedModels.map(async (model) => {
250
				console.log(model);
Timothy J. Baek's avatar
Timothy J. Baek committed
251
252
253
				const modelTag = $models.filter((m) => m.name === model).at(0);

				if (modelTag?.external) {
254
					await sendPromptOpenAI(model, prompt, parentId, _chatId);
Timothy J. Baek's avatar
Timothy J. Baek committed
255
				} else if (modelTag) {
256
					await sendPromptOllama(model, prompt, parentId, _chatId);
Timothy J. Baek's avatar
Timothy J. Baek committed
257
258
				} else {
					toast.error(`Model ${model} not found`);
Timothy J. Baek's avatar
Timothy J. Baek committed
259
260
261
				}
			})
		);
262

Timothy J. Baek's avatar
Timothy J. Baek committed
263
		await chats.set(await getChatList(localStorage.token));
264
265
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
266
	const sendPromptOllama = async (model, userPrompt, parentId, _chatId) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
267
		// Create response message
Timothy J. Baek's avatar
Timothy J. Baek committed
268
		let responseMessageId = uuidv4();
269
		let responseMessage = {
Timothy J. Baek's avatar
Timothy J. Baek committed
270
271
272
			parentId: parentId,
			id: responseMessageId,
			childrenIds: [],
273
			role: 'assistant',
Timothy J. Baek's avatar
Timothy J. Baek committed
274
			content: '',
Timothy J. Baek's avatar
Timothy J. Baek committed
275
			model: model,
Timothy J. Baek's avatar
Timothy J. Baek committed
276
			timestamp: Math.floor(Date.now() / 1000) // Unix epoch
277
278
		};

Timothy J. Baek's avatar
Timothy J. Baek committed
279
		// Add message to history and Set currentId to messageId
Timothy J. Baek's avatar
Timothy J. Baek committed
280
281
		history.messages[responseMessageId] = responseMessage;
		history.currentId = responseMessageId;
Timothy J. Baek's avatar
Timothy J. Baek committed
282
283

		// Append messageId to childrenIds of parent message
Timothy J. Baek's avatar
Timothy J. Baek committed
284
285
286
287
288
289
290
		if (parentId !== null) {
			history.messages[parentId].childrenIds = [
				...history.messages[parentId].childrenIds,
				responseMessageId
			];
		}

Timothy J. Baek's avatar
Timothy J. Baek committed
291
		// Wait until history/message have been updated
Timothy J. Baek's avatar
Timothy J. Baek committed
292
		await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
293
294

		// Scroll down
Timothy J. Baek's avatar
Timothy J. Baek committed
295
		window.scrollTo({ top: document.body.scrollHeight });
296

297
		const [res, controller] = await generateChatCompletion(localStorage.token, {
298
299
300
301
302
303
304
305
306
307
308
			model: model,
			messages: [
				$settings.system
					? {
							role: 'system',
							content: $settings.system
					  }
					: undefined,
				...messages
			]
				.filter((message) => message)
309
				.map((message, idx, arr) => ({
310
					role: message.role,
311
					content: arr.length - 2 !== idx ? message.content : message?.raContent ?? message.content,
312
313
314
315
316
317
318
319
320
321
322
					...(message.files && {
						images: message.files
							.filter((file) => file.type === 'image')
							.map((file) => file.url.slice(file.url.indexOf(',') + 1))
					})
				})),
			options: {
				...($settings.options ?? {})
			},
			format: $settings.requestFormat ?? undefined
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
323

324
		if (res && res.ok) {
325
326
			console.log('controller', controller);

Rohit Das's avatar
Rohit Das committed
327
328
329
330
331
332
333
334
335
336
			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;
337
338
339
340
341
342
343
344

					if (stopResponseFlag) {
						controller.abort('User: Stop Response');
						await cancelChatCompletion(localStorage.token, currentRequestId);
					}

					currentRequestId = null;

Rohit Das's avatar
Rohit Das committed
345
346
					break;
				}
347

Rohit Das's avatar
Rohit Das committed
348
349
				try {
					let lines = value.split('\n');
350

Rohit Das's avatar
Rohit Das committed
351
352
353
354
					for (const line of lines) {
						if (line !== '') {
							console.log(line);
							let data = JSON.parse(line);
Timothy J. Baek's avatar
Timothy J. Baek committed
355

Rohit Das's avatar
Rohit Das committed
356
357
358
							if ('detail' in data) {
								throw data;
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
359

360
361
362
363
364
365
366
367
368
369
370
							if ('id' in data) {
								console.log(data);
								currentRequestId = data.id;
							} else {
								if (data.done == false) {
									if (responseMessage.content == '' && data.message.content == '\n') {
										continue;
									} else {
										responseMessage.content += data.message.content;
										messages = messages;
									}
Rohit Das's avatar
Rohit Das committed
371
								} else {
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
									responseMessage.done = true;

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

									responseMessage.context = data.context ?? null;
									responseMessage.info = {
										total_duration: data.total_duration,
										load_duration: data.load_duration,
										sample_count: data.sample_count,
										sample_duration: data.sample_duration,
										prompt_eval_count: data.prompt_eval_count,
										prompt_eval_duration: data.prompt_eval_duration,
										eval_count: data.eval_count,
										eval_duration: data.eval_duration
									};
Rohit Das's avatar
Rohit Das committed
391
									messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
392

393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
									if ($settings.notificationEnabled && !document.hasFocus()) {
										const notification = new Notification(
											selectedModelfile
												? `${
														selectedModelfile.title.charAt(0).toUpperCase() +
														selectedModelfile.title.slice(1)
												  }`
												: `Ollama - ${model}`,
											{
												body: responseMessage.content,
												icon: selectedModelfile?.imageUrl ?? '/favicon.png'
											}
										);
									}

									if ($settings.responseAutoCopy) {
										copyToClipboard(responseMessage.content);
									}
Timothy J. Baek's avatar
Timothy J. Baek committed
411
								}
412
413
414
							}
						}
					}
Rohit Das's avatar
Rohit Das committed
415
416
417
418
419
420
				} catch (error) {
					console.log(error);
					if ('detail' in error) {
						toast.error(error.detail);
					}
					break;
421
				}
Rohit Das's avatar
Rohit Das committed
422
423
424

				if (autoScroll) {
					window.scrollTo({ top: document.body.scrollHeight });
425
				}
426
			}
427

428
			if ($chatId == _chatId) {
429
430
431
432
433
434
435
				if ($settings.saveChatHistory ?? true) {
					chat = await updateChatById(localStorage.token, _chatId, {
						messages: messages,
						history: history
					});
					await chats.set(await getChatList(localStorage.token));
				}
436
			}
437
438
439
		} else {
			if (res !== null) {
				const error = await res.json();
440
				console.log(error);
441
442
				if ('detail' in error) {
					toast.error(error.detail);
443
					responseMessage.content = error.detail;
444
445
				} else {
					toast.error(error.error);
446
					responseMessage.content = error.error;
447
				}
448
449
			} else {
				toast.error(`Uh-oh! There was an issue connecting to Ollama.`);
450
				responseMessage.content = `Uh-oh! There was an issue connecting to Ollama.`;
451
452
			}

453
454
455
456
			responseMessage.error = true;
			responseMessage.content = `Uh-oh! There was an issue connecting to Ollama.`;
			responseMessage.done = true;
			messages = messages;
457
458
459
460
		}

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

462
463
464
		if (autoScroll) {
			window.scrollTo({ top: document.body.scrollHeight });
		}
465

466
		if (messages.length == 2 && messages.at(1).content !== '') {
Timothy J. Baek's avatar
Timothy J. Baek committed
467
468
			window.history.replaceState(history.state, '', `/c/${_chatId}`);
			await generateChatTitle(_chatId, userPrompt);
469
470
471
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
472
	const sendPromptOpenAI = async (model, userPrompt, parentId, _chatId) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
473
		let responseMessageId = uuidv4();
474

Timothy J. Baek's avatar
Timothy J. Baek committed
475
476
477
478
479
480
		let responseMessage = {
			parentId: parentId,
			id: responseMessageId,
			childrenIds: [],
			role: 'assistant',
			content: '',
Timothy J. Baek's avatar
Timothy J. Baek committed
481
			model: model,
Timothy J. Baek's avatar
Timothy J. Baek committed
482
			timestamp: Math.floor(Date.now() / 1000) // Unix epoch
Timothy J. Baek's avatar
Timothy J. Baek committed
483
		};
484

Timothy J. Baek's avatar
Timothy J. Baek committed
485
486
487
488
489
490
491
492
		history.messages[responseMessageId] = responseMessage;
		history.currentId = responseMessageId;
		if (parentId !== null) {
			history.messages[parentId].childrenIds = [
				...history.messages[parentId].childrenIds,
				responseMessageId
			];
		}
493

Timothy J. Baek's avatar
Timothy J. Baek committed
494
		window.scrollTo({ top: document.body.scrollHeight });
495

Timothy J. Baek's avatar
Timothy J. Baek committed
496
497
498
499
500
501
502
503
504
505
506
507
508
		const res = await generateOpenAIChatCompletion(localStorage.token, {
			model: model,
			stream: true,
			messages: [
				$settings.system
					? {
							role: 'system',
							content: $settings.system
					  }
					: undefined,
				...messages
			]
				.filter((message) => message)
509
				.map((message, idx, arr) => ({
Timothy J. Baek's avatar
Timothy J. Baek committed
510
511
512
513
514
515
					role: message.role,
					...(message.files
						? {
								content: [
									{
										type: 'text',
516
517
518
519
										text:
											arr.length - 1 !== idx
												? message.content
												: message?.raContent ?? message.content
Timothy J. Baek's avatar
Timothy J. Baek committed
520
521
522
523
524
525
526
527
528
529
530
									},
									...message.files
										.filter((file) => file.type === 'image')
										.map((file) => ({
											type: 'image_url',
											image_url: {
												url: file.url
											}
										}))
								]
						  }
531
532
533
534
						: {
								content:
									arr.length - 1 !== idx ? message.content : message?.raContent ?? message.content
						  })
Timothy J. Baek's avatar
Timothy J. Baek committed
535
536
537
538
539
540
541
542
543
				})),
			seed: $settings?.options?.seed ?? undefined,
			stop: $settings?.options?.stop ?? undefined,
			temperature: $settings?.options?.temperature ?? undefined,
			top_p: $settings?.options?.top_p ?? undefined,
			num_ctx: $settings?.options?.num_ctx ?? undefined,
			frequency_penalty: $settings?.options?.repeat_penalty ?? undefined,
			max_tokens: $settings?.options?.num_predict ?? undefined
		});
544

Timothy J. Baek's avatar
Timothy J. Baek committed
545
546
547
548
549
		if (res && res.ok) {
			const reader = res.body
				.pipeThrough(new TextDecoderStream())
				.pipeThrough(splitStream('\n'))
				.getReader();
550

Timothy J. Baek's avatar
Timothy J. Baek committed
551
552
553
554
555
556
557
			while (true) {
				const { value, done } = await reader.read();
				if (done || stopResponseFlag || _chatId !== $chatId) {
					responseMessage.done = true;
					messages = messages;
					break;
				}
558

Timothy J. Baek's avatar
Timothy J. Baek committed
559
560
561
562
563
564
565
566
567
				try {
					let lines = value.split('\n');

					for (const line of lines) {
						if (line !== '') {
							console.log(line);
							if (line === 'data: [DONE]') {
								responseMessage.done = true;
								messages = messages;
568
							} else {
Timothy J. Baek's avatar
Timothy J. Baek committed
569
570
571
572
573
574
575
576
577
								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;
								}
578
579
580
							}
						}
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
581
582
583
				} catch (error) {
					console.log(error);
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
584

Timothy J. Baek's avatar
Timothy J. Baek committed
585
586
587
588
589
				if ($settings.notificationEnabled && !document.hasFocus()) {
					const notification = new Notification(`OpenAI ${model}`, {
						body: responseMessage.content,
						icon: '/favicon.png'
					});
Timothy J. Baek's avatar
Timothy J. Baek committed
590
591
				}

Timothy J. Baek's avatar
Timothy J. Baek committed
592
593
594
				if ($settings.responseAutoCopy) {
					copyToClipboard(responseMessage.content);
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
595

596
597
598
				if (autoScroll) {
					window.scrollTo({ top: document.body.scrollHeight });
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
599
			}
600

Timothy J. Baek's avatar
Timothy J. Baek committed
601
			if ($chatId == _chatId) {
602
603
604
605
606
607
608
				if ($settings.saveChatHistory ?? true) {
					chat = await updateChatById(localStorage.token, _chatId, {
						messages: messages,
						history: history
					});
					await chats.set(await getChatList(localStorage.token));
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
			}
		} 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;
					}
625
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
626
627
628
			} else {
				toast.error(`Uh-oh! There was an issue connecting to ${model}.`);
				responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`;
629
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646

			responseMessage.error = true;
			responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`;
			responseMessage.done = true;
			messages = messages;
		}

		stopResponseFlag = false;
		await tick();

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

		if (messages.length == 2) {
			window.history.replaceState(history.state, '', `/c/${_chatId}`);
			await setChatTitle(_chatId, userPrompt);
647
		}
648
649
	};

650
651
652
653
654
	const stopResponse = () => {
		stopResponseFlag = true;
		console.log('stopResponse');
	};

655
	const regenerateResponse = async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
656
		console.log('regenerateResponse');
657
658
659
		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
660

661
662
			let userMessage = messages.at(-1);
			let userPrompt = userMessage.content;
663

Timothy J. Baek's avatar
Timothy J. Baek committed
664
			await sendPrompt(userPrompt, userMessage.id);
Timothy J. Baek's avatar
Timothy J. Baek committed
665
		}
666
	};
667

668
	const generateChatTitle = async (_chatId, userPrompt) => {
669
		if ($settings.titleAutoGenerate ?? true) {
Timothy J. Baek's avatar
Timothy J. Baek committed
670
671
			const title = await generateTitle(
				localStorage.token,
672
				$settings?.titleAutoGenerateModel ?? selectedModels[0],
Timothy J. Baek's avatar
Timothy J. Baek committed
673
674
675
676
677
				userPrompt
			);

			if (title) {
				await setChatTitle(_chatId, title);
678
679
680
			}
		} else {
			await setChatTitle(_chatId, `${userPrompt}`);
681
682
683
		}
	};

684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
	const getTags = async () => {
		return await getTagsById(localStorage.token, $chatId).catch(async (error) => {
			return [];
		});
	};

	const addTag = async (tagName) => {
		const res = await addTagById(localStorage.token, $chatId, tagName);
		tags = await getTags();
	};

	const deleteTag = async (tagName) => {
		const res = await deleteTagById(localStorage.token, $chatId, tagName);
		tags = await getTags();
	};

700
	const setChatTitle = async (_chatId, _title) => {
701
		if (_chatId === $chatId) {
702
			title = _title;
703
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
704

705
706
707
708
		if ($settings.saveChatHistory ?? true) {
			chat = await updateChatById(localStorage.token, _chatId, { title: _title });
			await chats.set(await getChatList(localStorage.token));
		}
709
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
710
711
</script>

712
713
<svelte:window
	on:scroll={(e) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
714
		autoScroll = window.innerHeight + window.scrollY >= document.body.offsetHeight - 40;
715
716
717
	}}
/>

718
<Navbar {title} shareEnabled={messages.length > 0} {initNewChat} {tags} {addTag} {deleteTag} />
719
720
721
722
<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
723
		</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
724

725
		<div class=" h-full mt-10 mb-32 w-full flex flex-col">
726
			<Messages
727
				chatId={$chatId}
728
				{selectedModels}
729
				{selectedModelfiles}
Timothy J. Baek's avatar
Timothy J. Baek committed
730
				{processing}
731
732
733
				bind:history
				bind:messages
				bind:autoScroll
Timothy J. Baek's avatar
Timothy J. Baek committed
734
				bottomPadding={files.length > 0}
735
736
737
				{sendPrompt}
				{regenerateResponse}
			/>
738
739
		</div>
	</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
740

741
742
	<MessageInput
		bind:files
Timothy J. Baek's avatar
Timothy J. Baek committed
743
		bind:prompt
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
		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}
	/>
767
</div>