+page.svelte 22.8 KB
Newer Older
1
2
3
4
5
6
<script lang="ts">
	import { v4 as uuidv4 } from 'uuid';
	import toast from 'svelte-french-toast';

	import { onMount, tick } from 'svelte';
	import { goto } from '$app/navigation';
7
8
	import { page } from '$app/stores';

Timothy J. Baek's avatar
Timothy J. Baek committed
9
10
11
12
13
14
15
16
17
18
	import {
		models,
		modelfiles,
		user,
		settings,
		chats,
		chatId,
		config,
		tags as _tags
	} from '$lib/stores';
Brandon Hulston's avatar
Brandon Hulston committed
19
	import { copyToClipboard, splitStream, convertMessagesToHistory } from '$lib/utils';
20

21
	import { generateChatCompletion, generateTitle, cancelChatCompletion } from '$lib/apis/ollama';
22
23
24
25
	import {
		addTagById,
		createNewChat,
		deleteTagById,
Timothy J. Baek's avatar
Timothy J. Baek committed
26
		getAllChatTags,
27
28
29
30
31
		getChatById,
		getChatList,
		getTagsById,
		updateChatById
	} from '$lib/apis/chats';
32
	import { queryCollection, queryDoc } from '$lib/apis/rag';
Timothy J. Baek's avatar
Timothy J. Baek committed
33
34
	import { generateOpenAIChatCompletion } from '$lib/apis/openai';

35
36
37
38
	import MessageInput from '$lib/components/chat/MessageInput.svelte';
	import Messages from '$lib/components/chat/Messages.svelte';
	import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
	import Navbar from '$lib/components/layout/Navbar.svelte';
Timothy J. Baek's avatar
Timothy J. Baek committed
39
	import { RAGTemplate } from '$lib/utils/rag';
40
	import { WEBUI_BASE_URL } from '$lib/constants';
41
42

	let loaded = false;
Timothy J. Baek's avatar
Timothy J. Baek committed
43

44
45
	let stopResponseFlag = false;
	let autoScroll = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
46
	let processing = '';
47

Timothy J. Baek's avatar
Timothy J. Baek committed
48
49
	let currentRequestId = null;

50
51
	// let chatId = $page.params.id;
	let selectedModels = [''];
52
53
54
55
56
57
	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;
58

Timothy J. Baek's avatar
Timothy J. Baek committed
59
60
61
62
63
64
65
66
67
68
69
	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 })
		};
	}, {});

70
	let chat = null;
71
	let tags = [];
72

73
74
	let title = '';
	let prompt = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
75
	let files = [];
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

	let messages = [];
	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
93
94
	} else {
		messages = [];
95
96
97
98
	}

	$: if ($page.params.id) {
		(async () => {
99
100
			if (await loadChat()) {
				await tick();
101
102
103
104
				loaded = true;
			} else {
				await goto('/');
			}
105
106
107
108
109
110
111
112
113
		})();
	}

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

	const loadChat = async () => {
		await chatId.set($page.params.id);
114
115
		chat = await getChatById(localStorage.token, $chatId).catch(async (error) => {
			await goto('/');
116
			return null;
117
118
119
		});

		if (chat) {
120
			tags = await getTags();
121
122
123
124
125
126
127
128
			const chatContent = chat.chat;

			if (chatContent) {
				console.log(chatContent);

				selectedModels =
					(chatContent?.models ?? undefined) !== undefined
						? chatContent.models
Brandon Hulston's avatar
Brandon Hulston committed
129
						: [chatContent.models ?? ''];
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
				history =
					(chatContent?.history ?? undefined) !== undefined
						? chatContent.history
						: convertMessagesToHistory(chatContent.messages);
				title = chatContent.title;

				let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
				await settings.set({
					..._settings,
					system: chatContent.system ?? _settings.system,
					options: chatContent.options ?? _settings.options
				});
				autoScroll = true;
				await tick();

				if (messages.length > 0) {
					history.messages[messages.at(-1).id].done = true;
				}
				await tick();

				return true;
			} else {
				return null;
			}
154
155
156
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
157
158
159
160
161
	const scrollToBottom = () => {
		const element = document.getElementById('messages-container');
		element.scrollTop = element.scrollHeight;
	};

162
163
164
165
	//////////////////////////
	// Ollama functions
	//////////////////////////

Timothy J. Baek's avatar
Timothy J. Baek committed
166
	const submitPrompt = async (userPrompt, _user = null) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
167
168
169
170
171
172
173
		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');
Timothy J. Baek's avatar
Timothy J. Baek committed
174
175
176
177
178
179
180
181
		} 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.`
			);
Timothy J. Baek's avatar
Timothy J. Baek committed
182
183
184
185
186
187
188
189
190
191
192
		} 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
193
				user: _user ?? undefined,
Timothy J. Baek's avatar
Timothy J. Baek committed
194
				content: userPrompt,
Timothy J. Baek's avatar
Timothy J. Baek committed
195
				files: files.length > 0 ? files : undefined,
Timothy J. Baek's avatar
Timothy J. Baek committed
196
				timestamp: Math.floor(Date.now() / 1000) // Unix epoch
Timothy J. Baek's avatar
Timothy J. Baek committed
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
			};

			// 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();
232
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
233
234
235
236
237
238
239
240
241
			// Reset chat input textarea
			prompt = '';
			files = [];

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

242
243
	const sendPrompt = async (prompt, parentId) => {
		const _chatId = JSON.parse(JSON.stringify($chatId));
Timothy J. Baek's avatar
Timothy J. Baek committed
244
245
246

		const docs = messages
			.filter((message) => message?.files ?? null)
247
248
249
			.map((message) =>
				message.files.filter((item) => item.type === 'doc' || item.type === 'collection')
			)
Timothy J. Baek's avatar
Timothy J. Baek committed
250
251
252
253
			.flat(1);

		console.log(docs);
		if (docs.length > 0) {
Timothy J. Baek's avatar
Timothy J. Baek committed
254
			processing = 'Reading';
Timothy J. Baek's avatar
Timothy J. Baek committed
255
256
			const query = history.messages[parentId].content;

257
258
			let relevantContexts = await Promise.all(
				docs.map(async (doc) => {
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
					if (doc.type === 'collection') {
						return await queryCollection(localStorage.token, doc.collection_names, query, 4).catch(
							(error) => {
								console.log(error);
								return null;
							}
						);
					} else {
						return await queryDoc(localStorage.token, doc.collection_name, query, 4).catch(
							(error) => {
								console.log(error);
								return null;
							}
						);
					}
274
275
276
				})
			);
			relevantContexts = relevantContexts.filter((context) => context);
Timothy J. Baek's avatar
Timothy J. Baek committed
277

278
279
280
			const contextString = relevantContexts.reduce((a, context, i, arr) => {
				return `${a}${context.documents.join(' ')}\n`;
			}, '');
Timothy J. Baek's avatar
Timothy J. Baek committed
281

282
			console.log(contextString);
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
283

Timothy J. Baek's avatar
Timothy J. Baek committed
284
285
286
287
288
			history.messages[parentId].raContent = await RAGTemplate(
				localStorage.token,
				contextString,
				query
			);
289
			history.messages[parentId].contexts = relevantContexts;
Timothy J. Baek's avatar
Timothy J. Baek committed
290
			await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
291
			processing = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
292
293
		}

294
295
		await Promise.all(
			selectedModels.map(async (model) => {
296
				console.log(model);
Timothy J. Baek's avatar
Timothy J. Baek committed
297
298
				const modelTag = $models.filter((m) => m.name === model).at(0);

Timothy J. Baek's avatar
Timothy J. Baek committed
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
				// Create response message
				let responseMessageId = uuidv4();
				let responseMessage = {
					parentId: parentId,
					id: responseMessageId,
					childrenIds: [],
					role: 'assistant',
					content: '',
					model: model,
					timestamp: Math.floor(Date.now() / 1000) // Unix epoch
				};

				// Add message to history and Set currentId to messageId
				history.messages[responseMessageId] = responseMessage;
				history.currentId = responseMessageId;

				// Append messageId to childrenIds of parent message
				if (parentId !== null) {
					history.messages[parentId].childrenIds = [
						...history.messages[parentId].childrenIds,
						responseMessageId
					];
				}

Timothy J. Baek's avatar
Timothy J. Baek committed
323
				if (modelTag?.external) {
Timothy J. Baek's avatar
Timothy J. Baek committed
324
					await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
Timothy J. Baek's avatar
Timothy J. Baek committed
325
				} else if (modelTag) {
Timothy J. Baek's avatar
Timothy J. Baek committed
326
					await sendPromptOllama(model, prompt, responseMessageId, _chatId);
Timothy J. Baek's avatar
Timothy J. Baek committed
327
328
				} else {
					toast.error(`Model ${model} not found`);
329
330
331
332
				}
			})
		);

Timothy J. Baek's avatar
Timothy J. Baek committed
333
		await chats.set(await getChatList(localStorage.token));
334
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
335

Timothy J. Baek's avatar
Timothy J. Baek committed
336
337
	const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
		const responseMessage = history.messages[responseMessageId];
338

Timothy J. Baek's avatar
Timothy J. Baek committed
339
		// Wait until history/message have been updated
Timothy J. Baek's avatar
Timothy J. Baek committed
340
		await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
341
342

		// Scroll down
Timothy J. Baek's avatar
Timothy J. Baek committed
343
		scrollToBottom();
344

345
346
347
348
349
350
351
		const messagesBody = [
			$settings.system
				? {
						role: 'system',
						content: $settings.system
				  }
				: undefined,
352
			...messages.filter((message) => !message.deleted)
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
		]
			.filter((message) => message)
			.map((message, idx, arr) => ({
				role: message.role,
				content: arr.length - 2 !== idx ? message.content : message?.raContent ?? message.content,
				...(message.files && {
					images: message.files
						.filter((file) => file.type === 'image')
						.map((file) => file.url.slice(file.url.indexOf(',') + 1))
				})
			}));

		let lastImageIndex = -1;

		// Find the index of the last object with images
		messagesBody.forEach((item, index) => {
			if (item.images) {
				lastImageIndex = index;
			}
		});

		// Remove images from all but the last one
		messagesBody.forEach((item, index) => {
			if (index !== lastImageIndex) {
				delete item.images;
			}
		});

381
		const [res, controller] = await generateChatCompletion(localStorage.token, {
382
			model: model,
383
			messages: messagesBody,
384
385
386
			options: {
				...($settings.options ?? {})
			},
Zohaib Rauf's avatar
Zohaib Rauf committed
387
388
			format: $settings.requestFormat ?? undefined,
			keep_alive: $settings.keepAlive ?? undefined
389
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
390

391
		if (res && res.ok) {
392
393
			console.log('controller', controller);

Rohit Das's avatar
Rohit Das committed
394
395
396
397
398
399
400
401
402
403
			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;
404
405
406

					if (stopResponseFlag) {
						controller.abort('User: Stop Response');
Timothy J. Baek's avatar
Timothy J. Baek committed
407
						await cancelChatCompletion(localStorage.token, currentRequestId);
408
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
409
410

					currentRequestId = null;
411

Rohit Das's avatar
Rohit Das committed
412
413
					break;
				}
414

Rohit Das's avatar
Rohit Das committed
415
416
				try {
					let lines = value.split('\n');
417

Rohit Das's avatar
Rohit Das committed
418
419
420
421
					for (const line of lines) {
						if (line !== '') {
							console.log(line);
							let data = JSON.parse(line);
Timothy J. Baek's avatar
Timothy J. Baek committed
422

Rohit Das's avatar
Rohit Das committed
423
424
425
							if ('detail' in data) {
								throw data;
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
426

427
428
							if ('id' in data) {
								console.log(data);
Timothy J. Baek's avatar
Timothy J. Baek committed
429
								currentRequestId = data.id;
430
431
432
433
434
435
436
437
							} 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
438
								} else {
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
									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
458
									messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
459

460
461
462
463
464
465
466
									if ($settings.notificationEnabled && !document.hasFocus()) {
										const notification = new Notification(
											selectedModelfile
												? `${
														selectedModelfile.title.charAt(0).toUpperCase() +
														selectedModelfile.title.slice(1)
												  }`
Timothy J. Baek's avatar
Timothy J. Baek committed
467
												: `${model}`,
468
469
											{
												body: responseMessage.content,
470
												icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
471
472
473
474
475
476
477
											}
										);
									}

									if ($settings.responseAutoCopy) {
										copyToClipboard(responseMessage.content);
									}
Timothy J. Baek's avatar
Timothy J. Baek committed
478
479

									if ($settings.responseAutoPlayback) {
Timothy J. Baek's avatar
Timothy J. Baek committed
480
										await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
481
482
										document.getElementById(`speak-button-${responseMessage.id}`)?.click();
									}
Timothy J. Baek's avatar
Timothy J. Baek committed
483
								}
484
485
486
							}
						}
					}
Rohit Das's avatar
Rohit Das committed
487
488
489
490
491
492
				} catch (error) {
					console.log(error);
					if ('detail' in error) {
						toast.error(error.detail);
					}
					break;
493
				}
Rohit Das's avatar
Rohit Das committed
494
495

				if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
496
					scrollToBottom();
497
				}
498
			}
499

500
			if ($chatId == _chatId) {
Timothy J. Baek's avatar
Timothy J. Baek committed
501
502
503
504
505
506
507
				if ($settings.saveChatHistory ?? true) {
					chat = await updateChatById(localStorage.token, _chatId, {
						messages: messages,
						history: history
					});
					await chats.set(await getChatList(localStorage.token));
				}
508
			}
509
510
511
		} else {
			if (res !== null) {
				const error = await res.json();
512
513
514
				console.log(error);
				if ('detail' in error) {
					toast.error(error.detail);
515
					responseMessage.content = error.detail;
516
517
				} else {
					toast.error(error.error);
518
					responseMessage.content = error.error;
519
				}
520
521
			} else {
				toast.error(`Uh-oh! There was an issue connecting to Ollama.`);
522
				responseMessage.content = `Uh-oh! There was an issue connecting to Ollama.`;
523
524
			}

525
526
527
528
			responseMessage.error = true;
			responseMessage.content = `Uh-oh! There was an issue connecting to Ollama.`;
			responseMessage.done = true;
			messages = messages;
529
530
531
532
		}

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

534
		if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
535
			scrollToBottom();
536
537
538
		}

		if (messages.length == 2 && messages.at(1).content !== '') {
Timothy J. Baek's avatar
Timothy J. Baek committed
539
540
			window.history.replaceState(history.state, '', `/c/${_chatId}`);
			await generateChatTitle(_chatId, userPrompt);
541
542
543
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
544
545
	const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => {
		const responseMessage = history.messages[responseMessageId];
546

Timothy J. Baek's avatar
Timothy J. Baek committed
547
		scrollToBottom();
548

Timothy J. Baek's avatar
Timothy J. Baek committed
549
550
551
552
553
554
555
556
557
558
		const res = await generateOpenAIChatCompletion(localStorage.token, {
			model: model,
			stream: true,
			messages: [
				$settings.system
					? {
							role: 'system',
							content: $settings.system
					  }
					: undefined,
559
				...messages.filter((message) => !message.deleted)
Timothy J. Baek's avatar
Timothy J. Baek committed
560
561
			]
				.filter((message) => message)
562
				.map((message, idx, arr) => ({
Timothy J. Baek's avatar
Timothy J. Baek committed
563
					role: message.role,
564
					...(message.files?.filter((file) => file.type === 'image').length > 0 ?? false
Timothy J. Baek's avatar
Timothy J. Baek committed
565
566
567
568
						? {
								content: [
									{
										type: 'text',
569
570
571
572
										text:
											arr.length - 1 !== idx
												? message.content
												: message?.raContent ?? message.content
Timothy J. Baek's avatar
Timothy J. Baek committed
573
574
575
576
577
578
579
580
581
582
583
									},
									...message.files
										.filter((file) => file.type === 'image')
										.map((file) => ({
											type: 'image_url',
											image_url: {
												url: file.url
											}
										}))
								]
						  }
584
585
586
587
						: {
								content:
									arr.length - 1 !== idx ? message.content : message?.raContent ?? message.content
						  })
Timothy J. Baek's avatar
Timothy J. Baek committed
588
589
590
591
592
593
594
595
596
				})),
			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
		});
597

Timothy J. Baek's avatar
Timothy J. Baek committed
598
599
600
601
602
		if (res && res.ok) {
			const reader = res.body
				.pipeThrough(new TextDecoderStream())
				.pipeThrough(splitStream('\n'))
				.getReader();
603

Timothy J. Baek's avatar
Timothy J. Baek committed
604
605
606
607
608
609
610
			while (true) {
				const { value, done } = await reader.read();
				if (done || stopResponseFlag || _chatId !== $chatId) {
					responseMessage.done = true;
					messages = messages;
					break;
				}
611

Timothy J. Baek's avatar
Timothy J. Baek committed
612
613
614
615
616
617
618
619
620
				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;
621
							} else {
Timothy J. Baek's avatar
Timothy J. Baek committed
622
623
624
625
626
627
628
629
630
								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;
								}
631
632
633
							}
						}
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
634
635
636
				} catch (error) {
					console.log(error);
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
637

Timothy J. Baek's avatar
Timothy J. Baek committed
638
639
640
				if ($settings.notificationEnabled && !document.hasFocus()) {
					const notification = new Notification(`OpenAI ${model}`, {
						body: responseMessage.content,
641
						icon: `${WEBUI_BASE_URL}/static/favicon.png`
Timothy J. Baek's avatar
Timothy J. Baek committed
642
					});
Timothy J. Baek's avatar
Timothy J. Baek committed
643
644
				}

Timothy J. Baek's avatar
Timothy J. Baek committed
645
646
647
				if ($settings.responseAutoCopy) {
					copyToClipboard(responseMessage.content);
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
648

Timothy J. Baek's avatar
Timothy J. Baek committed
649
				if ($settings.responseAutoPlayback) {
Timothy J. Baek's avatar
Timothy J. Baek committed
650
					await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
651
652
653
					document.getElementById(`speak-button-${responseMessage.id}`)?.click();
				}

654
				if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
655
					scrollToBottom();
656
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
657
			}
658

Timothy J. Baek's avatar
Timothy J. Baek committed
659
			if ($chatId == _chatId) {
Timothy J. Baek's avatar
Timothy J. Baek committed
660
661
662
663
664
665
666
				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
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
			}
		} 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;
					}
683
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
684
685
686
			} else {
				toast.error(`Uh-oh! There was an issue connecting to ${model}.`);
				responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`;
687
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
688
689
690
691
692
693
694
695
696
697
698

			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) {
Timothy J. Baek's avatar
Timothy J. Baek committed
699
			scrollToBottom();
Timothy J. Baek's avatar
Timothy J. Baek committed
700
701
702
703
704
		}

		if (messages.length == 2) {
			window.history.replaceState(history.state, '', `/c/${_chatId}`);
			await setChatTitle(_chatId, userPrompt);
705
706
707
708
709
710
711
712
		}
	};

	const stopResponse = () => {
		stopResponseFlag = true;
		console.log('stopResponse');
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
713
714
715
716
717
718
	const continueGeneration = async () => {
		console.log('continueGeneration');
		const _chatId = JSON.parse(JSON.stringify($chatId));

		if (messages.length != 0 && messages.at(-1).done == true) {
			const responseMessage = history.messages[history.currentId];
719
720
721
			responseMessage.done = false;
			await tick();

Timothy J. Baek's avatar
Timothy J. Baek committed
722
723
724
			const modelTag = $models.filter((m) => m.name === responseMessage.model).at(0);

			if (modelTag?.external) {
Timothy J. Baek's avatar
Timothy J. Baek committed
725
726
727
728
729
730
				await sendPromptOpenAI(
					responseMessage.model,
					history.messages[responseMessage.parentId].content,
					responseMessage.id,
					_chatId
				);
Timothy J. Baek's avatar
Timothy J. Baek committed
731
			} else if (modelTag) {
Timothy J. Baek's avatar
Timothy J. Baek committed
732
733
734
735
736
737
				await sendPromptOllama(
					responseMessage.model,
					history.messages[responseMessage.parentId].content,
					responseMessage.id,
					_chatId
				);
Timothy J. Baek's avatar
Timothy J. Baek committed
738
739
740
741
742
743
			} else {
				toast.error(`Model ${model} not found`);
			}
		}
	};

744
	const regenerateResponse = async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
745
		console.log('regenerateResponse');
746
747
748
749
750
751
752
		if (messages.length != 0 && messages.at(-1).done == true) {
			messages.splice(messages.length - 1, 1);
			messages = messages;

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

Timothy J. Baek's avatar
Timothy J. Baek committed
753
			await sendPrompt(userPrompt, userMessage.id);
754
755
756
757
		}
	};

	const generateChatTitle = async (_chatId, userPrompt) => {
758
		if ($settings.titleAutoGenerate ?? true) {
759
760
761
762
763
764
765
			const title = await generateTitle(
				localStorage.token,
				$settings?.titleGenerationPrompt ??
					"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}}",
				$settings?.titleAutoGenerateModel ?? selectedModels[0],
				userPrompt
			);
Timothy J. Baek's avatar
Timothy J. Baek committed
766
767
768

			if (title) {
				await setChatTitle(_chatId, title);
769
770
771
			}
		} else {
			await setChatTitle(_chatId, `${userPrompt}`);
772
773
774
775
		}
	};

	const setChatTitle = async (_chatId, _title) => {
776
		if (_chatId === $chatId) {
777
778
			title = _title;
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
779
780
781

		chat = await updateChatById(localStorage.token, _chatId, { title: _title });
		await chats.set(await getChatList(localStorage.token));
782
	};
783

784
785
786
787
788
789
790
791
792
	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();
Timothy J. Baek's avatar
Timothy J. Baek committed
793
794

		chat = await updateChatById(localStorage.token, $chatId, {
Timothy J. Baek's avatar
Timothy J. Baek committed
795
			tags: tags
Timothy J. Baek's avatar
Timothy J. Baek committed
796
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
797
798

		_tags.set(await getAllChatTags(localStorage.token));
799
800
801
802
803
	};

	const deleteTag = async (tagName) => {
		const res = await deleteTagById(localStorage.token, $chatId, tagName);
		tags = await getTags();
Timothy J. Baek's avatar
Timothy J. Baek committed
804
805

		chat = await updateChatById(localStorage.token, $chatId, {
Timothy J. Baek's avatar
Timothy J. Baek committed
806
			tags: tags
Timothy J. Baek's avatar
Timothy J. Baek committed
807
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
808
809

		_tags.set(await getAllChatTags(localStorage.token));
810
811
	};

812
813
814
815
816
	onMount(async () => {
		if (!($settings.saveChatHistory ?? true)) {
			await goto('/');
		}
	});
817
818
</script>

Timothy J. Baek's avatar
Timothy J. Baek committed
819
{#if loaded}
Timothy J. Baek's avatar
Timothy J. Baek committed
820
	<div class="min-h-screen max-h-screen w-full flex flex-col">
Timothy J. Baek's avatar
Timothy J. Baek committed
821
822
823
824
825
826
827
828
		<Navbar
			{title}
			shareEnabled={messages.length > 0}
			initNewChat={async () => {
				if (currentRequestId !== null) {
					await cancelChatCompletion(localStorage.token, currentRequestId);
					currentRequestId = null;
				}
829

Timothy J. Baek's avatar
Timothy J. Baek committed
830
831
832
833
834
835
				goto('/');
			}}
			{tags}
			{addTag}
			{deleteTag}
		/>
Timothy J. Baek's avatar
Timothy J. Baek committed
836
837
838
839
840
841
842
843
		<div class="flex flex-col flex-auto">
			<div
				class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
				id="messages-container"
				on:scroll={(e) => {
					autoScroll = e.target.scrollHeight - e.target.scrollTop <= e.target.clientHeight + 50;
				}}
			>
Timothy J. Baek's avatar
Timothy J. Baek committed
844
845
846
847
848
849
850
851
852
853
854
				<div
					class="{$settings?.fullScreenMode ?? null
						? 'max-w-full'
						: 'max-w-2xl md:px-0'} mx-auto w-full px-4"
				>
					<ModelSelector
						bind:selectedModels
						disabled={messages.length > 0 && !selectedModels.includes('')}
					/>
				</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
855
				<div class=" h-full w-full flex flex-col py-8">
Timothy J. Baek's avatar
Timothy J. Baek committed
856
857
858
859
860
861
862
863
864
865
866
867
868
869
					<Messages
						chatId={$chatId}
						{selectedModels}
						{selectedModelfiles}
						{processing}
						bind:history
						bind:messages
						bind:autoScroll
						bottomPadding={files.length > 0}
						{sendPrompt}
						{continueGeneration}
						{regenerateResponse}
					/>
				</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
870
871
			</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
872
873
874
875
876
877
878
879
880
881
			<MessageInput
				bind:files
				bind:prompt
				bind:autoScroll
				suggestionPrompts={selectedModelfile?.suggestionPrompts ??
					$config.default_prompt_suggestions}
				{messages}
				{submitPrompt}
				{stopResponse}
			/>
882
883
		</div>
	</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
884
{/if}